@lerna-labs/hydra-sdk 1.0.0-beta.16 → 1.0.0-beta.18
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.d.ts +28 -0
- package/dist/hydra/hydra-http-client.js +56 -0
- package/dist/hydra/hydra-websocket.d.ts +41 -0
- package/dist/hydra/hydra-websocket.js +159 -0
- package/dist/hydra/messages.d.ts +3 -20
- package/dist/hydra/types.d.ts +196 -0
- package/dist/hydra/types.js +2 -0
- package/dist/hydra/utxo-conversion.d.ts +10 -0
- package/dist/hydra/utxo-conversion.js +111 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/test.js +1 -1
- package/dist/{mesh/wrangler.d.ts → wrangler.d.ts} +22 -26
- package/dist/{mesh/wrangler.js → wrangler.js} +77 -59
- package/package.json +7 -4
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CommitBlueprintPayload, HydraTransaction, HydraUTxOEntry, HydraUTxOs } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP client for the Hydra node REST API.
|
|
4
|
+
*
|
|
5
|
+
* Uses native `fetch` (Node 18+). Accepts 200 and 202 as success responses.
|
|
6
|
+
*/
|
|
7
|
+
export declare class HydraHttpClient {
|
|
8
|
+
private readonly baseUrl;
|
|
9
|
+
constructor(baseUrl: string);
|
|
10
|
+
/**
|
|
11
|
+
* Build a commit transaction.
|
|
12
|
+
*
|
|
13
|
+
* `POST /commit` — returns the unsigned L1 transaction CBOR hex.
|
|
14
|
+
*/
|
|
15
|
+
buildCommit(payload: HydraUTxOs | CommitBlueprintPayload | Record<string, never>): Promise<string>;
|
|
16
|
+
/**
|
|
17
|
+
* Publish a decommit transaction.
|
|
18
|
+
*
|
|
19
|
+
* `POST /decommit` — submits the decommit request to the Hydra node.
|
|
20
|
+
*/
|
|
21
|
+
publishDecommit(transaction: HydraTransaction): Promise<unknown>;
|
|
22
|
+
/** Fetch the current UTxO snapshot. `GET /snapshot/utxo` */
|
|
23
|
+
getSnapshotUtxo(): Promise<Record<string, HydraUTxOEntry>>;
|
|
24
|
+
/** Fetch protocol parameters. `GET /protocol-parameters` */
|
|
25
|
+
getProtocolParameters(): Promise<unknown>;
|
|
26
|
+
private post;
|
|
27
|
+
private get;
|
|
28
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Hydra node REST API.
|
|
3
|
+
*
|
|
4
|
+
* Uses native `fetch` (Node 18+). Accepts 200 and 202 as success responses.
|
|
5
|
+
*/
|
|
6
|
+
export class HydraHttpClient {
|
|
7
|
+
baseUrl;
|
|
8
|
+
constructor(baseUrl) {
|
|
9
|
+
this.baseUrl = baseUrl.replace(/\/+$/, '');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Build a commit transaction.
|
|
13
|
+
*
|
|
14
|
+
* `POST /commit` — returns the unsigned L1 transaction CBOR hex.
|
|
15
|
+
*/
|
|
16
|
+
async buildCommit(payload) {
|
|
17
|
+
const response = await this.post('/commit', payload);
|
|
18
|
+
return response.cborHex;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Publish a decommit transaction.
|
|
22
|
+
*
|
|
23
|
+
* `POST /decommit` — submits the decommit request to the Hydra node.
|
|
24
|
+
*/
|
|
25
|
+
async publishDecommit(transaction) {
|
|
26
|
+
return this.post('/decommit', { tag: 'Decommit', transaction });
|
|
27
|
+
}
|
|
28
|
+
/** Fetch the current UTxO snapshot. `GET /snapshot/utxo` */
|
|
29
|
+
async getSnapshotUtxo() {
|
|
30
|
+
return this.get('/snapshot/utxo');
|
|
31
|
+
}
|
|
32
|
+
/** Fetch protocol parameters. `GET /protocol-parameters` */
|
|
33
|
+
async getProtocolParameters() {
|
|
34
|
+
return this.get('/protocol-parameters');
|
|
35
|
+
}
|
|
36
|
+
async post(path, payload) {
|
|
37
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify(payload),
|
|
41
|
+
});
|
|
42
|
+
if (!response.ok && response.status !== 202) {
|
|
43
|
+
const body = await response.text().catch(() => '');
|
|
44
|
+
throw new Error(`Hydra HTTP ${response.status} on POST ${path}: ${body}`);
|
|
45
|
+
}
|
|
46
|
+
return response.json();
|
|
47
|
+
}
|
|
48
|
+
async get(path) {
|
|
49
|
+
const response = await fetch(`${this.baseUrl}${path}`);
|
|
50
|
+
if (!response.ok && response.status !== 202) {
|
|
51
|
+
const body = await response.text().catch(() => '');
|
|
52
|
+
throw new Error(`Hydra HTTP ${response.status} on GET ${path}: ${body}`);
|
|
53
|
+
}
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { ClientInput, ConnectionState, HydraStatus } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Thin WebSocket wrapper for the Hydra node.
|
|
5
|
+
*
|
|
6
|
+
* Uses `EventEmitter` for multi-listener message dispatch (no single-callback
|
|
7
|
+
* overwrite). Emits:
|
|
8
|
+
*
|
|
9
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
10
|
+
* - `'status'` — `(status: HydraStatus)` on head-status changes
|
|
11
|
+
* - `'error'` — `(err: Error)`
|
|
12
|
+
* - `'close'` — `()`
|
|
13
|
+
*/
|
|
14
|
+
export declare class HydraWebSocket extends EventEmitter {
|
|
15
|
+
private ws;
|
|
16
|
+
private readonly url;
|
|
17
|
+
private _status;
|
|
18
|
+
private _connectionState;
|
|
19
|
+
constructor(wsUrl: string);
|
|
20
|
+
/** Current connection state. */
|
|
21
|
+
get connectionState(): ConnectionState;
|
|
22
|
+
/** Open the WebSocket connection. Resolves when the socket is open. */
|
|
23
|
+
connect(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Connect and wait for the Greetings message from the Hydra node.
|
|
26
|
+
*
|
|
27
|
+
* Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
|
|
28
|
+
* message handlers — it uses a one-time EventEmitter listener.
|
|
29
|
+
*/
|
|
30
|
+
waitForGreetings(timeoutMs?: number): Promise<boolean>;
|
|
31
|
+
/** Close the WebSocket. */
|
|
32
|
+
disconnect(timeoutMs?: number): Promise<void>;
|
|
33
|
+
/** Send a ClientInput message as JSON over the WebSocket. */
|
|
34
|
+
send(message: ClientInput): void;
|
|
35
|
+
/** Current Hydra head status derived from messages. */
|
|
36
|
+
getStatus(): HydraStatus;
|
|
37
|
+
/** Register a status-change listener. Returns current status. */
|
|
38
|
+
onStatusChange(callback: (status: HydraStatus) => void): HydraStatus;
|
|
39
|
+
private handleMessage;
|
|
40
|
+
private updateStatus;
|
|
41
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
const HEAD_STATUS_TO_HYDRA = {
|
|
4
|
+
Idle: 'IDLE',
|
|
5
|
+
Initializing: 'INITIALIZING',
|
|
6
|
+
Open: 'OPEN',
|
|
7
|
+
Closed: 'CLOSED',
|
|
8
|
+
FanoutPossible: 'FANOUT_POSSIBLE',
|
|
9
|
+
Final: 'FINAL',
|
|
10
|
+
};
|
|
11
|
+
const TAG_TO_HYDRA = {
|
|
12
|
+
HeadIsInitializing: 'INITIALIZING',
|
|
13
|
+
HeadIsOpen: 'OPEN',
|
|
14
|
+
HeadIsClosed: 'CLOSED',
|
|
15
|
+
ReadyToFanout: 'FANOUT_POSSIBLE',
|
|
16
|
+
HeadIsFinalized: 'FINAL',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Thin WebSocket wrapper for the Hydra node.
|
|
20
|
+
*
|
|
21
|
+
* Uses `EventEmitter` for multi-listener message dispatch (no single-callback
|
|
22
|
+
* overwrite). Emits:
|
|
23
|
+
*
|
|
24
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
25
|
+
* - `'status'` — `(status: HydraStatus)` on head-status changes
|
|
26
|
+
* - `'error'` — `(err: Error)`
|
|
27
|
+
* - `'close'` — `()`
|
|
28
|
+
*/
|
|
29
|
+
export class HydraWebSocket extends EventEmitter {
|
|
30
|
+
ws = null;
|
|
31
|
+
url;
|
|
32
|
+
_status = 'IDLE';
|
|
33
|
+
_connectionState = 'IDLE';
|
|
34
|
+
constructor(wsUrl) {
|
|
35
|
+
super();
|
|
36
|
+
this.url = wsUrl;
|
|
37
|
+
}
|
|
38
|
+
/** Current connection state. */
|
|
39
|
+
get connectionState() {
|
|
40
|
+
return this._connectionState;
|
|
41
|
+
}
|
|
42
|
+
/** Open the WebSocket connection. Resolves when the socket is open. */
|
|
43
|
+
async connect() {
|
|
44
|
+
if (this.ws?.readyState === WebSocket.OPEN)
|
|
45
|
+
return;
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const ws = new WebSocket(this.url);
|
|
48
|
+
ws.on('open', () => {
|
|
49
|
+
this.ws = ws;
|
|
50
|
+
this._connectionState = 'CONNECTING';
|
|
51
|
+
resolve();
|
|
52
|
+
});
|
|
53
|
+
ws.on('message', (data) => {
|
|
54
|
+
try {
|
|
55
|
+
const msg = JSON.parse(data.toString());
|
|
56
|
+
this.handleMessage(msg);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Ignore non-JSON messages
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
ws.on('error', (err) => {
|
|
63
|
+
this._connectionState = 'FAILED';
|
|
64
|
+
this.emit('error', err);
|
|
65
|
+
reject(new Error(`WebSocket error: ${err.message}`));
|
|
66
|
+
});
|
|
67
|
+
ws.on('close', () => {
|
|
68
|
+
this._connectionState = 'DISCONNECTED';
|
|
69
|
+
this.ws = null;
|
|
70
|
+
this.emit('close');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Connect and wait for the Greetings message from the Hydra node.
|
|
76
|
+
*
|
|
77
|
+
* Unlike `HydraProvider.isConnected()`, this does NOT overwrite existing
|
|
78
|
+
* message handlers — it uses a one-time EventEmitter listener.
|
|
79
|
+
*/
|
|
80
|
+
async waitForGreetings(timeoutMs = 30_000) {
|
|
81
|
+
if (this._connectionState === 'CONNECTED')
|
|
82
|
+
return true;
|
|
83
|
+
await this.connect();
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
this.removeListener('message', onMsg);
|
|
87
|
+
this._connectionState = 'FAILED';
|
|
88
|
+
reject(new Error('Connection timed out: no Greetings from Hydra node'));
|
|
89
|
+
}, timeoutMs);
|
|
90
|
+
const onMsg = (msg) => {
|
|
91
|
+
if (msg.tag === 'Greetings') {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
this.removeListener('message', onMsg);
|
|
94
|
+
this._connectionState = 'CONNECTED';
|
|
95
|
+
resolve(true);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
this.on('message', onMsg);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** Close the WebSocket. */
|
|
102
|
+
async disconnect(timeoutMs = 5000) {
|
|
103
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
104
|
+
this._connectionState = 'DISCONNECTED';
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
this.ws?.terminate();
|
|
110
|
+
this._connectionState = 'DISCONNECTED';
|
|
111
|
+
this.ws = null;
|
|
112
|
+
resolve();
|
|
113
|
+
}, timeoutMs);
|
|
114
|
+
this.ws.on('close', () => {
|
|
115
|
+
clearTimeout(timer);
|
|
116
|
+
this._connectionState = 'DISCONNECTED';
|
|
117
|
+
this.ws = null;
|
|
118
|
+
resolve();
|
|
119
|
+
});
|
|
120
|
+
this.ws.close();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
/** Send a ClientInput message as JSON over the WebSocket. */
|
|
124
|
+
send(message) {
|
|
125
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
126
|
+
throw new Error('WebSocket is not connected');
|
|
127
|
+
}
|
|
128
|
+
this.ws.send(JSON.stringify(message));
|
|
129
|
+
}
|
|
130
|
+
/** Current Hydra head status derived from messages. */
|
|
131
|
+
getStatus() {
|
|
132
|
+
return this._status;
|
|
133
|
+
}
|
|
134
|
+
/** Register a status-change listener. Returns current status. */
|
|
135
|
+
onStatusChange(callback) {
|
|
136
|
+
this.on('status', callback);
|
|
137
|
+
return this._status;
|
|
138
|
+
}
|
|
139
|
+
handleMessage(msg) {
|
|
140
|
+
this.emit('message', msg);
|
|
141
|
+
// Update status from Greetings headStatus
|
|
142
|
+
if (msg.tag === 'Greetings' && 'headStatus' in msg) {
|
|
143
|
+
const mapped = HEAD_STATUS_TO_HYDRA[msg.headStatus];
|
|
144
|
+
if (mapped)
|
|
145
|
+
this.updateStatus(mapped);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// Update status from state transition messages
|
|
149
|
+
const mapped = TAG_TO_HYDRA[msg.tag];
|
|
150
|
+
if (mapped)
|
|
151
|
+
this.updateStatus(mapped);
|
|
152
|
+
}
|
|
153
|
+
updateStatus(newStatus) {
|
|
154
|
+
if (this._status !== newStatus) {
|
|
155
|
+
this._status = newStatus;
|
|
156
|
+
this.emit('status', newStatus);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
package/dist/hydra/messages.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-exported Hydra WebSocket message types
|
|
2
|
+
* Re-exported Hydra WebSocket message types.
|
|
3
3
|
*
|
|
4
4
|
* `ServerOutput` is a discriminated union (on `.tag`) of all possible messages
|
|
5
5
|
* the Hydra node can send over its WebSocket API. Use `Extract` to narrow:
|
|
@@ -7,25 +7,8 @@
|
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
9
|
* function handleOpen(msg: HydraMessage<"HeadIsOpen">) {
|
|
10
|
-
* console.log(msg.
|
|
10
|
+
* console.log(msg.tag);
|
|
11
11
|
* }
|
|
12
12
|
* ```
|
|
13
13
|
*/
|
|
14
|
-
export type { ClientInput, ClientMessage, ConnectionState, ServerOutput } from '
|
|
15
|
-
import type { ClientMessage, ServerOutput } from '@meshsdk/hydra';
|
|
16
|
-
/** Any message received via the Hydra WebSocket (server output or client echo). */
|
|
17
|
-
export type HydraWsMessage = ServerOutput | ClientMessage;
|
|
18
|
-
/**
|
|
19
|
-
* Extract a specific message type from the `ServerOutput` union by its `tag`.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* type Greetings = HydraMessage<"Greetings">;
|
|
24
|
-
* // { tag: "Greetings"; me: { vkey: string }; headStatus: HeadStatus; ... }
|
|
25
|
-
* ```
|
|
26
|
-
*/
|
|
27
|
-
export type HydraMessage<T extends ServerOutput['tag']> = Extract<ServerOutput, {
|
|
28
|
-
tag: T;
|
|
29
|
-
}>;
|
|
30
|
-
/** Possible Hydra head states as reported in the `Greetings` message. */
|
|
31
|
-
export type HeadStatus = 'Idle' | 'Initializing' | 'Open' | 'Closed' | 'FanoutPossible' | 'Final';
|
|
14
|
+
export type { ClientInput, ClientMessage, ConnectionState, HeadStatus, HydraMessage, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, } from './types.js';
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/** A Cardano transaction in Hydra's wire format. */
|
|
2
|
+
export type HydraTransaction = {
|
|
3
|
+
type: 'Tx ConwayEra' | 'Unwitnessed Tx ConwayEra' | 'Witnessed Tx ConwayEra';
|
|
4
|
+
description: string;
|
|
5
|
+
cborHex: string;
|
|
6
|
+
txId?: string;
|
|
7
|
+
};
|
|
8
|
+
/** @deprecated Use HydraTransaction */
|
|
9
|
+
export type hydraTransaction = HydraTransaction;
|
|
10
|
+
/** Hydra's nested asset value format: `{ lovelace: N, policyId: { assetNameHex: N } }`. */
|
|
11
|
+
export type HydraAssets = {
|
|
12
|
+
lovelace: number;
|
|
13
|
+
} & {
|
|
14
|
+
[policyId: string]: number | {
|
|
15
|
+
[assetNameHex: string]: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export interface HydraReferenceScript {
|
|
19
|
+
script: {
|
|
20
|
+
cborHex: string;
|
|
21
|
+
description: string;
|
|
22
|
+
type: string | null;
|
|
23
|
+
};
|
|
24
|
+
scriptLanguage: string;
|
|
25
|
+
}
|
|
26
|
+
/** A single UTxO entry in Hydra's wire format. */
|
|
27
|
+
export interface HydraUTxOEntry {
|
|
28
|
+
address: string;
|
|
29
|
+
datum: string | null;
|
|
30
|
+
inlineDatum: object | null;
|
|
31
|
+
inlineDatumRaw: string | null;
|
|
32
|
+
inlineDatumhash: string | null;
|
|
33
|
+
referenceScript: HydraReferenceScript | null;
|
|
34
|
+
value: HydraAssets;
|
|
35
|
+
}
|
|
36
|
+
/** A set of UTxOs keyed by `"txHash#outputIndex"`. */
|
|
37
|
+
export type HydraUTxOs = {
|
|
38
|
+
[txRef: string]: HydraUTxOEntry;
|
|
39
|
+
};
|
|
40
|
+
/** Payload for blueprint-based commits. */
|
|
41
|
+
export interface CommitBlueprintPayload {
|
|
42
|
+
blueprintTx: HydraTransaction;
|
|
43
|
+
utxo: HydraUTxOs;
|
|
44
|
+
}
|
|
45
|
+
/** Possible Hydra head states as reported in the `Greetings` message (mixed-case). */
|
|
46
|
+
export type HeadStatus = 'Idle' | 'Initializing' | 'Open' | 'Closed' | 'FanoutPossible' | 'Final';
|
|
47
|
+
/** Hydra status values (uppercase, as used by HydraProvider status tracking). */
|
|
48
|
+
export type HydraStatus = 'IDLE' | 'INITIALIZING' | 'OPEN' | 'CLOSED' | 'FANOUT_POSSIBLE' | 'FINAL';
|
|
49
|
+
/** @deprecated Use HydraStatus */
|
|
50
|
+
export type hydraStatus = HydraStatus;
|
|
51
|
+
/** Connection state of the WebSocket. */
|
|
52
|
+
export type ConnectionState = 'IDLE' | 'CONNECTING' | 'CONNECTED' | 'FAILED' | 'DISCONNECTED';
|
|
53
|
+
/** Messages that can be sent to the Hydra node over WebSocket. */
|
|
54
|
+
export type ClientInput = {
|
|
55
|
+
tag: 'Init';
|
|
56
|
+
} | {
|
|
57
|
+
tag: 'Abort';
|
|
58
|
+
} | {
|
|
59
|
+
tag: 'NewTx';
|
|
60
|
+
transaction: HydraTransaction;
|
|
61
|
+
} | {
|
|
62
|
+
tag: 'Close';
|
|
63
|
+
} | {
|
|
64
|
+
tag: 'Contest';
|
|
65
|
+
} | {
|
|
66
|
+
tag: 'Fanout';
|
|
67
|
+
} | {
|
|
68
|
+
tag: 'Decommit';
|
|
69
|
+
transaction: HydraTransaction;
|
|
70
|
+
} | {
|
|
71
|
+
tag: 'Recover';
|
|
72
|
+
recoverTxId: string;
|
|
73
|
+
};
|
|
74
|
+
/** Greetings message received on initial WebSocket connection. */
|
|
75
|
+
export interface GreetingsMessage {
|
|
76
|
+
tag: 'Greetings';
|
|
77
|
+
me: {
|
|
78
|
+
vkey: string;
|
|
79
|
+
};
|
|
80
|
+
headStatus: HeadStatus;
|
|
81
|
+
hydraHeadId?: string;
|
|
82
|
+
snapshotUtxo?: HydraUTxOs;
|
|
83
|
+
hydraNodeVersion?: string;
|
|
84
|
+
env?: Record<string, unknown>;
|
|
85
|
+
networkInfo?: Record<string, unknown>;
|
|
86
|
+
}
|
|
87
|
+
export interface HeadIsInitializingMessage {
|
|
88
|
+
tag: 'HeadIsInitializing';
|
|
89
|
+
headId: string;
|
|
90
|
+
parties: {
|
|
91
|
+
vkey: string;
|
|
92
|
+
}[];
|
|
93
|
+
[key: string]: unknown;
|
|
94
|
+
}
|
|
95
|
+
export interface CommittedMessage {
|
|
96
|
+
tag: 'Committed';
|
|
97
|
+
party: {
|
|
98
|
+
vkey: string;
|
|
99
|
+
};
|
|
100
|
+
utxo: HydraUTxOs;
|
|
101
|
+
[key: string]: unknown;
|
|
102
|
+
}
|
|
103
|
+
export interface HeadIsOpenMessage {
|
|
104
|
+
tag: 'HeadIsOpen';
|
|
105
|
+
utxo: HydraUTxOs;
|
|
106
|
+
[key: string]: unknown;
|
|
107
|
+
}
|
|
108
|
+
export interface HeadIsClosedMessage {
|
|
109
|
+
tag: 'HeadIsClosed';
|
|
110
|
+
[key: string]: unknown;
|
|
111
|
+
}
|
|
112
|
+
export interface HeadIsContestedMessage {
|
|
113
|
+
tag: 'HeadIsContested';
|
|
114
|
+
[key: string]: unknown;
|
|
115
|
+
}
|
|
116
|
+
export interface ReadyToFanoutMessage {
|
|
117
|
+
tag: 'ReadyToFanout';
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
export interface HeadIsAbortedMessage {
|
|
121
|
+
tag: 'HeadIsAborted';
|
|
122
|
+
[key: string]: unknown;
|
|
123
|
+
}
|
|
124
|
+
export interface HeadIsFinalizedMessage {
|
|
125
|
+
tag: 'HeadIsFinalized';
|
|
126
|
+
[key: string]: unknown;
|
|
127
|
+
}
|
|
128
|
+
export interface TxValidMessage {
|
|
129
|
+
tag: 'TxValid';
|
|
130
|
+
transaction: HydraTransaction;
|
|
131
|
+
[key: string]: unknown;
|
|
132
|
+
}
|
|
133
|
+
export interface TxInvalidMessage {
|
|
134
|
+
tag: 'TxInvalid';
|
|
135
|
+
transaction: HydraTransaction;
|
|
136
|
+
validationError: {
|
|
137
|
+
reason: string;
|
|
138
|
+
};
|
|
139
|
+
[key: string]: unknown;
|
|
140
|
+
}
|
|
141
|
+
export interface SnapshotConfirmedMessage {
|
|
142
|
+
tag: 'SnapshotConfirmed';
|
|
143
|
+
[key: string]: unknown;
|
|
144
|
+
}
|
|
145
|
+
export interface DecommitApprovedMessage {
|
|
146
|
+
tag: 'DecommitApproved';
|
|
147
|
+
[key: string]: unknown;
|
|
148
|
+
}
|
|
149
|
+
export interface DecommitInvalidMessage {
|
|
150
|
+
tag: 'DecommitInvalid';
|
|
151
|
+
decommitInvalidReason: unknown;
|
|
152
|
+
[key: string]: unknown;
|
|
153
|
+
}
|
|
154
|
+
export interface DecommitFinalizedMessage {
|
|
155
|
+
tag: 'DecommitFinalized';
|
|
156
|
+
[key: string]: unknown;
|
|
157
|
+
}
|
|
158
|
+
export interface CommitFinalizedMessage {
|
|
159
|
+
tag: 'CommitFinalized';
|
|
160
|
+
[key: string]: unknown;
|
|
161
|
+
}
|
|
162
|
+
export interface CommitApprovedMessage {
|
|
163
|
+
tag: 'CommitApproved';
|
|
164
|
+
[key: string]: unknown;
|
|
165
|
+
}
|
|
166
|
+
export interface CommandFailedMessage {
|
|
167
|
+
tag: 'CommandFailed';
|
|
168
|
+
clientInput: ClientInput;
|
|
169
|
+
[key: string]: unknown;
|
|
170
|
+
}
|
|
171
|
+
export interface PostTxOnChainFailedMessage {
|
|
172
|
+
tag: 'PostTxOnChainFailed';
|
|
173
|
+
[key: string]: unknown;
|
|
174
|
+
}
|
|
175
|
+
/** Catch-all for message types not explicitly defined above. */
|
|
176
|
+
export interface UnknownMessage {
|
|
177
|
+
tag: string;
|
|
178
|
+
[key: string]: unknown;
|
|
179
|
+
}
|
|
180
|
+
/** All possible messages the Hydra node can send over its WebSocket API. */
|
|
181
|
+
export type ServerOutput = GreetingsMessage | HeadIsInitializingMessage | CommittedMessage | HeadIsOpenMessage | HeadIsClosedMessage | HeadIsContestedMessage | ReadyToFanoutMessage | HeadIsAbortedMessage | HeadIsFinalizedMessage | TxValidMessage | TxInvalidMessage | SnapshotConfirmedMessage | DecommitApprovedMessage | DecommitInvalidMessage | DecommitFinalizedMessage | CommitFinalizedMessage | CommitApprovedMessage | CommandFailedMessage | PostTxOnChainFailedMessage | UnknownMessage;
|
|
182
|
+
/** Client echo messages (errors, command failures). */
|
|
183
|
+
export type ClientMessage = CommandFailedMessage | PostTxOnChainFailedMessage;
|
|
184
|
+
/** Any message received via the Hydra WebSocket. */
|
|
185
|
+
export type HydraWsMessage = ServerOutput | ClientMessage;
|
|
186
|
+
/**
|
|
187
|
+
* Extract a specific message type from the `ServerOutput` union by its `tag`.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```ts
|
|
191
|
+
* type Greetings = HydraMessage<"Greetings">;
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
export type HydraMessage<T extends ServerOutput['tag']> = Extract<ServerOutput, {
|
|
195
|
+
tag: T;
|
|
196
|
+
}>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Asset, UTxO } from '@meshsdk/common';
|
|
2
|
+
import type { HydraAssets, HydraUTxOEntry, HydraUTxOs } from './types.js';
|
|
3
|
+
/** Convert MeshSDK `Asset[]` to Hydra's nested value format. */
|
|
4
|
+
export declare function toHydraAssets(assets: Asset[]): HydraAssets;
|
|
5
|
+
/** Convert Hydra's nested value format back to MeshSDK `Asset[]`. */
|
|
6
|
+
export declare function fromHydraAssets(assetsObj: HydraAssets): Asset[];
|
|
7
|
+
/** Convert a single MeshSDK UTxO to Hydra wire format. */
|
|
8
|
+
export declare function toHydraUTxO(utxo: UTxO): HydraUTxOEntry;
|
|
9
|
+
/** Convert multiple MeshSDK UTxOs to Hydra wire format (keyed by `"txHash#outputIndex"`). */
|
|
10
|
+
export declare function toHydraUTxOs(utxos: UTxO[]): HydraUTxOs;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { fromScriptRef, parseDatumCbor } from '@meshsdk/core-cst';
|
|
2
|
+
// ── Asset Conversion ─────────────────────────────────────────────────
|
|
3
|
+
/** Convert MeshSDK `Asset[]` to Hydra's nested value format. */
|
|
4
|
+
export function toHydraAssets(assets) {
|
|
5
|
+
return assets.reduce((acc, asset) => {
|
|
6
|
+
if (asset.unit === '' || asset.unit === 'lovelace') {
|
|
7
|
+
acc.lovelace += Number(asset.quantity);
|
|
8
|
+
}
|
|
9
|
+
else {
|
|
10
|
+
const policyId = asset.unit.slice(0, 56);
|
|
11
|
+
const assetNameHex = asset.unit.slice(56) || '';
|
|
12
|
+
if (!acc[policyId] || typeof acc[policyId] === 'number') {
|
|
13
|
+
acc[policyId] = {};
|
|
14
|
+
}
|
|
15
|
+
const policy = acc[policyId];
|
|
16
|
+
policy[assetNameHex] = (policy[assetNameHex] ?? 0) + Number(asset.quantity);
|
|
17
|
+
}
|
|
18
|
+
return acc;
|
|
19
|
+
}, { lovelace: 0 });
|
|
20
|
+
}
|
|
21
|
+
/** Convert Hydra's nested value format back to MeshSDK `Asset[]`. */
|
|
22
|
+
export function fromHydraAssets(assetsObj) {
|
|
23
|
+
const result = [];
|
|
24
|
+
if (assetsObj.lovelace && assetsObj.lovelace > 0) {
|
|
25
|
+
result.push({ unit: 'lovelace', quantity: String(assetsObj.lovelace) });
|
|
26
|
+
}
|
|
27
|
+
for (const [policyId, assets] of Object.entries(assetsObj)) {
|
|
28
|
+
if (policyId === 'lovelace')
|
|
29
|
+
continue;
|
|
30
|
+
if (typeof assets !== 'object')
|
|
31
|
+
continue;
|
|
32
|
+
for (const [assetNameHex, quantity] of Object.entries(assets)) {
|
|
33
|
+
result.push({ unit: policyId + assetNameHex, quantity: String(quantity) });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
// ── Reference Script Conversion ──────────────────────────────────────
|
|
39
|
+
function resolveReferenceScript(scriptRef) {
|
|
40
|
+
if (!scriptRef)
|
|
41
|
+
return null;
|
|
42
|
+
const scriptInstance = fromScriptRef(scriptRef);
|
|
43
|
+
if (!scriptInstance)
|
|
44
|
+
return null;
|
|
45
|
+
let scriptType = 'Unknown';
|
|
46
|
+
let scriptLanguage = null;
|
|
47
|
+
if ('code' in scriptInstance) {
|
|
48
|
+
switch (scriptInstance.version) {
|
|
49
|
+
case 'V1':
|
|
50
|
+
scriptType = 'PlutusScriptV1';
|
|
51
|
+
scriptLanguage = 'PlutusScriptLanguage PlutusScriptV1';
|
|
52
|
+
break;
|
|
53
|
+
case 'V2':
|
|
54
|
+
scriptType = 'PlutusScriptV2';
|
|
55
|
+
scriptLanguage = 'PlutusScriptLanguage PlutusScriptV2';
|
|
56
|
+
break;
|
|
57
|
+
case 'V3':
|
|
58
|
+
scriptType = 'PlutusScriptV3';
|
|
59
|
+
scriptLanguage = 'PlutusScriptLanguage PlutusScriptV3';
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
scriptType = 'SimpleScript';
|
|
65
|
+
scriptLanguage = 'NativeScriptLanguage SimpleScript';
|
|
66
|
+
}
|
|
67
|
+
if (!scriptLanguage || scriptType === 'Unknown')
|
|
68
|
+
return null;
|
|
69
|
+
return {
|
|
70
|
+
script: {
|
|
71
|
+
cborHex: scriptRef,
|
|
72
|
+
description: '',
|
|
73
|
+
type: scriptType,
|
|
74
|
+
},
|
|
75
|
+
scriptLanguage,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
// ── Datum Resolution ─────────────────────────────────────────────────
|
|
79
|
+
function resolvePlutusData(datumCbor) {
|
|
80
|
+
const data = parseDatumCbor(datumCbor);
|
|
81
|
+
function normalize(value) {
|
|
82
|
+
if (typeof value === 'bigint') {
|
|
83
|
+
return value <= Number.MAX_SAFE_INTEGER && value >= Number.MIN_SAFE_INTEGER ? Number(value) : value.toString();
|
|
84
|
+
}
|
|
85
|
+
if (Array.isArray(value)) {
|
|
86
|
+
return value.map(normalize);
|
|
87
|
+
}
|
|
88
|
+
if (value && typeof value === 'object') {
|
|
89
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, normalize(v)]));
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
return { inlineDatum: normalize(data) };
|
|
94
|
+
}
|
|
95
|
+
// ── UTxO Conversion ──────────────────────────────────────────────────
|
|
96
|
+
/** Convert a single MeshSDK UTxO to Hydra wire format. */
|
|
97
|
+
export function toHydraUTxO(utxo) {
|
|
98
|
+
return {
|
|
99
|
+
address: utxo.output.address,
|
|
100
|
+
datum: null,
|
|
101
|
+
inlineDatum: utxo.output.plutusData ? resolvePlutusData(utxo.output.plutusData).inlineDatum : null,
|
|
102
|
+
inlineDatumRaw: utxo.output.plutusData ?? null,
|
|
103
|
+
inlineDatumhash: utxo.output.dataHash ?? null,
|
|
104
|
+
referenceScript: utxo.output.scriptRef ? resolveReferenceScript(utxo.output.scriptRef) : null,
|
|
105
|
+
value: toHydraAssets(utxo.output.amount),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Convert multiple MeshSDK UTxOs to Hydra wire format (keyed by `"txHash#outputIndex"`). */
|
|
109
|
+
export function toHydraUTxOs(utxos) {
|
|
110
|
+
return Object.fromEntries(utxos.map((utxo) => [`${utxo.input.txHash}#${utxo.input.outputIndex}`, toHydraUTxO(utxo)]));
|
|
111
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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 type { HeadStatus, HydraMessage, HydraWsMessage, ServerOutput } from './hydra/messages.js';
|
|
4
|
+
export type { HeadStatus, HydraMessage, HydraStatus, HydraTransaction, HydraWsMessage, hydraStatus, hydraTransaction, ServerOutput, } from './hydra/messages.js';
|
|
5
5
|
export type { ParsedUtxo } from './hydra/utxo.js';
|
|
6
6
|
export { getUtxoSet, queryUtxoByAddress } from './hydra/utxo.js';
|
|
7
7
|
export type { IpfsClient, IpfsConfig, PinResult } from './ipfs/ipfs.js';
|
|
8
8
|
export { createIpfsClient } from './ipfs/ipfs.js';
|
|
9
9
|
export { getAdmin } from './mesh/get-admin.js';
|
|
10
10
|
export { createMultisigAddress, createNativeScript } from './mesh/native-script.js';
|
|
11
|
-
export { CommitArgs, UTxORef, Wrangler } from './mesh/wrangler.js';
|
|
12
11
|
export { submitTx } from './tx3/submit-tx.js';
|
|
13
12
|
export { chunkString } from './utils/chunk-string.js';
|
|
14
13
|
export { bufferToAscii, bufferToHex, verifySignature } from './utils/verify-signature.js';
|
|
14
|
+
export { CommitArgs, UTxORef, Wrangler } from './wrangler.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ export { getUtxoSet, queryUtxoByAddress } from './hydra/utxo.js';
|
|
|
4
4
|
export { createIpfsClient } from './ipfs/ipfs.js';
|
|
5
5
|
export { getAdmin } from './mesh/get-admin.js';
|
|
6
6
|
export { createMultisigAddress, createNativeScript } from './mesh/native-script.js';
|
|
7
|
-
export { Wrangler } from './mesh/wrangler.js';
|
|
8
7
|
export { submitTx } from './tx3/submit-tx.js';
|
|
9
8
|
export { chunkString } from './utils/chunk-string.js';
|
|
10
9
|
export { bufferToAscii, bufferToHex, verifySignature } from './utils/verify-signature.js';
|
|
10
|
+
export { Wrangler } from './wrangler.js';
|
package/dist/test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as dotenv from 'dotenv';
|
|
2
2
|
dotenv.config({ path: '.local.env' });
|
|
3
3
|
import { BlockfrostProvider, MeshTxBuilder, MeshWallet } from '@meshsdk/core';
|
|
4
|
-
import { Wrangler } from './
|
|
4
|
+
import { Wrangler } from './wrangler.js';
|
|
5
5
|
(async () => {
|
|
6
6
|
const admin_wallet = new MeshWallet({
|
|
7
7
|
networkId: parseInt(process.env.HYDRA_NETWORK_ID || '0', 10),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import type { HeadStatus } from '
|
|
1
|
+
import { HydraHttpClient } from './hydra/hydra-http-client.js';
|
|
2
|
+
import { HydraWebSocket } from './hydra/hydra-websocket.js';
|
|
3
|
+
import type { HeadStatus, HydraStatus, HydraTransaction } from './hydra/types.js';
|
|
4
4
|
/** UTxO reference for committing funds into a Hydra head. */
|
|
5
5
|
export interface UTxORef {
|
|
6
6
|
/** Transaction hash containing the UTxO. */
|
|
@@ -13,17 +13,17 @@ export interface CommitArgs {
|
|
|
13
13
|
/** One or more UTxO references to commit. */
|
|
14
14
|
utxos: UTxORef[];
|
|
15
15
|
/** Blueprint transaction that spends the committed UTxOs (CBOR-encoded, unsigned). Optional — omit for simple ADA-only commits. */
|
|
16
|
-
blueprintTx?:
|
|
16
|
+
blueprintTx?: HydraTransaction;
|
|
17
17
|
}
|
|
18
18
|
/**
|
|
19
19
|
* High-level controller for Hydra head lifecycle operations.
|
|
20
20
|
*
|
|
21
|
-
* Wraps `
|
|
21
|
+
* Wraps `HydraWebSocket` and `HydraHttpClient` to provide a simplified API
|
|
22
22
|
* for initializing, opening, and closing a Hydra head.
|
|
23
23
|
*
|
|
24
24
|
* @example
|
|
25
25
|
* ```ts
|
|
26
|
-
* const wrangler = new Wrangler("http://localhost:4001");
|
|
26
|
+
* const wrangler = new Wrangler("http://localhost:4001", "ws://localhost:4001");
|
|
27
27
|
* await wrangler.waitForHeadOpen({
|
|
28
28
|
* utxos: [{ txHash: "abc...", outputIndex: 0 }],
|
|
29
29
|
* blueprintTx: { type: "Tx ConwayEra", cborHex: "...", description: "" },
|
|
@@ -32,20 +32,15 @@ export interface CommitArgs {
|
|
|
32
32
|
*/
|
|
33
33
|
export declare class Wrangler {
|
|
34
34
|
private mode;
|
|
35
|
-
readonly
|
|
36
|
-
|
|
35
|
+
readonly ws: HydraWebSocket;
|
|
36
|
+
readonly http: HydraHttpClient;
|
|
37
37
|
private readonly blockfrost;
|
|
38
|
-
private readonly url;
|
|
39
|
-
private readonly wsUrl;
|
|
40
38
|
constructor(url?: string, wsUrl?: string);
|
|
41
|
-
private createHydraProvider;
|
|
42
|
-
private createHydraInstance;
|
|
43
39
|
/**
|
|
44
40
|
* Connect to the Hydra node with exponential-backoff retry.
|
|
45
41
|
*
|
|
46
|
-
* Uses `
|
|
47
|
-
* **and** waits for the Hydra `Greetings` handshake
|
|
48
|
-
* `connect()` which only opens the socket.
|
|
42
|
+
* Uses `HydraWebSocket.waitForGreetings()` which establishes the WebSocket
|
|
43
|
+
* **and** waits for the Hydra `Greetings` handshake.
|
|
49
44
|
*
|
|
50
45
|
* @param maxAttempts - Maximum number of connection attempts (default 5).
|
|
51
46
|
* @param baseDelayMs - Initial retry delay in milliseconds (default 1000). Doubles each attempt, capped at 30 s.
|
|
@@ -54,16 +49,19 @@ export declare class Wrangler {
|
|
|
54
49
|
/**
|
|
55
50
|
* Shared helper for promise-based methods that wait for a specific
|
|
56
51
|
* Hydra message. Handles connection, timeout, and settlement in one place.
|
|
52
|
+
*
|
|
53
|
+
* Uses EventEmitter `on`/`removeListener` for multi-listener support,
|
|
54
|
+
* avoiding the single-callback overwrite bug in HydraProvider.
|
|
57
55
|
*/
|
|
58
56
|
private awaitMessage;
|
|
59
|
-
/** Connect the underlying
|
|
57
|
+
/** Connect the underlying WebSocket with retry logic. */
|
|
60
58
|
connect(): Promise<void>;
|
|
61
|
-
/** Disconnect the underlying
|
|
59
|
+
/** Disconnect the underlying WebSocket. */
|
|
62
60
|
disconnect(timeout?: number): Promise<void>;
|
|
63
|
-
/** Return the current
|
|
64
|
-
getStatus():
|
|
65
|
-
/** Register a callback for
|
|
66
|
-
onStatusChange(callback: (status:
|
|
61
|
+
/** Return the current Hydra head status (uppercase). */
|
|
62
|
+
getStatus(): HydraStatus;
|
|
63
|
+
/** Register a callback for head status changes. */
|
|
64
|
+
onStatusChange(callback: (status: HydraStatus) => void): HydraStatus;
|
|
67
65
|
/** Begin the head-opening sequence: init, commit, and listen for state changes. */
|
|
68
66
|
startHead(commitArgs: CommitArgs): Promise<void>;
|
|
69
67
|
/** Begin the head-closing sequence: close, fanout, and finalize. */
|
|
@@ -89,16 +87,13 @@ export declare class Wrangler {
|
|
|
89
87
|
/**
|
|
90
88
|
* Decommit funds from an open Hydra head back to L1.
|
|
91
89
|
*
|
|
92
|
-
* Posts the decommit transaction via
|
|
93
|
-
* instead of `provider.decommit()` to avoid overwriting the Wrangler's
|
|
94
|
-
* `onMessage` handler (single-callback replacement pattern).
|
|
95
|
-
*
|
|
90
|
+
* Posts the decommit transaction via HTTP to avoid overwriting message handlers.
|
|
96
91
|
* Resolves on `DecommitApproved` — L1 settlement happens asynchronously.
|
|
97
92
|
*
|
|
98
93
|
* @param transaction - The decommit transaction (CBOR-encoded).
|
|
99
94
|
* @param timeoutMs - Maximum time to wait for approval (default 60s).
|
|
100
95
|
*/
|
|
101
|
-
decommit(transaction:
|
|
96
|
+
decommit(transaction: HydraTransaction, timeoutMs?: number): Promise<void>;
|
|
102
97
|
/**
|
|
103
98
|
* Incrementally commit funds into an already-open Hydra head.
|
|
104
99
|
*
|
|
@@ -112,6 +107,7 @@ export declare class Wrangler {
|
|
|
112
107
|
*/
|
|
113
108
|
incrementalCommit(commitArgs: CommitArgs, timeoutMs?: number): Promise<void>;
|
|
114
109
|
private doIncrementalCommit;
|
|
110
|
+
private fetchUtxos;
|
|
115
111
|
private handleIncoming;
|
|
116
112
|
private onGreetings;
|
|
117
113
|
}
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { BlockfrostProvider } from '@meshsdk/core';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { requireEnv } from './config.js';
|
|
3
|
+
import { HydraHttpClient } from './hydra/hydra-http-client.js';
|
|
4
|
+
import { HydraWebSocket } from './hydra/hydra-websocket.js';
|
|
5
|
+
import { toHydraUTxO, toHydraUTxOs } from './hydra/utxo-conversion.js';
|
|
4
6
|
/**
|
|
5
7
|
* High-level controller for Hydra head lifecycle operations.
|
|
6
8
|
*
|
|
7
|
-
* Wraps `
|
|
9
|
+
* Wraps `HydraWebSocket` and `HydraHttpClient` to provide a simplified API
|
|
8
10
|
* for initializing, opening, and closing a Hydra head.
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* ```ts
|
|
12
|
-
* const wrangler = new Wrangler("http://localhost:4001");
|
|
14
|
+
* const wrangler = new Wrangler("http://localhost:4001", "ws://localhost:4001");
|
|
13
15
|
* await wrangler.waitForHeadOpen({
|
|
14
16
|
* utxos: [{ txHash: "abc...", outputIndex: 0 }],
|
|
15
17
|
* blueprintTx: { type: "Tx ConwayEra", cborHex: "...", description: "" },
|
|
@@ -18,34 +20,21 @@ import { requireEnv } from '../config.js';
|
|
|
18
20
|
*/
|
|
19
21
|
export class Wrangler {
|
|
20
22
|
mode;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
ws;
|
|
24
|
+
http;
|
|
23
25
|
blockfrost;
|
|
24
|
-
url;
|
|
25
|
-
wsUrl;
|
|
26
26
|
constructor(url, wsUrl) {
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const httpUrl = url || requireEnv('HYDRA_API_URL');
|
|
28
|
+
const socketUrl = wsUrl || requireEnv('HYDRA_WS_URL');
|
|
29
29
|
this.blockfrost = new BlockfrostProvider(requireEnv('BLOCKFROST_API_KEY'));
|
|
30
|
-
this.
|
|
31
|
-
this.
|
|
32
|
-
}
|
|
33
|
-
createHydraProvider() {
|
|
34
|
-
return new HydraProvider({ httpUrl: this.url, history: false });
|
|
35
|
-
}
|
|
36
|
-
createHydraInstance() {
|
|
37
|
-
return new HydraInstance({
|
|
38
|
-
provider: this.provider,
|
|
39
|
-
fetcher: this.blockfrost,
|
|
40
|
-
submitter: this.provider,
|
|
41
|
-
});
|
|
30
|
+
this.ws = new HydraWebSocket(socketUrl);
|
|
31
|
+
this.http = new HydraHttpClient(httpUrl);
|
|
42
32
|
}
|
|
43
33
|
/**
|
|
44
34
|
* Connect to the Hydra node with exponential-backoff retry.
|
|
45
35
|
*
|
|
46
|
-
* Uses `
|
|
47
|
-
* **and** waits for the Hydra `Greetings` handshake
|
|
48
|
-
* `connect()` which only opens the socket.
|
|
36
|
+
* Uses `HydraWebSocket.waitForGreetings()` which establishes the WebSocket
|
|
37
|
+
* **and** waits for the Hydra `Greetings` handshake.
|
|
49
38
|
*
|
|
50
39
|
* @param maxAttempts - Maximum number of connection attempts (default 5).
|
|
51
40
|
* @param baseDelayMs - Initial retry delay in milliseconds (default 1000). Doubles each attempt, capped at 30 s.
|
|
@@ -53,10 +42,10 @@ export class Wrangler {
|
|
|
53
42
|
async connectWithRetry(maxAttempts = 5, baseDelayMs = 1000) {
|
|
54
43
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
55
44
|
try {
|
|
56
|
-
const connected = await this.
|
|
45
|
+
const connected = await this.ws.waitForGreetings();
|
|
57
46
|
if (connected)
|
|
58
47
|
return;
|
|
59
|
-
throw new Error('
|
|
48
|
+
throw new Error('waitForGreetings() returned false');
|
|
60
49
|
}
|
|
61
50
|
catch (err) {
|
|
62
51
|
if (attempt === maxAttempts - 1) {
|
|
@@ -70,6 +59,9 @@ export class Wrangler {
|
|
|
70
59
|
/**
|
|
71
60
|
* Shared helper for promise-based methods that wait for a specific
|
|
72
61
|
* Hydra message. Handles connection, timeout, and settlement in one place.
|
|
62
|
+
*
|
|
63
|
+
* Uses EventEmitter `on`/`removeListener` for multi-listener support,
|
|
64
|
+
* avoiding the single-callback overwrite bug in HydraProvider.
|
|
73
65
|
*/
|
|
74
66
|
awaitMessage(handler, timeoutMs, timeoutMessage) {
|
|
75
67
|
return new Promise((resolve, reject) => {
|
|
@@ -79,42 +71,47 @@ export class Wrangler {
|
|
|
79
71
|
return;
|
|
80
72
|
settled = true;
|
|
81
73
|
clearTimeout(timer);
|
|
74
|
+
this.ws.removeListener('message', onMsg);
|
|
82
75
|
fn(value);
|
|
83
76
|
};
|
|
84
|
-
this.provider.onMessage((message) => {
|
|
85
|
-
handler(message, (value) => settle(resolve, value), (reason) => settle(reject, reason));
|
|
86
|
-
});
|
|
87
77
|
const timer = setTimeout(() => settle(reject, new Error(timeoutMessage)), timeoutMs);
|
|
88
|
-
|
|
78
|
+
const onMsg = (message) => {
|
|
79
|
+
handler(message, (value) => settle(resolve, value), (reason) => settle(reject, reason));
|
|
80
|
+
};
|
|
81
|
+
this.connectWithRetry()
|
|
82
|
+
.then(() => {
|
|
83
|
+
this.ws.on('message', onMsg);
|
|
84
|
+
})
|
|
85
|
+
.catch((err) => settle(reject, new Error(`Failed to connect: ${String(err)}`)));
|
|
89
86
|
});
|
|
90
87
|
}
|
|
91
|
-
/** Connect the underlying
|
|
88
|
+
/** Connect the underlying WebSocket with retry logic. */
|
|
92
89
|
async connect() {
|
|
93
90
|
return await this.connectWithRetry();
|
|
94
91
|
}
|
|
95
|
-
/** Disconnect the underlying
|
|
92
|
+
/** Disconnect the underlying WebSocket. */
|
|
96
93
|
async disconnect(timeout) {
|
|
97
|
-
return this.
|
|
94
|
+
return this.ws.disconnect(timeout);
|
|
98
95
|
}
|
|
99
|
-
/** Return the current
|
|
96
|
+
/** Return the current Hydra head status (uppercase). */
|
|
100
97
|
getStatus() {
|
|
101
|
-
return this.
|
|
98
|
+
return this.ws.getStatus();
|
|
102
99
|
}
|
|
103
|
-
/** Register a callback for
|
|
100
|
+
/** Register a callback for head status changes. */
|
|
104
101
|
onStatusChange(callback) {
|
|
105
|
-
return this.
|
|
102
|
+
return this.ws.onStatusChange(callback);
|
|
106
103
|
}
|
|
107
104
|
/** Begin the head-opening sequence: init, commit, and listen for state changes. */
|
|
108
105
|
async startHead(commitArgs) {
|
|
109
106
|
this.mode = 'start';
|
|
110
|
-
this.provider.onMessage((msg) => this.handleIncoming(msg, commitArgs));
|
|
111
107
|
await this.connectWithRetry();
|
|
108
|
+
this.ws.on('message', (msg) => this.handleIncoming(msg, commitArgs));
|
|
112
109
|
}
|
|
113
110
|
/** Begin the head-closing sequence: close, fanout, and finalize. */
|
|
114
111
|
async shutdownHead() {
|
|
115
112
|
this.mode = 'shutdown';
|
|
116
|
-
this.provider.onMessage((msg) => this.handleIncoming(msg));
|
|
117
113
|
await this.connectWithRetry();
|
|
114
|
+
this.ws.on('message', (msg) => this.handleIncoming(msg));
|
|
118
115
|
}
|
|
119
116
|
/**
|
|
120
117
|
* Wait for the Hydra head to fully close and finalize.
|
|
@@ -129,7 +126,7 @@ export class Wrangler {
|
|
|
129
126
|
resolve();
|
|
130
127
|
break;
|
|
131
128
|
case 'ReadyToFanout':
|
|
132
|
-
this.
|
|
129
|
+
this.ws.send({ tag: 'Fanout' });
|
|
133
130
|
break;
|
|
134
131
|
case 'Greetings':
|
|
135
132
|
this.onGreetings(message.headStatus).catch((err) => reject(new Error(`Greetings handler failed: ${String(err)}`)));
|
|
@@ -171,29 +168,32 @@ export class Wrangler {
|
|
|
171
168
|
}, timeoutMs, 'Timeout waiting for head status');
|
|
172
169
|
}
|
|
173
170
|
async doCommit(commitArgs) {
|
|
174
|
-
let
|
|
171
|
+
let cborHex;
|
|
175
172
|
if (commitArgs.blueprintTx) {
|
|
176
|
-
|
|
173
|
+
const utxos = await this.fetchUtxos(commitArgs.utxos);
|
|
174
|
+
const hydraUtxos = toHydraUTxOs(utxos);
|
|
175
|
+
cborHex = await this.http.buildCommit({ blueprintTx: commitArgs.blueprintTx, utxo: hydraUtxos });
|
|
177
176
|
}
|
|
178
177
|
else if (commitArgs.utxos.length === 0) {
|
|
179
|
-
|
|
178
|
+
cborHex = await this.http.buildCommit({});
|
|
180
179
|
}
|
|
181
180
|
else if (commitArgs.utxos.length === 1) {
|
|
182
181
|
const { txHash, outputIndex } = commitArgs.utxos[0];
|
|
183
|
-
|
|
182
|
+
const utxos = await this.blockfrost.fetchUTxOs(txHash, outputIndex);
|
|
183
|
+
if (!utxos[0])
|
|
184
|
+
throw new Error('UTxO not found');
|
|
185
|
+
const hydraUtxo = toHydraUTxO(utxos[0]);
|
|
186
|
+
cborHex = await this.http.buildCommit({ [`${txHash}#${outputIndex}`]: hydraUtxo });
|
|
184
187
|
}
|
|
185
188
|
else {
|
|
186
189
|
throw new Error('Multiple UTxOs without a blueprintTx require a blueprint transaction');
|
|
187
190
|
}
|
|
188
|
-
return await this.blockfrost.submitTx(
|
|
191
|
+
return await this.blockfrost.submitTx(cborHex);
|
|
189
192
|
}
|
|
190
193
|
/**
|
|
191
194
|
* Decommit funds from an open Hydra head back to L1.
|
|
192
195
|
*
|
|
193
|
-
* Posts the decommit transaction via
|
|
194
|
-
* instead of `provider.decommit()` to avoid overwriting the Wrangler's
|
|
195
|
-
* `onMessage` handler (single-callback replacement pattern).
|
|
196
|
-
*
|
|
196
|
+
* Posts the decommit transaction via HTTP to avoid overwriting message handlers.
|
|
197
197
|
* Resolves on `DecommitApproved` — L1 settlement happens asynchronously.
|
|
198
198
|
*
|
|
199
199
|
* @param transaction - The decommit transaction (CBOR-encoded).
|
|
@@ -212,7 +212,7 @@ export class Wrangler {
|
|
|
212
212
|
reject(new Error(`Decommit invalid: ${JSON.stringify(message.decommitInvalidReason)}`));
|
|
213
213
|
}
|
|
214
214
|
}, timeoutMs, 'Timeout waiting for decommit approval');
|
|
215
|
-
await this.
|
|
215
|
+
await this.http.publishDecommit(transaction);
|
|
216
216
|
return result;
|
|
217
217
|
}
|
|
218
218
|
/**
|
|
@@ -243,14 +243,32 @@ export class Wrangler {
|
|
|
243
243
|
throw new Error('Incremental commit requires exactly one UTxO');
|
|
244
244
|
}
|
|
245
245
|
const { txHash, outputIndex } = commitArgs.utxos[0];
|
|
246
|
-
|
|
246
|
+
const utxos = await this.blockfrost.fetchUTxOs(txHash, outputIndex);
|
|
247
|
+
if (!utxos[0])
|
|
248
|
+
throw new Error('UTxO not found');
|
|
249
|
+
const hydraUtxo = toHydraUTxO(utxos[0]);
|
|
250
|
+
let cborHex;
|
|
247
251
|
if (commitArgs.blueprintTx) {
|
|
248
|
-
|
|
252
|
+
cborHex = await this.http.buildCommit({
|
|
253
|
+
blueprintTx: commitArgs.blueprintTx,
|
|
254
|
+
utxo: { [`${txHash}#${outputIndex}`]: hydraUtxo },
|
|
255
|
+
});
|
|
249
256
|
}
|
|
250
257
|
else {
|
|
251
|
-
|
|
258
|
+
cborHex = await this.http.buildCommit({ [`${txHash}#${outputIndex}`]: hydraUtxo });
|
|
259
|
+
}
|
|
260
|
+
return await this.blockfrost.submitTx(cborHex);
|
|
261
|
+
}
|
|
262
|
+
async fetchUtxos(utxoRefs) {
|
|
263
|
+
const results = [];
|
|
264
|
+
for (const { txHash, outputIndex } of utxoRefs) {
|
|
265
|
+
const utxos = await this.blockfrost.fetchUTxOs(txHash, outputIndex);
|
|
266
|
+
if (!utxos.length) {
|
|
267
|
+
throw new Error(`UTxO not found for ${txHash}#${outputIndex}`);
|
|
268
|
+
}
|
|
269
|
+
results.push(...utxos);
|
|
252
270
|
}
|
|
253
|
-
return
|
|
271
|
+
return results;
|
|
254
272
|
}
|
|
255
273
|
async handleIncoming(message, commitArgs) {
|
|
256
274
|
if (message.tag === 'Greetings') {
|
|
@@ -277,7 +295,7 @@ export class Wrangler {
|
|
|
277
295
|
break;
|
|
278
296
|
case 'shutdown':
|
|
279
297
|
if (message.tag === 'ReadyToFanout') {
|
|
280
|
-
|
|
298
|
+
this.ws.send({ tag: 'Fanout' });
|
|
281
299
|
}
|
|
282
300
|
break;
|
|
283
301
|
}
|
|
@@ -289,7 +307,7 @@ export class Wrangler {
|
|
|
289
307
|
switch (status) {
|
|
290
308
|
case 'Idle':
|
|
291
309
|
console.log('Idle → init()');
|
|
292
|
-
|
|
310
|
+
this.ws.send({ tag: 'Init' });
|
|
293
311
|
break;
|
|
294
312
|
case 'Initializing':
|
|
295
313
|
console.log('Initializing -> commit()');
|
|
@@ -310,11 +328,11 @@ export class Wrangler {
|
|
|
310
328
|
switch (status) {
|
|
311
329
|
case 'Open':
|
|
312
330
|
console.log('Shutting down: closing head…');
|
|
313
|
-
|
|
331
|
+
this.ws.send({ tag: 'Close' });
|
|
314
332
|
break;
|
|
315
333
|
case 'FanoutPossible':
|
|
316
334
|
console.log('Fanout now possible: fanning out…');
|
|
317
|
-
|
|
335
|
+
this.ws.send({ tag: 'Fanout' });
|
|
318
336
|
break;
|
|
319
337
|
default:
|
|
320
338
|
console.log(`Greetings in shutdown mode, ignoring status: ${status}`);
|
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.18",
|
|
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",
|
|
@@ -42,10 +42,13 @@
|
|
|
42
42
|
"@emurgo/cardano-message-signing-nodejs": "^1.1.0",
|
|
43
43
|
"@emurgo/cardano-serialization-lib-nodejs": "^15.0.3",
|
|
44
44
|
"@meshsdk/core": "1.9.0-beta.99",
|
|
45
|
-
"@meshsdk/
|
|
46
|
-
"axios": "^1.11.0"
|
|
45
|
+
"@meshsdk/core-cst": "1.9.0-beta.99",
|
|
46
|
+
"axios": "^1.11.0",
|
|
47
|
+
"ws": "^8.18.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@types/ws": "^8.18.1"
|
|
47
51
|
},
|
|
48
|
-
"devDependencies": {},
|
|
49
52
|
"homepage": "https://github.com/Lerna-Labs/hydra-sdk#readme",
|
|
50
53
|
"bugs": {
|
|
51
54
|
"url": "https://github.com/Lerna-Labs/hydra-sdk/issues"
|