@lerna-labs/hydra-sdk 1.0.0-beta.20 → 1.0.0-beta.21
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/hydra/hydra-http-client.js +1 -1
- package/dist/hydra/hydra-monitor.d.ts +74 -0
- package/dist/hydra/hydra-monitor.js +231 -0
- package/dist/hydra/hydra-websocket.d.ts +3 -1
- package/dist/hydra/hydra-websocket.js +2 -2
- package/dist/hydra/messages.d.ts +1 -1
- package/dist/hydra/types.d.ts +25 -0
- package/dist/hydra/utxo.d.ts +12 -2
- package/dist/hydra/utxo.js +12 -9
- package/dist/index.d.ts +3 -2
- package/dist/index.js +1 -0
- package/dist/wrangler.d.ts +3 -1
- package/dist/wrangler.js +26 -3
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ export class HydraHttpClient {
|
|
|
23
23
|
* `POST /decommit` — submits the decommit request to the Hydra node.
|
|
24
24
|
*/
|
|
25
25
|
async publishDecommit(transaction) {
|
|
26
|
-
return this.post('/decommit',
|
|
26
|
+
return this.post('/decommit', transaction);
|
|
27
27
|
}
|
|
28
28
|
/** Fetch the current UTxO snapshot. `GET /snapshot/utxo` */
|
|
29
29
|
async getSnapshotUtxo() {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { HydraWebSocket } from './hydra-websocket.js';
|
|
3
|
+
import type { GreetingsMessage, HeadStatus, HydraMessage, HydraMonitorOptions, HydraStatus, ServerOutput, TimestampedEvent } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Persistent WebSocket monitor for a Hydra head.
|
|
6
|
+
*
|
|
7
|
+
* Maintains a single long-lived WebSocket connection with auto-reconnect,
|
|
8
|
+
* real-time head state tracking, and proactive error surfacing.
|
|
9
|
+
*
|
|
10
|
+
* Events emitted:
|
|
11
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
12
|
+
* - `'status'` — `(status: HydraStatus, previous: HydraStatus)` on head-status changes
|
|
13
|
+
* - `'error:tx'` — `(msg)` on PostTxOnChainFailed or TxInvalid
|
|
14
|
+
* - `'error:command'` — `(msg)` on CommandFailed
|
|
15
|
+
* - `'error:decommit'` — `(msg)` on DecommitInvalid
|
|
16
|
+
* - `'connected'` — `()` WebSocket open + Greetings received
|
|
17
|
+
* - `'disconnected'` — `()` WebSocket closed (reconnect may follow)
|
|
18
|
+
* - `'reconnecting'` — `(attempt: number, delayMs: number)`
|
|
19
|
+
* - `'reconnect_failed'` — `()` maxAttempts exhausted
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const monitor = new HydraMonitor({ wsUrl: 'ws://hydra-node:4102' });
|
|
24
|
+
* await monitor.start();
|
|
25
|
+
* console.log(monitor.headStatus); // 'IDLE'
|
|
26
|
+
* monitor.on('status', (s, prev) => console.log(`${prev} → ${s}`));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class HydraMonitor extends EventEmitter {
|
|
30
|
+
readonly ws: HydraWebSocket;
|
|
31
|
+
private _headStatus;
|
|
32
|
+
private _previousStatus;
|
|
33
|
+
private _events;
|
|
34
|
+
private _stopped;
|
|
35
|
+
private _reconnecting;
|
|
36
|
+
private readonly reconnectEnabled;
|
|
37
|
+
private readonly baseDelayMs;
|
|
38
|
+
private readonly maxDelayMs;
|
|
39
|
+
private readonly maxAttempts;
|
|
40
|
+
private readonly eventBufferSize;
|
|
41
|
+
private readonly boundOnMessage;
|
|
42
|
+
private readonly boundOnClose;
|
|
43
|
+
constructor(options: HydraMonitorOptions);
|
|
44
|
+
/** Connect to the Hydra node. Resolves after Greetings received. */
|
|
45
|
+
start(): Promise<void>;
|
|
46
|
+
/** Disconnect and stop reconnecting. */
|
|
47
|
+
stop(): Promise<void>;
|
|
48
|
+
/** Whether the monitor is actively connected and listening. */
|
|
49
|
+
get connected(): boolean;
|
|
50
|
+
/** Current head status (uppercase). */
|
|
51
|
+
get headStatus(): HydraStatus;
|
|
52
|
+
/** Current head status (mixed-case, as reported in Greetings). */
|
|
53
|
+
get headStatusMixed(): HeadStatus;
|
|
54
|
+
/** The full Greetings message from the most recent connection. */
|
|
55
|
+
get greetings(): GreetingsMessage | null;
|
|
56
|
+
/** The last N events (configurable via eventBufferSize). Most recent last. */
|
|
57
|
+
get recentEvents(): readonly TimestampedEvent[];
|
|
58
|
+
/**
|
|
59
|
+
* Wait for headStatus to reach the target. Resolves immediately if already there.
|
|
60
|
+
* @param target - The HydraStatus to wait for.
|
|
61
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
62
|
+
*/
|
|
63
|
+
waitForStatus(target: HydraStatus, timeoutMs?: number): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Wait for the next message matching the given tag.
|
|
66
|
+
* @param tag - The message tag to wait for.
|
|
67
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
68
|
+
*/
|
|
69
|
+
waitForMessage<T extends ServerOutput['tag']>(tag: T, timeoutMs?: number): Promise<HydraMessage<T>>;
|
|
70
|
+
private onMessage;
|
|
71
|
+
private updateStatus;
|
|
72
|
+
private onClose;
|
|
73
|
+
private reconnectLoop;
|
|
74
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
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
|
+
INITIALIZING: 'Initializing',
|
|
6
|
+
OPEN: 'Open',
|
|
7
|
+
CLOSED: 'Closed',
|
|
8
|
+
FANOUT_POSSIBLE: 'FanoutPossible',
|
|
9
|
+
FINAL: 'Final',
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Persistent WebSocket monitor for a Hydra head.
|
|
13
|
+
*
|
|
14
|
+
* Maintains a single long-lived WebSocket connection with auto-reconnect,
|
|
15
|
+
* real-time head state tracking, and proactive error surfacing.
|
|
16
|
+
*
|
|
17
|
+
* Events emitted:
|
|
18
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
19
|
+
* - `'status'` — `(status: HydraStatus, previous: HydraStatus)` on head-status changes
|
|
20
|
+
* - `'error:tx'` — `(msg)` on PostTxOnChainFailed or TxInvalid
|
|
21
|
+
* - `'error:command'` — `(msg)` on CommandFailed
|
|
22
|
+
* - `'error:decommit'` — `(msg)` on DecommitInvalid
|
|
23
|
+
* - `'connected'` — `()` WebSocket open + Greetings received
|
|
24
|
+
* - `'disconnected'` — `()` WebSocket closed (reconnect may follow)
|
|
25
|
+
* - `'reconnecting'` — `(attempt: number, delayMs: number)`
|
|
26
|
+
* - `'reconnect_failed'` — `()` maxAttempts exhausted
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const monitor = new HydraMonitor({ wsUrl: 'ws://hydra-node:4102' });
|
|
31
|
+
* await monitor.start();
|
|
32
|
+
* console.log(monitor.headStatus); // 'IDLE'
|
|
33
|
+
* monitor.on('status', (s, prev) => console.log(`${prev} → ${s}`));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class HydraMonitor extends EventEmitter {
|
|
37
|
+
ws;
|
|
38
|
+
_headStatus = 'IDLE';
|
|
39
|
+
_previousStatus = 'IDLE';
|
|
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
|
+
/** The last N events (configurable via eventBufferSize). Most recent last. */
|
|
96
|
+
get recentEvents() {
|
|
97
|
+
return this._events;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Wait for headStatus to reach the target. Resolves immediately if already there.
|
|
101
|
+
* @param target - The HydraStatus to wait for.
|
|
102
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
103
|
+
*/
|
|
104
|
+
waitForStatus(target, timeoutMs = 60_000) {
|
|
105
|
+
if (this._headStatus === target)
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
return new Promise((resolve, reject) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
this.removeListener('status', onStatus);
|
|
110
|
+
reject(new Error(`Timeout waiting for status "${target}" (current: "${this._headStatus}")`));
|
|
111
|
+
}, timeoutMs);
|
|
112
|
+
const onStatus = (status) => {
|
|
113
|
+
if (status === target) {
|
|
114
|
+
clearTimeout(timer);
|
|
115
|
+
this.removeListener('status', onStatus);
|
|
116
|
+
resolve();
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
this.on('status', onStatus);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Wait for the next message matching the given tag.
|
|
124
|
+
* @param tag - The message tag to wait for.
|
|
125
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
126
|
+
*/
|
|
127
|
+
waitForMessage(tag, timeoutMs = 60_000) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
const timer = setTimeout(() => {
|
|
130
|
+
this.removeListener('message', onMsg);
|
|
131
|
+
reject(new Error(`Timeout waiting for message "${tag}"`));
|
|
132
|
+
}, timeoutMs);
|
|
133
|
+
const onMsg = (msg) => {
|
|
134
|
+
if (msg.tag === tag) {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
this.removeListener('message', onMsg);
|
|
137
|
+
resolve(msg);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
this.on('message', onMsg);
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
onMessage(msg) {
|
|
144
|
+
// Ring buffer
|
|
145
|
+
this._events.push({ timestamp: Date.now(), message: msg });
|
|
146
|
+
if (this._events.length > this.eventBufferSize) {
|
|
147
|
+
this._events.shift();
|
|
148
|
+
}
|
|
149
|
+
// Forward to listeners
|
|
150
|
+
this.emit('message', msg);
|
|
151
|
+
// Status tracking
|
|
152
|
+
if (msg.tag === 'Greetings' && 'headStatus' in msg) {
|
|
153
|
+
const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
|
|
154
|
+
if (mapped)
|
|
155
|
+
this.updateStatus(mapped);
|
|
156
|
+
}
|
|
157
|
+
else if (msg.tag === 'HeadIsAborted') {
|
|
158
|
+
this.updateStatus('IDLE');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
const mapped = TAG_TO_HYDRA[msg.tag];
|
|
162
|
+
if (mapped)
|
|
163
|
+
this.updateStatus(mapped);
|
|
164
|
+
}
|
|
165
|
+
// Error routing
|
|
166
|
+
if (msg.tag === 'PostTxOnChainFailed' || msg.tag === 'TxInvalid') {
|
|
167
|
+
this.emit('error:tx', msg);
|
|
168
|
+
}
|
|
169
|
+
else if (msg.tag === 'CommandFailed') {
|
|
170
|
+
this.emit('error:command', msg);
|
|
171
|
+
}
|
|
172
|
+
else if (msg.tag === 'DecommitInvalid') {
|
|
173
|
+
this.emit('error:decommit', msg);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
updateStatus(newStatus) {
|
|
177
|
+
if (this._headStatus !== newStatus) {
|
|
178
|
+
this._previousStatus = this._headStatus;
|
|
179
|
+
this._headStatus = newStatus;
|
|
180
|
+
this.emit('status', newStatus, this._previousStatus);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
onClose() {
|
|
184
|
+
if (this._stopped)
|
|
185
|
+
return;
|
|
186
|
+
this.ws.removeListener('message', this.boundOnMessage);
|
|
187
|
+
this.ws.removeListener('close', this.boundOnClose);
|
|
188
|
+
if (this.reconnectEnabled) {
|
|
189
|
+
this.reconnectLoop();
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
this.emit('disconnected');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async reconnectLoop() {
|
|
196
|
+
if (this._reconnecting)
|
|
197
|
+
return;
|
|
198
|
+
this._reconnecting = true;
|
|
199
|
+
this.emit('disconnected');
|
|
200
|
+
for (let attempt = 0; attempt < this.maxAttempts; attempt++) {
|
|
201
|
+
if (this._stopped) {
|
|
202
|
+
this._reconnecting = false;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const delay = Math.min(this.baseDelayMs * 2 ** attempt, this.maxDelayMs);
|
|
206
|
+
this.emit('reconnecting', attempt + 1, delay);
|
|
207
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
208
|
+
if (this._stopped) {
|
|
209
|
+
this._reconnecting = false;
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
await this.ws.waitForGreetings();
|
|
214
|
+
this.ws.on('message', this.boundOnMessage);
|
|
215
|
+
this.ws.on('close', this.boundOnClose);
|
|
216
|
+
// Process the Greetings from the new connection
|
|
217
|
+
if (this.ws.lastGreetings) {
|
|
218
|
+
this.onMessage(this.ws.lastGreetings);
|
|
219
|
+
}
|
|
220
|
+
this._reconnecting = false;
|
|
221
|
+
this.emit('connected');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Retry on next iteration
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this._reconnecting = false;
|
|
229
|
+
this.emit('reconnect_failed');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import type { ClientInput, ConnectionState, HydraStatus, HydraWsMessage } from './types.js';
|
|
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>;
|
|
3
5
|
/**
|
|
4
6
|
* Thin WebSocket wrapper for the Hydra node.
|
|
5
7
|
*
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
|
-
const HEAD_STATUS_TO_HYDRA = {
|
|
3
|
+
export const HEAD_STATUS_TO_HYDRA = {
|
|
4
4
|
Idle: 'IDLE',
|
|
5
5
|
Initializing: 'INITIALIZING',
|
|
6
6
|
Open: 'OPEN',
|
|
@@ -8,7 +8,7 @@ const HEAD_STATUS_TO_HYDRA = {
|
|
|
8
8
|
FanoutPossible: 'FANOUT_POSSIBLE',
|
|
9
9
|
Final: 'FINAL',
|
|
10
10
|
};
|
|
11
|
-
const TAG_TO_HYDRA = {
|
|
11
|
+
export const TAG_TO_HYDRA = {
|
|
12
12
|
HeadIsInitializing: 'INITIALIZING',
|
|
13
13
|
HeadIsOpen: 'OPEN',
|
|
14
14
|
HeadIsClosed: 'CLOSED',
|
package/dist/hydra/messages.d.ts
CHANGED
|
@@ -11,4 +11,4 @@
|
|
|
11
11
|
* }
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
export type { ClientInput, ClientMessage, ConnectionState, HeadStatus, HydraMessage, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, } from './types.js';
|
|
14
|
+
export type { ClientInput, ClientMessage, ConnectionState, HeadStatus, HydraMessage, HydraMonitorOptions, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, TimestampedEvent, } from './types.js';
|
package/dist/hydra/types.d.ts
CHANGED
|
@@ -194,3 +194,28 @@ export type HydraWsMessage = ServerOutput | ClientMessage;
|
|
|
194
194
|
export type HydraMessage<T extends ServerOutput['tag']> = Extract<ServerOutput, {
|
|
195
195
|
tag: T;
|
|
196
196
|
}>;
|
|
197
|
+
/** Configuration options for HydraMonitor. */
|
|
198
|
+
export interface HydraMonitorOptions {
|
|
199
|
+
/** Hydra node WebSocket URL. */
|
|
200
|
+
wsUrl: string;
|
|
201
|
+
/** Auto-reconnect configuration. */
|
|
202
|
+
reconnect?: {
|
|
203
|
+
/** Whether to reconnect on unexpected close. Default: `true`. */
|
|
204
|
+
enabled?: boolean;
|
|
205
|
+
/** Initial retry delay in milliseconds. Default: `1000`. */
|
|
206
|
+
baseDelayMs?: number;
|
|
207
|
+
/** Maximum retry delay in milliseconds. Default: `30000`. */
|
|
208
|
+
maxDelayMs?: number;
|
|
209
|
+
/** Maximum reconnect attempts. Default: `Infinity` (keep trying forever). */
|
|
210
|
+
maxAttempts?: number;
|
|
211
|
+
};
|
|
212
|
+
/** Number of recent events to retain in the ring buffer. Default: `100`. */
|
|
213
|
+
eventBufferSize?: number;
|
|
214
|
+
}
|
|
215
|
+
/** A timestamped Hydra WebSocket event for the recent events buffer. */
|
|
216
|
+
export interface TimestampedEvent {
|
|
217
|
+
/** Unix timestamp (ms) when the message was received. */
|
|
218
|
+
timestamp: number;
|
|
219
|
+
/** The raw Hydra message. */
|
|
220
|
+
message: HydraWsMessage;
|
|
221
|
+
}
|
package/dist/hydra/utxo.d.ts
CHANGED
|
@@ -7,19 +7,29 @@ export interface ParsedUtxo {
|
|
|
7
7
|
unit: string;
|
|
8
8
|
quantity: string;
|
|
9
9
|
}[];
|
|
10
|
+
datum?: string | null;
|
|
11
|
+
datumHash?: string | null;
|
|
12
|
+
inlineDatum?: unknown | null;
|
|
13
|
+
referenceScript?: unknown | null;
|
|
14
|
+
}
|
|
15
|
+
/** Options for UTxO query functions. */
|
|
16
|
+
export interface UtxoQueryOptions {
|
|
17
|
+
/** Include datum, datumHash, inlineDatum, and referenceScript fields. Defaults to `false`. */
|
|
18
|
+
includeDatums?: boolean;
|
|
10
19
|
}
|
|
11
20
|
/**
|
|
12
21
|
* Fetch the full UTxO set from the Hydra head snapshot.
|
|
13
22
|
*
|
|
14
23
|
* Reads `HYDRA_API_URL` from the environment.
|
|
15
24
|
*
|
|
25
|
+
* @param options - Query options. Set `includeDatums: true` to include datum/script fields.
|
|
16
26
|
* @returns All UTxOs currently held in the Hydra head.
|
|
17
27
|
*/
|
|
18
|
-
export declare function getUtxoSet(): Promise<ParsedUtxo[]>;
|
|
28
|
+
export declare function getUtxoSet(options?: UtxoQueryOptions): Promise<ParsedUtxo[]>;
|
|
19
29
|
/**
|
|
20
30
|
* Fetch UTxOs belonging to a specific address from the Hydra head snapshot.
|
|
21
31
|
*
|
|
22
32
|
* @param address - Bech32 address to filter by.
|
|
23
33
|
* @returns UTxOs matching the given address.
|
|
24
34
|
*/
|
|
25
|
-
export declare function queryUtxoByAddress(address: string): Promise<ParsedUtxo[]>;
|
|
35
|
+
export declare function queryUtxoByAddress(address: string, options?: UtxoQueryOptions): Promise<ParsedUtxo[]>;
|
package/dist/hydra/utxo.js
CHANGED
|
@@ -4,9 +4,10 @@ import { requireEnv } from '../config.js';
|
|
|
4
4
|
*
|
|
5
5
|
* Reads `HYDRA_API_URL` from the environment.
|
|
6
6
|
*
|
|
7
|
+
* @param options - Query options. Set `includeDatums: true` to include datum/script fields.
|
|
7
8
|
* @returns All UTxOs currently held in the Hydra head.
|
|
8
9
|
*/
|
|
9
|
-
export async function getUtxoSet() {
|
|
10
|
+
export async function getUtxoSet(options) {
|
|
10
11
|
const baseUrl = requireEnv('HYDRA_API_URL');
|
|
11
12
|
const url = `${baseUrl}/snapshot/utxo`;
|
|
12
13
|
try {
|
|
@@ -23,12 +24,14 @@ export async function getUtxoSet() {
|
|
|
23
24
|
unit,
|
|
24
25
|
quantity,
|
|
25
26
|
}));
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
const parsed = { tx_hash, output_index, address: utxo.address, amount };
|
|
28
|
+
if (options?.includeDatums) {
|
|
29
|
+
parsed.datum = utxo.datum ?? null;
|
|
30
|
+
parsed.datumHash = utxo.datumHash ?? null;
|
|
31
|
+
parsed.inlineDatum = utxo.inlineDatum ?? null;
|
|
32
|
+
parsed.referenceScript = utxo.referenceScript ?? null;
|
|
33
|
+
}
|
|
34
|
+
UtxoSet.push(parsed);
|
|
32
35
|
}
|
|
33
36
|
return UtxoSet;
|
|
34
37
|
}
|
|
@@ -43,9 +46,9 @@ export async function getUtxoSet() {
|
|
|
43
46
|
* @param address - Bech32 address to filter by.
|
|
44
47
|
* @returns UTxOs matching the given address.
|
|
45
48
|
*/
|
|
46
|
-
export async function queryUtxoByAddress(address) {
|
|
49
|
+
export async function queryUtxoByAddress(address, options) {
|
|
47
50
|
const result = [];
|
|
48
|
-
const data = await getUtxoSet();
|
|
51
|
+
const data = await getUtxoSet(options);
|
|
49
52
|
for (const utxo of data) {
|
|
50
53
|
if (utxo.address === address) {
|
|
51
54
|
result.push(utxo);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export type { DiskCache, DiskCacheConfig } from './cache/disk-cache.js';
|
|
2
2
|
export { createDiskCache } from './cache/disk-cache.js';
|
|
3
3
|
export { optionalEnv, requireEnv } from './config.js';
|
|
4
|
-
export
|
|
5
|
-
export type {
|
|
4
|
+
export { HydraMonitor } from './hydra/hydra-monitor.js';
|
|
5
|
+
export type { HeadStatus, HydraMessage, HydraMonitorOptions, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, TimestampedEvent, } from './hydra/messages.js';
|
|
6
|
+
export type { ParsedUtxo, UtxoQueryOptions } from './hydra/utxo.js';
|
|
6
7
|
export { getUtxoSet, queryUtxoByAddress } from './hydra/utxo.js';
|
|
7
8
|
export type { IpfsClient, IpfsConfig, PinResult } from './ipfs/ipfs.js';
|
|
8
9
|
export { createIpfsClient } from './ipfs/ipfs.js';
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createDiskCache } from './cache/disk-cache.js';
|
|
2
2
|
export { optionalEnv, requireEnv } from './config.js';
|
|
3
|
+
export { HydraMonitor } from './hydra/hydra-monitor.js';
|
|
3
4
|
export { getUtxoSet, queryUtxoByAddress } from './hydra/utxo.js';
|
|
4
5
|
export { createIpfsClient } from './ipfs/ipfs.js';
|
|
5
6
|
export { getAdmin } from './mesh/get-admin.js';
|
package/dist/wrangler.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { HydraHttpClient } from './hydra/hydra-http-client.js';
|
|
2
|
+
import type { HydraMonitor } from './hydra/hydra-monitor.js';
|
|
2
3
|
import { HydraWebSocket } from './hydra/hydra-websocket.js';
|
|
3
4
|
import type { HeadStatus, HydraStatus, HydraTransaction } from './hydra/types.js';
|
|
4
5
|
/** UTxO reference for committing funds into a Hydra head. */
|
|
@@ -35,7 +36,8 @@ export declare class Wrangler {
|
|
|
35
36
|
readonly ws: HydraWebSocket;
|
|
36
37
|
readonly http: HydraHttpClient;
|
|
37
38
|
private readonly blockfrost;
|
|
38
|
-
|
|
39
|
+
private readonly monitor;
|
|
40
|
+
constructor(url?: string, wsUrl?: string, monitor?: HydraMonitor);
|
|
39
41
|
/**
|
|
40
42
|
* Connect to the Hydra node with exponential-backoff retry.
|
|
41
43
|
*
|
package/dist/wrangler.js
CHANGED
|
@@ -3,6 +3,14 @@ import { requireEnv } from './config.js';
|
|
|
3
3
|
import { HydraHttpClient } from './hydra/hydra-http-client.js';
|
|
4
4
|
import { HydraWebSocket } from './hydra/hydra-websocket.js';
|
|
5
5
|
import { toHydraUTxO, toHydraUTxOs } from './hydra/utxo-conversion.js';
|
|
6
|
+
const HYDRA_TO_HEAD_STATUS = {
|
|
7
|
+
IDLE: 'Idle',
|
|
8
|
+
INITIALIZING: 'Initializing',
|
|
9
|
+
OPEN: 'Open',
|
|
10
|
+
CLOSED: 'Closed',
|
|
11
|
+
FANOUT_POSSIBLE: 'FanoutPossible',
|
|
12
|
+
FINAL: 'Final',
|
|
13
|
+
};
|
|
6
14
|
/**
|
|
7
15
|
* High-level controller for Hydra head lifecycle operations.
|
|
8
16
|
*
|
|
@@ -23,12 +31,20 @@ export class Wrangler {
|
|
|
23
31
|
ws;
|
|
24
32
|
http;
|
|
25
33
|
blockfrost;
|
|
26
|
-
|
|
34
|
+
monitor;
|
|
35
|
+
constructor(url, wsUrl, monitor) {
|
|
27
36
|
const httpUrl = url || requireEnv('HYDRA_API_URL');
|
|
28
|
-
const socketUrl = wsUrl || requireEnv('HYDRA_WS_URL');
|
|
29
37
|
this.blockfrost = new BlockfrostProvider(requireEnv('BLOCKFROST_API_KEY'));
|
|
30
|
-
this.ws = new HydraWebSocket(socketUrl);
|
|
31
38
|
this.http = new HydraHttpClient(httpUrl);
|
|
39
|
+
this.monitor = monitor ?? null;
|
|
40
|
+
if (monitor) {
|
|
41
|
+
// Share the monitor's WebSocket — no new connections
|
|
42
|
+
this.ws = monitor.ws;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const socketUrl = wsUrl || requireEnv('HYDRA_WS_URL');
|
|
46
|
+
this.ws = new HydraWebSocket(socketUrl);
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
49
|
/**
|
|
34
50
|
* Connect to the Hydra node with exponential-backoff retry.
|
|
@@ -40,6 +56,9 @@ export class Wrangler {
|
|
|
40
56
|
* @param baseDelayMs - Initial retry delay in milliseconds (default 1000). Doubles each attempt, capped at 30 s.
|
|
41
57
|
*/
|
|
42
58
|
async connectWithRetry(maxAttempts = 5, baseDelayMs = 1000) {
|
|
59
|
+
// When using a monitor, the WebSocket is already connected
|
|
60
|
+
if (this.monitor?.connected)
|
|
61
|
+
return;
|
|
43
62
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
44
63
|
try {
|
|
45
64
|
const connected = await this.ws.waitForGreetings();
|
|
@@ -167,6 +186,10 @@ export class Wrangler {
|
|
|
167
186
|
* @returns The head status string (e.g. `"Idle"`, `"Open"`, `"Closed"`).
|
|
168
187
|
*/
|
|
169
188
|
async getHeadStatus(timeoutMs = 5000) {
|
|
189
|
+
// When using a monitor, read the cached status directly — no new WebSocket
|
|
190
|
+
if (this.monitor?.connected) {
|
|
191
|
+
return HYDRA_TO_HEAD_STATUS[this.monitor.headStatus];
|
|
192
|
+
}
|
|
170
193
|
return this.awaitMessage((message, resolve, _reject) => {
|
|
171
194
|
if (message.tag === 'Greetings') {
|
|
172
195
|
resolve(message.headStatus);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lerna-labs/hydra-sdk",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.21",
|
|
4
4
|
"description": "TypeScript SDK for managing Cardano Hydra Heads — lifecycle, UTxO queries, wallet management, transaction submission, and signature verification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cardano",
|