@kasflow/passkey-wallet 0.1.0

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/index.mjs ADDED
@@ -0,0 +1,1798 @@
1
+ // src/constants.ts
2
+ var WEBAUTHN_RP_NAME = "KasFlow";
3
+ var WEBAUTHN_RP_ID = typeof window !== "undefined" ? window.location.hostname : "localhost";
4
+ var WEBAUTHN_TIMEOUT_MS = 6e4;
5
+ var WEBAUTHN_PUB_KEY_CRED_PARAMS = [
6
+ { alg: -7, type: "public-key" },
7
+ // ES256 (recommended)
8
+ { alg: -257, type: "public-key" }
9
+ // RS256 (fallback)
10
+ ];
11
+ var WEBAUTHN_USER_VERIFICATION = "required";
12
+ var WEBAUTHN_AUTHENTICATOR_ATTACHMENT = "platform";
13
+ var IDB_DATABASE_NAME = "kasflow-wallet";
14
+ var IDB_STORE_NAME = "keystore";
15
+ var STORAGE_KEY_CREDENTIAL_ID = "credential-id";
16
+ var STORAGE_KEY_METADATA = "wallet-metadata";
17
+ var NETWORK_ID = {
18
+ MAINNET: "mainnet",
19
+ TESTNET_10: "testnet-10",
20
+ TESTNET_11: "testnet-11"
21
+ };
22
+ var DEFAULT_NETWORK = NETWORK_ID.TESTNET_10;
23
+ var RPC_TIMEOUT_MS = 3e4;
24
+ var DEFAULT_PRIORITY_FEE_SOMPI = 100000n;
25
+ var SOMPI_PER_KAS = 100000000n;
26
+ var MIN_FEE_SOMPI = 1000n;
27
+ var ERROR_MESSAGES = {
28
+ WALLET_NOT_FOUND: "No wallet found. Please create a wallet first.",
29
+ WALLET_ALREADY_EXISTS: "A wallet already exists. Delete it before creating a new one.",
30
+ PASSKEY_REGISTRATION_FAILED: "Failed to register passkey. Please try again.",
31
+ PASSKEY_AUTHENTICATION_FAILED: "Failed to authenticate with passkey. Please try again.",
32
+ INVALID_ADDRESS: "Invalid Kaspa address format.",
33
+ INSUFFICIENT_BALANCE: "Insufficient balance for this transaction.",
34
+ WEBAUTHN_NOT_SUPPORTED: "WebAuthn is not supported in this browser.",
35
+ USER_CANCELLED: "Operation was cancelled by the user.",
36
+ WASM_NOT_INITIALIZED: "Kaspa WASM module not initialized. Call initKaspa() first.",
37
+ RPC_NOT_CONNECTED: "Not connected to Kaspa network. Call connect() first.",
38
+ RPC_CONNECTION_FAILED: "Failed to connect to Kaspa network.",
39
+ TRANSACTION_FAILED: "Transaction failed to submit.",
40
+ INVALID_AMOUNT: "Invalid transaction amount."
41
+ };
42
+
43
+ // src/crypto.ts
44
+ var generateRandomBytes = (length) => {
45
+ return crypto.getRandomValues(new Uint8Array(length));
46
+ };
47
+ var uint8ArrayToBase64 = (bytes) => {
48
+ const binary = String.fromCharCode(...bytes);
49
+ return btoa(binary);
50
+ };
51
+ var base64ToUint8Array = (base64) => {
52
+ const binary = atob(base64);
53
+ const bytes = new Uint8Array(binary.length);
54
+ for (let i = 0; i < binary.length; i++) {
55
+ bytes[i] = binary.charCodeAt(i);
56
+ }
57
+ return bytes;
58
+ };
59
+ var base64urlToUint8Array = (base64url) => {
60
+ let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
61
+ while (base64.length % 4 !== 0) {
62
+ base64 += "=";
63
+ }
64
+ return base64ToUint8Array(base64);
65
+ };
66
+ var uint8ArrayToHex = (bytes) => {
67
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
68
+ };
69
+ var hexToUint8Array = (hex) => {
70
+ const matches = hex.match(/.{1,2}/g);
71
+ if (!matches) {
72
+ throw new Error("Invalid hex string");
73
+ }
74
+ return new Uint8Array(matches.map((byte) => parseInt(byte, 16)));
75
+ };
76
+
77
+ // src/deterministic-keys.ts
78
+ import { sha256 } from "@noble/hashes/sha256";
79
+ import { bytesToHex } from "@noble/hashes/utils";
80
+
81
+ // src/logger.ts
82
+ var DEBUG_ENABLED = typeof process !== "undefined" ? process.env.NODE_ENV === "development" : true;
83
+ var Logger = class {
84
+ module;
85
+ constructor(module) {
86
+ this.module = module;
87
+ }
88
+ formatMessage(level, ...args) {
89
+ return [`[${this.module}] ${level}:`, ...args];
90
+ }
91
+ /**
92
+ * Log info message
93
+ */
94
+ info(...args) {
95
+ console.log(...this.formatMessage("INFO", ...args));
96
+ }
97
+ /**
98
+ * Log warning message
99
+ */
100
+ warn(...args) {
101
+ console.warn(...this.formatMessage("WARN", ...args));
102
+ }
103
+ /**
104
+ * Log error message
105
+ */
106
+ error(...args) {
107
+ console.error(...this.formatMessage("ERROR", ...args));
108
+ }
109
+ /**
110
+ * Log debug message (only in development)
111
+ */
112
+ debug(...args) {
113
+ if (DEBUG_ENABLED) {
114
+ console.log(...this.formatMessage("DEBUG", ...args));
115
+ }
116
+ }
117
+ };
118
+ function createLogger(module) {
119
+ return new Logger(module);
120
+ }
121
+
122
+ // src/deterministic-keys.ts
123
+ var logger = createLogger("DeterministicKeys");
124
+ function deriveKaspaKeysFromPasskey(passkeyPublicKey) {
125
+ logger.info("[DeterministicKeys] Deriving Kaspa keys from passkey public key");
126
+ if (passkeyPublicKey.length !== 65) {
127
+ throw new Error(`Invalid passkey public key length: ${passkeyPublicKey.length} (expected 65)`);
128
+ }
129
+ if (passkeyPublicKey[0] !== 4) {
130
+ throw new Error(`Invalid passkey public key prefix: 0x${passkeyPublicKey[0].toString(16)} (expected 0x04)`);
131
+ }
132
+ const seed = sha256(passkeyPublicKey);
133
+ logger.info("[DeterministicKeys] Seed derived:", {
134
+ seedLength: seed.length,
135
+ seedHex: bytesToHex(seed).substring(0, 16) + "..."
136
+ // Log first 8 bytes only
137
+ });
138
+ const privateKeyHex = bytesToHex(seed);
139
+ logger.info("[DeterministicKeys] Kaspa private key derived successfully");
140
+ return {
141
+ privateKeyHex,
142
+ seed
143
+ };
144
+ }
145
+ function verifyDeterminism(passkeyPublicKey, expectedPrivateKeyHex) {
146
+ try {
147
+ const { privateKeyHex } = deriveKaspaKeysFromPasskey(passkeyPublicKey);
148
+ return privateKeyHex === expectedPrivateKeyHex;
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ // src/kaspa.ts
155
+ import {
156
+ PrivateKey,
157
+ PublicKey,
158
+ Address,
159
+ NetworkType,
160
+ signTransaction as wasmSignTransaction,
161
+ signMessage as wasmSignMessage,
162
+ kaspaToSompi as wasmKaspaToSompi,
163
+ sompiToKaspaString as wasmSompiToKaspaString,
164
+ sompiToKaspaStringWithSuffix as wasmSompiToKaspaStringWithSuffix,
165
+ createAddress
166
+ } from "@onekeyfe/kaspa-wasm";
167
+ import { KaspaAddress, KaspaPrefix } from "@kluster/kaspa-address";
168
+
169
+ // src/wasm-init.ts
170
+ import init, { initConsolePanicHook } from "@onekeyfe/kaspa-wasm";
171
+ var wasmInitialized = false;
172
+ var initPromise = null;
173
+ async function ensureWasmInitialized() {
174
+ if (wasmInitialized) {
175
+ return;
176
+ }
177
+ if (initPromise) {
178
+ return initPromise;
179
+ }
180
+ initPromise = (async () => {
181
+ try {
182
+ if (typeof WebAssembly === "undefined") {
183
+ throw new Error(
184
+ "WebAssembly is not supported in this browser. Please use a modern browser (Chrome 57+, Firefox 52+, Safari 11+, Edge 16+)"
185
+ );
186
+ }
187
+ console.log("[WASM] Initializing Kaspa WASM module...");
188
+ await init();
189
+ if (process.env.NODE_ENV !== "production") {
190
+ try {
191
+ initConsolePanicHook();
192
+ console.log("[WASM] Console panic hook enabled for better error messages");
193
+ } catch (error) {
194
+ console.warn("[WASM] Failed to initialize console panic hook:", error);
195
+ }
196
+ }
197
+ wasmInitialized = true;
198
+ console.log("[WASM] Kaspa WASM module initialized successfully");
199
+ } catch (error) {
200
+ initPromise = null;
201
+ wasmInitialized = false;
202
+ const errorMessage = error instanceof Error ? error.message : String(error);
203
+ console.error("[WASM] Initialization failed:", errorMessage);
204
+ throw new Error(`WASM initialization failed: ${errorMessage}`);
205
+ }
206
+ })();
207
+ return initPromise;
208
+ }
209
+
210
+ // src/kaspa.ts
211
+ var getNetworkType = (network) => {
212
+ switch (network) {
213
+ case NETWORK_ID.MAINNET:
214
+ return "mainnet";
215
+ case NETWORK_ID.TESTNET_10:
216
+ return "testnet-10";
217
+ case NETWORK_ID.TESTNET_11:
218
+ return "testnet-11";
219
+ default:
220
+ return "testnet-11";
221
+ }
222
+ };
223
+ var getNetworkIdString = (network) => getNetworkType(network);
224
+ var generatePrivateKey = () => {
225
+ const randomBytes = generateRandomBytes(32);
226
+ return uint8ArrayToHex(randomBytes);
227
+ };
228
+ var createPrivateKey = async (privateKeyHex) => {
229
+ await ensureWasmInitialized();
230
+ return new PrivateKey(privateKeyHex);
231
+ };
232
+ var getPublicKeyHex = async (privateKeyHex) => {
233
+ await ensureWasmInitialized();
234
+ const privateKey = new PrivateKey(privateKeyHex);
235
+ return privateKey.toPublicKey().toString();
236
+ };
237
+ var getAddressFromPrivateKey = async (privateKeyHex, network) => {
238
+ await ensureWasmInitialized();
239
+ const privateKey = new PrivateKey(privateKeyHex);
240
+ const networkType = getNetworkType(network);
241
+ return privateKey.toAddress(networkType).toString();
242
+ };
243
+ var isValidPrivateKey = async (privateKeyHex) => {
244
+ await ensureWasmInitialized();
245
+ try {
246
+ new PrivateKey(privateKeyHex);
247
+ return true;
248
+ } catch {
249
+ return false;
250
+ }
251
+ };
252
+ var publicKeyToAddress = async (publicKeyHex, network) => {
253
+ await ensureWasmInitialized();
254
+ const networkType = getNetworkType(network);
255
+ const address = createAddress(publicKeyHex, networkType);
256
+ return address.toString();
257
+ };
258
+ var isValidAddress = (address) => {
259
+ try {
260
+ KaspaAddress.fromString(address);
261
+ return true;
262
+ } catch {
263
+ return false;
264
+ }
265
+ };
266
+ var parseAddress = (address) => {
267
+ const parsed = KaspaAddress.fromString(address);
268
+ return {
269
+ prefix: parsed.prefix,
270
+ version: parsed.version,
271
+ payload: parsed.payload
272
+ };
273
+ };
274
+ var getNetworkFromAddress = (address) => {
275
+ const parsed = KaspaAddress.fromString(address);
276
+ switch (parsed.prefix) {
277
+ case KaspaPrefix.MAINNET:
278
+ return NETWORK_ID.MAINNET;
279
+ case KaspaPrefix.TESTNET:
280
+ return NETWORK_ID.TESTNET_11;
281
+ default:
282
+ return NETWORK_ID.TESTNET_11;
283
+ }
284
+ };
285
+ var signMessageWithKey = async (message, privateKeyHex) => {
286
+ await ensureWasmInitialized();
287
+ const privateKey = new PrivateKey(privateKeyHex);
288
+ return wasmSignMessage({
289
+ message,
290
+ privateKey
291
+ });
292
+ };
293
+ var signTransaction = async (transaction, privateKeys, verifySig = true) => {
294
+ await ensureWasmInitialized();
295
+ const keys = privateKeys.map((k) => typeof k === "string" ? new PrivateKey(k) : k);
296
+ return wasmSignTransaction(transaction, keys, verifySig);
297
+ };
298
+ var kasStringToSompi = (kasString) => {
299
+ const result = wasmKaspaToSompi(kasString);
300
+ if (result === void 0) {
301
+ throw new Error(ERROR_MESSAGES.INVALID_AMOUNT);
302
+ }
303
+ return result;
304
+ };
305
+ var sompiToKasString = (sompi) => {
306
+ return wasmSompiToKaspaString(sompi);
307
+ };
308
+ var sompiToKasStringWithSuffix = (sompi, network) => {
309
+ return wasmSompiToKaspaStringWithSuffix(sompi, getNetworkType(network));
310
+ };
311
+ var sompiToKas = (sompi) => {
312
+ return Number(sompi) / Number(SOMPI_PER_KAS);
313
+ };
314
+ var kasToSompi = (kas) => {
315
+ return BigInt(Math.floor(kas * Number(SOMPI_PER_KAS)));
316
+ };
317
+ var formatKas = (sompi, decimals = 8) => {
318
+ const kas = sompiToKas(sompi);
319
+ return kas.toFixed(decimals).replace(/\.?0+$/, "");
320
+ };
321
+ var parseKas = kasStringToSompi;
322
+ var computeTransactionHash = (tx) => {
323
+ const txId = tx.id;
324
+ if (!txId || typeof txId !== "string") {
325
+ throw new Error("Invalid transaction: missing transaction ID");
326
+ }
327
+ const hashBytes = hexToUint8Array(txId);
328
+ if (hashBytes.length !== 32) {
329
+ throw new Error(`Invalid transaction hash length: expected 32 bytes, got ${hashBytes.length}`);
330
+ }
331
+ return hashBytes;
332
+ };
333
+
334
+ // src/webauthn.ts
335
+ import {
336
+ startRegistration,
337
+ startAuthentication,
338
+ browserSupportsWebAuthn
339
+ } from "@simplewebauthn/browser";
340
+
341
+ // src/cose-parser.ts
342
+ import { decode } from "cbor-x";
343
+ var logger2 = createLogger("COSEParser");
344
+ function extractPublicKeyFromAttestation(attestationObjectBase64) {
345
+ logger2.info("[COSEParser] Extracting public key from attestation object");
346
+ try {
347
+ const attestationBuffer = base64urlToUint8Array(attestationObjectBase64);
348
+ logger2.info("[COSEParser] Attestation buffer length:", attestationBuffer.length);
349
+ const attestation = decode(attestationBuffer);
350
+ logger2.info("[COSEParser] Attestation decoded:", {
351
+ fmt: attestation.fmt,
352
+ authDataLength: attestation.authData?.length
353
+ });
354
+ if (!attestation.authData) {
355
+ throw new Error("Attestation object missing authData");
356
+ }
357
+ const publicKey = parseAuthenticatorData(attestation.authData);
358
+ logger2.info("[COSEParser] Public key extracted:", {
359
+ length: publicKey.length,
360
+ prefix: publicKey[0]
361
+ });
362
+ return publicKey;
363
+ } catch (error) {
364
+ logger2.error("[COSEParser] Failed to extract public key:", error);
365
+ if (error instanceof Error) {
366
+ throw new Error(`Failed to extract public key: ${error.message}`);
367
+ }
368
+ throw new Error("Failed to extract public key from attestation object");
369
+ }
370
+ }
371
+ function parseAuthenticatorData(authData) {
372
+ logger2.info("[COSEParser] Parsing authenticator data:", {
373
+ length: authData.length
374
+ });
375
+ if (authData.length < 37) {
376
+ throw new Error(`Authenticator data too short: ${authData.length} bytes`);
377
+ }
378
+ const flags = authData[32];
379
+ const attestedCredentialDataIncluded = (flags & 64) !== 0;
380
+ logger2.info("[COSEParser] Flags:", {
381
+ raw: flags.toString(2).padStart(8, "0"),
382
+ attestedCredentialData: attestedCredentialDataIncluded
383
+ });
384
+ if (!attestedCredentialDataIncluded) {
385
+ throw new Error("Attested credential data not included in authenticator data");
386
+ }
387
+ let offset = 37;
388
+ let publicKeyOffset = 0;
389
+ let credIdLength = 0;
390
+ if (authData.length >= 55) {
391
+ offset = 53;
392
+ const view = new DataView(authData.buffer, authData.byteOffset + offset, 2);
393
+ credIdLength = view.getUint16(0, false);
394
+ logger2.info("[COSEParser] Attempting parse WITH AAGUID - Credential ID length:", credIdLength);
395
+ if (credIdLength > 0 && credIdLength <= 1024) {
396
+ publicKeyOffset = 55 + credIdLength;
397
+ if (authData.length > publicKeyOffset) {
398
+ logger2.info("[COSEParser] Successfully parsed with AAGUID");
399
+ } else {
400
+ logger2.info("[COSEParser] AAGUID parse failed, trying without AAGUID");
401
+ offset = 37;
402
+ }
403
+ } else {
404
+ logger2.info("[COSEParser] Unreasonable cred ID length with AAGUID, trying without");
405
+ offset = 37;
406
+ }
407
+ }
408
+ if (offset === 37) {
409
+ if (authData.length < 39) {
410
+ throw new Error("Authenticator data too short for credential ID length");
411
+ }
412
+ const view = new DataView(authData.buffer, authData.byteOffset + offset, 2);
413
+ credIdLength = view.getUint16(0, false);
414
+ logger2.info("[COSEParser] Attempting parse WITHOUT AAGUID - Credential ID length:", credIdLength);
415
+ if (credIdLength > 1024) {
416
+ throw new Error(`Credential ID length too large: ${credIdLength} bytes`);
417
+ }
418
+ publicKeyOffset = 39 + credIdLength;
419
+ }
420
+ logger2.info("[COSEParser] Offset calculation:", {
421
+ authDataLength: authData.length,
422
+ credIdLength,
423
+ publicKeyOffset,
424
+ remainingBytes: authData.length - publicKeyOffset
425
+ });
426
+ if (authData.length <= publicKeyOffset) {
427
+ throw new Error(
428
+ `Authenticator data missing COSE public key (authData length: ${authData.length}, expected offset: ${publicKeyOffset})`
429
+ );
430
+ }
431
+ const coseKey = authData.slice(publicKeyOffset);
432
+ logger2.info("[COSEParser] COSE key length:", coseKey.length);
433
+ return parseCOSEPublicKey(coseKey);
434
+ }
435
+ function parseCOSEPublicKey(coseKey) {
436
+ logger2.info("[COSEParser] Parsing COSE public key");
437
+ try {
438
+ const key = decode(coseKey);
439
+ logger2.info("[COSEParser] COSE key decoded:", {
440
+ kty: key[1],
441
+ alg: key[3],
442
+ crv: key[-1],
443
+ hasX: !!key[-2],
444
+ hasY: !!key[-3]
445
+ });
446
+ const x = key[-2];
447
+ const y = key[-3];
448
+ if (!x || !y) {
449
+ throw new Error("COSE key missing x or y coordinate");
450
+ }
451
+ if (x.length !== 32 || y.length !== 32) {
452
+ throw new Error(`Invalid coordinate length: x=${x.length}, y=${y.length}`);
453
+ }
454
+ const publicKey = new Uint8Array(65);
455
+ publicKey[0] = 4;
456
+ publicKey.set(x, 1);
457
+ publicKey.set(y, 33);
458
+ logger2.info("[COSEParser] Public key constructed successfully");
459
+ return publicKey;
460
+ } catch (error) {
461
+ logger2.error("[COSEParser] Failed to parse COSE key:", error);
462
+ if (error instanceof Error) {
463
+ throw new Error(`Failed to parse COSE public key: ${error.message}`);
464
+ }
465
+ throw new Error("Failed to parse COSE public key");
466
+ }
467
+ }
468
+
469
+ // src/webauthn.ts
470
+ var logger3 = createLogger("WebAuthn");
471
+ var isWebAuthnSupported = () => {
472
+ return browserSupportsWebAuthn();
473
+ };
474
+ var registerPasskey = async (walletName) => {
475
+ logger3.info("[WebAuthn] registerPasskey() called with name:", walletName);
476
+ if (!isWebAuthnSupported()) {
477
+ logger3.error("[WebAuthn] WebAuthn not supported");
478
+ return {
479
+ success: false,
480
+ error: ERROR_MESSAGES.WEBAUTHN_NOT_SUPPORTED
481
+ };
482
+ }
483
+ try {
484
+ logger3.info("[WebAuthn] Generating challenge...");
485
+ const challenge = generateRandomBytes(32);
486
+ const challengeBase64 = uint8ArrayToBase64(challenge);
487
+ const userId = generateRandomBytes(32);
488
+ const userIdBase64 = uint8ArrayToBase64(userId);
489
+ const registrationOptions = {
490
+ challenge: challengeBase64,
491
+ rp: {
492
+ name: WEBAUTHN_RP_NAME,
493
+ id: WEBAUTHN_RP_ID
494
+ },
495
+ user: {
496
+ id: userIdBase64,
497
+ name: walletName,
498
+ displayName: walletName
499
+ },
500
+ pubKeyCredParams: WEBAUTHN_PUB_KEY_CRED_PARAMS,
501
+ timeout: WEBAUTHN_TIMEOUT_MS,
502
+ authenticatorSelection: {
503
+ authenticatorAttachment: WEBAUTHN_AUTHENTICATOR_ATTACHMENT,
504
+ userVerification: WEBAUTHN_USER_VERIFICATION,
505
+ residentKey: "required",
506
+ requireResidentKey: true
507
+ },
508
+ attestation: "none"
509
+ };
510
+ logger3.info("[WebAuthn] Registration options:", {
511
+ rpName: registrationOptions.rp.name,
512
+ rpId: registrationOptions.rp.id,
513
+ userName: registrationOptions.user.name,
514
+ timeout: registrationOptions.timeout
515
+ });
516
+ logger3.info("[WebAuthn] Starting WebAuthn registration ceremony...");
517
+ const credential = await startRegistration({ optionsJSON: registrationOptions });
518
+ logger3.info("[WebAuthn] Registration ceremony completed, credential received:", {
519
+ id: credential.id,
520
+ type: credential.type,
521
+ hasPublicKey: !!credential.response.publicKey,
522
+ hasAttestationObject: !!credential.response.attestationObject,
523
+ rawId: credential.rawId
524
+ });
525
+ const publicKeyBytes = credential.response.publicKey ? credential.response.publicKey : "";
526
+ logger3.info("[WebAuthn] Extracting passkey public key from attestation object...");
527
+ let passkeyPublicKey;
528
+ try {
529
+ passkeyPublicKey = extractPublicKeyFromAttestation(credential.response.attestationObject);
530
+ logger3.info("[WebAuthn] Passkey public key extracted successfully:", {
531
+ length: passkeyPublicKey.length,
532
+ prefix: passkeyPublicKey[0]
533
+ // Should be 0x04 for uncompressed
534
+ });
535
+ } catch (error) {
536
+ logger3.error("[WebAuthn] Failed to extract passkey public key:", error);
537
+ }
538
+ const storedCredential = {
539
+ id: credential.id,
540
+ rawId: base64urlToUint8Array(credential.rawId),
541
+ publicKey: publicKeyBytes,
542
+ counter: 0,
543
+ transports: credential.response.transports
544
+ };
545
+ logger3.info("[WebAuthn] Passkey registered successfully");
546
+ return {
547
+ success: true,
548
+ credential: storedCredential,
549
+ userId,
550
+ // Included for WebAuthn spec compliance (not used for key derivation)
551
+ passkeyPublicKey
552
+ // Used for deterministic key derivation
553
+ };
554
+ } catch (error) {
555
+ logger3.error("[WebAuthn] Registration failed with error:", error);
556
+ if (error instanceof Error && error.name === "NotAllowedError") {
557
+ logger3.warn("[WebAuthn] User cancelled the registration");
558
+ return {
559
+ success: false,
560
+ error: ERROR_MESSAGES.USER_CANCELLED
561
+ };
562
+ }
563
+ if (error instanceof Error) {
564
+ logger3.error("[WebAuthn] Error details:", {
565
+ name: error.name,
566
+ message: error.message,
567
+ stack: error.stack
568
+ });
569
+ }
570
+ return {
571
+ success: false,
572
+ error: ERROR_MESSAGES.PASSKEY_REGISTRATION_FAILED
573
+ };
574
+ }
575
+ };
576
+ var authenticateWithPasskey = async (credentialId) => {
577
+ logger3.info("[WebAuthn] authenticateWithPasskey() called with credentialId:", credentialId);
578
+ if (!isWebAuthnSupported()) {
579
+ logger3.error("[WebAuthn] WebAuthn not supported");
580
+ return {
581
+ success: false,
582
+ error: ERROR_MESSAGES.WEBAUTHN_NOT_SUPPORTED
583
+ };
584
+ }
585
+ try {
586
+ logger3.info("[WebAuthn] Generating challenge for authentication...");
587
+ const challenge = generateRandomBytes(32);
588
+ const challengeBase64 = uint8ArrayToBase64(challenge);
589
+ const allowCredentials = credentialId ? [
590
+ {
591
+ id: credentialId,
592
+ type: "public-key"
593
+ }
594
+ ] : void 0;
595
+ const authenticationOptions = {
596
+ challenge: challengeBase64,
597
+ rpId: WEBAUTHN_RP_ID,
598
+ timeout: WEBAUTHN_TIMEOUT_MS,
599
+ userVerification: WEBAUTHN_USER_VERIFICATION,
600
+ allowCredentials
601
+ };
602
+ logger3.info("[WebAuthn] Authentication options:", {
603
+ rpId: authenticationOptions.rpId,
604
+ timeout: authenticationOptions.timeout,
605
+ hasAllowedCredentials: !!allowCredentials
606
+ });
607
+ logger3.info("[WebAuthn] Starting WebAuthn authentication ceremony...");
608
+ const assertion = await startAuthentication({ optionsJSON: authenticationOptions });
609
+ logger3.info("[WebAuthn] Authentication ceremony completed, assertion received");
610
+ const authenticatorData = base64urlToUint8Array(assertion.response.authenticatorData);
611
+ const clientDataJSON = base64urlToUint8Array(assertion.response.clientDataJSON);
612
+ const signature = base64urlToUint8Array(assertion.response.signature);
613
+ logger3.info("[WebAuthn] Authentication successful");
614
+ return {
615
+ success: true,
616
+ authenticatorData,
617
+ clientDataJSON,
618
+ signature
619
+ };
620
+ } catch (error) {
621
+ logger3.error("[WebAuthn] Authentication failed with error:", error);
622
+ if (error instanceof Error && error.name === "NotAllowedError") {
623
+ logger3.warn("[WebAuthn] User cancelled the authentication");
624
+ return {
625
+ success: false,
626
+ error: ERROR_MESSAGES.USER_CANCELLED
627
+ };
628
+ }
629
+ if (error instanceof Error) {
630
+ logger3.error("[WebAuthn] Error details:", {
631
+ name: error.name,
632
+ message: error.message,
633
+ stack: error.stack
634
+ });
635
+ }
636
+ return {
637
+ success: false,
638
+ error: ERROR_MESSAGES.PASSKEY_AUTHENTICATION_FAILED
639
+ };
640
+ }
641
+ };
642
+ var authenticateWithChallenge = async (credentialId, challenge) => {
643
+ logger3.info("[WebAuthn] authenticateWithChallenge() called");
644
+ logger3.info("[WebAuthn] Challenge length:", challenge.length, "bytes");
645
+ if (!isWebAuthnSupported()) {
646
+ logger3.error("[WebAuthn] WebAuthn not supported");
647
+ return {
648
+ success: false,
649
+ error: ERROR_MESSAGES.WEBAUTHN_NOT_SUPPORTED
650
+ };
651
+ }
652
+ if (!credentialId) {
653
+ logger3.error("[WebAuthn] No credential ID provided");
654
+ return {
655
+ success: false,
656
+ error: "Credential ID is required for authentication"
657
+ };
658
+ }
659
+ try {
660
+ const challengeBase64 = uint8ArrayToBase64(challenge);
661
+ const authenticationOptions = {
662
+ challenge: challengeBase64,
663
+ rpId: WEBAUTHN_RP_ID,
664
+ timeout: WEBAUTHN_TIMEOUT_MS,
665
+ userVerification: WEBAUTHN_USER_VERIFICATION,
666
+ allowCredentials: [
667
+ {
668
+ id: credentialId,
669
+ type: "public-key"
670
+ }
671
+ ]
672
+ };
673
+ logger3.info("[WebAuthn] Authentication options with custom challenge:", {
674
+ rpId: authenticationOptions.rpId,
675
+ timeout: authenticationOptions.timeout,
676
+ challengePreview: challengeBase64.substring(0, 16) + "..."
677
+ });
678
+ logger3.info("[WebAuthn] Starting WebAuthn authentication ceremony with custom challenge...");
679
+ const assertion = await startAuthentication({ optionsJSON: authenticationOptions });
680
+ logger3.info("[WebAuthn] Authentication ceremony completed, assertion received");
681
+ const authenticatorData = base64urlToUint8Array(assertion.response.authenticatorData);
682
+ const clientDataJSON = base64urlToUint8Array(assertion.response.clientDataJSON);
683
+ const signature = base64urlToUint8Array(assertion.response.signature);
684
+ logger3.info("[WebAuthn] Authentication with custom challenge successful");
685
+ return {
686
+ success: true,
687
+ authenticatorData,
688
+ clientDataJSON,
689
+ signature
690
+ };
691
+ } catch (error) {
692
+ logger3.error("[WebAuthn] Authentication with custom challenge failed:", error);
693
+ if (error instanceof Error && error.name === "NotAllowedError") {
694
+ logger3.warn("[WebAuthn] User cancelled the authentication");
695
+ return {
696
+ success: false,
697
+ error: ERROR_MESSAGES.USER_CANCELLED
698
+ };
699
+ }
700
+ if (error instanceof Error) {
701
+ logger3.error("[WebAuthn] Error details:", {
702
+ name: error.name,
703
+ message: error.message,
704
+ stack: error.stack
705
+ });
706
+ }
707
+ return {
708
+ success: false,
709
+ error: ERROR_MESSAGES.PASSKEY_AUTHENTICATION_FAILED
710
+ };
711
+ }
712
+ };
713
+
714
+ // src/keystore.ts
715
+ import { get, set, del, createStore } from "idb-keyval";
716
+ var customStore = null;
717
+ var getStore = () => {
718
+ if (!customStore) {
719
+ customStore = createStore(IDB_DATABASE_NAME, IDB_STORE_NAME);
720
+ }
721
+ return customStore;
722
+ };
723
+ var hasStoredWallet = async () => {
724
+ try {
725
+ const metadata = await get(STORAGE_KEY_METADATA, getStore());
726
+ return metadata !== void 0;
727
+ } catch {
728
+ return false;
729
+ }
730
+ };
731
+ var storeWalletMetadata = async (metadata) => {
732
+ await set(STORAGE_KEY_METADATA, metadata, getStore());
733
+ };
734
+ var getWalletMetadata = async () => {
735
+ try {
736
+ const data = await get(STORAGE_KEY_METADATA, getStore());
737
+ return data ?? null;
738
+ } catch {
739
+ return null;
740
+ }
741
+ };
742
+ var deleteWalletMetadata = async () => {
743
+ await del(STORAGE_KEY_METADATA, getStore());
744
+ };
745
+ var storeCredentialId = async (credentialId) => {
746
+ await set(STORAGE_KEY_CREDENTIAL_ID, credentialId, getStore());
747
+ };
748
+ var getCredentialId = async () => {
749
+ try {
750
+ const id = await get(STORAGE_KEY_CREDENTIAL_ID, getStore());
751
+ return id ?? null;
752
+ } catch {
753
+ return null;
754
+ }
755
+ };
756
+ var deleteCredentialId = async () => {
757
+ await del(STORAGE_KEY_CREDENTIAL_ID, getStore());
758
+ };
759
+ var clearAllData = async () => {
760
+ await Promise.all([
761
+ deleteWalletMetadata(),
762
+ deleteCredentialId()
763
+ ]);
764
+ };
765
+
766
+ // src/rpc.ts
767
+ import {
768
+ RpcClient,
769
+ Resolver
770
+ } from "@onekeyfe/kaspa-wasm";
771
+ var KaspaRpc = class {
772
+ rpc = null;
773
+ network = null;
774
+ eventHandlers = {};
775
+ /**
776
+ * Check if connected to the network
777
+ */
778
+ get isConnected() {
779
+ return this.rpc !== null;
780
+ }
781
+ /**
782
+ * Get the current network
783
+ */
784
+ get currentNetwork() {
785
+ return this.network;
786
+ }
787
+ /**
788
+ * Get the underlying RpcClient instance
789
+ * Only use for advanced operations
790
+ */
791
+ get client() {
792
+ return this.rpc;
793
+ }
794
+ /**
795
+ * Set event handlers for connection events
796
+ */
797
+ setEventHandlers(handlers) {
798
+ this.eventHandlers = { ...this.eventHandlers, ...handlers };
799
+ }
800
+ /**
801
+ * Connect to the Kaspa network
802
+ *
803
+ * @param options - Connection options
804
+ * @throws Error if connection fails
805
+ */
806
+ async connect(options) {
807
+ await ensureWasmInitialized();
808
+ const { network, url, timeout = RPC_TIMEOUT_MS } = options;
809
+ const networkType = getNetworkType(network);
810
+ if (this.rpc) {
811
+ await this.disconnect();
812
+ }
813
+ try {
814
+ if (url) {
815
+ this.rpc = new RpcClient({
816
+ url,
817
+ networkId: networkType
818
+ });
819
+ } else {
820
+ this.rpc = new RpcClient({
821
+ resolver: new Resolver(),
822
+ networkId: networkType
823
+ });
824
+ }
825
+ this.rpc.addEventListener("connect", (event) => {
826
+ this.eventHandlers.onConnect?.(event?.url || "unknown");
827
+ });
828
+ this.rpc.addEventListener("disconnect", () => {
829
+ this.eventHandlers.onDisconnect?.();
830
+ });
831
+ await Promise.race([
832
+ this.rpc.connect(),
833
+ new Promise(
834
+ (_, reject) => setTimeout(() => reject(new Error("Connection timeout")), timeout)
835
+ )
836
+ ]);
837
+ this.network = network;
838
+ } catch (error) {
839
+ this.rpc = null;
840
+ this.network = null;
841
+ const message = error instanceof Error ? error.message : String(error);
842
+ this.eventHandlers.onError?.(message);
843
+ throw new Error(`${ERROR_MESSAGES.RPC_CONNECTION_FAILED}: ${message}`);
844
+ }
845
+ }
846
+ /**
847
+ * Disconnect from the network
848
+ */
849
+ async disconnect() {
850
+ if (this.rpc) {
851
+ try {
852
+ await this.rpc.disconnect();
853
+ } catch {
854
+ }
855
+ this.rpc = null;
856
+ this.network = null;
857
+ }
858
+ }
859
+ /**
860
+ * Ensure RPC is connected, throw if not
861
+ */
862
+ ensureConnected() {
863
+ if (!this.rpc) {
864
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
865
+ }
866
+ return this.rpc;
867
+ }
868
+ /**
869
+ * Get balance for an address
870
+ *
871
+ * @param address - Kaspa address
872
+ * @returns Balance info with available, pending, and total amounts
873
+ */
874
+ async getBalance(address) {
875
+ const rpc = this.ensureConnected();
876
+ const response = await rpc.getBalanceByAddress({ address });
877
+ const available = response.balance;
878
+ return {
879
+ available,
880
+ pending: 0n,
881
+ total: available
882
+ };
883
+ }
884
+ /**
885
+ * Get UTXOs for an address
886
+ *
887
+ * @param address - Kaspa address
888
+ * @returns Array of UTXO entries
889
+ */
890
+ async getUtxos(address) {
891
+ const rpc = this.ensureConnected();
892
+ const response = await rpc.getUtxosByAddresses({ addresses: [address] });
893
+ return response.entries;
894
+ }
895
+ /**
896
+ * Get UTXOs for multiple addresses
897
+ *
898
+ * @param addresses - Array of Kaspa addresses
899
+ * @returns Array of UTXO entries
900
+ */
901
+ async getUtxosForAddresses(addresses) {
902
+ const rpc = this.ensureConnected();
903
+ const response = await rpc.getUtxosByAddresses({ addresses });
904
+ return response.entries;
905
+ }
906
+ /**
907
+ * Submit a signed transaction to the network
908
+ *
909
+ * @param transaction - Signed transaction object
910
+ * @param allowOrphan - Allow orphan transactions (default: false)
911
+ * @returns Transaction ID
912
+ */
913
+ async submitTransaction(transaction, allowOrphan = false) {
914
+ const rpc = this.ensureConnected();
915
+ const response = await rpc.submitTransaction({
916
+ transaction,
917
+ allowOrphan
918
+ });
919
+ return response.transactionId;
920
+ }
921
+ /**
922
+ * Get the current block count
923
+ */
924
+ async getBlockCount() {
925
+ const rpc = this.ensureConnected();
926
+ const response = await rpc.getBlockCount();
927
+ return response.blockCount;
928
+ }
929
+ /**
930
+ * Get network info (DAG info)
931
+ */
932
+ async getNetworkInfo() {
933
+ const rpc = this.ensureConnected();
934
+ const response = await rpc.getBlockDagInfo();
935
+ return {
936
+ network: response.network,
937
+ blockCount: response.blockCount,
938
+ headerCount: response.headerCount,
939
+ tipHashes: response.tipHashes,
940
+ difficulty: response.difficulty,
941
+ pastMedianTime: response.pastMedianTime,
942
+ virtualParentHashes: response.virtualParentHashes,
943
+ pruningPointHash: response.pruningPointHash,
944
+ virtualDaaScore: response.virtualDaaScore,
945
+ sink: response.sink
946
+ };
947
+ }
948
+ /**
949
+ * Get the current server info
950
+ */
951
+ async getServerInfo() {
952
+ const rpc = this.ensureConnected();
953
+ const response = await rpc.getServerInfo();
954
+ return {
955
+ serverVersion: response.serverVersion,
956
+ rpcApiVersion: response.rpcApiVersion,
957
+ isSynced: response.isSynced,
958
+ hasUtxoIndex: response.hasUtxoIndex
959
+ };
960
+ }
961
+ };
962
+ var defaultRpcInstance = null;
963
+ var getDefaultRpc = () => {
964
+ if (!defaultRpcInstance) {
965
+ defaultRpcInstance = new KaspaRpc();
966
+ }
967
+ return defaultRpcInstance;
968
+ };
969
+ var resetDefaultRpc = async () => {
970
+ if (defaultRpcInstance) {
971
+ await defaultRpcInstance.disconnect();
972
+ defaultRpcInstance = null;
973
+ }
974
+ };
975
+
976
+ // src/transaction.ts
977
+ import {
978
+ createTransactions,
979
+ estimateTransactions,
980
+ Generator
981
+ } from "@onekeyfe/kaspa-wasm";
982
+ import { PendingTransaction as PendingTransaction2, Generator as Generator2 } from "@onekeyfe/kaspa-wasm";
983
+ var buildTransactions = async (utxos, outputs, changeAddress, priorityFee = DEFAULT_PRIORITY_FEE_SOMPI, network) => {
984
+ await ensureWasmInitialized();
985
+ const settings = {
986
+ outputs,
987
+ changeAddress,
988
+ priorityFee,
989
+ entries: utxos,
990
+ networkId: getNetworkType(network)
991
+ };
992
+ const result = await createTransactions(settings);
993
+ return {
994
+ transactions: result.transactions,
995
+ summary: {
996
+ transactionCount: result.summary.transactions,
997
+ fees: result.summary.fees,
998
+ utxoCount: result.summary.utxos,
999
+ finalAmount: result.summary.finalAmount
1000
+ }
1001
+ };
1002
+ };
1003
+ var estimateFee = async (utxos, outputs, changeAddress, priorityFee = DEFAULT_PRIORITY_FEE_SOMPI, network) => {
1004
+ await ensureWasmInitialized();
1005
+ const settings = {
1006
+ outputs,
1007
+ changeAddress,
1008
+ priorityFee,
1009
+ entries: utxos,
1010
+ networkId: getNetworkType(network)
1011
+ };
1012
+ const summary = await estimateTransactions(settings);
1013
+ return {
1014
+ transactionCount: summary.transactions,
1015
+ fees: summary.fees,
1016
+ utxoCount: summary.utxos,
1017
+ finalAmount: summary.finalAmount ?? 0n
1018
+ };
1019
+ };
1020
+ var signTransactions = async (transactions, privateKeyHex) => {
1021
+ await ensureWasmInitialized();
1022
+ const privateKey = await createPrivateKey(privateKeyHex);
1023
+ for (const tx of transactions) {
1024
+ tx.sign([privateKey]);
1025
+ }
1026
+ return transactions;
1027
+ };
1028
+ var submitTransactions = async (transactions, rpc) => {
1029
+ if (!rpc.client) {
1030
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1031
+ }
1032
+ const transactionIds = [];
1033
+ for (const tx of transactions) {
1034
+ const txId = await tx.submit(rpc.client);
1035
+ transactionIds.push(txId);
1036
+ }
1037
+ return transactionIds;
1038
+ };
1039
+ var sendTransaction = async (options, utxos, changeAddress, privateKeyHex, rpc, network) => {
1040
+ await ensureWasmInitialized();
1041
+ const { to, amount, priorityFee = DEFAULT_PRIORITY_FEE_SOMPI } = options;
1042
+ if (!rpc.isConnected) {
1043
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1044
+ }
1045
+ const outputs = [{ address: to, amount }];
1046
+ const { transactions, summary } = await buildTransactions(
1047
+ utxos,
1048
+ outputs,
1049
+ changeAddress,
1050
+ priorityFee,
1051
+ network
1052
+ );
1053
+ if (transactions.length === 0) {
1054
+ throw new Error(ERROR_MESSAGES.TRANSACTION_FAILED);
1055
+ }
1056
+ const signedTransactions = await signTransactions(transactions, privateKeyHex);
1057
+ const transactionIds = await submitTransactions(signedTransactions, rpc);
1058
+ return {
1059
+ transactionId: transactionIds[0],
1060
+ amount,
1061
+ fee: summary.fees
1062
+ };
1063
+ };
1064
+ var createGenerator = async (settings) => {
1065
+ await ensureWasmInitialized();
1066
+ return new Generator(settings);
1067
+ };
1068
+
1069
+ // src/wallet.ts
1070
+ var logger4 = createLogger("PasskeyWallet");
1071
+ var PasskeyWallet = class _PasskeyWallet {
1072
+ privateKeyHex;
1073
+ publicKeyHex;
1074
+ address;
1075
+ network;
1076
+ eventHandlers = /* @__PURE__ */ new Set();
1077
+ rpc;
1078
+ credentialId = null;
1079
+ passkeyPublicKey = null;
1080
+ constructor(privateKeyHex, publicKeyHex, address, network, credentialId, passkeyPublicKey) {
1081
+ this.privateKeyHex = privateKeyHex;
1082
+ this.publicKeyHex = publicKeyHex;
1083
+ this.address = address;
1084
+ this.network = network;
1085
+ this.credentialId = credentialId ?? null;
1086
+ this.passkeyPublicKey = passkeyPublicKey ?? null;
1087
+ this.rpc = new KaspaRpc();
1088
+ this.rpc.setEventHandlers({
1089
+ onConnect: (url) => {
1090
+ logger4.info(`Connected to Kaspa network: ${url}`);
1091
+ },
1092
+ onDisconnect: () => {
1093
+ logger4.info("Disconnected from Kaspa network");
1094
+ },
1095
+ onError: (error) => {
1096
+ logger4.error("RPC error:", error);
1097
+ }
1098
+ });
1099
+ }
1100
+ // ===========================================================================
1101
+ // Static Factory Methods
1102
+ // ===========================================================================
1103
+ /**
1104
+ * Check if WebAuthn/passkeys are supported in the current browser
1105
+ */
1106
+ static isSupported() {
1107
+ return isWebAuthnSupported();
1108
+ }
1109
+ /**
1110
+ * Check if a wallet already exists in storage
1111
+ */
1112
+ static async exists() {
1113
+ return hasStoredWallet();
1114
+ }
1115
+ /**
1116
+ * Create a new passkey-protected wallet
1117
+ *
1118
+ * @param options - Configuration options
1119
+ * @returns Result containing the wallet instance or error
1120
+ */
1121
+ static async create(options = {}) {
1122
+ logger4.info("create() called with options:", options);
1123
+ const { name = "KasFlow Wallet", network = DEFAULT_NETWORK } = options;
1124
+ logger4.debug("Checking if wallet already exists...");
1125
+ if (await hasStoredWallet()) {
1126
+ logger4.warn("Wallet already exists in storage");
1127
+ return {
1128
+ success: false,
1129
+ error: ERROR_MESSAGES.WALLET_ALREADY_EXISTS
1130
+ };
1131
+ }
1132
+ logger4.debug("Checking WebAuthn support...");
1133
+ if (!isWebAuthnSupported()) {
1134
+ logger4.error("WebAuthn not supported");
1135
+ return {
1136
+ success: false,
1137
+ error: ERROR_MESSAGES.WEBAUTHN_NOT_SUPPORTED
1138
+ };
1139
+ }
1140
+ logger4.debug("WebAuthn is supported");
1141
+ logger4.info("Starting passkey registration...");
1142
+ const registration = await registerPasskey(name);
1143
+ logger4.debug("Registration result:", {
1144
+ success: registration.success,
1145
+ hasCredential: !!registration.credential,
1146
+ hasPasskeyPublicKey: !!registration.passkeyPublicKey,
1147
+ error: registration.error
1148
+ });
1149
+ if (!registration.success || !registration.credential || !registration.passkeyPublicKey) {
1150
+ logger4.error("Passkey registration failed:", registration.error);
1151
+ return {
1152
+ success: false,
1153
+ error: registration.error ?? ERROR_MESSAGES.PASSKEY_REGISTRATION_FAILED
1154
+ };
1155
+ }
1156
+ logger4.info("Deriving Kaspa keys from passkey public key...");
1157
+ const { privateKeyHex } = deriveKaspaKeysFromPasskey(registration.passkeyPublicKey);
1158
+ logger4.debug("Getting public key...");
1159
+ const publicKeyHex = await getPublicKeyHex(privateKeyHex);
1160
+ logger4.info("Generating addresses for all networks...");
1161
+ const addresses = {
1162
+ [NETWORK_ID.MAINNET]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.MAINNET),
1163
+ [NETWORK_ID.TESTNET_10]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.TESTNET_10),
1164
+ [NETWORK_ID.TESTNET_11]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.TESTNET_11)
1165
+ };
1166
+ const address = addresses[network];
1167
+ logger4.info("Wallet created for all networks:", {
1168
+ primaryNetwork: network,
1169
+ mainnetAddress: addresses[NETWORK_ID.MAINNET],
1170
+ testnet10Address: addresses[NETWORK_ID.TESTNET_10],
1171
+ testnet11Address: addresses[NETWORK_ID.TESTNET_11],
1172
+ publicKeyPrefix: publicKeyHex.substring(0, 10) + "..."
1173
+ });
1174
+ const passkeyPublicKeyBase64 = uint8ArrayToBase64(registration.passkeyPublicKey);
1175
+ logger4.debug("Storing wallet metadata...");
1176
+ await storeWalletMetadata({
1177
+ passkeyPublicKey: passkeyPublicKeyBase64,
1178
+ addresses,
1179
+ primaryNetwork: network,
1180
+ createdAt: Date.now(),
1181
+ // Backward compatibility fields
1182
+ address,
1183
+ network
1184
+ });
1185
+ await storeCredentialId(registration.credential.id);
1186
+ logger4.debug("Wallet metadata stored successfully");
1187
+ logger4.debug("Creating wallet instance...");
1188
+ const wallet = new _PasskeyWallet(
1189
+ privateKeyHex,
1190
+ publicKeyHex,
1191
+ address,
1192
+ network,
1193
+ registration.credential.id,
1194
+ registration.passkeyPublicKey
1195
+ );
1196
+ wallet.emit({ type: "connected", address });
1197
+ logger4.info("Wallet created successfully!");
1198
+ return {
1199
+ success: true,
1200
+ data: wallet
1201
+ };
1202
+ }
1203
+ /**
1204
+ * Unlock an existing wallet using passkey authentication
1205
+ *
1206
+ * @param options - Configuration options
1207
+ * @returns Result containing the wallet instance or error
1208
+ */
1209
+ static async unlock(options = {}) {
1210
+ logger4.info("unlock() called with options:", options);
1211
+ logger4.debug("Checking if wallet exists in storage...");
1212
+ if (!await hasStoredWallet()) {
1213
+ logger4.error("No wallet found in storage");
1214
+ return {
1215
+ success: false,
1216
+ error: ERROR_MESSAGES.WALLET_NOT_FOUND
1217
+ };
1218
+ }
1219
+ logger4.debug("Wallet exists in storage");
1220
+ logger4.debug("Getting stored credential ID and wallet metadata...");
1221
+ const credentialId = await getCredentialId();
1222
+ const metadata = await getWalletMetadata();
1223
+ logger4.debug("Stored credential ID:", credentialId);
1224
+ logger4.debug("Wallet metadata:", metadata ? "found" : "not found");
1225
+ if (!metadata || !metadata.passkeyPublicKey) {
1226
+ logger4.error("No passkey public key found in metadata");
1227
+ return {
1228
+ success: false,
1229
+ error: ERROR_MESSAGES.WALLET_NOT_FOUND
1230
+ };
1231
+ }
1232
+ logger4.info("Authenticating with passkey...");
1233
+ const auth = await authenticateWithPasskey(credentialId ?? void 0);
1234
+ logger4.debug("Authentication result:", {
1235
+ success: auth.success,
1236
+ error: auth.error
1237
+ });
1238
+ if (!auth.success) {
1239
+ logger4.error("Passkey authentication failed:", auth.error);
1240
+ return {
1241
+ success: false,
1242
+ error: auth.error ?? ERROR_MESSAGES.PASSKEY_AUTHENTICATION_FAILED
1243
+ };
1244
+ }
1245
+ logger4.info("Deriving keys from passkey public key...");
1246
+ const passkeyPublicKey = base64ToUint8Array(metadata.passkeyPublicKey);
1247
+ const { privateKeyHex } = deriveKaspaKeysFromPasskey(passkeyPublicKey);
1248
+ const publicKeyHex = await getPublicKeyHex(privateKeyHex);
1249
+ let currentMetadata = metadata;
1250
+ if (!metadata.addresses && metadata.address && metadata.network) {
1251
+ logger4.info("Migrating wallet to multi-network format...");
1252
+ const addresses = {
1253
+ [NETWORK_ID.MAINNET]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.MAINNET),
1254
+ [NETWORK_ID.TESTNET_10]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.TESTNET_10),
1255
+ [NETWORK_ID.TESTNET_11]: await getAddressFromPrivateKey(privateKeyHex, NETWORK_ID.TESTNET_11)
1256
+ };
1257
+ if (addresses[metadata.network] !== metadata.address) {
1258
+ logger4.error("Migration validation failed", {
1259
+ expectedAddress: metadata.address,
1260
+ derivedAddress: addresses[metadata.network],
1261
+ network: metadata.network
1262
+ });
1263
+ return {
1264
+ success: false,
1265
+ error: "Failed to migrate wallet to multi-network format"
1266
+ };
1267
+ }
1268
+ currentMetadata = {
1269
+ passkeyPublicKey: metadata.passkeyPublicKey,
1270
+ addresses,
1271
+ primaryNetwork: metadata.network,
1272
+ createdAt: metadata.createdAt,
1273
+ // Keep old fields for rollback safety
1274
+ address: metadata.address,
1275
+ network: metadata.network
1276
+ };
1277
+ await storeWalletMetadata(currentMetadata);
1278
+ logger4.info("Migration complete - wallet now supports all networks");
1279
+ }
1280
+ const network = options.network ?? currentMetadata.primaryNetwork ?? currentMetadata.network ?? NETWORK_ID.TESTNET_10;
1281
+ const address = await getAddressFromPrivateKey(privateKeyHex, network);
1282
+ const expectedAddress = currentMetadata.addresses ? currentMetadata.addresses[network] : currentMetadata.address;
1283
+ if (address !== expectedAddress) {
1284
+ logger4.error("Derived address mismatch for network", {
1285
+ network,
1286
+ expected: expectedAddress,
1287
+ derived: address
1288
+ });
1289
+ return {
1290
+ success: false,
1291
+ error: "Failed to derive correct wallet address for network"
1292
+ };
1293
+ }
1294
+ logger4.debug("Keys derived successfully, address verified for network:", network);
1295
+ const wallet = new _PasskeyWallet(
1296
+ privateKeyHex,
1297
+ publicKeyHex,
1298
+ address,
1299
+ network,
1300
+ credentialId ?? void 0,
1301
+ passkeyPublicKey
1302
+ );
1303
+ wallet.emit({ type: "connected", address });
1304
+ logger4.info("Wallet unlocked successfully!");
1305
+ return {
1306
+ success: true,
1307
+ data: wallet
1308
+ };
1309
+ }
1310
+ /**
1311
+ * Delete the wallet from storage
1312
+ * This is irreversible - make sure user has backed up their keys!
1313
+ */
1314
+ static async delete() {
1315
+ await clearAllData();
1316
+ return { success: true };
1317
+ }
1318
+ // ===========================================================================
1319
+ // Instance Methods - Basic Info
1320
+ // ===========================================================================
1321
+ /**
1322
+ * Get the wallet's Kaspa address
1323
+ */
1324
+ getAddress() {
1325
+ return this.address;
1326
+ }
1327
+ /**
1328
+ * Get the wallet's public key as hex string
1329
+ */
1330
+ getPublicKey() {
1331
+ return this.publicKeyHex;
1332
+ }
1333
+ /**
1334
+ * Get the current network
1335
+ */
1336
+ getNetwork() {
1337
+ return this.network;
1338
+ }
1339
+ /**
1340
+ * Sign a message with the wallet's private key
1341
+ * Uses Kaspa's message signing scheme
1342
+ *
1343
+ * @param message - Message to sign
1344
+ * @returns Signature as hex string
1345
+ */
1346
+ async signMessage(message) {
1347
+ return await signMessageWithKey(message, this.privateKeyHex);
1348
+ }
1349
+ /**
1350
+ * Get the private key hex (for advanced usage like transaction signing)
1351
+ * WARNING: Handle with care - this is sensitive data
1352
+ */
1353
+ getPrivateKeyHex() {
1354
+ return this.privateKeyHex;
1355
+ }
1356
+ // ===========================================================================
1357
+ // Network Connection
1358
+ // ===========================================================================
1359
+ /**
1360
+ * Check if connected to the network
1361
+ */
1362
+ get isConnected() {
1363
+ return this.rpc.isConnected;
1364
+ }
1365
+ /**
1366
+ * Get the underlying RPC client for advanced usage
1367
+ */
1368
+ getRpcClient() {
1369
+ return this.rpc;
1370
+ }
1371
+ /**
1372
+ * Connect to the Kaspa network
1373
+ *
1374
+ * @param options - Optional connection options (defaults to current network with public resolver)
1375
+ */
1376
+ async connect(options) {
1377
+ await this.rpc.connect({
1378
+ network: options?.network ?? this.network,
1379
+ url: options?.url,
1380
+ timeout: options?.timeout
1381
+ });
1382
+ }
1383
+ /**
1384
+ * Disconnect from the Kaspa network
1385
+ */
1386
+ async disconnectNetwork() {
1387
+ await this.rpc.disconnect();
1388
+ }
1389
+ /**
1390
+ * Switch to a different network without re-authentication
1391
+ * Uses pre-computed addresses from wallet creation
1392
+ *
1393
+ * @param network - Target network to switch to
1394
+ * @returns Result with success status
1395
+ */
1396
+ async switchNetwork(network) {
1397
+ logger4.info("switchNetwork() called:", { from: this.network, to: network });
1398
+ if (network === this.network) {
1399
+ logger4.info("Already on target network");
1400
+ return { success: true };
1401
+ }
1402
+ const metadata = await getWalletMetadata();
1403
+ if (!metadata || !metadata.addresses || !metadata.addresses[network]) {
1404
+ logger4.error("Address for target network not found in metadata");
1405
+ return {
1406
+ success: false,
1407
+ error: `Address for network ${network} not found. Wallet may need to be recreated.`
1408
+ };
1409
+ }
1410
+ const newAddress = await getAddressFromPrivateKey(this.privateKeyHex, network);
1411
+ if (newAddress !== metadata.addresses[network]) {
1412
+ logger4.error("Derived address does not match stored address for network", {
1413
+ network,
1414
+ stored: metadata.addresses[network],
1415
+ derived: newAddress
1416
+ });
1417
+ return {
1418
+ success: false,
1419
+ error: "Failed to validate address for network switch"
1420
+ };
1421
+ }
1422
+ if (this.isConnected) {
1423
+ logger4.debug("Disconnecting from current network...");
1424
+ await this.disconnectNetwork();
1425
+ }
1426
+ this.network = network;
1427
+ this.address = newAddress;
1428
+ logger4.info("Network switched successfully:", { network, address: newAddress });
1429
+ this.emit({ type: "connected", address: newAddress });
1430
+ return { success: true };
1431
+ }
1432
+ // ===========================================================================
1433
+ // Balance & UTXOs
1434
+ // ===========================================================================
1435
+ /**
1436
+ * Get the wallet's balance
1437
+ *
1438
+ * @returns Balance info with available, pending, and total amounts
1439
+ * @throws Error if not connected to network
1440
+ */
1441
+ async getBalance() {
1442
+ if (!this.rpc.isConnected) {
1443
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1444
+ }
1445
+ const balance = await this.rpc.getBalance(this.address);
1446
+ this.emit({ type: "balance_updated", balance });
1447
+ return balance;
1448
+ }
1449
+ // ===========================================================================
1450
+ // Transactions
1451
+ // ===========================================================================
1452
+ /**
1453
+ * Send KAS to an address
1454
+ *
1455
+ * @param options - Send options (to, amount, priorityFee)
1456
+ * @returns Send result with transaction ID and details
1457
+ * @throws Error if not connected or insufficient balance
1458
+ *
1459
+ * @example
1460
+ * ```typescript
1461
+ * const result = await wallet.send({
1462
+ * to: 'kaspatest:qz...',
1463
+ * amount: 100000000n, // 1 KAS
1464
+ * });
1465
+ * console.log('Transaction ID:', result.transactionId);
1466
+ * ```
1467
+ */
1468
+ async send(options) {
1469
+ if (!this.rpc.isConnected) {
1470
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1471
+ }
1472
+ const utxos = await this.rpc.getUtxos(this.address);
1473
+ const totalAvailable = utxos.reduce((sum, utxo) => sum + utxo.amount, 0n);
1474
+ if (totalAvailable < options.amount) {
1475
+ throw new Error(ERROR_MESSAGES.INSUFFICIENT_BALANCE);
1476
+ }
1477
+ const result = await sendTransaction(
1478
+ options,
1479
+ utxos,
1480
+ this.address,
1481
+ // Use same address for change
1482
+ this.privateKeyHex,
1483
+ this.rpc,
1484
+ this.network
1485
+ );
1486
+ this.emit({ type: "transaction_sent", txId: result.transactionId });
1487
+ return result;
1488
+ }
1489
+ /**
1490
+ * Send transaction with per-transaction passkey authentication
1491
+ *
1492
+ * **RECOMMENDED:** Use this method instead of `send()` for better security.
1493
+ *
1494
+ * This method prompts for passkey authentication for EACH transaction, using the
1495
+ * transaction hash as the WebAuthn challenge. This provides cryptographic proof
1496
+ * that the user approved THIS specific transaction.
1497
+ *
1498
+ * Benefits over `send()`:
1499
+ * - Per-transaction authentication (user approves each transaction)
1500
+ * - Transaction hash cryptographically bound to WebAuthn signature
1501
+ * - Keys derived only when needed, not stored in memory
1502
+ * - Matches standard wallet UX (MetaMask, hardware wallets)
1503
+ *
1504
+ * @param options - Send options (to, amount, priorityFee)
1505
+ * @returns Transaction result with ID and fees
1506
+ * @throws Error if authentication fails, insufficient balance, or network error
1507
+ *
1508
+ * @example
1509
+ * ```typescript
1510
+ * const result = await wallet.sendWithAuth({
1511
+ * to: 'kaspatest:qz...',
1512
+ * amount: 100000000n, // 1 KAS
1513
+ * });
1514
+ * console.log('Transaction ID:', result.transactionId);
1515
+ * ```
1516
+ */
1517
+ async sendWithAuth(options) {
1518
+ if (!this.rpc.isConnected) {
1519
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1520
+ }
1521
+ if (!this.credentialId) {
1522
+ throw new Error("Credential ID not available. Cannot authenticate transaction.");
1523
+ }
1524
+ if (!this.passkeyPublicKey) {
1525
+ throw new Error("Passkey public key not available. Cannot derive signing key.");
1526
+ }
1527
+ logger4.info("[PasskeyWallet] sendWithAuth() - Starting per-transaction auth flow");
1528
+ const utxos = await this.rpc.getUtxos(this.address);
1529
+ const totalAvailable = utxos.reduce((sum, utxo) => sum + utxo.amount, 0n);
1530
+ if (totalAvailable < options.amount) {
1531
+ throw new Error(ERROR_MESSAGES.INSUFFICIENT_BALANCE);
1532
+ }
1533
+ const outputs = [
1534
+ {
1535
+ address: options.to,
1536
+ amount: options.amount
1537
+ }
1538
+ ];
1539
+ const buildResult = await buildTransactions(
1540
+ utxos,
1541
+ outputs,
1542
+ this.address,
1543
+ options.priorityFee,
1544
+ this.network
1545
+ );
1546
+ const { transactions, summary } = buildResult;
1547
+ logger4.info("[PasskeyWallet] Built unsigned transactions:", {
1548
+ count: transactions.length,
1549
+ totalFee: summary.fees.toString()
1550
+ });
1551
+ const signedTransactions = [];
1552
+ for (let i = 0; i < transactions.length; i++) {
1553
+ const tx = transactions[i];
1554
+ logger4.info(`[PasskeyWallet] Processing transaction ${i + 1}/${transactions.length}`);
1555
+ const txHash = computeTransactionHash(tx);
1556
+ logger4.info("[PasskeyWallet] Transaction hash computed:", {
1557
+ length: txHash.length,
1558
+ previewHex: Array.from(txHash.slice(0, 8)).map((b) => b.toString(16).padStart(2, "0")).join("")
1559
+ });
1560
+ logger4.info(
1561
+ "[PasskeyWallet] Requesting passkey authentication for this transaction..."
1562
+ );
1563
+ const authResult = await authenticateWithChallenge(this.credentialId, txHash);
1564
+ if (!authResult.success) {
1565
+ throw new Error(
1566
+ authResult.error ?? "Passkey authentication failed for transaction"
1567
+ );
1568
+ }
1569
+ logger4.info("[PasskeyWallet] User authenticated - passkey signature received");
1570
+ const verified = this.verifyAuthenticationForTransaction(authResult, txHash);
1571
+ if (!verified) {
1572
+ throw new Error(
1573
+ "Authentication signature does not match transaction. Possible security issue."
1574
+ );
1575
+ }
1576
+ logger4.info("[PasskeyWallet] \u2713 WebAuthn signature verified for this transaction");
1577
+ const { privateKeyHex } = deriveKaspaKeysFromPasskey(this.passkeyPublicKey);
1578
+ logger4.info("[PasskeyWallet] Derived secp256k1 signing key for this transaction");
1579
+ await signTransactions([tx], privateKeyHex);
1580
+ logger4.info("[PasskeyWallet] Transaction signed successfully");
1581
+ signedTransactions.push(tx);
1582
+ }
1583
+ logger4.info("[PasskeyWallet] All transactions signed successfully");
1584
+ const transactionIds = await submitTransactions(signedTransactions, this.rpc);
1585
+ logger4.info("[PasskeyWallet] Transactions submitted:", transactionIds);
1586
+ this.emit({
1587
+ type: "transaction_sent",
1588
+ txId: transactionIds[0]
1589
+ });
1590
+ return {
1591
+ transactionId: transactionIds[0],
1592
+ amount: options.amount,
1593
+ fee: summary.fees
1594
+ };
1595
+ }
1596
+ /**
1597
+ * Estimate the fee for a transaction
1598
+ *
1599
+ * @param options - Send options (to, amount, priorityFee)
1600
+ * @returns Transaction estimate with fees and UTXO count
1601
+ * @throws Error if not connected
1602
+ */
1603
+ async estimateFee(options) {
1604
+ if (!this.rpc.isConnected) {
1605
+ throw new Error(ERROR_MESSAGES.RPC_NOT_CONNECTED);
1606
+ }
1607
+ const utxos = await this.rpc.getUtxos(this.address);
1608
+ return estimateFee(
1609
+ utxos,
1610
+ [{ address: options.to, amount: options.amount }],
1611
+ this.address,
1612
+ options.priorityFee,
1613
+ this.network
1614
+ );
1615
+ }
1616
+ // ===========================================================================
1617
+ // Event Handling
1618
+ // ===========================================================================
1619
+ /**
1620
+ * Subscribe to wallet events
1621
+ *
1622
+ * @param handler - Event handler function
1623
+ * @returns Unsubscribe function
1624
+ */
1625
+ on(handler) {
1626
+ this.eventHandlers.add(handler);
1627
+ return () => this.eventHandlers.delete(handler);
1628
+ }
1629
+ /**
1630
+ * Emit an event to all subscribers
1631
+ */
1632
+ emit(event) {
1633
+ this.eventHandlers.forEach((handler) => {
1634
+ try {
1635
+ handler(event);
1636
+ } catch (error) {
1637
+ logger4.error("Event handler error:", error);
1638
+ }
1639
+ });
1640
+ }
1641
+ // ===========================================================================
1642
+ // Per-Transaction Authentication Helpers
1643
+ // ===========================================================================
1644
+ /**
1645
+ * Verify that WebAuthn authentication result is for specific transaction
1646
+ *
1647
+ * This method ensures the WebAuthn signature was created for THIS specific
1648
+ * transaction by verifying that the challenge in clientDataJSON matches the
1649
+ * transaction hash.
1650
+ *
1651
+ * This prevents replay attacks where an attacker could try to reuse a signature
1652
+ * from a different transaction.
1653
+ *
1654
+ * @param authResult - Authentication result from passkey
1655
+ * @param txHash - Transaction hash that should be in the challenge
1656
+ * @returns true if verified, false otherwise
1657
+ */
1658
+ verifyAuthenticationForTransaction(authResult, txHash) {
1659
+ try {
1660
+ const clientDataText = new TextDecoder().decode(authResult.clientDataJSON);
1661
+ const clientData = JSON.parse(clientDataText);
1662
+ logger4.debug("[PasskeyWallet] Verifying clientDataJSON:", {
1663
+ type: clientData.type,
1664
+ origin: clientData.origin,
1665
+ challengePreview: clientData.challenge?.substring(0, 16)
1666
+ });
1667
+ const challengeBase64url = clientData.challenge;
1668
+ if (!challengeBase64url) {
1669
+ logger4.error("[PasskeyWallet] No challenge found in clientDataJSON");
1670
+ return false;
1671
+ }
1672
+ const challengeBytes = base64urlToUint8Array(challengeBase64url);
1673
+ if (challengeBytes.length !== txHash.length) {
1674
+ logger4.error("[PasskeyWallet] Challenge length mismatch:", {
1675
+ expected: txHash.length,
1676
+ actual: challengeBytes.length
1677
+ });
1678
+ return false;
1679
+ }
1680
+ for (let i = 0; i < txHash.length; i++) {
1681
+ if (challengeBytes[i] !== txHash[i]) {
1682
+ logger4.error("[PasskeyWallet] Challenge byte mismatch at index:", i);
1683
+ return false;
1684
+ }
1685
+ }
1686
+ logger4.info("[PasskeyWallet] \u2713 WebAuthn challenge verified = transaction hash");
1687
+ if (clientData.type !== "webauthn.get") {
1688
+ logger4.error("[PasskeyWallet] Invalid clientData type:", clientData.type);
1689
+ return false;
1690
+ }
1691
+ return true;
1692
+ } catch (error) {
1693
+ logger4.error("[PasskeyWallet] Failed to verify authentication:", error);
1694
+ return false;
1695
+ }
1696
+ }
1697
+ /**
1698
+ * Disconnect the wallet (clears in-memory keys and network connection)
1699
+ */
1700
+ async disconnect() {
1701
+ await this.rpc.disconnect();
1702
+ this.privateKeyHex = "";
1703
+ this.emit({ type: "disconnected" });
1704
+ this.eventHandlers.clear();
1705
+ }
1706
+ };
1707
+
1708
+ // src/wasm.ts
1709
+ import {
1710
+ PrivateKey as PrivateKey2,
1711
+ PublicKey as PublicKey2,
1712
+ Address as Address2,
1713
+ NetworkType as NetworkType2,
1714
+ NetworkId,
1715
+ RpcClient as RpcClient2,
1716
+ Resolver as Resolver2,
1717
+ UtxoProcessor,
1718
+ UtxoContext,
1719
+ Generator as Generator3,
1720
+ PendingTransaction as PendingTransaction3,
1721
+ Transaction,
1722
+ UtxoEntryReference as UtxoEntryReference3,
1723
+ kaspaToSompi,
1724
+ sompiToKaspaString,
1725
+ sompiToKaspaStringWithSuffix,
1726
+ signMessage,
1727
+ verifyMessage,
1728
+ signTransaction as signTransaction2,
1729
+ createAddress as createAddress2,
1730
+ payToAddressScript,
1731
+ addressFromScriptPublicKey,
1732
+ initConsolePanicHook as initConsolePanicHook2,
1733
+ initBrowserPanicHook
1734
+ } from "@onekeyfe/kaspa-wasm";
1735
+ var initDebugMode = async () => {
1736
+ const { initConsolePanicHook: initConsolePanicHook3 } = await import("@onekeyfe/kaspa-wasm");
1737
+ initConsolePanicHook3();
1738
+ };
1739
+ export {
1740
+ Address,
1741
+ DEFAULT_NETWORK,
1742
+ DEFAULT_PRIORITY_FEE_SOMPI,
1743
+ ERROR_MESSAGES,
1744
+ Generator3 as Generator,
1745
+ KaspaRpc,
1746
+ MIN_FEE_SOMPI,
1747
+ NETWORK_ID,
1748
+ NetworkType,
1749
+ PasskeyWallet,
1750
+ PendingTransaction3 as PendingTransaction,
1751
+ PrivateKey,
1752
+ PublicKey,
1753
+ Resolver2 as Resolver,
1754
+ RpcClient2 as RpcClient,
1755
+ SOMPI_PER_KAS,
1756
+ Transaction,
1757
+ UtxoContext,
1758
+ UtxoEntryReference3 as UtxoEntryReference,
1759
+ UtxoProcessor,
1760
+ authenticateWithChallenge,
1761
+ buildTransactions,
1762
+ computeTransactionHash,
1763
+ createGenerator,
1764
+ createLogger,
1765
+ createPrivateKey,
1766
+ deriveKaspaKeysFromPasskey,
1767
+ estimateFee,
1768
+ extractPublicKeyFromAttestation,
1769
+ formatKas,
1770
+ generatePrivateKey,
1771
+ getAddressFromPrivateKey,
1772
+ getDefaultRpc,
1773
+ getNetworkFromAddress,
1774
+ getNetworkIdString,
1775
+ getNetworkType,
1776
+ getPublicKeyHex,
1777
+ hexToUint8Array,
1778
+ initDebugMode,
1779
+ isValidAddress,
1780
+ isValidPrivateKey,
1781
+ isWebAuthnSupported,
1782
+ kasStringToSompi,
1783
+ kasToSompi,
1784
+ parseAddress,
1785
+ parseKas,
1786
+ publicKeyToAddress,
1787
+ resetDefaultRpc,
1788
+ sendTransaction,
1789
+ signMessageWithKey,
1790
+ signTransaction,
1791
+ signTransactions,
1792
+ sompiToKas,
1793
+ sompiToKasString,
1794
+ sompiToKasStringWithSuffix,
1795
+ submitTransactions,
1796
+ uint8ArrayToHex,
1797
+ verifyDeterminism
1798
+ };