@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.
- package/README.md +214 -108
- package/{packages/sdk/dist → dist}/client.d.ts +25 -1
- package/{packages/sdk/dist → dist}/client.js +36 -1
- package/{packages/sdk/dist → dist}/crypto-ed25519.d.ts +14 -0
- package/{packages/sdk/dist → dist}/crypto-ed25519.js +14 -0
- package/{packages/sdk/dist → dist}/index.d.ts +3 -3
- package/{packages/sdk/dist → dist}/index.js +3 -3
- package/{packages/sdk/dist → dist}/instructions.d.ts +28 -11
- package/{packages/sdk/dist → dist}/instructions.js +45 -15
- package/{packages/sdk/dist → dist}/stealth.d.ts +104 -9
- package/{packages/sdk/dist → dist}/stealth.js +146 -14
- package/{packages/sdk/dist → dist}/taproot.d.ts +39 -2
- package/{packages/sdk/dist → dist}/taproot.js +54 -2
- package/package.json +86 -63
- package/src/announcement-client.ts +457 -0
- package/src/auditor-ciphertext.ts +181 -0
- package/src/auditor.ts +409 -0
- package/src/bitcoin/ika.ts +103 -0
- package/src/bitcoin/index.ts +5 -0
- package/src/bound-params.ts +322 -0
- package/src/chadbuffer.ts +603 -0
- package/src/circomlibjs.d.ts +51 -0
- package/src/claim-link.ts +53 -0
- package/src/client.ts +638 -0
- package/src/commitment-tree.ts +736 -0
- package/src/config.ts +772 -0
- package/src/core/esplora.ts +332 -0
- package/src/core/mempool.ts +159 -0
- package/src/crypto-babyjub.ts +385 -0
- package/src/crypto-ed25519.ts +297 -0
- package/src/crypto.ts +199 -0
- package/src/event-client.ts +231 -0
- package/src/events.ts +384 -0
- package/src/explorer.ts +300 -0
- package/src/index.ts +902 -0
- package/src/instructions.ts +2820 -0
- package/src/keys.ts +1228 -0
- package/src/logger.ts +41 -0
- package/src/magicblock.ts +278 -0
- package/src/merkle.ts +197 -0
- package/src/note.ts +754 -0
- package/src/pda.ts +516 -0
- package/src/pool-state.ts +176 -0
- package/src/poseidon.ts +175 -0
- package/src/prover/index.ts +19 -0
- package/src/prover/mobile.ts +303 -0
- package/src/prover/web.ts +771 -0
- package/src/psbt.ts +333 -0
- package/src/selective-disclosure.ts +284 -0
- package/src/sender-memo.ts +343 -0
- package/src/snarkjs.d.ts +19 -0
- package/src/sns-resolver.ts +333 -0
- package/src/solana/connection.ts +189 -0
- package/src/solana/priority-fee.ts +201 -0
- package/src/spend-doc.ts +163 -0
- package/src/stealth.ts +1477 -0
- package/src/taproot.ts +707 -0
- package/src/token-registry.ts +207 -0
- package/src/utils/encoding.ts +33 -0
- package/src/vk-registry.ts +295 -0
- package/LICENSE +0 -21
- package/packages/btc-client/src/esplora-client.ts +0 -153
- package/packages/btc-client/src/index.ts +0 -3
- package/packages/btc-client/src/op-return.ts +0 -93
- package/packages/btc-client/src/types.ts +0 -112
- package/packages/sdk/README.md +0 -277
- /package/{packages/sdk/dist → dist}/announcement-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/announcement-client.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor-ciphertext.js +0 -0
- /package/{packages/sdk/dist → dist}/auditor.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/auditor.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/ika.js +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bitcoin/index.js +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/bound-params.js +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/chadbuffer.js +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/claim-link.js +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/commitment-tree.js +0 -0
- /package/{packages/sdk/dist → dist}/config.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/config.js +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/esplora.js +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/core/mempool.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto-babyjub.js +0 -0
- /package/{packages/sdk/dist → dist}/crypto.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/crypto.js +0 -0
- /package/{packages/sdk/dist → dist}/event-client.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/event-client.js +0 -0
- /package/{packages/sdk/dist → dist}/events.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/events.js +0 -0
- /package/{packages/sdk/dist → dist}/explorer.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/explorer.js +0 -0
- /package/{packages/sdk/dist → dist}/keys.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/keys.js +0 -0
- /package/{packages/sdk/dist → dist}/logger.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/logger.js +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/magicblock.js +0 -0
- /package/{packages/sdk/dist → dist}/merkle.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/merkle.js +0 -0
- /package/{packages/sdk/dist → dist}/note.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/note.js +0 -0
- /package/{packages/sdk/dist → dist}/pda.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pda.js +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/pool-state.js +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/poseidon.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/index.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/mobile.js +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/prover/web.js +0 -0
- /package/{packages/sdk/dist → dist}/psbt.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/psbt.js +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/selective-disclosure.js +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sender-memo.js +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/sns-resolver.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/connection.js +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/solana/priority-fee.js +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/spend-doc.js +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/token-registry.js +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.d.ts +0 -0
- /package/{packages/sdk/dist → dist}/utils/encoding.js +0 -0
- /package/{packages/sdk/dist → dist}/vk-registry.d.ts +0 -0
- /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
|
+
}
|