@utxopia/sdk 0.1.0-alpha.2 → 0.1.0-alpha.3

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.
Files changed (142) hide show
  1. package/README.md +214 -108
  2. package/{packages/sdk/dist → dist}/client.d.ts +25 -1
  3. package/{packages/sdk/dist → dist}/client.js +36 -1
  4. package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
  5. package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
  6. package/{packages/sdk/dist → dist}/index.d.ts +3 -3
  7. package/{packages/sdk/dist → dist}/index.js +3 -3
  8. package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
  9. package/{packages/sdk/dist → dist}/instructions.js +45 -15
  10. package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
  11. package/{packages/sdk/dist → dist}/stealth.js +146 -14
  12. package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
  13. package/{packages/sdk/dist → dist}/taproot.js +54 -2
  14. package/package.json +86 -63
  15. package/src/announcement-client.ts +457 -0
  16. package/src/auditor-ciphertext.ts +181 -0
  17. package/src/auditor.ts +409 -0
  18. package/src/bitcoin/ika.ts +103 -0
  19. package/src/bitcoin/index.ts +5 -0
  20. package/src/bound-params.ts +322 -0
  21. package/src/chadbuffer.ts +603 -0
  22. package/src/circomlibjs.d.ts +51 -0
  23. package/src/claim-link.ts +53 -0
  24. package/src/client.ts +638 -0
  25. package/src/commitment-tree.ts +736 -0
  26. package/src/config.ts +772 -0
  27. package/src/core/esplora.ts +332 -0
  28. package/src/core/mempool.ts +159 -0
  29. package/src/crypto-babyjub.ts +385 -0
  30. package/src/crypto-ed25519.ts +297 -0
  31. package/src/crypto.ts +199 -0
  32. package/src/event-client.ts +231 -0
  33. package/src/events.ts +384 -0
  34. package/src/explorer.ts +300 -0
  35. package/src/index.ts +902 -0
  36. package/src/instructions.ts +2820 -0
  37. package/src/keys.ts +1228 -0
  38. package/src/logger.ts +41 -0
  39. package/src/magicblock.ts +278 -0
  40. package/src/merkle.ts +197 -0
  41. package/src/note.ts +754 -0
  42. package/src/pda.ts +516 -0
  43. package/src/pool-state.ts +176 -0
  44. package/src/poseidon.ts +175 -0
  45. package/src/prover/index.ts +19 -0
  46. package/src/prover/mobile.ts +303 -0
  47. package/src/prover/web.ts +771 -0
  48. package/src/psbt.ts +333 -0
  49. package/src/selective-disclosure.ts +284 -0
  50. package/src/sender-memo.ts +343 -0
  51. package/src/snarkjs.d.ts +19 -0
  52. package/src/sns-resolver.ts +333 -0
  53. package/src/solana/connection.ts +189 -0
  54. package/src/solana/priority-fee.ts +201 -0
  55. package/src/spend-doc.ts +163 -0
  56. package/src/stealth.ts +1477 -0
  57. package/src/taproot.ts +707 -0
  58. package/src/token-registry.ts +207 -0
  59. package/src/utils/encoding.ts +33 -0
  60. package/src/vk-registry.ts +295 -0
  61. package/LICENSE +0 -21
  62. package/packages/btc-client/src/esplora-client.ts +0 -153
  63. package/packages/btc-client/src/index.ts +0 -3
  64. package/packages/btc-client/src/op-return.ts +0 -93
  65. package/packages/btc-client/src/types.ts +0 -112
  66. package/packages/sdk/README.md +0 -277
  67. /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
  68. /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
  69. /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
  70. /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
  71. /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
  72. /package/{packages/sdk/dist → dist}/auditor.js +0 -0
  73. /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
  74. /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
  75. /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
  76. /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
  77. /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
  78. /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
  79. /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
  80. /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
  81. /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
  82. /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
  83. /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
  84. /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
  85. /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
  86. /package/{packages/sdk/dist → dist}/config.js +0 -0
  87. /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
  88. /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
  89. /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
  90. /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
  91. /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
  92. /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
  93. /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
  94. /package/{packages/sdk/dist → dist}/crypto.js +0 -0
  95. /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
  96. /package/{packages/sdk/dist → dist}/event-client.js +0 -0
  97. /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
  98. /package/{packages/sdk/dist → dist}/events.js +0 -0
  99. /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
  100. /package/{packages/sdk/dist → dist}/explorer.js +0 -0
  101. /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
  102. /package/{packages/sdk/dist → dist}/keys.js +0 -0
  103. /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
  104. /package/{packages/sdk/dist → dist}/logger.js +0 -0
  105. /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
  106. /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
  107. /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
  108. /package/{packages/sdk/dist → dist}/merkle.js +0 -0
  109. /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
  110. /package/{packages/sdk/dist → dist}/note.js +0 -0
  111. /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
  112. /package/{packages/sdk/dist → dist}/pda.js +0 -0
  113. /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
  114. /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
  115. /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
  116. /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
  117. /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
  118. /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
  119. /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
  120. /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
  121. /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
  122. /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
  123. /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
  124. /package/{packages/sdk/dist → dist}/psbt.js +0 -0
  125. /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
  126. /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
  127. /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
  128. /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
  129. /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
  130. /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
  131. /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
  132. /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
  133. /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
  134. /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
  135. /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
  136. /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
  137. /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
  138. /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
  139. /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
  140. /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
  141. /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
  142. /package/{packages/sdk/dist → dist}/vk-registry.js +0 -0
package/src/crypto.ts ADDED
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Cryptographic utilities for UTXOpia
3
+ *
4
+ * This module provides:
5
+ * - Field constants (BN254)
6
+ * - Byte conversion utilities
7
+ * - SHA-256 and tagged hashing
8
+ * - Re-exports from Baby Jubjub (spending keys) and Ed25519 (viewing keys)
9
+ *
10
+ * Baby Jubjub: Twisted Edwards curve for SNARK-friendly spending key operations
11
+ * Ed25519/X25519: Fast standard curve for viewing key ECDH (off-chain only)
12
+ *
13
+ * @module crypto
14
+ */
15
+
16
+ import { sha256 } from "@noble/hashes/sha2.js";
17
+
18
+ // =============================================================================
19
+ // Field Constants
20
+ // =============================================================================
21
+
22
+ /** BN254 field prime (used by circom/snarkjs, also Baby Jubjub base field) */
23
+ export const BN254_FIELD_PRIME =
24
+ 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
25
+
26
+ // =============================================================================
27
+ // Re-exports from Baby Jubjub (spending keys)
28
+ // =============================================================================
29
+
30
+ export {
31
+ BABYJUB_FIELD_PRIME,
32
+ BABYJUB_A,
33
+ BABYJUB_D,
34
+ BABYJUB_ORDER,
35
+ BABYJUB_BASE8,
36
+ BABYJUB_IDENTITY,
37
+ babyJubAdd,
38
+ babyJubDouble,
39
+ babyJubMul,
40
+ babyJubNegate,
41
+ isOnBabyJubCurve,
42
+ isIdentity,
43
+ babyJubCompress,
44
+ babyJubDecompress,
45
+ generateBabyJubKeyPair,
46
+ deriveBabyJubKeyFromSeed,
47
+ babyJubScalarFromBytes,
48
+ babyJubScalarToBytes,
49
+ type BabyJubPoint,
50
+ } from "./crypto-babyjub";
51
+
52
+ // =============================================================================
53
+ // Re-exports from Ed25519 (viewing keys)
54
+ // =============================================================================
55
+
56
+ export {
57
+ ed25519GenerateKeyPair,
58
+ ed25519GetPublicKey,
59
+ ed25519DeriveKeyFromSeed,
60
+ ed25519PubToX25519,
61
+ ed25519PrivToX25519,
62
+ x25519Ecdh,
63
+ x25519PubFromPriv,
64
+ x25519EcdhRaw,
65
+ deriveAmountKey,
66
+ encryptAmountEd25519,
67
+ decryptAmountEd25519,
68
+ encryptNoteData,
69
+ decryptNoteData,
70
+ } from "./crypto-ed25519";
71
+
72
+ // =============================================================================
73
+ // Byte Conversion Utilities
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Generate a random field element (< BN254 prime)
78
+ */
79
+ export function randomFieldElement(): bigint {
80
+ const bytes = new Uint8Array(32);
81
+ crypto.getRandomValues(bytes);
82
+ return bytesToBigint(bytes) % BN254_FIELD_PRIME;
83
+ }
84
+
85
+ /**
86
+ * Convert bigint to 32-byte Uint8Array (big-endian)
87
+ */
88
+ export function bigintToBytes(value: bigint): Uint8Array {
89
+ const bytes = new Uint8Array(32);
90
+ let temp = value;
91
+ for (let i = 31; i >= 0; i--) {
92
+ bytes[i] = Number(temp & 0xffn);
93
+ temp = temp >> 8n;
94
+ }
95
+ return bytes;
96
+ }
97
+
98
+ /**
99
+ * Convert Uint8Array to bigint (big-endian)
100
+ */
101
+ export function bytesToBigint(bytes: Uint8Array): bigint {
102
+ let result = 0n;
103
+ for (let i = 0; i < bytes.length; i++) {
104
+ result = (result << 8n) | BigInt(bytes[i]);
105
+ }
106
+ return result;
107
+ }
108
+
109
+ /**
110
+ * Convert hex string to Uint8Array
111
+ */
112
+ export function hexToBytes(hex: string): Uint8Array {
113
+ const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex;
114
+ const bytes = new Uint8Array(cleanHex.length / 2);
115
+ for (let i = 0; i < cleanHex.length; i += 2) {
116
+ bytes[i / 2] = parseInt(cleanHex.substr(i, 2), 16);
117
+ }
118
+ return bytes;
119
+ }
120
+
121
+ /**
122
+ * Convert Uint8Array to hex string
123
+ */
124
+ export function bytesToHex(bytes: Uint8Array): string {
125
+ return Array.from(bytes)
126
+ .map((b) => b.toString(16).padStart(2, "0"))
127
+ .join("");
128
+ }
129
+
130
+ // =============================================================================
131
+ // Hashing Utilities
132
+ // =============================================================================
133
+
134
+ /**
135
+ * SHA-256 hash using @noble/hashes
136
+ */
137
+ export function sha256Hash(data: Uint8Array): Uint8Array {
138
+ return sha256(data);
139
+ }
140
+
141
+ /**
142
+ * Double SHA256 hash (Bitcoin standard)
143
+ */
144
+ export function doubleSha256(data: Uint8Array): Uint8Array {
145
+ return sha256(sha256(data));
146
+ }
147
+
148
+ /**
149
+ * Tagged hash as used in BIP-340/341 (Taproot)
150
+ * H_tag(x) = SHA256(SHA256(tag) || SHA256(tag) || x)
151
+ */
152
+ export function taggedHash(tag: string, data: Uint8Array): Uint8Array {
153
+ const encoder = new TextEncoder();
154
+ const tagBytes = encoder.encode(tag);
155
+ const tagHash = sha256(tagBytes);
156
+
157
+ // Concatenate: SHA256(tag) || SHA256(tag) || data
158
+ const combined = new Uint8Array(64 + data.length);
159
+ combined.set(tagHash, 0);
160
+ combined.set(tagHash, 32);
161
+ combined.set(data, 64);
162
+
163
+ return sha256(combined);
164
+ }
165
+
166
+ // =============================================================================
167
+ // Scalar Utilities (curve-agnostic, uses Baby Jubjub order)
168
+ // =============================================================================
169
+
170
+ import { BABYJUB_ORDER } from "./crypto-babyjub";
171
+
172
+ function mod(n: bigint, p: bigint): bigint {
173
+ const result = n % p;
174
+ return result >= 0n ? result : result + p;
175
+ }
176
+
177
+ /**
178
+ * Derive a scalar from bytes (reduces modulo Baby Jubjub subgroup order)
179
+ */
180
+ export function scalarFromBytes(bytes: Uint8Array): bigint {
181
+ let result = 0n;
182
+ for (let i = 0; i < bytes.length; i++) {
183
+ result = (result << 8n) | BigInt(bytes[i]);
184
+ }
185
+ return mod(result, BABYJUB_ORDER);
186
+ }
187
+
188
+ /**
189
+ * Convert a bigint scalar to 32 bytes (big-endian)
190
+ */
191
+ export function scalarToBytes(scalar: bigint): Uint8Array {
192
+ const bytes = new Uint8Array(32);
193
+ let temp = mod(scalar, BABYJUB_ORDER);
194
+ for (let i = 31; i >= 0; i--) {
195
+ bytes[i] = Number(temp & 0xffn);
196
+ temp = temp >> 8n;
197
+ }
198
+ return bytes;
199
+ }
@@ -0,0 +1,231 @@
1
+ /**
2
+ * EventClient — Unified event stream client extending AnnouncementClient
3
+ *
4
+ * Handles all three backend event types via /ws/events:
5
+ * - leaf_inserted (tree updates)
6
+ * - nullifier_spent
7
+ * - stealth_announcement (delegated to parent AnnouncementClient)
8
+ *
9
+ * Also provides REST helpers for tree status and nullifier fetching.
10
+ */
11
+
12
+ import {
13
+ AnnouncementClient,
14
+ type AnnouncementClientConfig,
15
+ } from "./announcement-client";
16
+ import type { OnChainStealthAnnouncement } from "./stealth";
17
+ import { hexToBytes } from "./crypto";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Types
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface LeafInsertedEvent {
24
+ type: "leaf_inserted";
25
+ leaf_index: number;
26
+ commitment: string;
27
+ new_root: string;
28
+ }
29
+
30
+ export interface NullifierSpentEvent {
31
+ type: "nullifier_spent";
32
+ nullifier_hash: string;
33
+ slot: number;
34
+ }
35
+
36
+ export interface AnnouncementEvent {
37
+ type: "stealth_announcement";
38
+ announcement_type: number;
39
+ ephemeral_pub: string;
40
+ encrypted_amount: string;
41
+ commitment: string;
42
+ leaf_index: number;
43
+ block_time?: number;
44
+ }
45
+
46
+ export type ServerEvent =
47
+ | LeafInsertedEvent
48
+ | NullifierSpentEvent
49
+ | AnnouncementEvent;
50
+
51
+ export type EventListener<T> = (event: T) => void;
52
+
53
+ export interface TreeStatusResponse {
54
+ root: string;
55
+ next_index: number;
56
+ size: number;
57
+ }
58
+
59
+ export interface NullifierPdasResponse {
60
+ pdas: string[];
61
+ total: number;
62
+ latest_slot: number;
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // EventClient
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export class EventClient extends AnnouncementClient {
70
+ private treeListeners = new Set<EventListener<LeafInsertedEvent>>();
71
+ private nullifierListeners = new Set<EventListener<NullifierSpentEvent>>();
72
+ private spentNullifiers = new Set<string>();
73
+ private nullifierLatestSlot = 0;
74
+
75
+ /** Subscribe to tree (leaf_inserted) events */
76
+ onTreeUpdate(listener: EventListener<LeafInsertedEvent>): () => void {
77
+ this.treeListeners.add(listener);
78
+ return () => this.treeListeners.delete(listener);
79
+ }
80
+
81
+ /** Subscribe to nullifier (nullifier_spent) events */
82
+ onNullifierSpent(listener: EventListener<NullifierSpentEvent>): () => void {
83
+ this.nullifierListeners.add(listener);
84
+ return () => this.nullifierListeners.delete(listener);
85
+ }
86
+
87
+ /** Fetch tree status from backend */
88
+ async fetchTreeStatus(): Promise<TreeStatusResponse | null> {
89
+ try {
90
+ const resp = await fetch(
91
+ `${this.config.backendUrl}/api/tree/status`,
92
+ { signal: AbortSignal.timeout(this.restTimeout) },
93
+ );
94
+ if (!resp.ok) return null;
95
+ return (await resp.json()) as TreeStatusResponse;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /** Fetch spent nullifier PDAs with incremental caching */
102
+ async fetchSpentNullifiers(): Promise<Set<string>> {
103
+ try {
104
+ const since =
105
+ this.nullifierLatestSlot > 0
106
+ ? `?since=${this.nullifierLatestSlot}`
107
+ : "";
108
+ const resp = await fetch(
109
+ `${this.config.backendUrl}/api/nullifiers${since}`,
110
+ { signal: AbortSignal.timeout(this.restTimeout) },
111
+ );
112
+ if (!resp.ok) return this.spentNullifiers;
113
+ const data: NullifierPdasResponse = await resp.json();
114
+ for (const pda of data.pdas || []) this.spentNullifiers.add(pda);
115
+ if (data.latest_slot > this.nullifierLatestSlot) {
116
+ this.nullifierLatestSlot = data.latest_slot;
117
+ }
118
+ return this.spentNullifiers;
119
+ } catch {
120
+ return this.spentNullifiers;
121
+ }
122
+ }
123
+
124
+ /** Check if a specific nullifier PDA is in the spent set */
125
+ isNullifierSpent(pda: string): boolean {
126
+ return this.spentNullifiers.has(pda);
127
+ }
128
+
129
+ /** Stop client, close WS, clear extra listeners */
130
+ close(): void {
131
+ super.close();
132
+ this.treeListeners.clear();
133
+ this.nullifierListeners.clear();
134
+ }
135
+
136
+ // -----------------------------------------------------------------------
137
+ // Override: connect to /ws/events instead of /ws/announcements
138
+ // -----------------------------------------------------------------------
139
+
140
+ protected connectWs(): void {
141
+ if (this.closed) return;
142
+
143
+ try {
144
+ const url = `${this.wsUrl}/ws/events`;
145
+ this.ws = new WebSocket(url);
146
+
147
+ this.ws.onopen = () => {
148
+ this.wsConnected = true;
149
+ this.wsReconnectDelay = 1000;
150
+ };
151
+
152
+ this.ws.onmessage = (event) => {
153
+ try {
154
+ const data: ServerEvent = JSON.parse(
155
+ typeof event.data === "string" ? event.data : "",
156
+ );
157
+ this.dispatchEvent(data);
158
+ } catch {
159
+ // Ignore malformed messages
160
+ }
161
+ };
162
+
163
+ this.ws.onclose = () => {
164
+ this.wsConnected = false;
165
+ this.ws = null;
166
+ this.scheduleReconnect();
167
+ };
168
+
169
+ this.ws.onerror = () => {
170
+ // onclose will fire after onerror
171
+ };
172
+ } catch {
173
+ this.wsConnected = false;
174
+ this.scheduleReconnect();
175
+ }
176
+ }
177
+
178
+ // -----------------------------------------------------------------------
179
+ // Internal: route events to appropriate listeners
180
+ // -----------------------------------------------------------------------
181
+
182
+ private dispatchEvent(event: ServerEvent): void {
183
+ switch (event.type) {
184
+ case "stealth_announcement":
185
+ this.handleAnnouncementEvent(event);
186
+ break;
187
+ case "leaf_inserted":
188
+ for (const listener of this.treeListeners) {
189
+ try { listener(event); } catch { /* ignore */ }
190
+ }
191
+ break;
192
+ case "nullifier_spent":
193
+ for (const listener of this.nullifierListeners) {
194
+ try { listener(event); } catch { /* ignore */ }
195
+ }
196
+ break;
197
+ }
198
+ }
199
+
200
+ private handleAnnouncementEvent(event: AnnouncementEvent): void {
201
+ const announcement: OnChainStealthAnnouncement = {
202
+ announcementType: event.announcement_type,
203
+ ephemeralPub: hexToBytes(event.ephemeral_pub),
204
+ encryptedAmount: hexToBytes(event.encrypted_amount),
205
+ commitment: hexToBytes(event.commitment),
206
+ leafIndex: event.leaf_index,
207
+ blockTime: event.block_time ?? 0,
208
+ };
209
+
210
+ // Update cache (same logic as parent AnnouncementClient.connectWs)
211
+ if (announcement.leafIndex > this.latestLeafIndex) {
212
+ this.latestLeafIndex = announcement.leafIndex;
213
+ }
214
+ if (
215
+ !this.cachedAnnouncements.some(
216
+ (a) => a.leafIndex === announcement.leafIndex,
217
+ )
218
+ ) {
219
+ this.cachedAnnouncements.push(announcement);
220
+ }
221
+
222
+ // Notify announcement listeners
223
+ for (const listener of this.listeners) {
224
+ try {
225
+ listener([announcement]);
226
+ } catch {
227
+ // Listener errors shouldn't crash the client
228
+ }
229
+ }
230
+ }
231
+ }