@toon-protocol/client 0.8.0 → 0.9.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.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-5WRI5ZAA.js";
2
+
1
3
  // src/ToonClient.ts
2
4
  import { generateSecretKey as generateSecretKey2, getPublicKey } from "nostr-tools/pure";
3
5
 
@@ -186,6 +188,42 @@ function buildSettlementInfo(config) {
186
188
  };
187
189
  }
188
190
 
191
+ // src/utils/binary.ts
192
+ function toBase64(bytes) {
193
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(bytes)) {
194
+ return bytes.toString("base64");
195
+ }
196
+ let binary = "";
197
+ for (const byte of bytes) {
198
+ binary += String.fromCharCode(byte);
199
+ }
200
+ return btoa(binary);
201
+ }
202
+ function fromBase64(base64) {
203
+ if (typeof Buffer !== "undefined" && typeof Buffer.from === "function") {
204
+ return new Uint8Array(Buffer.from(base64, "base64"));
205
+ }
206
+ const binary = atob(base64);
207
+ const bytes = new Uint8Array(binary.length);
208
+ for (let i = 0; i < binary.length; i++) {
209
+ bytes[i] = binary.charCodeAt(i);
210
+ }
211
+ return bytes;
212
+ }
213
+ function toHex(bytes) {
214
+ let hex = "";
215
+ for (const byte of bytes) {
216
+ hex += byte.toString(16).padStart(2, "0");
217
+ }
218
+ return hex;
219
+ }
220
+ function encodeUtf8(str) {
221
+ return new TextEncoder().encode(str);
222
+ }
223
+ function isBase64(str) {
224
+ return /^[A-Za-z0-9+/]*={0,2}$/.test(str);
225
+ }
226
+
189
227
  // src/modes/http.ts
190
228
  import { BootstrapService, createDiscoveryTracker } from "@toon-protocol/core";
191
229
 
@@ -287,8 +325,7 @@ var HttpRuntimeClient = class {
287
325
  throw new ValidationError("Data cannot be empty");
288
326
  }
289
327
  try {
290
- Buffer.from(params.data, "base64");
291
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(params.data)) {
328
+ if (!isBase64(params.data)) {
292
329
  throw new ValidationError(
293
330
  `Data must be valid Base64 encoding: "${params.data}"`
294
331
  );
@@ -378,33 +415,474 @@ var HttpRuntimeClient = class {
378
415
  }
379
416
  };
380
417
 
381
- // src/adapters/BtpRuntimeClient.ts
382
- import { BTPClient } from "@toon-protocol/connector";
383
- var BTP_CLAIM_PROTOCOL = {
384
- NAME: "payment-channel-claim",
385
- CONTENT_TYPE: 1
418
+ // src/btp/protocol.ts
419
+ var textEncoder = new TextEncoder();
420
+ var textDecoder = new TextDecoder();
421
+ var BTPMessageType = {
422
+ RESPONSE: 1,
423
+ ERROR: 2,
424
+ MESSAGE: 6
386
425
  };
387
- function createConsoleLogger() {
388
- const noop = (..._args) => {
389
- };
390
- const logger = {
391
- level: "info",
392
- silent: noop,
393
- info: console.info.bind(console),
394
- warn: console.warn.bind(console),
395
- error: console.error.bind(console),
396
- debug: console.debug.bind(console),
397
- trace: console.debug.bind(console),
398
- fatal: console.error.bind(console),
399
- child: () => createConsoleLogger()
400
- };
401
- return logger;
402
- }
403
- var ILP_PACKET_TYPE = {
426
+ var ILPPacketType = {
404
427
  PREPARE: 12,
405
428
  FULFILL: 13,
406
429
  REJECT: 14
407
430
  };
431
+ function concat(...arrays) {
432
+ const totalLength = arrays.reduce((sum, a) => sum + a.length, 0);
433
+ const result = new Uint8Array(totalLength);
434
+ let offset = 0;
435
+ for (const a of arrays) {
436
+ result.set(a, offset);
437
+ offset += a.length;
438
+ }
439
+ return result;
440
+ }
441
+ function readUint8(buf, offset) {
442
+ if (offset >= buf.length) throw new Error("Buffer underflow reading uint8");
443
+ return buf[offset];
444
+ }
445
+ function readUint16BE(buf, offset) {
446
+ if (offset + 2 > buf.length)
447
+ throw new Error("Buffer underflow reading uint16");
448
+ return buf[offset] << 8 | buf[offset + 1];
449
+ }
450
+ function readUint32BE(buf, offset) {
451
+ if (offset + 4 > buf.length)
452
+ throw new Error("Buffer underflow reading uint32");
453
+ return (buf[offset] << 24 | buf[offset + 1] << 16 | buf[offset + 2] << 8 | buf[offset + 3]) >>> 0;
454
+ }
455
+ function writeUint8(value) {
456
+ return new Uint8Array([value]);
457
+ }
458
+ function writeUint16BE(value) {
459
+ return new Uint8Array([value >> 8 & 255, value & 255]);
460
+ }
461
+ function writeUint32BE(value) {
462
+ return new Uint8Array([
463
+ value >> 24 & 255,
464
+ value >> 16 & 255,
465
+ value >> 8 & 255,
466
+ value & 255
467
+ ]);
468
+ }
469
+ function sliceUtf8(buf, offset, length) {
470
+ return textDecoder.decode(buf.slice(offset, offset + length));
471
+ }
472
+ function encodeVarUInt(value) {
473
+ if (value >= 0n && value <= 127n) {
474
+ return new Uint8Array([Number(value)]);
475
+ }
476
+ const bytes = [];
477
+ let remaining = value;
478
+ while (remaining > 0n) {
479
+ bytes.unshift(Number(remaining & 0xffn));
480
+ remaining = remaining >> 8n;
481
+ }
482
+ return new Uint8Array([128 | bytes.length, ...bytes]);
483
+ }
484
+ function decodeVarUInt(buf, offset) {
485
+ const firstByte = readUint8(buf, offset);
486
+ if (firstByte <= 127) {
487
+ return { value: BigInt(firstByte), bytesRead: 1 };
488
+ }
489
+ const length = firstByte & 127;
490
+ if (offset + 1 + length > buf.length)
491
+ throw new Error("VarUInt buffer underflow");
492
+ let value = 0n;
493
+ for (let i = 0; i < length; i++) {
494
+ value = value << 8n | BigInt(buf[offset + 1 + i]);
495
+ }
496
+ return { value, bytesRead: 1 + length };
497
+ }
498
+ function encodeVarOctetString(data) {
499
+ return concat(encodeVarUInt(BigInt(data.length)), data);
500
+ }
501
+ function decodeVarOctetString(buf, offset) {
502
+ const { value: length, bytesRead: lenBytes } = decodeVarUInt(buf, offset);
503
+ const dataLen = Number(length);
504
+ const start = offset + lenBytes;
505
+ if (start + dataLen > buf.length)
506
+ throw new Error("VarOctetString buffer underflow");
507
+ return {
508
+ value: buf.slice(start, start + dataLen),
509
+ bytesRead: lenBytes + dataLen
510
+ };
511
+ }
512
+ function encodeGeneralizedTime(date) {
513
+ const y = date.getUTCFullYear().toString().padStart(4, "0");
514
+ const mo = (date.getUTCMonth() + 1).toString().padStart(2, "0");
515
+ const d = date.getUTCDate().toString().padStart(2, "0");
516
+ const h = date.getUTCHours().toString().padStart(2, "0");
517
+ const mi = date.getUTCMinutes().toString().padStart(2, "0");
518
+ const s = date.getUTCSeconds().toString().padStart(2, "0");
519
+ const ms = date.getUTCMilliseconds().toString().padStart(3, "0");
520
+ return textEncoder.encode(`${y}${mo}${d}${h}${mi}${s}.${ms}Z`);
521
+ }
522
+ function serializeIlpPrepare(packet) {
523
+ const condition = packet.executionCondition.length === 32 ? packet.executionCondition : new Uint8Array(32);
524
+ return concat(
525
+ writeUint8(ILPPacketType.PREPARE),
526
+ encodeVarUInt(packet.amount),
527
+ encodeGeneralizedTime(packet.expiresAt),
528
+ condition,
529
+ encodeVarOctetString(textEncoder.encode(packet.destination)),
530
+ encodeVarOctetString(packet.data)
531
+ );
532
+ }
533
+ function deserializeIlpPacket(buf) {
534
+ if (buf.length === 0) throw new Error("Empty ILP packet");
535
+ const type = buf[0];
536
+ if (type === ILPPacketType.FULFILL) return deserializeIlpFulfill(buf);
537
+ if (type === ILPPacketType.REJECT) return deserializeIlpReject(buf);
538
+ throw new Error(`Unknown ILP packet type: ${type}`);
539
+ }
540
+ function deserializeIlpFulfill(buf) {
541
+ let offset = 1;
542
+ offset += 32;
543
+ const { value: data } = decodeVarOctetString(buf, offset);
544
+ return { type: ILPPacketType.FULFILL, data };
545
+ }
546
+ function deserializeIlpReject(buf) {
547
+ let offset = 1;
548
+ const code = sliceUtf8(buf, offset, 3);
549
+ offset += 3;
550
+ const { bytesRead: tbBytes } = decodeVarOctetString(buf, offset);
551
+ offset += tbBytes;
552
+ const { value: msgBuf, bytesRead: msgBytes } = decodeVarOctetString(
553
+ buf,
554
+ offset
555
+ );
556
+ offset += msgBytes;
557
+ const message = textDecoder.decode(msgBuf);
558
+ const { value: data } = decodeVarOctetString(buf, offset);
559
+ return { type: ILPPacketType.REJECT, code, message, data };
560
+ }
561
+ function serializeBtpMessage(message) {
562
+ const parts = [
563
+ writeUint8(message.type),
564
+ writeUint32BE(message.requestId)
565
+ ];
566
+ const data = message.data;
567
+ const protocolData = data.protocolData ?? [];
568
+ parts.push(writeUint8(protocolData.length));
569
+ for (const pd of protocolData) {
570
+ const nameBytes = textEncoder.encode(pd.protocolName);
571
+ parts.push(writeUint8(nameBytes.length));
572
+ parts.push(nameBytes);
573
+ parts.push(writeUint16BE(pd.contentType));
574
+ parts.push(writeUint32BE(pd.data.length));
575
+ if (pd.data.length > 0) parts.push(pd.data);
576
+ }
577
+ const ilpPacket = data.ilpPacket ?? new Uint8Array(0);
578
+ parts.push(writeUint32BE(ilpPacket.length));
579
+ if (ilpPacket.length > 0) parts.push(ilpPacket);
580
+ return concat(...parts);
581
+ }
582
+ function parseBtpMessage(buf) {
583
+ if (buf.length < 5) throw new Error("BTP message too short");
584
+ let offset = 0;
585
+ const type = readUint8(buf, offset);
586
+ offset += 1;
587
+ const requestId = readUint32BE(buf, offset);
588
+ offset += 4;
589
+ if (type === BTPMessageType.ERROR) {
590
+ const codeLen = readUint8(buf, offset);
591
+ offset += 1;
592
+ const code = sliceUtf8(buf, offset, codeLen);
593
+ offset += codeLen;
594
+ const nameLen = readUint8(buf, offset);
595
+ offset += 1;
596
+ const name = sliceUtf8(buf, offset, nameLen);
597
+ offset += nameLen;
598
+ const taLen = readUint8(buf, offset);
599
+ offset += 1;
600
+ const triggeredAt = sliceUtf8(buf, offset, taLen);
601
+ offset += taLen;
602
+ const dataLen = readUint32BE(buf, offset);
603
+ offset += 4;
604
+ const data = buf.slice(offset, offset + dataLen);
605
+ return { type, requestId, data: { code, name, triggeredAt, data } };
606
+ }
607
+ const pdCount = readUint8(buf, offset);
608
+ offset += 1;
609
+ const protocolData = [];
610
+ for (let i = 0; i < pdCount; i++) {
611
+ const nameLen = readUint8(buf, offset);
612
+ offset += 1;
613
+ const protocolName = sliceUtf8(buf, offset, nameLen);
614
+ offset += nameLen;
615
+ const contentType = readUint16BE(buf, offset);
616
+ offset += 2;
617
+ const dataLen = readUint32BE(buf, offset);
618
+ offset += 4;
619
+ const data = buf.slice(offset, offset + dataLen);
620
+ offset += dataLen;
621
+ protocolData.push({ protocolName, contentType, data });
622
+ }
623
+ let ilpPacket;
624
+ if (offset + 4 <= buf.length) {
625
+ const ilpLen = readUint32BE(buf, offset);
626
+ offset += 4;
627
+ if (ilpLen > 0 && offset + ilpLen <= buf.length) {
628
+ ilpPacket = buf.slice(offset, offset + ilpLen);
629
+ }
630
+ }
631
+ return { type, requestId, data: { protocolData, ilpPacket } };
632
+ }
633
+
634
+ // src/btp/IsomorphicBtpClient.ts
635
+ var textEncoder2 = new TextEncoder();
636
+ var BtpConnectionError = class extends Error {
637
+ constructor(message) {
638
+ super(message);
639
+ this.name = "BtpConnectionError";
640
+ }
641
+ };
642
+ var BtpAuthError = class extends Error {
643
+ constructor(message) {
644
+ super(message);
645
+ this.name = "BtpAuthError";
646
+ }
647
+ };
648
+ var IsomorphicBtpClient = class {
649
+ ws = null;
650
+ _isConnected = false;
651
+ requestIdCounter = 0;
652
+ pendingRequests = /* @__PURE__ */ new Map();
653
+ config;
654
+ constructor(config) {
655
+ this.config = {
656
+ sendTimeoutMs: 3e4,
657
+ authTimeoutMs: 5e3,
658
+ ...config
659
+ };
660
+ }
661
+ get isConnected() {
662
+ return this._isConnected;
663
+ }
664
+ async connect() {
665
+ if (this._isConnected) return;
666
+ return new Promise((resolve, reject) => {
667
+ try {
668
+ this.ws = new WebSocket(this.config.url);
669
+ this.ws.binaryType = "arraybuffer";
670
+ } catch (err) {
671
+ reject(
672
+ new BtpConnectionError(
673
+ `Failed to create WebSocket: ${err instanceof Error ? err.message : String(err)}`
674
+ )
675
+ );
676
+ return;
677
+ }
678
+ this.ws.onopen = async () => {
679
+ try {
680
+ await this.authenticate();
681
+ this._isConnected = true;
682
+ resolve();
683
+ } catch (err) {
684
+ this._isConnected = false;
685
+ this.ws?.close();
686
+ reject(err);
687
+ }
688
+ };
689
+ this.ws.onmessage = (event) => {
690
+ this.handleMessage(event.data);
691
+ };
692
+ this.ws.onerror = () => {
693
+ reject(new BtpConnectionError("WebSocket connection error"));
694
+ };
695
+ this.ws.onclose = () => {
696
+ this._isConnected = false;
697
+ for (const [id, pending] of this.pendingRequests) {
698
+ clearTimeout(pending.timeoutId);
699
+ pending.reject(new BtpConnectionError("Connection closed"));
700
+ this.pendingRequests.delete(id);
701
+ }
702
+ };
703
+ });
704
+ }
705
+ async disconnect() {
706
+ this._isConnected = false;
707
+ if (this.ws) {
708
+ this.ws.close();
709
+ this.ws = null;
710
+ }
711
+ for (const [id, pending] of this.pendingRequests) {
712
+ clearTimeout(pending.timeoutId);
713
+ pending.reject(new BtpConnectionError("Disconnected"));
714
+ this.pendingRequests.delete(id);
715
+ }
716
+ }
717
+ /**
718
+ * Send an ILP PREPARE packet, optionally with protocol data (e.g. payment channel claim).
719
+ * Returns the ILP response (FULFILL or REJECT).
720
+ */
721
+ async sendPacket(packet, protocolData) {
722
+ if (!this._isConnected || !this.ws) {
723
+ throw new BtpConnectionError("Not connected");
724
+ }
725
+ const serializedIlp = serializeIlpPrepare(packet);
726
+ const requestId = this.nextRequestId();
727
+ const btpMessage = serializeBtpMessage({
728
+ type: BTPMessageType.MESSAGE,
729
+ requestId,
730
+ data: {
731
+ protocolData: protocolData ?? [],
732
+ ilpPacket: serializedIlp
733
+ }
734
+ });
735
+ this.ws.send(btpMessage);
736
+ let timeoutMs = this.config.sendTimeoutMs;
737
+ if (packet.expiresAt) {
738
+ const remaining = packet.expiresAt.getTime() - Date.now();
739
+ timeoutMs = Math.max(remaining - 500, 1e3);
740
+ }
741
+ return new Promise((resolve, reject) => {
742
+ const timeoutId = setTimeout(() => {
743
+ this.pendingRequests.delete(requestId);
744
+ reject(new BtpConnectionError(`Packet send timeout (${timeoutMs}ms)`));
745
+ }, timeoutMs);
746
+ this.pendingRequests.set(requestId, { resolve, reject, timeoutId });
747
+ });
748
+ }
749
+ // ─── Private ────────────────────────────────────────────────────────────
750
+ async authenticate() {
751
+ if (!this.ws) throw new BtpAuthError("WebSocket not connected");
752
+ const authData = JSON.stringify({
753
+ peerId: this.config.peerId,
754
+ secret: this.config.authToken
755
+ });
756
+ const requestId = this.nextRequestId();
757
+ const authMessage = serializeBtpMessage({
758
+ type: BTPMessageType.MESSAGE,
759
+ requestId,
760
+ data: {
761
+ protocolData: [
762
+ {
763
+ protocolName: "auth",
764
+ contentType: 0,
765
+ data: textEncoder2.encode(authData)
766
+ }
767
+ ],
768
+ ilpPacket: new Uint8Array(0)
769
+ }
770
+ });
771
+ return new Promise((resolve, reject) => {
772
+ const timeout = setTimeout(() => {
773
+ reject(new BtpAuthError("Authentication timeout"));
774
+ }, this.config.authTimeoutMs);
775
+ const originalHandler = this.ws.onmessage;
776
+ this.ws.onmessage = (event) => {
777
+ try {
778
+ const data = this.toUint8Array(event.data);
779
+ try {
780
+ const jsonStr = new TextDecoder().decode(data);
781
+ if (jsonStr.startsWith("{")) {
782
+ }
783
+ } catch {
784
+ }
785
+ const message = parseBtpMessage(data);
786
+ if (message.requestId === requestId) {
787
+ clearTimeout(timeout);
788
+ this.ws.onmessage = originalHandler;
789
+ if (message.type === BTPMessageType.ERROR) {
790
+ const errData = message.data;
791
+ reject(
792
+ new BtpAuthError(`Authentication failed: ${errData.code}`)
793
+ );
794
+ } else if (message.type === BTPMessageType.RESPONSE) {
795
+ resolve();
796
+ }
797
+ }
798
+ } catch (err) {
799
+ clearTimeout(timeout);
800
+ this.ws.onmessage = originalHandler;
801
+ reject(
802
+ new BtpAuthError(err instanceof Error ? err.message : String(err))
803
+ );
804
+ }
805
+ };
806
+ this.ws.send(authMessage);
807
+ });
808
+ }
809
+ handleMessage(raw) {
810
+ try {
811
+ const data = this.toUint8Array(raw);
812
+ const jsonStr = new TextDecoder().decode(data);
813
+ if (jsonStr.startsWith("{")) {
814
+ const json = JSON.parse(jsonStr);
815
+ if (json["type"] === "FULFILL" || json["type"] === "REJECT") {
816
+ const first = this.pendingRequests.entries().next();
817
+ if (!first.done) {
818
+ const [id, pending] = first.value;
819
+ clearTimeout(pending.timeoutId);
820
+ this.pendingRequests.delete(id);
821
+ if (json["type"] === "FULFILL") {
822
+ const responseData = json["data"] ? this.base64ToUint8Array(json["data"]) : new Uint8Array(0);
823
+ pending.resolve({
824
+ type: ILPPacketType.FULFILL,
825
+ data: responseData
826
+ });
827
+ } else {
828
+ pending.resolve({
829
+ type: ILPPacketType.REJECT,
830
+ code: json["code"] || "F00",
831
+ message: json["message"] || "Unknown error",
832
+ data: json["data"] ? this.base64ToUint8Array(json["data"]) : new Uint8Array(0)
833
+ });
834
+ }
835
+ }
836
+ return;
837
+ }
838
+ }
839
+ } catch {
840
+ }
841
+ try {
842
+ const data = this.toUint8Array(raw);
843
+ const message = parseBtpMessage(data);
844
+ if (message.type === BTPMessageType.RESPONSE || message.type === BTPMessageType.ERROR) {
845
+ const pending = this.pendingRequests.get(message.requestId);
846
+ if (!pending) return;
847
+ clearTimeout(pending.timeoutId);
848
+ this.pendingRequests.delete(message.requestId);
849
+ if (message.type === BTPMessageType.ERROR) {
850
+ const errData = message.data;
851
+ pending.reject(
852
+ new BtpConnectionError(`BTP error: ${errData.code} ${errData.name}`)
853
+ );
854
+ return;
855
+ }
856
+ const msgData = message.data;
857
+ if (msgData.ilpPacket && msgData.ilpPacket.length > 0) {
858
+ const ilpResponse = deserializeIlpPacket(msgData.ilpPacket);
859
+ pending.resolve(ilpResponse);
860
+ }
861
+ }
862
+ } catch {
863
+ }
864
+ }
865
+ toUint8Array(data) {
866
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
867
+ if (data instanceof Uint8Array) return data;
868
+ if (typeof data === "string") return textEncoder2.encode(data);
869
+ throw new Error(`Unexpected WebSocket data type: ${typeof data}`);
870
+ }
871
+ base64ToUint8Array(base64) {
872
+ const binary = atob(base64);
873
+ const bytes = new Uint8Array(binary.length);
874
+ for (let i = 0; i < binary.length; i++) {
875
+ bytes[i] = binary.charCodeAt(i);
876
+ }
877
+ return bytes;
878
+ }
879
+ nextRequestId() {
880
+ this.requestIdCounter = this.requestIdCounter + 1 & 4294967295;
881
+ return this.requestIdCounter;
882
+ }
883
+ };
884
+
885
+ // src/adapters/BtpRuntimeClient.ts
408
886
  function isConnectionError(error) {
409
887
  const msg = error.message.toLowerCase();
410
888
  return msg.includes("not connected") || msg.includes("connection") || msg.includes("websocket") || msg.includes("econnrefused") || msg.includes("econnreset") || msg.includes("socket hang up") || msg.includes("timeout");
@@ -413,33 +891,23 @@ var BtpRuntimeClient = class {
413
891
  btpClient = null;
414
892
  config;
415
893
  _isConnected = false;
416
- logger;
417
894
  constructor(config) {
418
895
  this.config = config;
419
- this.logger = config.logger ?? createConsoleLogger();
420
896
  }
421
897
  /**
422
898
  * Connects to the BTP peer via WebSocket.
423
899
  */
424
900
  async connect() {
425
- const peer = {
426
- id: this.config.peerId,
901
+ this.btpClient = new IsomorphicBtpClient({
427
902
  url: this.config.btpUrl,
428
- authToken: this.config.authToken,
429
- connected: false,
430
- lastSeen: /* @__PURE__ */ new Date()
431
- };
432
- this.btpClient = new BTPClient(
433
- peer,
434
- this.config.peerId,
435
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
436
- this.logger
437
- );
903
+ peerId: this.config.peerId,
904
+ authToken: this.config.authToken
905
+ });
438
906
  await this.btpClient.connect();
439
907
  this._isConnected = true;
440
908
  }
441
909
  /**
442
- * Attempts to reconnect by creating a fresh BTPClient and connecting.
910
+ * Attempts to reconnect by creating a fresh client and connecting.
443
911
  */
444
912
  async reconnect() {
445
913
  if (this.btpClient) {
@@ -450,7 +918,6 @@ var BtpRuntimeClient = class {
450
918
  this.btpClient = null;
451
919
  this._isConnected = false;
452
920
  }
453
- this.logger.info("[BtpRuntimeClient] Reconnecting...");
454
921
  await this.connect();
455
922
  }
456
923
  /**
@@ -503,73 +970,66 @@ var BtpRuntimeClient = class {
503
970
  if (!this._isConnected) {
504
971
  await this.reconnect();
505
972
  }
506
- const packet = {
507
- type: ILP_PACKET_TYPE.PREPARE,
973
+ const response = await this.btpClient.sendPacket({
974
+ type: 12,
508
975
  amount: BigInt(params.amount),
509
976
  destination: params.destination,
510
- executionCondition: Buffer.alloc(32),
977
+ executionCondition: new Uint8Array(32),
511
978
  expiresAt: new Date(Date.now() + (params.timeout ?? 3e4)),
512
- data: Buffer.from(params.data, "base64")
513
- };
514
- const response = await this.btpClient?.sendPacket(packet);
515
- if (!response) {
516
- throw new Error("BTP client not connected");
517
- }
518
- if (response.type === ILP_PACKET_TYPE.FULFILL) {
519
- const fulfill = response;
979
+ data: fromBase64(params.data)
980
+ });
981
+ if (response.type === ILPPacketType.FULFILL) {
520
982
  return {
521
983
  accepted: true,
522
- data: fulfill.data.length > 0 ? fulfill.data.toString("base64") : void 0
984
+ data: response.data.length > 0 ? toBase64(response.data) : void 0
523
985
  };
524
986
  }
525
- const reject = response;
526
987
  return {
527
988
  accepted: false,
528
- code: reject.code,
529
- message: reject.message,
530
- data: reject.data.length > 0 ? reject.data.toString("base64") : void 0
989
+ code: response.code,
990
+ message: response.message,
991
+ data: response.data.length > 0 ? toBase64(response.data) : void 0
531
992
  };
532
993
  }
533
994
  /**
534
995
  * Single-attempt claim + ILP packet send. Reconnects if not connected.
535
- * Embeds the claim in the same BTP message as the ILP PREPARE packet.
536
996
  */
537
997
  async _sendIlpPacketWithClaimOnce(params, claim) {
538
998
  if (!this._isConnected) {
539
999
  await this.reconnect();
540
1000
  }
541
1001
  if (!this.btpClient) {
542
- throw new Error("BTP client not connected");
1002
+ throw new BtpConnectionError("BTP client not connected");
543
1003
  }
544
- const packet = {
545
- type: ILP_PACKET_TYPE.PREPARE,
546
- amount: BigInt(params.amount),
547
- destination: params.destination,
548
- executionCondition: Buffer.alloc(32),
549
- expiresAt: new Date(Date.now() + (params.timeout ?? 3e4)),
550
- data: Buffer.from(params.data, "base64")
551
- };
552
1004
  const protocolData = [
553
1005
  {
554
- protocolName: BTP_CLAIM_PROTOCOL.NAME,
555
- contentType: BTP_CLAIM_PROTOCOL.CONTENT_TYPE,
556
- data: Buffer.from(JSON.stringify(claim))
1006
+ protocolName: "payment-channel-claim",
1007
+ contentType: 1,
1008
+ data: encodeUtf8(JSON.stringify(claim))
557
1009
  }
558
1010
  ];
559
- const response = await this.btpClient.sendPacket(packet, protocolData);
560
- if (response.type === ILP_PACKET_TYPE.FULFILL) {
561
- const fulfill = response;
1011
+ const response = await this.btpClient.sendPacket(
1012
+ {
1013
+ type: 12,
1014
+ amount: BigInt(params.amount),
1015
+ destination: params.destination,
1016
+ executionCondition: new Uint8Array(32),
1017
+ expiresAt: new Date(Date.now() + (params.timeout ?? 3e4)),
1018
+ data: fromBase64(params.data)
1019
+ },
1020
+ protocolData
1021
+ );
1022
+ if (response.type === ILPPacketType.FULFILL) {
562
1023
  return {
563
1024
  accepted: true,
564
- data: fulfill.data.length > 0 ? fulfill.data.toString("base64") : void 0
1025
+ data: response.data.length > 0 ? toBase64(response.data) : void 0
565
1026
  };
566
1027
  }
567
- const reject = response;
568
1028
  return {
569
1029
  accepted: false,
570
- code: reject.code,
571
- message: reject.message,
572
- data: reject.data.length > 0 ? reject.data.toString("base64") : void 0
1030
+ code: response.code,
1031
+ message: response.message,
1032
+ data: response.data.length > 0 ? toBase64(response.data) : void 0
573
1033
  };
574
1034
  }
575
1035
  };
@@ -661,10 +1121,14 @@ var STATE_MAP = {
661
1121
  var OnChainChannelClient = class {
662
1122
  evmSigner;
663
1123
  chainRpcUrls;
1124
+ solanaConfig;
1125
+ minaConfig;
664
1126
  channelContext = /* @__PURE__ */ new Map();
665
1127
  constructor(config) {
666
1128
  this.evmSigner = config.evmSigner;
667
1129
  this.chainRpcUrls = config.chainRpcUrls;
1130
+ this.solanaConfig = config.solanaConfig;
1131
+ this.minaConfig = config.minaConfig;
668
1132
  }
669
1133
  /**
670
1134
  * Parse chain identifier to extract chainId.
@@ -726,6 +1190,67 @@ var OnChainChannelClient = class {
726
1190
  * 4. Deposit initial funds if specified
727
1191
  */
728
1192
  async openChannel(params) {
1193
+ const chainPrefix = params.chain.split(":")[0];
1194
+ if (chainPrefix === "solana") return this.openSolanaChannel(params);
1195
+ if (chainPrefix === "mina") return this.openMinaChannel(params);
1196
+ return this.openEvmChannel(params);
1197
+ }
1198
+ /**
1199
+ * Opens a Solana payment channel (PDA creation).
1200
+ */
1201
+ async openSolanaChannel(params) {
1202
+ if (!this.solanaConfig) {
1203
+ throw new Error(
1204
+ "Solana channel config not provided \u2014 cannot open Solana channel"
1205
+ );
1206
+ }
1207
+ const encoder = new TextEncoder();
1208
+ const channelSeed = encoder.encode(
1209
+ `channel:${toHex(this.solanaConfig.keypair).slice(0, 32)}:${params.peerAddress}:${Date.now()}`
1210
+ );
1211
+ const channelIdBytes = new Uint8Array(
1212
+ await crypto.subtle.digest("SHA-256", channelSeed)
1213
+ );
1214
+ const channelId = "0x" + toHex(channelIdBytes);
1215
+ this.channelContext.set(channelId, {
1216
+ chain: params.chain,
1217
+ tokenNetworkAddress: this.solanaConfig.programId
1218
+ });
1219
+ return { channelId, status: "opening" };
1220
+ }
1221
+ /**
1222
+ * Opens a Mina payment channel (zkApp state transition).
1223
+ * Dynamically imports o1js to avoid bundle bloat.
1224
+ */
1225
+ async openMinaChannel(params) {
1226
+ if (!this.minaConfig) {
1227
+ throw new Error(
1228
+ "Mina channel config not provided \u2014 cannot open Mina channel"
1229
+ );
1230
+ }
1231
+ const encoder = new TextEncoder();
1232
+ const channelSeed = encoder.encode(
1233
+ `channel:${this.minaConfig.privateKey.slice(0, 16)}:${params.peerAddress}:${Date.now()}`
1234
+ );
1235
+ const channelIdBytes = new Uint8Array(
1236
+ await crypto.subtle.digest("SHA-256", channelSeed)
1237
+ );
1238
+ const channelId = "0x" + toHex(channelIdBytes);
1239
+ this.channelContext.set(channelId, {
1240
+ chain: params.chain,
1241
+ tokenNetworkAddress: this.minaConfig.zkAppAddress
1242
+ });
1243
+ return { channelId, status: "opening" };
1244
+ }
1245
+ /**
1246
+ * Opens an EVM payment channel on-chain.
1247
+ *
1248
+ * 1. Approve token spend if needed
1249
+ * 2. Call TokenNetwork.openChannel()
1250
+ * 3. Extract channelId from ChannelOpened event
1251
+ * 4. Deposit initial funds if specified
1252
+ */
1253
+ async openEvmChannel(params) {
729
1254
  const {
730
1255
  chain,
731
1256
  tokenNetwork,
@@ -830,7 +1355,7 @@ var OnChainChannelClient = class {
830
1355
 
831
1356
  // src/signing/evm-signer.ts
832
1357
  import { privateKeyToAccount } from "viem/accounts";
833
- import { toHex } from "viem";
1358
+ import { toHex as toHex2 } from "viem";
834
1359
  function getBalanceProofDomain(chainId, tokenNetworkAddress) {
835
1360
  return {
836
1361
  name: "TokenNetwork",
@@ -849,6 +1374,7 @@ var BALANCE_PROOF_TYPES = {
849
1374
  ]
850
1375
  };
851
1376
  var EvmSigner = class {
1377
+ chainType = "evm";
852
1378
  _account;
853
1379
  /**
854
1380
  * @param privateKey - EVM private key as hex string (with or without 0x prefix) or Uint8Array
@@ -856,7 +1382,7 @@ var EvmSigner = class {
856
1382
  constructor(privateKey) {
857
1383
  let hexKey;
858
1384
  if (privateKey instanceof Uint8Array) {
859
- hexKey = toHex(privateKey);
1385
+ hexKey = toHex2(privateKey);
860
1386
  } else {
861
1387
  hexKey = privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`;
862
1388
  }
@@ -866,6 +1392,10 @@ var EvmSigner = class {
866
1392
  get address() {
867
1393
  return this._account.address;
868
1394
  }
1395
+ /** ChainSigner identifier — EVM address */
1396
+ get signerIdentifier() {
1397
+ return this._account.address;
1398
+ }
869
1399
  /** Viem PrivateKeyAccount — usable with walletClient for on-chain transactions */
870
1400
  get account() {
871
1401
  return this._account;
@@ -1003,19 +1533,125 @@ async function initializeHttpMode(config) {
1003
1533
 
1004
1534
  // src/channel/ChannelManager.ts
1005
1535
  var ChannelManager = class {
1006
- evmSigner;
1007
1536
  channels = /* @__PURE__ */ new Map();
1537
+ chainSigners = /* @__PURE__ */ new Map();
1538
+ peerChannels = /* @__PURE__ */ new Map();
1539
+ pendingOpens = /* @__PURE__ */ new Map();
1008
1540
  store;
1009
- constructor(evmSigner, store) {
1541
+ defaultInitialDeposit;
1542
+ defaultSettlementTimeout;
1543
+ channelClient;
1544
+ // Legacy: keep EvmSigner reference for backwards compatibility
1545
+ evmSigner;
1546
+ constructor(evmSigner, store, config) {
1010
1547
  this.evmSigner = evmSigner;
1011
1548
  this.store = store;
1549
+ this.defaultInitialDeposit = config?.initialDeposit ?? "100000";
1550
+ this.defaultSettlementTimeout = config?.settlementTimeout ?? 86400;
1551
+ }
1552
+ /**
1553
+ * Register a chain-specific signer.
1554
+ */
1555
+ registerChainSigner(chainType, signer) {
1556
+ this.chainSigners.set(chainType, signer);
1557
+ }
1558
+ /**
1559
+ * Set the on-chain channel client for lazy channel opening.
1560
+ */
1561
+ setChannelClient(client) {
1562
+ this.channelClient = client;
1563
+ }
1564
+ /**
1565
+ * Get the signer for a tracked channel's chain type.
1566
+ * For EVM, returns an adapter wrapping the EvmSigner.
1567
+ */
1568
+ getSignerForChannel(channelId) {
1569
+ const tracking = this.channels.get(channelId);
1570
+ if (!tracking) {
1571
+ throw new Error(`Channel "${channelId}" is not being tracked.`);
1572
+ }
1573
+ const signer = this.chainSigners.get(tracking.chainType);
1574
+ if (signer) return signer;
1575
+ if (tracking.chainType === "evm" && this.evmSigner) {
1576
+ const evmSigner = this.evmSigner;
1577
+ return {
1578
+ chainType: "evm",
1579
+ signerIdentifier: evmSigner.address,
1580
+ async signBalanceProof(params) {
1581
+ if (params.metadata.chainType !== "evm")
1582
+ throw new Error("Expected EVM metadata");
1583
+ return evmSigner.signBalanceProof({
1584
+ channelId: params.channelId,
1585
+ nonce: params.nonce,
1586
+ transferredAmount: params.transferredAmount,
1587
+ lockedAmount: params.lockedAmount,
1588
+ locksRoot: params.locksRoot,
1589
+ chainId: params.metadata.chainId,
1590
+ tokenNetworkAddress: params.metadata.tokenNetworkAddress,
1591
+ tokenAddress: params.metadata.tokenAddress
1592
+ });
1593
+ },
1594
+ buildClaimMessage(proof, senderId) {
1595
+ return EvmSigner.buildClaimMessage(proof, senderId);
1596
+ }
1597
+ };
1598
+ }
1599
+ throw new Error(
1600
+ `No signer registered for chain type: ${tracking.chainType}`
1601
+ );
1602
+ }
1603
+ /**
1604
+ * Lazily open a channel for a peer. Idempotent — returns existing channel
1605
+ * if already open. Deduplicates concurrent opens for the same peer.
1606
+ */
1607
+ async ensureChannel(peerId, negotiation) {
1608
+ const existing = this.peerChannels.get(peerId);
1609
+ if (existing) return existing;
1610
+ const pending = this.pendingOpens.get(peerId);
1611
+ if (pending) return pending;
1612
+ if (!this.channelClient) {
1613
+ throw new Error(
1614
+ "No channel client configured \u2014 cannot open payment channel"
1615
+ );
1616
+ }
1617
+ const openPromise = (async () => {
1618
+ try {
1619
+ const result = await this.channelClient.openChannel({
1620
+ peerId,
1621
+ chain: negotiation.chain,
1622
+ token: negotiation.tokenAddress,
1623
+ tokenNetwork: negotiation.tokenNetwork,
1624
+ peerAddress: negotiation.settlementAddress,
1625
+ initialDeposit: negotiation.initialDeposit ?? this.defaultInitialDeposit,
1626
+ settlementTimeout: negotiation.settlementTimeout ?? this.defaultSettlementTimeout
1627
+ });
1628
+ this.trackChannel(result.channelId, {
1629
+ chainType: negotiation.chainType,
1630
+ chainId: typeof negotiation.chainId === "number" ? negotiation.chainId : 0,
1631
+ tokenNetworkAddress: negotiation.tokenNetwork ?? "",
1632
+ tokenAddress: negotiation.tokenAddress
1633
+ });
1634
+ this.peerChannels.set(peerId, result.channelId);
1635
+ return result.channelId;
1636
+ } finally {
1637
+ this.pendingOpens.delete(peerId);
1638
+ }
1639
+ })();
1640
+ this.pendingOpens.set(peerId, openPromise);
1641
+ return openPromise;
1642
+ }
1643
+ /**
1644
+ * Get channel ID for a peer (if any).
1645
+ */
1646
+ getChannelForPeer(peerId) {
1647
+ return this.peerChannels.get(peerId);
1012
1648
  }
1013
1649
  /**
1014
1650
  * Start tracking a channel.
1015
1651
  * Called after bootstrap returns a channelId.
1016
1652
  *
1017
1653
  * @param channelId - Payment channel identifier
1018
- * @param chainContext - Chain context for EIP-712 signing (chainId + tokenNetworkAddress)
1654
+ * @param chainContext - Chain context for signing (chainType + chainId + tokenNetworkAddress)
1019
1655
  * @param initialNonce - Starting nonce (default: 0)
1020
1656
  * @param initialAmount - Starting cumulative amount (default: 0n)
1021
1657
  */
@@ -1028,6 +1664,7 @@ var ChannelManager = class {
1028
1664
  this.channels.set(channelId, {
1029
1665
  nonce: persisted.nonce,
1030
1666
  cumulativeAmount: persisted.cumulativeAmount,
1667
+ chainType: chainContext?.chainType ?? "evm",
1031
1668
  chainId: cId,
1032
1669
  tokenNetworkAddress: tnAddr,
1033
1670
  tokenAddress: chainContext?.tokenAddress
@@ -1038,6 +1675,7 @@ var ChannelManager = class {
1038
1675
  this.channels.set(channelId, {
1039
1676
  nonce: initialNonce,
1040
1677
  cumulativeAmount: initialAmount,
1678
+ chainType: chainContext?.chainType ?? "evm",
1041
1679
  chainId: cId,
1042
1680
  tokenNetworkAddress: tnAddr,
1043
1681
  tokenAddress: chainContext?.tokenAddress
@@ -1046,6 +1684,7 @@ var ChannelManager = class {
1046
1684
  /**
1047
1685
  * Signs a balance proof for the given channel.
1048
1686
  * Auto-increments nonce and adds to cumulative amount.
1687
+ * Routes to the correct ChainSigner based on the channel's chain type.
1049
1688
  *
1050
1689
  * @param channelId - Payment channel identifier
1051
1690
  * @param additionalAmount - Amount to add to cumulative transferred amount
@@ -1067,6 +1706,21 @@ var ChannelManager = class {
1067
1706
  cumulativeAmount: tracking.cumulativeAmount
1068
1707
  });
1069
1708
  }
1709
+ const signer = this.chainSigners.get(tracking.chainType);
1710
+ if (signer && tracking.chainType !== "evm") {
1711
+ const metadata = this.buildMetadata(tracking);
1712
+ return signer.signBalanceProof({
1713
+ channelId,
1714
+ nonce: tracking.nonce,
1715
+ transferredAmount: tracking.cumulativeAmount,
1716
+ lockedAmount: 0n,
1717
+ locksRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
1718
+ metadata
1719
+ });
1720
+ }
1721
+ if (!this.evmSigner) {
1722
+ throw new Error("No EVM signer configured for EVM channel signing.");
1723
+ }
1070
1724
  return this.evmSigner.signBalanceProof({
1071
1725
  channelId,
1072
1726
  nonce: tracking.nonce,
@@ -1078,6 +1732,24 @@ var ChannelManager = class {
1078
1732
  tokenAddress: tracking.tokenAddress
1079
1733
  });
1080
1734
  }
1735
+ buildMetadata(tracking) {
1736
+ switch (tracking.chainType) {
1737
+ case "solana":
1738
+ return { chainType: "solana", programId: tracking.tokenNetworkAddress };
1739
+ case "mina":
1740
+ return {
1741
+ chainType: "mina",
1742
+ zkAppAddress: tracking.tokenNetworkAddress
1743
+ };
1744
+ default:
1745
+ return {
1746
+ chainType: "evm",
1747
+ chainId: tracking.chainId,
1748
+ tokenNetworkAddress: tracking.tokenNetworkAddress,
1749
+ tokenAddress: tracking.tokenAddress
1750
+ };
1751
+ }
1752
+ }
1081
1753
  /**
1082
1754
  * Gets the current nonce for a tracked channel.
1083
1755
  */
@@ -1162,6 +1834,7 @@ var ToonClient = class {
1162
1834
  state = null;
1163
1835
  evmSigner;
1164
1836
  channelManager;
1837
+ peerNegotiations = /* @__PURE__ */ new Map();
1165
1838
  /**
1166
1839
  * Creates a new ToonClient instance.
1167
1840
  *
@@ -1236,13 +1909,50 @@ var ToonClient = class {
1236
1909
  );
1237
1910
  }
1238
1911
  const bootstrapResults = await bootstrapService.bootstrap();
1239
- if (this.channelManager) {
1240
- for (const result of bootstrapResults) {
1241
- if (result.channelId && !this.channelManager.isTracking(result.channelId)) {
1242
- const chainCtx = this.getChainContext(result.negotiatedChain);
1243
- this.channelManager.trackChannel(result.channelId, chainCtx);
1912
+ for (const result of bootstrapResults) {
1913
+ if (result.negotiatedChain && result.settlementAddress) {
1914
+ const chainType = result.negotiatedChain.split(":")[0] ?? "evm";
1915
+ const parts = result.negotiatedChain.split(":");
1916
+ const chainId = parts.length >= 3 ? parseInt(parts[2] ?? "0", 10) : 0;
1917
+ const r = result;
1918
+ this.peerNegotiations.set(result.registeredPeerId, {
1919
+ chain: result.negotiatedChain,
1920
+ chainType,
1921
+ chainId: isNaN(chainId) ? 0 : chainId,
1922
+ settlementAddress: result.settlementAddress,
1923
+ tokenAddress: r.tokenAddress,
1924
+ tokenNetwork: r.tokenNetwork
1925
+ });
1926
+ } else if (result.registeredPeerId && !this.peerNegotiations.has(result.registeredPeerId)) {
1927
+ const peerInfo = result.peerInfo;
1928
+ const peerChains = peerInfo.supportedChains ?? [];
1929
+ const ourChains = this.config.supportedChains ?? [];
1930
+ const matchedChain = ourChains.find((c) => peerChains.includes(c)) ?? ourChains[0];
1931
+ if (matchedChain) {
1932
+ const peerAddr = peerInfo.settlementAddresses?.[matchedChain];
1933
+ const parts = matchedChain.split(":");
1934
+ const chainId = parts.length >= 3 ? parseInt(parts[2] ?? "0", 10) : 0;
1935
+ if (peerAddr) {
1936
+ this.peerNegotiations.set(result.registeredPeerId, {
1937
+ chain: matchedChain,
1938
+ chainType: parts[0] ?? "evm",
1939
+ chainId: isNaN(chainId) ? 0 : chainId,
1940
+ settlementAddress: peerAddr,
1941
+ tokenAddress: peerInfo.preferredTokens?.[matchedChain] ?? this.config.preferredTokens?.[matchedChain],
1942
+ tokenNetwork: peerInfo.tokenNetworks?.[matchedChain] ?? this.config.tokenNetworks?.[matchedChain]
1943
+ });
1944
+ }
1244
1945
  }
1245
1946
  }
1947
+ if (this.channelManager && result.channelId && !this.channelManager.isTracking(result.channelId)) {
1948
+ const chainCtx = this.getChainContext(result.negotiatedChain);
1949
+ this.channelManager.trackChannel(result.channelId, chainCtx);
1950
+ }
1951
+ }
1952
+ if (this.channelManager && initialization.onChainChannelClient) {
1953
+ this.channelManager.setChannelClient(
1954
+ initialization.onChainChannelClient
1955
+ );
1246
1956
  }
1247
1957
  this.state = {
1248
1958
  bootstrapService,
@@ -1286,27 +1996,50 @@ var ToonClient = class {
1286
1996
  const basePricePerByte = 10n;
1287
1997
  const amount = String(BigInt(toonData.length) * basePricePerByte);
1288
1998
  const destination = options?.destination ?? this.config.destinationAddress;
1289
- if (!options?.claim) {
1290
- throw new ToonClientError(
1291
- "Signed balance proof required. Call signBalanceProof() first.",
1292
- "MISSING_CLAIM"
1293
- );
1294
- }
1295
1999
  if (!this.state.btpClient) {
1296
2000
  throw new ToonClientError(
1297
2001
  "BTP client required for publishing. Configure btpUrl.",
1298
2002
  "NO_BTP_CLIENT"
1299
2003
  );
1300
2004
  }
1301
- const claimMessage = EvmSigner.buildClaimMessage(
1302
- options.claim,
1303
- this.getPublicKey()
1304
- );
2005
+ let claimMessage;
2006
+ if (options?.claim) {
2007
+ claimMessage = EvmSigner.buildClaimMessage(
2008
+ options.claim,
2009
+ this.getPublicKey()
2010
+ );
2011
+ } else if (this.channelManager) {
2012
+ const peerId = this.resolvePeerId(destination);
2013
+ const negotiation = this.peerNegotiations.get(peerId);
2014
+ if (!negotiation) {
2015
+ throw new ToonClientError(
2016
+ `No negotiation metadata for peer "${peerId}" \u2014 was bootstrap completed?`,
2017
+ "PEER_NOT_NEGOTIATED"
2018
+ );
2019
+ }
2020
+ const channelId = await this.channelManager.ensureChannel(
2021
+ peerId,
2022
+ negotiation
2023
+ );
2024
+ const proof = await this.channelManager.signBalanceProof(
2025
+ channelId,
2026
+ BigInt(amount)
2027
+ );
2028
+ const signer = this.channelManager.getSignerForChannel(channelId);
2029
+ claimMessage = signer.buildClaimMessage(proof, this.getPublicKey());
2030
+ } else {
2031
+ throw new ToonClientError(
2032
+ "No claim provided and no channel manager configured",
2033
+ "MISSING_CLAIM"
2034
+ );
2035
+ }
1305
2036
  const response = await this.state.btpClient.sendIlpPacketWithClaim(
1306
2037
  {
1307
2038
  destination,
1308
2039
  amount,
1309
- data: Buffer.from(toonData).toString("base64")
2040
+ data: toBase64(
2041
+ toonData instanceof Uint8Array ? toonData : new Uint8Array(toonData)
2042
+ )
1310
2043
  },
1311
2044
  claimMessage
1312
2045
  );
@@ -1322,6 +2055,11 @@ var ToonClient = class {
1322
2055
  data: response.data
1323
2056
  };
1324
2057
  } catch (error) {
2058
+ console.error(
2059
+ "[ToonClient.publishEvent] ROOT CAUSE:",
2060
+ String(error),
2061
+ error instanceof Error ? error.stack : ""
2062
+ );
1325
2063
  throw new ToonClientError(
1326
2064
  "Failed to publish event",
1327
2065
  "PUBLISH_ERROR",
@@ -1367,6 +2105,30 @@ var ToonClient = class {
1367
2105
  if (!this.channelManager) throw new Error("ChannelManager not initialized");
1368
2106
  return this.channelManager.getCumulativeAmount(channelId);
1369
2107
  }
2108
+ /**
2109
+ * Resolves an ILP destination address to a peer ID.
2110
+ * Convention: destination "g.toon.peer1" → peerId "peer1" (last segment).
2111
+ * Falls back to first known peer if no match.
2112
+ */
2113
+ resolvePeerId(destination) {
2114
+ const segments = destination.split(".");
2115
+ const lastSegment = segments[segments.length - 1] ?? "";
2116
+ if (lastSegment && this.peerNegotiations.has(lastSegment)) {
2117
+ return lastSegment;
2118
+ }
2119
+ for (const peerId of this.peerNegotiations.keys()) {
2120
+ if (destination.endsWith(`.${peerId}`) || destination.endsWith(`.${peerId.replace("nostr-", "")}`)) {
2121
+ return peerId;
2122
+ }
2123
+ }
2124
+ const firstPeerResult = this.peerNegotiations.keys().next();
2125
+ if (!firstPeerResult.done && firstPeerResult.value)
2126
+ return firstPeerResult.value;
2127
+ throw new ToonClientError(
2128
+ `Cannot resolve peer for destination: ${destination}`,
2129
+ "PEER_NOT_FOUND"
2130
+ );
2131
+ }
1370
2132
  /**
1371
2133
  * Extracts chain context (chainId + tokenNetworkAddress) from a chain key like 'evm:base:421614'.
1372
2134
  */
@@ -1424,7 +2186,10 @@ var ToonClient = class {
1424
2186
  params.claim,
1425
2187
  this.getPublicKey()
1426
2188
  );
1427
- return this.state.btpClient.sendIlpPacketWithClaim(ilpParams, claimMessage);
2189
+ return this.state.btpClient.sendIlpPacketWithClaim(
2190
+ ilpParams,
2191
+ claimMessage
2192
+ );
1428
2193
  }
1429
2194
  /**
1430
2195
  * Stops the ToonClient and cleans up resources.
@@ -1798,6 +2563,1212 @@ var HttpConnectorAdmin = class {
1798
2563
  }
1799
2564
  }
1800
2565
  };
2566
+
2567
+ // src/signing/solana-signer.ts
2568
+ var BASE58_ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2569
+ function toBase58(bytes) {
2570
+ let num = BigInt(0);
2571
+ for (const b of bytes) num = num * 256n + BigInt(b);
2572
+ let result = "";
2573
+ while (num > 0n) {
2574
+ result = BASE58_ALPHABET[Number(num % 58n)] + result;
2575
+ num = num / 58n;
2576
+ }
2577
+ for (const b of bytes) {
2578
+ if (b === 0) result = "1" + result;
2579
+ else break;
2580
+ }
2581
+ return result;
2582
+ }
2583
+ var _ed25519 = null;
2584
+ async function getEd25519() {
2585
+ if (!_ed25519) {
2586
+ const mod = await import("@noble/curves/ed25519");
2587
+ _ed25519 = mod.ed25519;
2588
+ }
2589
+ return _ed25519;
2590
+ }
2591
+ var SolanaSigner = class {
2592
+ chainType = "solana";
2593
+ privateKey;
2594
+ publicKey;
2595
+ pubkeyBase58Cache;
2596
+ constructor(privateKey) {
2597
+ this.privateKey = privateKey;
2598
+ }
2599
+ async ensurePublicKey() {
2600
+ if (this.publicKey && this.pubkeyBase58Cache) {
2601
+ return { publicKey: this.publicKey, base58: this.pubkeyBase58Cache };
2602
+ }
2603
+ const ed = await getEd25519();
2604
+ const pk = ed.getPublicKey(this.privateKey);
2605
+ const b58 = toBase58(pk);
2606
+ this.publicKey = pk;
2607
+ this.pubkeyBase58Cache = b58;
2608
+ return { publicKey: pk, base58: b58 };
2609
+ }
2610
+ get signerIdentifier() {
2611
+ return this.pubkeyBase58Cache ?? "uninitialized";
2612
+ }
2613
+ async signBalanceProof(params) {
2614
+ if (params.metadata.chainType !== "solana") {
2615
+ throw new Error(
2616
+ `SolanaSigner cannot sign for chain type: ${params.metadata.chainType}`
2617
+ );
2618
+ }
2619
+ const ed = await getEd25519();
2620
+ const { base58 } = await this.ensurePublicKey();
2621
+ const encoder = new TextEncoder();
2622
+ const message = encoder.encode(
2623
+ `${params.channelId}:${params.nonce}:${params.transferredAmount}:${params.lockedAmount}:${params.locksRoot}`
2624
+ );
2625
+ const signature = ed.sign(message, this.privateKey);
2626
+ const signatureHex = "0x" + toHex(new Uint8Array(signature));
2627
+ return {
2628
+ channelId: params.channelId,
2629
+ nonce: params.nonce,
2630
+ transferredAmount: params.transferredAmount,
2631
+ lockedAmount: params.lockedAmount,
2632
+ locksRoot: params.locksRoot,
2633
+ signature: signatureHex,
2634
+ signerAddress: base58,
2635
+ chainId: 0,
2636
+ tokenNetworkAddress: params.metadata.programId
2637
+ };
2638
+ }
2639
+ buildClaimMessage(proof, senderId) {
2640
+ const claim = {
2641
+ version: "1.0",
2642
+ blockchain: "solana",
2643
+ messageId: crypto.randomUUID(),
2644
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2645
+ senderId,
2646
+ channelId: proof.channelId,
2647
+ nonce: proof.nonce,
2648
+ transferredAmount: proof.transferredAmount.toString(),
2649
+ signature: proof.signature,
2650
+ signerAddress: this.pubkeyBase58Cache ?? proof.signerAddress,
2651
+ programId: proof.tokenNetworkAddress
2652
+ };
2653
+ return claim;
2654
+ }
2655
+ };
2656
+
2657
+ // src/signing/mina-signer.ts
2658
+ var MinaSigner = class {
2659
+ chainType = "mina";
2660
+ privateKeyBase58;
2661
+ publicKeyBase58 = "uninitialized";
2662
+ constructor(privateKeyBase58) {
2663
+ this.privateKeyBase58 = privateKeyBase58;
2664
+ }
2665
+ get signerIdentifier() {
2666
+ return this.publicKeyBase58;
2667
+ }
2668
+ async ensurePublicKey() {
2669
+ if (this.publicKeyBase58 !== "uninitialized") return this.publicKeyBase58;
2670
+ const o1js = await import("o1js");
2671
+ const pk = o1js.PrivateKey.fromBase58(this.privateKeyBase58);
2672
+ this.publicKeyBase58 = pk.toPublicKey().toBase58();
2673
+ return this.publicKeyBase58;
2674
+ }
2675
+ async signBalanceProof(params) {
2676
+ if (params.metadata.chainType !== "mina") {
2677
+ throw new Error(
2678
+ `MinaSigner cannot sign for chain type: ${params.metadata.chainType}`
2679
+ );
2680
+ }
2681
+ const o1js = await import("o1js");
2682
+ const pubkey = await this.ensurePublicKey();
2683
+ const channelIdNum = BigInt(
2684
+ "0x" + params.channelId.replace(/^0x/, "").slice(0, 16)
2685
+ );
2686
+ const commitment = o1js.Poseidon.hash([
2687
+ o1js.Field(channelIdNum),
2688
+ o1js.Field(params.nonce),
2689
+ o1js.Field(params.transferredAmount),
2690
+ o1js.Field(params.lockedAmount)
2691
+ ]);
2692
+ const pk = o1js.PrivateKey.fromBase58(this.privateKeyBase58);
2693
+ const signature = o1js.Signature.create(pk, [commitment]);
2694
+ return {
2695
+ channelId: params.channelId,
2696
+ nonce: params.nonce,
2697
+ transferredAmount: params.transferredAmount,
2698
+ lockedAmount: params.lockedAmount,
2699
+ locksRoot: params.locksRoot,
2700
+ signature: signature.toBase58(),
2701
+ signerAddress: pubkey,
2702
+ chainId: 0,
2703
+ tokenNetworkAddress: params.metadata.zkAppAddress
2704
+ };
2705
+ }
2706
+ buildClaimMessage(proof, senderId) {
2707
+ const claim = {
2708
+ version: "1.0",
2709
+ blockchain: "mina",
2710
+ messageId: crypto.randomUUID(),
2711
+ timestamp: (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, ".000Z"),
2712
+ senderId,
2713
+ channelId: proof.channelId,
2714
+ nonce: proof.nonce,
2715
+ transferredAmount: proof.transferredAmount.toString(),
2716
+ commitment: proof.signature,
2717
+ signerAddress: proof.signerAddress,
2718
+ zkAppAddress: proof.tokenNetworkAddress
2719
+ };
2720
+ return claim;
2721
+ }
2722
+ };
2723
+
2724
+ // src/keys/KeyManager.ts
2725
+ import { finalizeEvent } from "nostr-tools/pure";
2726
+ import { nip19 } from "nostr-tools";
2727
+
2728
+ // src/keys/KeyDerivation.ts
2729
+ import { generateSecretKey as generateSecretKey3, getPublicKey as getPublicKey2 } from "nostr-tools/pure";
2730
+ import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
2731
+ import { toHex as toHex3 } from "viem";
2732
+ import {
2733
+ generateMnemonic as _genMnemonic,
2734
+ validateMnemonic as _validateMnemonic,
2735
+ mnemonicToSeedSync
2736
+ } from "@scure/bip39";
2737
+ import { wordlist as english } from "@scure/bip39/wordlists/english";
2738
+ import { HDKey } from "@scure/bip32";
2739
+ function generateMnemonic() {
2740
+ return _genMnemonic(english, 128);
2741
+ }
2742
+ function validateMnemonic(mnemonic) {
2743
+ return _validateMnemonic(mnemonic, english);
2744
+ }
2745
+ function deriveNostrKey(seed) {
2746
+ const master = HDKey.fromMasterSeed(seed);
2747
+ const child = master.derive("m/44'/1237'/0'/0/0");
2748
+ if (!child.privateKey) {
2749
+ throw new Error("Failed to derive Nostr private key from seed");
2750
+ }
2751
+ const secretKey = new Uint8Array(child.privateKey);
2752
+ const pubkey = getPublicKey2(secretKey);
2753
+ return { secretKey, pubkey };
2754
+ }
2755
+ function deriveEvmIdentity(secretKey) {
2756
+ const account = privateKeyToAccount2(toHex3(secretKey));
2757
+ return {
2758
+ privateKey: secretKey,
2759
+ address: account.address
2760
+ };
2761
+ }
2762
+ async function deriveSolanaKey(seed) {
2763
+ const { hmac } = await import("@noble/hashes/hmac");
2764
+ const { sha512 } = await import("@noble/hashes/sha512");
2765
+ const { ed25519 } = await import("@noble/curves/ed25519");
2766
+ const encoder = new TextEncoder();
2767
+ let I = hmac(sha512, encoder.encode("ed25519 seed"), seed);
2768
+ let key = I.slice(0, 32);
2769
+ let chainCode = I.slice(32);
2770
+ const indices = [
2771
+ 2147483692,
2772
+ // 44'
2773
+ 2147484149,
2774
+ // 501'
2775
+ 2147483648,
2776
+ // 0'
2777
+ 2147483648
2778
+ // 0'
2779
+ ];
2780
+ for (const index of indices) {
2781
+ const data = new Uint8Array(37);
2782
+ data[0] = 0;
2783
+ data.set(key, 1);
2784
+ data[33] = index >>> 24 & 255;
2785
+ data[34] = index >>> 16 & 255;
2786
+ data[35] = index >>> 8 & 255;
2787
+ data[36] = index & 255;
2788
+ I = hmac(sha512, chainCode, data);
2789
+ key = I.slice(0, 32);
2790
+ chainCode = I.slice(32);
2791
+ }
2792
+ const publicKeyBytes = ed25519.getPublicKey(key);
2793
+ const keypair = new Uint8Array(64);
2794
+ keypair.set(key, 0);
2795
+ keypair.set(publicKeyBytes, 32);
2796
+ const publicKey = toBase582(publicKeyBytes);
2797
+ return { secretKey: keypair, publicKey };
2798
+ }
2799
+ async function deriveMinaKey(seed) {
2800
+ const master = HDKey.fromMasterSeed(seed);
2801
+ const child = master.derive("m/44'/12586'/0'/0/0");
2802
+ if (!child.privateKey) {
2803
+ throw new Error("Failed to derive Mina private key from seed");
2804
+ }
2805
+ const keyBytes = new Uint8Array(child.privateKey);
2806
+ try {
2807
+ const MinaSignerLib = await import("./mina-signer-J7GFWOGO.js");
2808
+ const Client = "default" in MinaSignerLib ? MinaSignerLib.default : MinaSignerLib;
2809
+ const client = new Client({ network: "mainnet" });
2810
+ const hexKey = Array.from(keyBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
2811
+ const keypair = client.derivePublicKey(hexKey);
2812
+ return {
2813
+ privateKey: hexKey,
2814
+ publicKey: keypair
2815
+ };
2816
+ } catch {
2817
+ throw new Error(
2818
+ "mina-signer is required for Mina key derivation. Install it as an optional dependency."
2819
+ );
2820
+ }
2821
+ }
2822
+ async function deriveFullIdentity(mnemonic) {
2823
+ const seed = mnemonicToSeedSync(mnemonic);
2824
+ const nostr = deriveNostrKey(seed);
2825
+ const evm = deriveEvmIdentity(nostr.secretKey);
2826
+ let solana;
2827
+ try {
2828
+ solana = await deriveSolanaKey(seed);
2829
+ } catch {
2830
+ solana = { secretKey: new Uint8Array(64), publicKey: "" };
2831
+ }
2832
+ let mina;
2833
+ try {
2834
+ mina = await deriveMinaKey(seed);
2835
+ } catch {
2836
+ mina = { privateKey: "", publicKey: "" };
2837
+ }
2838
+ seed.fill(0);
2839
+ return { nostr, evm, solana, mina };
2840
+ }
2841
+ function deriveFromNsec(secretKey) {
2842
+ const keyCopy = new Uint8Array(secretKey);
2843
+ const pubkey = getPublicKey2(keyCopy);
2844
+ const evm = deriveEvmIdentity(keyCopy);
2845
+ return {
2846
+ nostr: { secretKey: keyCopy, pubkey },
2847
+ evm,
2848
+ solana: { secretKey: new Uint8Array(64), publicKey: "" },
2849
+ mina: { privateKey: "", publicKey: "" }
2850
+ };
2851
+ }
2852
+ function generateRandomIdentity() {
2853
+ const secretKey = generateSecretKey3();
2854
+ return deriveFromNsec(secretKey);
2855
+ }
2856
+ var BASE58_ALPHABET2 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
2857
+ function toBase582(bytes) {
2858
+ let num = BigInt(0);
2859
+ for (const b of bytes) num = num * 256n + BigInt(b);
2860
+ let result = "";
2861
+ while (num > 0n) {
2862
+ result = BASE58_ALPHABET2[Number(num % 58n)] + result;
2863
+ num = num / 58n;
2864
+ }
2865
+ for (const b of bytes) {
2866
+ if (b === 0) result = "1" + result;
2867
+ else break;
2868
+ }
2869
+ return result;
2870
+ }
2871
+
2872
+ // src/keys/PasskeyAuth.ts
2873
+ async function registerPasskey(params) {
2874
+ const { rpId, rpName, userId, userName, prfSalt } = params;
2875
+ const publicKeyOptions = {
2876
+ rp: { id: rpId, name: rpName },
2877
+ user: {
2878
+ id: userId,
2879
+ name: userName,
2880
+ displayName: userName
2881
+ },
2882
+ challenge: crypto.getRandomValues(
2883
+ new Uint8Array(32)
2884
+ ),
2885
+ pubKeyCredParams: [
2886
+ { alg: -7, type: "public-key" },
2887
+ // ES256
2888
+ { alg: -257, type: "public-key" }
2889
+ // RS256
2890
+ ],
2891
+ authenticatorSelection: {
2892
+ residentKey: "required",
2893
+ userVerification: "required"
2894
+ },
2895
+ extensions: {
2896
+ prf: {
2897
+ eval: {
2898
+ first: prfSalt
2899
+ }
2900
+ }
2901
+ }
2902
+ };
2903
+ const credential = await navigator.credentials.create({
2904
+ publicKey: publicKeyOptions
2905
+ });
2906
+ if (!credential) {
2907
+ throw new Error("Passkey registration was cancelled or failed");
2908
+ }
2909
+ const response = credential.response;
2910
+ const extensionResults = credential.getClientExtensionResults();
2911
+ const prfResults = extensionResults["prf"];
2912
+ if (!prfResults?.results?.first) {
2913
+ throw new Error(
2914
+ "PRF extension not supported by this authenticator. Passkey was created but cannot be used for key encryption. Use password-based encryption as fallback."
2915
+ );
2916
+ }
2917
+ const credentialId = new Uint8Array(credential.rawId);
2918
+ if (!response.attestationObject) {
2919
+ throw new Error("Invalid attestation response");
2920
+ }
2921
+ return {
2922
+ prfOutput: prfResults.results.first,
2923
+ credentialId
2924
+ };
2925
+ }
2926
+ async function assertPasskey(params) {
2927
+ const { rpId, prfSalt, allowCredentials } = params;
2928
+ const publicKeyOptions = {
2929
+ rpId,
2930
+ challenge: crypto.getRandomValues(
2931
+ new Uint8Array(32)
2932
+ ),
2933
+ userVerification: "required",
2934
+ ...allowCredentials && {
2935
+ allowCredentials: allowCredentials.map((id) => ({
2936
+ id,
2937
+ type: "public-key"
2938
+ }))
2939
+ },
2940
+ extensions: {
2941
+ prf: {
2942
+ eval: {
2943
+ first: prfSalt
2944
+ }
2945
+ }
2946
+ }
2947
+ };
2948
+ const credential = await navigator.credentials.get({
2949
+ publicKey: publicKeyOptions
2950
+ });
2951
+ if (!credential) {
2952
+ throw new Error("Passkey assertion was cancelled or failed");
2953
+ }
2954
+ const response = credential.response;
2955
+ const extensionResults = credential.getClientExtensionResults();
2956
+ const prfResults = extensionResults["prf"];
2957
+ if (!prfResults?.results?.first) {
2958
+ throw new Error(
2959
+ "PRF extension did not return a result. The authenticator may not support PRF."
2960
+ );
2961
+ }
2962
+ return {
2963
+ prfOutput: prfResults.results.first,
2964
+ credentialId: new Uint8Array(credential.rawId),
2965
+ userHandle: response.userHandle ? new Uint8Array(response.userHandle) : null
2966
+ };
2967
+ }
2968
+ function isPrfSupported() {
2969
+ if (typeof window === "undefined") return false;
2970
+ if (typeof navigator === "undefined") return false;
2971
+ if (!navigator.credentials) return false;
2972
+ if (typeof PublicKeyCredential === "undefined") return false;
2973
+ return true;
2974
+ }
2975
+ async function hashCredentialId(credentialId) {
2976
+ const arrayBuffer = credentialId.buffer.slice(
2977
+ credentialId.byteOffset,
2978
+ credentialId.byteOffset + credentialId.byteLength
2979
+ );
2980
+ const hash = await crypto.subtle.digest("SHA-256", arrayBuffer);
2981
+ return Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
2982
+ }
2983
+
2984
+ // src/keys/encoding.ts
2985
+ function toBase642(data) {
2986
+ const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
2987
+ let binary = "";
2988
+ for (const b of bytes) binary += String.fromCharCode(b);
2989
+ return btoa(binary);
2990
+ }
2991
+ function fromBase642(b64) {
2992
+ const binary = atob(b64);
2993
+ const bytes = new Uint8Array(binary.length);
2994
+ for (let i = 0; i < binary.length; i++) {
2995
+ bytes[i] = binary.charCodeAt(i);
2996
+ }
2997
+ return bytes;
2998
+ }
2999
+ function hexToBytes(hex) {
3000
+ const bytes = new Uint8Array(hex.length / 2);
3001
+ for (let i = 0; i < hex.length; i += 2) {
3002
+ bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16);
3003
+ }
3004
+ return bytes;
3005
+ }
3006
+ function bytesToHex(bytes) {
3007
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3008
+ }
3009
+
3010
+ // src/keys/KeyVault.ts
3011
+ async function generateDek() {
3012
+ return crypto.subtle.generateKey(
3013
+ { name: "AES-GCM", length: 256 },
3014
+ true,
3015
+ // extractable — needed for AES-KW wrapping
3016
+ ["encrypt", "decrypt"]
3017
+ );
3018
+ }
3019
+ async function encryptMnemonic(dek, mnemonic) {
3020
+ const encoder = new TextEncoder();
3021
+ const iv = crypto.getRandomValues(new Uint8Array(12));
3022
+ const ciphertext = await crypto.subtle.encrypt(
3023
+ { name: "AES-GCM", iv },
3024
+ dek,
3025
+ encoder.encode(mnemonic)
3026
+ );
3027
+ return {
3028
+ encryptedMnemonic: toBase642(ciphertext),
3029
+ iv: toBase642(iv)
3030
+ };
3031
+ }
3032
+ async function decryptMnemonic(dek, encryptedMnemonic, iv) {
3033
+ const decoder = new TextDecoder();
3034
+ const plaintext = await crypto.subtle.decrypt(
3035
+ { name: "AES-GCM", iv: fromBase642(iv) },
3036
+ dek,
3037
+ fromBase642(encryptedMnemonic)
3038
+ );
3039
+ return decoder.decode(plaintext);
3040
+ }
3041
+ async function deriveKek(prfOutput) {
3042
+ const keyMaterial = await crypto.subtle.importKey(
3043
+ "raw",
3044
+ prfOutput,
3045
+ "HKDF",
3046
+ false,
3047
+ ["deriveKey"]
3048
+ );
3049
+ const encoder = new TextEncoder();
3050
+ return crypto.subtle.deriveKey(
3051
+ {
3052
+ name: "HKDF",
3053
+ hash: "SHA-256",
3054
+ salt: new Uint8Array(0),
3055
+ // PRF salt was already applied at the WebAuthn level
3056
+ info: encoder.encode("toon:kek")
3057
+ },
3058
+ keyMaterial,
3059
+ { name: "AES-KW", length: 256 },
3060
+ false,
3061
+ // not extractable
3062
+ ["wrapKey", "unwrapKey"]
3063
+ );
3064
+ }
3065
+ async function deriveKekFromPassword(password, salt) {
3066
+ const encoder = new TextEncoder();
3067
+ const keyMaterial = await crypto.subtle.importKey(
3068
+ "raw",
3069
+ encoder.encode(password),
3070
+ "PBKDF2",
3071
+ false,
3072
+ ["deriveKey"]
3073
+ );
3074
+ return crypto.subtle.deriveKey(
3075
+ {
3076
+ name: "PBKDF2",
3077
+ hash: "SHA-256",
3078
+ salt,
3079
+ iterations: 6e5
3080
+ // OWASP 2023 recommendation for SHA-256
3081
+ },
3082
+ keyMaterial,
3083
+ { name: "AES-KW", length: 256 },
3084
+ false,
3085
+ ["wrapKey", "unwrapKey"]
3086
+ );
3087
+ }
3088
+ async function wrapDek(kek, dek) {
3089
+ const wrapped = await crypto.subtle.wrapKey("raw", dek, kek, "AES-KW");
3090
+ return toBase642(new Uint8Array(wrapped));
3091
+ }
3092
+ async function unwrapDek(kek, wrappedDek) {
3093
+ return crypto.subtle.unwrapKey(
3094
+ "raw",
3095
+ fromBase642(wrappedDek),
3096
+ kek,
3097
+ "AES-KW",
3098
+ { name: "AES-GCM", length: 256 },
3099
+ true,
3100
+ // extractable — needed for re-wrapping when adding new KEKs
3101
+ ["encrypt", "decrypt"]
3102
+ );
3103
+ }
3104
+ async function createVault(mnemonic, kek, credentialIdHash, prfSalt) {
3105
+ const dek = await generateDek();
3106
+ const { encryptedMnemonic, iv } = await encryptMnemonic(dek, mnemonic);
3107
+ const wrappedDek = await wrapDek(kek, dek);
3108
+ const entry = {
3109
+ id: credentialIdHash,
3110
+ wrapped_dek: wrappedDek,
3111
+ salt: toBase642(prfSalt),
3112
+ created_at: Math.floor(Date.now() / 1e3)
3113
+ };
3114
+ return {
3115
+ encryptedMnemonic,
3116
+ iv,
3117
+ wrappedKeys: [entry]
3118
+ };
3119
+ }
3120
+ async function unlockVault(vault, kek, credentialIdHash) {
3121
+ const entry = vault.wrappedKeys.find((e) => e.id === credentialIdHash);
3122
+ if (!entry) {
3123
+ throw new Error(`No wrapped key found for credential ${credentialIdHash}`);
3124
+ }
3125
+ const dek = await unwrapDek(kek, entry.wrapped_dek);
3126
+ return decryptMnemonic(dek, vault.encryptedMnemonic, vault.iv);
3127
+ }
3128
+ async function addKekToVault(vault, existingKek, existingCredentialIdHash, newKek, newCredentialIdHash, newPrfSalt) {
3129
+ const existingEntry = vault.wrappedKeys.find(
3130
+ (e) => e.id === existingCredentialIdHash
3131
+ );
3132
+ if (!existingEntry) {
3133
+ throw new Error(
3134
+ `No wrapped key found for credential ${existingCredentialIdHash}`
3135
+ );
3136
+ }
3137
+ const dek = await unwrapDek(existingKek, existingEntry.wrapped_dek);
3138
+ const newWrappedDek = await wrapDek(newKek, dek);
3139
+ const newEntry = {
3140
+ id: newCredentialIdHash,
3141
+ wrapped_dek: newWrappedDek,
3142
+ salt: toBase642(newPrfSalt),
3143
+ created_at: Math.floor(Date.now() / 1e3)
3144
+ };
3145
+ return {
3146
+ ...vault,
3147
+ wrappedKeys: [...vault.wrappedKeys, newEntry]
3148
+ };
3149
+ }
3150
+ function removeKekFromVault(vault, credentialIdHash) {
3151
+ const remaining = vault.wrappedKeys.filter((e) => e.id !== credentialIdHash);
3152
+ if (remaining.length === 0) {
3153
+ throw new Error(
3154
+ "Cannot remove the last passkey \u2014 at least one passkey must remain for vault access"
3155
+ );
3156
+ }
3157
+ return {
3158
+ ...vault,
3159
+ wrappedKeys: remaining
3160
+ };
3161
+ }
3162
+ async function addRecoveryCodeToVault(vault, existingKek, existingCredentialIdHash, recoveryKek, recoverySalt) {
3163
+ const existingEntry = vault.wrappedKeys.find(
3164
+ (e) => e.id === existingCredentialIdHash
3165
+ );
3166
+ if (!existingEntry) {
3167
+ throw new Error(
3168
+ `No wrapped key found for credential ${existingCredentialIdHash}`
3169
+ );
3170
+ }
3171
+ const dek = await unwrapDek(existingKek, existingEntry.wrapped_dek);
3172
+ const recoveryWrappedDek = await wrapDek(recoveryKek, dek);
3173
+ return {
3174
+ ...vault,
3175
+ recoveryCodeWrappedDek: recoveryWrappedDek,
3176
+ recoveryCodeSalt: toBase642(recoverySalt)
3177
+ };
3178
+ }
3179
+ async function unlockVaultWithRecoveryCode(vault, recoveryKek) {
3180
+ if (!vault.recoveryCodeWrappedDek) {
3181
+ throw new Error("No recovery code is configured for this vault");
3182
+ }
3183
+ const dek = await unwrapDek(recoveryKek, vault.recoveryCodeWrappedDek);
3184
+ return decryptMnemonic(dek, vault.encryptedMnemonic, vault.iv);
3185
+ }
3186
+ function generateRecoveryCode() {
3187
+ const bytes = crypto.getRandomValues(new Uint8Array(12));
3188
+ const hex = Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
3189
+ const groups = hex.match(/.{4}/g) ?? [];
3190
+ return groups.join("-");
3191
+ }
3192
+
3193
+ // src/keys/BackupService.ts
3194
+ import { getPublicKey as getPublicKey3 } from "nostr-tools/pure";
3195
+ var BACKUP_KIND = 30078;
3196
+ var BACKUP_D_TAG = "toon:identity-backup";
3197
+ var BACKUP_VERSION = "1";
3198
+ function buildBackupEvent(vault, secretKey, chains = "nostr,evm,solana,mina") {
3199
+ const pubkey = getPublicKey3(secretKey);
3200
+ const payload = {
3201
+ encrypted_mnemonic: vault.encryptedMnemonic,
3202
+ wrapped_keys: vault.wrappedKeys,
3203
+ iv: vault.iv,
3204
+ ...vault.recoveryCodeWrappedDek && {
3205
+ recovery_code_wrapped_dek: vault.recoveryCodeWrappedDek
3206
+ },
3207
+ ...vault.recoveryCodeSalt && {
3208
+ recovery_code_salt: vault.recoveryCodeSalt
3209
+ }
3210
+ };
3211
+ return {
3212
+ kind: BACKUP_KIND,
3213
+ pubkey,
3214
+ created_at: Math.floor(Date.now() / 1e3),
3215
+ tags: [
3216
+ ["d", BACKUP_D_TAG],
3217
+ ["v", BACKUP_VERSION],
3218
+ ["chains", chains]
3219
+ ],
3220
+ content: JSON.stringify(payload)
3221
+ };
3222
+ }
3223
+ function buildBackupFilter(pubkey) {
3224
+ return {
3225
+ kinds: [BACKUP_KIND],
3226
+ authors: [pubkey],
3227
+ "#d": [BACKUP_D_TAG]
3228
+ };
3229
+ }
3230
+ function parseBackupPayload(content) {
3231
+ let parsed;
3232
+ try {
3233
+ parsed = JSON.parse(content);
3234
+ } catch {
3235
+ throw new Error("Invalid backup event content: not valid JSON");
3236
+ }
3237
+ if (typeof parsed !== "object" || parsed === null) {
3238
+ throw new Error("Invalid backup event content: not an object");
3239
+ }
3240
+ const payload = parsed;
3241
+ if (typeof payload["encrypted_mnemonic"] !== "string") {
3242
+ throw new Error("Invalid backup: missing encrypted_mnemonic");
3243
+ }
3244
+ if (typeof payload["iv"] !== "string") {
3245
+ throw new Error("Invalid backup: missing iv");
3246
+ }
3247
+ if (!Array.isArray(payload["wrapped_keys"])) {
3248
+ throw new Error("Invalid backup: missing wrapped_keys array");
3249
+ }
3250
+ for (const entry of payload["wrapped_keys"]) {
3251
+ if (typeof entry !== "object" || entry === null) {
3252
+ throw new Error("Invalid backup: wrapped_keys entry is not an object");
3253
+ }
3254
+ const e = entry;
3255
+ if (typeof e["id"] !== "string") {
3256
+ throw new Error("Invalid backup: wrapped key missing id");
3257
+ }
3258
+ if (typeof e["wrapped_dek"] !== "string") {
3259
+ throw new Error("Invalid backup: wrapped key missing wrapped_dek");
3260
+ }
3261
+ if (typeof e["salt"] !== "string") {
3262
+ throw new Error("Invalid backup: wrapped key missing salt");
3263
+ }
3264
+ if (typeof e["created_at"] !== "number") {
3265
+ throw new Error(
3266
+ "Invalid backup: wrapped key missing or invalid created_at"
3267
+ );
3268
+ }
3269
+ }
3270
+ return {
3271
+ encryptedMnemonic: payload["encrypted_mnemonic"],
3272
+ iv: payload["iv"],
3273
+ wrappedKeys: payload["wrapped_keys"],
3274
+ ...typeof payload["recovery_code_wrapped_dek"] === "string" && {
3275
+ recoveryCodeWrappedDek: payload["recovery_code_wrapped_dek"]
3276
+ },
3277
+ ...typeof payload["recovery_code_salt"] === "string" && {
3278
+ recoveryCodeSalt: payload["recovery_code_salt"]
3279
+ }
3280
+ };
3281
+ }
3282
+ async function publishBackupToRelays(signedEvent, relayUrls) {
3283
+ const { SimplePool } = await import("nostr-tools/pool");
3284
+ const pool = new SimplePool();
3285
+ try {
3286
+ await Promise.allSettled(
3287
+ relayUrls.map((url) => pool.publish([url], signedEvent))
3288
+ );
3289
+ } finally {
3290
+ pool.close(relayUrls);
3291
+ }
3292
+ }
3293
+ async function fetchBackupFromRelays(pubkey, relayUrls) {
3294
+ const { SimplePool } = await import("nostr-tools/pool");
3295
+ const pool = new SimplePool();
3296
+ try {
3297
+ const filter = buildBackupFilter(pubkey);
3298
+ const events = await pool.querySync(relayUrls, filter);
3299
+ if (!events || events.length === 0) {
3300
+ return null;
3301
+ }
3302
+ events.sort(
3303
+ (a, b) => b.created_at - a.created_at
3304
+ );
3305
+ const latest = events[0];
3306
+ if (!latest) return null;
3307
+ return parseBackupPayload(latest.content);
3308
+ } finally {
3309
+ pool.close(relayUrls);
3310
+ }
3311
+ }
3312
+
3313
+ // src/keys/KeyManager.ts
3314
+ var KeyManager = class {
3315
+ config;
3316
+ identity = null;
3317
+ vault = null;
3318
+ activeCredentialIdHash = null;
3319
+ constructor(config) {
3320
+ if (!config.relayUrls || config.relayUrls.length === 0) {
3321
+ throw new Error("KeyManager requires at least one relay URL");
3322
+ }
3323
+ this.config = {
3324
+ relayUrls: config.relayUrls,
3325
+ rpId: config.rpId ?? (typeof window !== "undefined" ? window.location.hostname : "localhost"),
3326
+ rpName: config.rpName ?? "TOON Protocol",
3327
+ storageKey: config.storageKey ?? "toon:keys"
3328
+ };
3329
+ }
3330
+ // --- Account Lifecycle ---
3331
+ /**
3332
+ * Create a new account: generate mnemonic, create Passkey, encrypt, backup.
3333
+ */
3334
+ async create() {
3335
+ const mnemonic = generateMnemonic();
3336
+ const identity = await deriveFullIdentity(mnemonic);
3337
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3338
+ const userIdBytes = hexToBytes(identity.nostr.pubkey);
3339
+ const registration = await registerPasskey({
3340
+ rpId: this.config.rpId,
3341
+ rpName: this.config.rpName,
3342
+ userId: userIdBytes,
3343
+ userName: `TOON ${identity.nostr.pubkey.slice(0, 8)}`,
3344
+ prfSalt
3345
+ });
3346
+ const kek = await deriveKek(registration.prfOutput);
3347
+ const credIdHash = await hashCredentialId(registration.credentialId);
3348
+ this.vault = await createVault(mnemonic, kek, credIdHash, prfSalt);
3349
+ this.identity = identity;
3350
+ this.activeCredentialIdHash = credIdHash;
3351
+ await this.saveToLocalStorage();
3352
+ await this.backupToRelay().catch(() => {
3353
+ });
3354
+ return identity;
3355
+ }
3356
+ /**
3357
+ * Recover an account using a synced Passkey.
3358
+ * The Nostr pubkey is extracted from the Passkey's userHandle.
3359
+ *
3360
+ * Flow: single assertion → userHandle → fetch backup → derive KEK → unlock.
3361
+ * If the local vault is available (has the PRF salt), we use a single assertion
3362
+ * with the correct salt. Otherwise, we need the backup from relays first, which
3363
+ * requires a discovery assertion to get the pubkey.
3364
+ */
3365
+ async recover() {
3366
+ const localVault = await this.loadFromLocalStorage();
3367
+ if (localVault) {
3368
+ return this.unlockWithVault(localVault);
3369
+ }
3370
+ const discovery = await assertPasskey({
3371
+ rpId: this.config.rpId,
3372
+ prfSalt: crypto.getRandomValues(new Uint8Array(32))
3373
+ // Dummy salt for discovery
3374
+ });
3375
+ if (!discovery.userHandle || discovery.userHandle.length === 0) {
3376
+ throw new Error(
3377
+ "Passkey did not return a userHandle. Cannot determine Nostr pubkey for recovery."
3378
+ );
3379
+ }
3380
+ const pubkey = bytesToHex(discovery.userHandle);
3381
+ const vault = await fetchBackupFromRelays(pubkey, this.config.relayUrls);
3382
+ if (!vault) {
3383
+ throw new Error(
3384
+ "No backup found on configured relays for this identity. Try importing with a mnemonic or nsec instead."
3385
+ );
3386
+ }
3387
+ const credIdHash = await hashCredentialId(discovery.credentialId);
3388
+ const entry = vault.wrappedKeys.find((e) => e.id === credIdHash);
3389
+ if (!entry) {
3390
+ throw new Error(
3391
+ "This Passkey is not registered with the backup. Try a different Passkey or use a recovery code."
3392
+ );
3393
+ }
3394
+ const saltBytes = fromBase642(entry.salt);
3395
+ const reassertion = await assertPasskey({
3396
+ rpId: this.config.rpId,
3397
+ prfSalt: saltBytes,
3398
+ allowCredentials: [discovery.credentialId]
3399
+ });
3400
+ const kek = await deriveKek(reassertion.prfOutput);
3401
+ const mnemonic = await unlockVault(vault, kek, credIdHash);
3402
+ const identity = await deriveFullIdentity(mnemonic);
3403
+ this.vault = vault;
3404
+ this.identity = identity;
3405
+ this.activeCredentialIdHash = credIdHash;
3406
+ await this.saveToLocalStorage();
3407
+ return identity;
3408
+ }
3409
+ /**
3410
+ * Import an existing BIP-39 mnemonic. Creates a Passkey and backup.
3411
+ */
3412
+ async importMnemonic(mnemonic) {
3413
+ if (!validateMnemonic(mnemonic)) {
3414
+ throw new Error("Invalid BIP-39 mnemonic phrase");
3415
+ }
3416
+ const identity = await deriveFullIdentity(mnemonic);
3417
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3418
+ const userIdBytes = hexToBytes(identity.nostr.pubkey);
3419
+ const registration = await registerPasskey({
3420
+ rpId: this.config.rpId,
3421
+ rpName: this.config.rpName,
3422
+ userId: userIdBytes,
3423
+ userName: `TOON ${identity.nostr.pubkey.slice(0, 8)}`,
3424
+ prfSalt
3425
+ });
3426
+ const kek = await deriveKek(registration.prfOutput);
3427
+ const credIdHash = await hashCredentialId(registration.credentialId);
3428
+ this.vault = await createVault(mnemonic, kek, credIdHash, prfSalt);
3429
+ this.identity = identity;
3430
+ this.activeCredentialIdHash = credIdHash;
3431
+ await this.saveToLocalStorage();
3432
+ await this.backupToRelay().catch(() => {
3433
+ });
3434
+ return identity;
3435
+ }
3436
+ /**
3437
+ * Import from an nsec (Nostr-only key).
3438
+ * Nostr + EVM are derived; Solana + Mina get fresh keys (not deterministically linked).
3439
+ */
3440
+ async importNsec(nsec) {
3441
+ const decoded = nip19.decode(nsec);
3442
+ if (decoded.type !== "nsec") {
3443
+ throw new Error("Invalid nsec string");
3444
+ }
3445
+ const secretKey = decoded.data;
3446
+ const identity = deriveFromNsec(secretKey);
3447
+ if (isPrfSupported()) {
3448
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3449
+ const userIdBytes = hexToBytes(identity.nostr.pubkey);
3450
+ try {
3451
+ const registration = await registerPasskey({
3452
+ rpId: this.config.rpId,
3453
+ rpName: this.config.rpName,
3454
+ userId: userIdBytes,
3455
+ userName: `TOON ${identity.nostr.pubkey.slice(0, 8)}`,
3456
+ prfSalt
3457
+ });
3458
+ const kek = await deriveKek(registration.prfOutput);
3459
+ const credIdHash = await hashCredentialId(registration.credentialId);
3460
+ const hexKey = bytesToHex(secretKey);
3461
+ this.vault = await createVault(hexKey, kek, credIdHash, prfSalt);
3462
+ this.activeCredentialIdHash = credIdHash;
3463
+ await this.saveToLocalStorage();
3464
+ } catch {
3465
+ }
3466
+ }
3467
+ this.identity = identity;
3468
+ return identity;
3469
+ }
3470
+ // --- Passkey Management ---
3471
+ /**
3472
+ * Register an additional Passkey for this identity.
3473
+ */
3474
+ async addPasskey() {
3475
+ if (!this.identity || !this.vault || !this.activeCredentialIdHash) {
3476
+ throw new Error("No active identity \u2014 call create() or recover() first");
3477
+ }
3478
+ const prfSalt = crypto.getRandomValues(new Uint8Array(32));
3479
+ const userIdBytes = hexToBytes(this.identity.nostr.pubkey);
3480
+ const registration = await registerPasskey({
3481
+ rpId: this.config.rpId,
3482
+ rpName: this.config.rpName,
3483
+ userId: userIdBytes,
3484
+ userName: `TOON ${this.identity.nostr.pubkey.slice(0, 8)}`,
3485
+ prfSalt
3486
+ });
3487
+ const newKek = await deriveKek(registration.prfOutput);
3488
+ const newCredIdHash = await hashCredentialId(registration.credentialId);
3489
+ const currentEntry = this.vault.wrappedKeys.find(
3490
+ (e) => e.id === this.activeCredentialIdHash
3491
+ );
3492
+ if (!currentEntry) {
3493
+ throw new Error("Active credential not found in vault");
3494
+ }
3495
+ const currentSaltBytes = fromBase642(currentEntry.salt);
3496
+ const currentAssertion = await assertPasskey({
3497
+ rpId: this.config.rpId,
3498
+ prfSalt: currentSaltBytes
3499
+ });
3500
+ const currentKek = await deriveKek(currentAssertion.prfOutput);
3501
+ this.vault = await addKekToVault(
3502
+ this.vault,
3503
+ currentKek,
3504
+ this.activeCredentialIdHash,
3505
+ newKek,
3506
+ newCredIdHash,
3507
+ prfSalt
3508
+ );
3509
+ await this.saveToLocalStorage();
3510
+ await this.backupToRelay().catch(() => {
3511
+ });
3512
+ }
3513
+ /**
3514
+ * List registered Passkey credentials.
3515
+ */
3516
+ listPasskeys() {
3517
+ if (!this.vault) return [];
3518
+ return this.vault.wrappedKeys.map((entry) => ({
3519
+ credentialIdHash: entry.id,
3520
+ createdAt: entry.created_at
3521
+ }));
3522
+ }
3523
+ /**
3524
+ * Remove a Passkey from the vault. Cannot remove the last one.
3525
+ */
3526
+ async removePasskey(credentialIdHash) {
3527
+ if (!this.vault) {
3528
+ throw new Error("No active vault");
3529
+ }
3530
+ this.vault = removeKekFromVault(this.vault, credentialIdHash);
3531
+ if (this.activeCredentialIdHash === credentialIdHash) {
3532
+ const remaining = this.vault.wrappedKeys[0];
3533
+ this.activeCredentialIdHash = remaining ? remaining.id : null;
3534
+ }
3535
+ await this.saveToLocalStorage();
3536
+ await this.backupToRelay().catch(() => {
3537
+ });
3538
+ }
3539
+ // --- Recovery ---
3540
+ /**
3541
+ * Generate a printable recovery code and add it to the vault.
3542
+ * The PBKDF2 salt is persisted alongside the wrapped DEK so the code
3543
+ * can be verified later without the original salt.
3544
+ *
3545
+ * @returns The recovery code — user must store it securely.
3546
+ */
3547
+ async generateRecoveryCode() {
3548
+ if (!this.vault || !this.activeCredentialIdHash) {
3549
+ throw new Error("No active vault");
3550
+ }
3551
+ const code = generateRecoveryCode();
3552
+ const salt = crypto.getRandomValues(new Uint8Array(16));
3553
+ const recoveryKek = await deriveKekFromPassword(code, salt);
3554
+ const currentEntry = this.vault.wrappedKeys.find(
3555
+ (e) => e.id === this.activeCredentialIdHash
3556
+ );
3557
+ if (!currentEntry) {
3558
+ throw new Error("Active credential not found in vault");
3559
+ }
3560
+ const currentSaltBytes = fromBase642(currentEntry.salt);
3561
+ const currentAssertion = await assertPasskey({
3562
+ rpId: this.config.rpId,
3563
+ prfSalt: currentSaltBytes
3564
+ });
3565
+ const currentKek = await deriveKek(currentAssertion.prfOutput);
3566
+ this.vault = await addRecoveryCodeToVault(
3567
+ this.vault,
3568
+ currentKek,
3569
+ this.activeCredentialIdHash,
3570
+ recoveryKek,
3571
+ salt
3572
+ );
3573
+ await this.saveToLocalStorage();
3574
+ await this.backupToRelay().catch(() => {
3575
+ });
3576
+ return code;
3577
+ }
3578
+ /**
3579
+ * Recover identity using a recovery code.
3580
+ * The PBKDF2 salt is read from the persisted vault data.
3581
+ */
3582
+ async recoverWithCode(code) {
3583
+ const vault = await this.loadFromLocalStorage();
3584
+ if (!vault) {
3585
+ throw new Error(
3586
+ "No local vault found. Recovery code requires the encrypted vault. If you have a Passkey, use recover() to fetch from relays first."
3587
+ );
3588
+ }
3589
+ if (!vault.recoveryCodeWrappedDek || !vault.recoveryCodeSalt) {
3590
+ throw new Error("No recovery code configured for this vault");
3591
+ }
3592
+ const salt = fromBase642(vault.recoveryCodeSalt);
3593
+ const recoveryKek = await deriveKekFromPassword(code, salt);
3594
+ const mnemonic = await unlockVaultWithRecoveryCode(vault, recoveryKek);
3595
+ const identity = await deriveFullIdentity(mnemonic);
3596
+ this.vault = vault;
3597
+ this.identity = identity;
3598
+ return identity;
3599
+ }
3600
+ // --- Key Access ---
3601
+ /**
3602
+ * Get the current identity, or null if not unlocked.
3603
+ */
3604
+ getIdentity() {
3605
+ return this.identity;
3606
+ }
3607
+ /**
3608
+ * Get the Nostr secret key. Throws if not unlocked.
3609
+ */
3610
+ getNostrSecretKey() {
3611
+ if (!this.identity) throw new Error("Identity not unlocked");
3612
+ return this.identity.nostr.secretKey;
3613
+ }
3614
+ /**
3615
+ * Get an EvmSigner instance. Throws if not unlocked.
3616
+ */
3617
+ getEvmSigner() {
3618
+ if (!this.identity) throw new Error("Identity not unlocked");
3619
+ return new EvmSigner(this.identity.evm.privateKey);
3620
+ }
3621
+ /**
3622
+ * Get a SolanaSigner instance. Throws if not unlocked or Solana not derived.
3623
+ */
3624
+ getSolanaSigner() {
3625
+ if (!this.identity) throw new Error("Identity not unlocked");
3626
+ if (!this.identity.solana.publicKey) {
3627
+ throw new Error(
3628
+ "Solana keys not available \u2014 was this imported from nsec?"
3629
+ );
3630
+ }
3631
+ return new SolanaSigner(this.identity.solana.secretKey);
3632
+ }
3633
+ /**
3634
+ * Get a MinaSigner instance. Throws if not unlocked or Mina not derived.
3635
+ */
3636
+ getMinaSigner() {
3637
+ if (!this.identity) throw new Error("Identity not unlocked");
3638
+ if (!this.identity.mina.publicKey) {
3639
+ throw new Error("Mina keys not available \u2014 was this imported from nsec?");
3640
+ }
3641
+ return new MinaSigner(this.identity.mina.privateKey);
3642
+ }
3643
+ // --- Backup ---
3644
+ /**
3645
+ * Publish the current vault to configured relays as a kind:30078 event.
3646
+ */
3647
+ async backupToRelay() {
3648
+ if (!this.identity || !this.vault) {
3649
+ throw new Error("No active identity or vault to backup");
3650
+ }
3651
+ const eventTemplate = buildBackupEvent(
3652
+ this.vault,
3653
+ this.identity.nostr.secretKey
3654
+ );
3655
+ const signedEvent = finalizeEvent(
3656
+ eventTemplate,
3657
+ this.identity.nostr.secretKey
3658
+ );
3659
+ await publishBackupToRelays(signedEvent, this.config.relayUrls);
3660
+ }
3661
+ // --- Lock/Unlock ---
3662
+ /**
3663
+ * Clear keys from memory. The vault remains in IndexedDB.
3664
+ * Note: JavaScript strings (mnemonics) cannot be zeroed — only Uint8Array keys are cleared.
3665
+ */
3666
+ lock() {
3667
+ if (this.identity) {
3668
+ this.identity.nostr.secretKey.fill(0);
3669
+ this.identity.evm.privateKey.fill(0);
3670
+ this.identity.solana.secretKey.fill(0);
3671
+ }
3672
+ this.identity = null;
3673
+ }
3674
+ /**
3675
+ * Re-assert Passkey to decrypt local vault and restore identity.
3676
+ * Uses the local vault's stored PRF salt for a single biometric prompt.
3677
+ */
3678
+ async unlock() {
3679
+ const vault = await this.loadFromLocalStorage();
3680
+ if (!vault) {
3681
+ throw new Error("No local vault found \u2014 use create() or recover()");
3682
+ }
3683
+ return this.unlockWithVault(vault);
3684
+ }
3685
+ // --- Private helpers ---
3686
+ /**
3687
+ * Unlock a vault with a single Passkey assertion using stored PRF salts.
3688
+ * If the vault has only one credential, uses allowCredentials to constrain.
3689
+ */
3690
+ async unlockWithVault(vault) {
3691
+ const firstEntry = vault.wrappedKeys[0];
3692
+ if (!firstEntry) {
3693
+ throw new Error("Vault has no registered credentials");
3694
+ }
3695
+ const assertion = await assertPasskey({
3696
+ rpId: this.config.rpId,
3697
+ prfSalt: fromBase642(firstEntry.salt)
3698
+ });
3699
+ const credIdHash = await hashCredentialId(assertion.credentialId);
3700
+ const matchingEntry = vault.wrappedKeys.find((e) => e.id === credIdHash);
3701
+ if (!matchingEntry) {
3702
+ throw new Error("This Passkey is not registered with the local vault");
3703
+ }
3704
+ let prfOutput = assertion.prfOutput;
3705
+ if (matchingEntry.id !== firstEntry.id) {
3706
+ const correctSalt = fromBase642(matchingEntry.salt);
3707
+ const reassertion = await assertPasskey({
3708
+ rpId: this.config.rpId,
3709
+ prfSalt: correctSalt,
3710
+ allowCredentials: [assertion.credentialId]
3711
+ });
3712
+ prfOutput = reassertion.prfOutput;
3713
+ }
3714
+ const kek = await deriveKek(prfOutput);
3715
+ const mnemonic = await unlockVault(vault, kek, credIdHash);
3716
+ const identity = await deriveFullIdentity(mnemonic);
3717
+ this.vault = vault;
3718
+ this.identity = identity;
3719
+ this.activeCredentialIdHash = credIdHash;
3720
+ return identity;
3721
+ }
3722
+ // --- IndexedDB Persistence ---
3723
+ async saveToLocalStorage() {
3724
+ if (!this.vault) return;
3725
+ if (typeof indexedDB === "undefined") return;
3726
+ const dbName = this.config.storageKey;
3727
+ const db = await openDb(dbName);
3728
+ const tx = db.transaction("vault", "readwrite");
3729
+ const store = tx.objectStore("vault");
3730
+ store.put(JSON.stringify(this.vault), "current");
3731
+ await new Promise((resolve, reject) => {
3732
+ tx.oncomplete = () => resolve();
3733
+ tx.onerror = () => reject(tx.error);
3734
+ });
3735
+ db.close();
3736
+ }
3737
+ async loadFromLocalStorage() {
3738
+ if (typeof indexedDB === "undefined") return null;
3739
+ const dbName = this.config.storageKey;
3740
+ try {
3741
+ const db = await openDb(dbName);
3742
+ const tx = db.transaction("vault", "readonly");
3743
+ const store = tx.objectStore("vault");
3744
+ const request = store.get("current");
3745
+ const result = await new Promise(
3746
+ (resolve, reject) => {
3747
+ request.onsuccess = () => resolve(request.result);
3748
+ request.onerror = () => reject(request.error);
3749
+ }
3750
+ );
3751
+ db.close();
3752
+ if (!result) return null;
3753
+ return JSON.parse(result);
3754
+ } catch {
3755
+ return null;
3756
+ }
3757
+ }
3758
+ };
3759
+ function openDb(name) {
3760
+ return new Promise((resolve, reject) => {
3761
+ const request = indexedDB.open(name, 1);
3762
+ request.onupgradeneeded = () => {
3763
+ const db = request.result;
3764
+ if (!db.objectStoreNames.contains("vault")) {
3765
+ db.createObjectStore("vault");
3766
+ }
3767
+ };
3768
+ request.onsuccess = () => resolve(request.result);
3769
+ request.onerror = () => reject(request.error);
3770
+ });
3771
+ }
1801
3772
  export {
1802
3773
  BtpRuntimeClient,
1803
3774
  ChannelManager,
@@ -1805,14 +3776,24 @@ export {
1805
3776
  EvmSigner,
1806
3777
  HttpConnectorAdmin,
1807
3778
  HttpRuntimeClient,
3779
+ KeyManager,
1808
3780
  NetworkError,
1809
3781
  OnChainChannelClient,
1810
3782
  ToonClient,
1811
3783
  ToonClientError,
1812
3784
  ValidationError,
1813
3785
  applyDefaults,
3786
+ buildBackupEvent,
3787
+ buildBackupFilter,
1814
3788
  buildSettlementInfo,
3789
+ deriveFromNsec,
3790
+ deriveFullIdentity,
3791
+ generateMnemonic,
3792
+ generateRandomIdentity,
3793
+ isPrfSupported,
3794
+ parseBackupPayload,
1815
3795
  validateConfig,
3796
+ validateMnemonic,
1816
3797
  withRetry
1817
3798
  };
1818
3799
  //# sourceMappingURL=index.js.map