@lerna-labs/hydra-sdk 1.0.0-beta.9 → 2.0.0-beta.1

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 (39) hide show
  1. package/README.md +159 -0
  2. package/dist/cache/disk-cache.d.ts +37 -0
  3. package/dist/cache/disk-cache.js +84 -0
  4. package/dist/config.d.ts +4 -0
  5. package/dist/config.js +12 -0
  6. package/dist/hydra/hydra-http-client.d.ts +36 -0
  7. package/dist/hydra/hydra-http-client.js +66 -0
  8. package/dist/hydra/hydra-monitor.d.ts +88 -0
  9. package/dist/hydra/hydra-monitor.js +270 -0
  10. package/dist/hydra/hydra-websocket.d.ts +46 -0
  11. package/dist/hydra/hydra-websocket.js +181 -0
  12. package/dist/hydra/messages.d.ts +14 -0
  13. package/dist/hydra/messages.js +1 -0
  14. package/dist/hydra/types.d.ts +486 -0
  15. package/dist/hydra/types.js +2 -0
  16. package/dist/hydra/utxo-conversion.d.ts +10 -0
  17. package/dist/hydra/utxo-conversion.js +111 -0
  18. package/dist/hydra/utxo.d.ts +25 -5
  19. package/dist/hydra/utxo.js +37 -31
  20. package/dist/index.d.ts +15 -7
  21. package/dist/index.js +11 -7
  22. package/dist/ipfs/ipfs.d.ts +22 -0
  23. package/dist/ipfs/ipfs.js +90 -0
  24. package/dist/mesh/get-admin.d.ts +13 -1
  25. package/dist/mesh/get-admin.js +37 -7
  26. package/dist/mesh/native-script.d.ts +30 -5
  27. package/dist/mesh/native-script.js +38 -10
  28. package/dist/test.js +3 -3
  29. package/dist/tx3/submit-tx.d.ts +8 -0
  30. package/dist/tx3/submit-tx.js +8 -0
  31. package/dist/utils/chunk-string.d.ts +7 -0
  32. package/dist/utils/chunk-string.js +7 -0
  33. package/dist/utils/verify-signature.d.ts +28 -5
  34. package/dist/utils/verify-signature.js +39 -18
  35. package/dist/wrangler.d.ts +179 -0
  36. package/dist/wrangler.js +452 -0
  37. package/package.json +25 -6
  38. package/dist/mesh/wrangler.d.ts +0 -29
  39. package/dist/mesh/wrangler.js +0 -277
@@ -0,0 +1,270 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { HEAD_STATUS_TO_HYDRA, HydraWebSocket, TAG_TO_HYDRA } from './hydra-websocket.js';
3
+ const HYDRA_TO_HEAD_STATUS = {
4
+ IDLE: 'Idle',
5
+ OPEN: 'Open',
6
+ CLOSED: 'Closed',
7
+ FANOUT_POSSIBLE: 'FanoutPossible',
8
+ FINAL: 'Final',
9
+ };
10
+ /**
11
+ * Persistent WebSocket monitor for a Hydra head.
12
+ *
13
+ * Maintains a single long-lived WebSocket connection with auto-reconnect,
14
+ * real-time head state tracking, and proactive error surfacing.
15
+ *
16
+ * Events emitted:
17
+ * - `'message'` — `(msg: HydraWsMessage)` for every incoming message
18
+ * - `'status'` — `(status: HydraStatus, previous: HydraStatus)` on head-status changes
19
+ * - `'error:tx'` — `(msg)` on PostTxOnChainFailed or TxInvalid
20
+ * - `'error:command'` — `(msg)` on CommandFailed
21
+ * - `'error:decommit'` — `(msg)` on DecommitInvalid
22
+ * - `'connected'` — `()` WebSocket open + Greetings received
23
+ * - `'disconnected'` — `()` WebSocket closed (reconnect may follow)
24
+ * - `'reconnecting'` — `(attempt: number, delayMs: number)`
25
+ * - `'reconnect_failed'` — `()` maxAttempts exhausted
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * const monitor = new HydraMonitor({ wsUrl: 'ws://hydra-node:4102' });
30
+ * await monitor.start();
31
+ * console.log(monitor.headStatus); // 'IDLE'
32
+ * monitor.on('status', (s, prev) => console.log(`${prev} → ${s}`));
33
+ * ```
34
+ */
35
+ export class HydraMonitor extends EventEmitter {
36
+ ws;
37
+ _headStatus = 'IDLE';
38
+ _previousStatus = 'IDLE';
39
+ _headId = null;
40
+ _events = [];
41
+ _stopped = true;
42
+ _reconnecting = false;
43
+ reconnectEnabled;
44
+ baseDelayMs;
45
+ maxDelayMs;
46
+ maxAttempts;
47
+ eventBufferSize;
48
+ boundOnMessage = (msg) => this.onMessage(msg);
49
+ boundOnClose = () => this.onClose();
50
+ constructor(options) {
51
+ super();
52
+ this.ws = new HydraWebSocket(options.wsUrl);
53
+ this.reconnectEnabled = options.reconnect?.enabled ?? true;
54
+ this.baseDelayMs = options.reconnect?.baseDelayMs ?? 1000;
55
+ this.maxDelayMs = options.reconnect?.maxDelayMs ?? 30_000;
56
+ this.maxAttempts = options.reconnect?.maxAttempts ?? Number.POSITIVE_INFINITY;
57
+ this.eventBufferSize = options.eventBufferSize ?? 100;
58
+ }
59
+ /** Connect to the Hydra node. Resolves after Greetings received. */
60
+ async start() {
61
+ this._stopped = false;
62
+ await this.ws.waitForGreetings();
63
+ this.ws.on('message', this.boundOnMessage);
64
+ this.ws.on('close', this.boundOnClose);
65
+ // Process the Greetings that was received during waitForGreetings
66
+ if (this.ws.lastGreetings) {
67
+ this.onMessage(this.ws.lastGreetings);
68
+ }
69
+ this.emit('connected');
70
+ }
71
+ /** Disconnect and stop reconnecting. */
72
+ async stop() {
73
+ this._stopped = true;
74
+ this.ws.removeListener('message', this.boundOnMessage);
75
+ this.ws.removeListener('close', this.boundOnClose);
76
+ await this.ws.disconnect();
77
+ this.emit('disconnected');
78
+ }
79
+ /** Whether the monitor is actively connected and listening. */
80
+ get connected() {
81
+ return this.ws.connectionState === 'CONNECTED';
82
+ }
83
+ /** Current head status (uppercase). */
84
+ get headStatus() {
85
+ return this._headStatus;
86
+ }
87
+ /** Current head status (mixed-case, as reported in Greetings). */
88
+ get headStatusMixed() {
89
+ return HYDRA_TO_HEAD_STATUS[this._headStatus];
90
+ }
91
+ /** The full Greetings message from the most recent connection. */
92
+ get greetings() {
93
+ return this.ws.lastGreetings;
94
+ }
95
+ /**
96
+ * Summary of Hydra head info derived from live state plus the last Greetings.
97
+ *
98
+ * `headStatus` and `headId` reflect the current state tracked from transition
99
+ * messages (`HeadIsOpen`, `HeadIsClosed`, etc.), not the snapshot taken
100
+ * at connection time. The remaining fields (node version, participants,
101
+ * contestation period, network info) come from the cached Greetings since
102
+ * they are static for the life of the head.
103
+ *
104
+ * Excludes the full UTxO snapshot to keep payloads small.
105
+ * Returns `null` if no Greetings has been received yet.
106
+ */
107
+ get headInfo() {
108
+ const g = this.greetings;
109
+ if (!g)
110
+ return null;
111
+ const peers = g.env?.configuredPeers;
112
+ const peerCount = peers ? peers.split(',').filter(Boolean).length : 0;
113
+ return {
114
+ headStatus: HYDRA_TO_HEAD_STATUS[this._headStatus],
115
+ headId: this._headId,
116
+ nodeVersion: g.hydraNodeVersion ?? null,
117
+ me: g.me.vkey,
118
+ contestationPeriod: g.env?.contestationPeriod ?? null,
119
+ depositPeriod: g.env?.depositPeriod ?? null,
120
+ participants: g.env?.participants ?? [],
121
+ networkConnected: g.networkInfo?.networkConnected ?? false,
122
+ peerCount,
123
+ chainSyncedStatus: g.chainSyncedStatus ?? null,
124
+ currentSlot: g.currentSlot ?? null,
125
+ };
126
+ }
127
+ /** The last N events (configurable via eventBufferSize). Most recent last. */
128
+ get recentEvents() {
129
+ return this._events;
130
+ }
131
+ /**
132
+ * Wait for headStatus to reach the target. Resolves immediately if already there.
133
+ * @param target - The HydraStatus to wait for.
134
+ * @param timeoutMs - Maximum wait time (default 60s).
135
+ */
136
+ waitForStatus(target, timeoutMs = 60_000) {
137
+ if (this._headStatus === target)
138
+ return Promise.resolve();
139
+ return new Promise((resolve, reject) => {
140
+ const timer = setTimeout(() => {
141
+ this.removeListener('status', onStatus);
142
+ reject(new Error(`Timeout waiting for status "${target}" (current: "${this._headStatus}")`));
143
+ }, timeoutMs);
144
+ const onStatus = (status) => {
145
+ if (status === target) {
146
+ clearTimeout(timer);
147
+ this.removeListener('status', onStatus);
148
+ resolve();
149
+ }
150
+ };
151
+ this.on('status', onStatus);
152
+ });
153
+ }
154
+ /**
155
+ * Wait for the next message matching the given tag.
156
+ * @param tag - The message tag to wait for.
157
+ * @param timeoutMs - Maximum wait time (default 60s).
158
+ */
159
+ waitForMessage(tag, timeoutMs = 60_000) {
160
+ return new Promise((resolve, reject) => {
161
+ const timer = setTimeout(() => {
162
+ this.removeListener('message', onMsg);
163
+ reject(new Error(`Timeout waiting for message "${tag}"`));
164
+ }, timeoutMs);
165
+ const onMsg = (msg) => {
166
+ if (msg.tag === tag) {
167
+ clearTimeout(timer);
168
+ this.removeListener('message', onMsg);
169
+ resolve(msg);
170
+ }
171
+ };
172
+ this.on('message', onMsg);
173
+ });
174
+ }
175
+ onMessage(msg) {
176
+ // Ring buffer
177
+ this._events.push({ timestamp: Date.now(), message: msg });
178
+ if (this._events.length > this.eventBufferSize) {
179
+ this._events.shift();
180
+ }
181
+ // Forward to listeners
182
+ this.emit('message', msg);
183
+ // Status tracking
184
+ if (msg.tag === 'Greetings' && 'headStatus' in msg) {
185
+ const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
186
+ if (mapped)
187
+ this.updateStatus(mapped);
188
+ const greetingHeadId = msg.hydraHeadId;
189
+ if (greetingHeadId)
190
+ this._headId = greetingHeadId;
191
+ }
192
+ else {
193
+ const mapped = TAG_TO_HYDRA[msg.tag];
194
+ if (mapped)
195
+ this.updateStatus(mapped);
196
+ // Track headId from transition messages (Greetings may not include
197
+ // it for a fresh node — under Hydra v2's direct-open flow, HeadIsOpen
198
+ // is the first place it reliably appears).
199
+ const msgHeadId = msg.headId;
200
+ if (typeof msgHeadId === 'string' && msgHeadId) {
201
+ this._headId = msgHeadId;
202
+ }
203
+ }
204
+ // Error routing
205
+ if (msg.tag === 'PostTxOnChainFailed' || msg.tag === 'TxInvalid') {
206
+ this.emit('error:tx', msg);
207
+ }
208
+ else if (msg.tag === 'CommandFailed') {
209
+ this.emit('error:command', msg);
210
+ }
211
+ else if (msg.tag === 'DecommitInvalid') {
212
+ this.emit('error:decommit', msg);
213
+ }
214
+ }
215
+ updateStatus(newStatus) {
216
+ if (this._headStatus !== newStatus) {
217
+ this._previousStatus = this._headStatus;
218
+ this._headStatus = newStatus;
219
+ this.emit('status', newStatus, this._previousStatus);
220
+ }
221
+ }
222
+ onClose() {
223
+ if (this._stopped)
224
+ return;
225
+ this.ws.removeListener('message', this.boundOnMessage);
226
+ this.ws.removeListener('close', this.boundOnClose);
227
+ if (this.reconnectEnabled) {
228
+ this.reconnectLoop();
229
+ }
230
+ else {
231
+ this.emit('disconnected');
232
+ }
233
+ }
234
+ async reconnectLoop() {
235
+ if (this._reconnecting)
236
+ return;
237
+ this._reconnecting = true;
238
+ this.emit('disconnected');
239
+ for (let attempt = 0; attempt < this.maxAttempts; attempt++) {
240
+ if (this._stopped) {
241
+ this._reconnecting = false;
242
+ return;
243
+ }
244
+ const delay = Math.min(this.baseDelayMs * 2 ** attempt, this.maxDelayMs);
245
+ this.emit('reconnecting', attempt + 1, delay);
246
+ await new Promise((r) => setTimeout(r, delay));
247
+ if (this._stopped) {
248
+ this._reconnecting = false;
249
+ return;
250
+ }
251
+ try {
252
+ await this.ws.waitForGreetings();
253
+ this.ws.on('message', this.boundOnMessage);
254
+ this.ws.on('close', this.boundOnClose);
255
+ // Process the Greetings from the new connection
256
+ if (this.ws.lastGreetings) {
257
+ this.onMessage(this.ws.lastGreetings);
258
+ }
259
+ this._reconnecting = false;
260
+ this.emit('connected');
261
+ return;
262
+ }
263
+ catch {
264
+ // Retry on next iteration
265
+ }
266
+ }
267
+ this._reconnecting = false;
268
+ this.emit('reconnect_failed');
269
+ }
270
+ }
@@ -0,0 +1,46 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import type { ClientInput, ConnectionState, HeadStatus, HydraStatus, HydraWsMessage } from './types.js';
3
+ export declare const HEAD_STATUS_TO_HYDRA: Record<HeadStatus, HydraStatus>;
4
+ export declare const TAG_TO_HYDRA: Record<string, HydraStatus>;
5
+ /**
6
+ * Thin WebSocket wrapper for the Hydra node.
7
+ *
8
+ * Uses `EventEmitter` for multi-listener message dispatch (no single-callback
9
+ * overwrite). Emits:
10
+ *
11
+ * - `'message'` — `(msg: HydraWsMessage)` for every incoming message
12
+ * - `'status'` — `(status: HydraStatus)` on head-status changes
13
+ * - `'error'` — `(err: Error)`
14
+ * - `'close'` — `()`
15
+ */
16
+ export declare class HydraWebSocket extends EventEmitter {
17
+ private ws;
18
+ private readonly url;
19
+ private _status;
20
+ private _connectionState;
21
+ private _lastGreetings;
22
+ constructor(wsUrl: string);
23
+ /** Current connection state. */
24
+ get connectionState(): ConnectionState;
25
+ /** Open the WebSocket connection. Resolves when the socket is open. */
26
+ connect(): Promise<void>;
27
+ /**
28
+ * Connect and wait for the Greetings message from the Hydra node.
29
+ *
30
+ * Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
31
+ * message handlers — it uses a one-time EventEmitter listener.
32
+ */
33
+ waitForGreetings(timeoutMs?: number): Promise<boolean>;
34
+ /** Close the WebSocket. */
35
+ disconnect(timeoutMs?: number): Promise<void>;
36
+ /** Send a ClientInput message as JSON over the WebSocket. */
37
+ send(message: ClientInput): void;
38
+ /** Current Hydra head status derived from messages. */
39
+ getStatus(): HydraStatus;
40
+ /** Register a status-change listener. Returns current status. */
41
+ onStatusChange(callback: (status: HydraStatus) => void): HydraStatus;
42
+ /** The last Greetings message received, if any. */
43
+ get lastGreetings(): HydraWsMessage | null;
44
+ private handleMessage;
45
+ private updateStatus;
46
+ }
@@ -0,0 +1,181 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import WebSocket from 'ws';
3
+ export const HEAD_STATUS_TO_HYDRA = {
4
+ Idle: 'IDLE',
5
+ Open: 'OPEN',
6
+ Closed: 'CLOSED',
7
+ FanoutPossible: 'FANOUT_POSSIBLE',
8
+ Final: 'FINAL',
9
+ };
10
+ export const TAG_TO_HYDRA = {
11
+ HeadIsOpen: 'OPEN',
12
+ HeadIsClosed: 'CLOSED',
13
+ ReadyToFanout: 'FANOUT_POSSIBLE',
14
+ HeadIsFinalized: 'FINAL',
15
+ };
16
+ /**
17
+ * Thin WebSocket wrapper for the Hydra node.
18
+ *
19
+ * Uses `EventEmitter` for multi-listener message dispatch (no single-callback
20
+ * overwrite). Emits:
21
+ *
22
+ * - `'message'` — `(msg: HydraWsMessage)` for every incoming message
23
+ * - `'status'` — `(status: HydraStatus)` on head-status changes
24
+ * - `'error'` — `(err: Error)`
25
+ * - `'close'` — `()`
26
+ */
27
+ export class HydraWebSocket extends EventEmitter {
28
+ ws = null;
29
+ url;
30
+ _status = 'IDLE';
31
+ _connectionState = 'IDLE';
32
+ _lastGreetings = null;
33
+ constructor(wsUrl) {
34
+ super();
35
+ this.url = wsUrl;
36
+ }
37
+ /** Current connection state. */
38
+ get connectionState() {
39
+ return this._connectionState;
40
+ }
41
+ /** Open the WebSocket connection. Resolves when the socket is open. */
42
+ async connect() {
43
+ if (this.ws?.readyState === WebSocket.OPEN)
44
+ return;
45
+ return new Promise((resolve, reject) => {
46
+ const ws = new WebSocket(this.url);
47
+ ws.on('open', () => {
48
+ this.ws = ws;
49
+ this._connectionState = 'CONNECTING';
50
+ resolve();
51
+ });
52
+ ws.on('message', (data) => {
53
+ try {
54
+ const msg = JSON.parse(data.toString());
55
+ this.handleMessage(msg);
56
+ }
57
+ catch {
58
+ // Ignore non-JSON messages
59
+ }
60
+ });
61
+ ws.on('error', (err) => {
62
+ this._connectionState = 'FAILED';
63
+ this.emit('error', err);
64
+ reject(new Error(`WebSocket error: ${err.message}`));
65
+ });
66
+ ws.on('close', () => {
67
+ this._connectionState = 'DISCONNECTED';
68
+ this.ws = null;
69
+ this.emit('close');
70
+ });
71
+ });
72
+ }
73
+ /**
74
+ * Connect and wait for the Greetings message from the Hydra node.
75
+ *
76
+ * Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
77
+ * message handlers — it uses a one-time EventEmitter listener.
78
+ */
79
+ async waitForGreetings(timeoutMs = 30_000) {
80
+ if (this._connectionState === 'CONNECTED')
81
+ return true;
82
+ // Register the listener BEFORE connecting so the Greetings message
83
+ // (which the Hydra node sends immediately after the socket opens)
84
+ // cannot arrive before we're listening for it.
85
+ return new Promise((resolve, reject) => {
86
+ const timer = setTimeout(() => {
87
+ this.removeListener('message', onMsg);
88
+ this._connectionState = 'FAILED';
89
+ reject(new Error('Connection timed out: no Greetings from Hydra node'));
90
+ }, timeoutMs);
91
+ const onMsg = (msg) => {
92
+ if (msg.tag === 'Greetings') {
93
+ clearTimeout(timer);
94
+ this.removeListener('message', onMsg);
95
+ this._connectionState = 'CONNECTED';
96
+ resolve(true);
97
+ }
98
+ };
99
+ this.on('message', onMsg);
100
+ this.connect().catch((err) => {
101
+ clearTimeout(timer);
102
+ this.removeListener('message', onMsg);
103
+ reject(err);
104
+ });
105
+ });
106
+ }
107
+ /** Close the WebSocket. */
108
+ async disconnect(timeoutMs = 5000) {
109
+ if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
110
+ this._connectionState = 'DISCONNECTED';
111
+ return;
112
+ }
113
+ return new Promise((resolve) => {
114
+ const timer = setTimeout(() => {
115
+ this.ws?.terminate();
116
+ this._connectionState = 'DISCONNECTED';
117
+ this.ws = null;
118
+ resolve();
119
+ }, timeoutMs);
120
+ this.ws.on('close', () => {
121
+ clearTimeout(timer);
122
+ this._connectionState = 'DISCONNECTED';
123
+ this.ws = null;
124
+ resolve();
125
+ });
126
+ this.ws.close();
127
+ });
128
+ }
129
+ /** Send a ClientInput message as JSON over the WebSocket. */
130
+ send(message) {
131
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
132
+ throw new Error('WebSocket is not connected');
133
+ }
134
+ this.ws.send(JSON.stringify(message));
135
+ }
136
+ /** Current Hydra head status derived from messages. */
137
+ getStatus() {
138
+ return this._status;
139
+ }
140
+ /** Register a status-change listener. Returns current status. */
141
+ onStatusChange(callback) {
142
+ this.on('status', callback);
143
+ return this._status;
144
+ }
145
+ /** The last Greetings message received, if any. */
146
+ get lastGreetings() {
147
+ return this._lastGreetings;
148
+ }
149
+ handleMessage(msg) {
150
+ // Strip sensitive fields before caching or emitting
151
+ if (msg.tag === 'Greetings') {
152
+ const sanitized = { ...msg };
153
+ if (sanitized.env && typeof sanitized.env === 'object') {
154
+ const { signingKey: _, ...safeEnv } = sanitized.env;
155
+ sanitized.env = safeEnv;
156
+ }
157
+ this._lastGreetings = sanitized;
158
+ this.emit('message', sanitized);
159
+ }
160
+ else {
161
+ this.emit('message', msg);
162
+ }
163
+ // Update status from Greetings headStatus
164
+ if (msg.tag === 'Greetings' && 'headStatus' in msg) {
165
+ const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
166
+ if (mapped)
167
+ this.updateStatus(mapped);
168
+ return;
169
+ }
170
+ // Update status from state transition messages
171
+ const mapped = TAG_TO_HYDRA[msg.tag];
172
+ if (mapped)
173
+ this.updateStatus(mapped);
174
+ }
175
+ updateStatus(newStatus) {
176
+ if (this._status !== newStatus) {
177
+ this._status = newStatus;
178
+ this.emit('status', newStatus);
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Re-exported Hydra WebSocket message types.
3
+ *
4
+ * `ServerOutput` is a discriminated union (on `.tag`) of all possible messages
5
+ * the Hydra node can send over its WebSocket API. Use `Extract` to narrow:
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * function handleOpen(msg: HydraMessage<"HeadIsOpen">) {
10
+ * console.log(msg.tag);
11
+ * }
12
+ * ```
13
+ */
14
+ export type { ClientInput, ClientMessage, ConfirmedSnapshot, ConnectionState, HeadStatus, HydraHeadInfo, HydraMessage, HydraMonitorOptions, HydraSnapshot, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, TimestampedEvent, } from './types.js';
@@ -0,0 +1 @@
1
+ export {};