@lerna-labs/hydra-sdk 1.0.0-beta.17 → 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} +73 -63
- 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,50 +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
77
|
const timer = setTimeout(() => settle(reject, new Error(timeoutMessage)), timeoutMs);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// Greetings message via its own handler. Once connected, we register our handler —
|
|
89
|
-
// onMessage() replays the _messageQueue, so the Greetings is re-delivered to us.
|
|
78
|
+
const onMsg = (message) => {
|
|
79
|
+
handler(message, (value) => settle(resolve, value), (reason) => settle(reject, reason));
|
|
80
|
+
};
|
|
90
81
|
this.connectWithRetry()
|
|
91
82
|
.then(() => {
|
|
92
|
-
this.
|
|
93
|
-
handler(message, (value) => settle(resolve, value), (reason) => settle(reject, reason));
|
|
94
|
-
});
|
|
83
|
+
this.ws.on('message', onMsg);
|
|
95
84
|
})
|
|
96
85
|
.catch((err) => settle(reject, new Error(`Failed to connect: ${String(err)}`)));
|
|
97
86
|
});
|
|
98
87
|
}
|
|
99
|
-
/** Connect the underlying
|
|
88
|
+
/** Connect the underlying WebSocket with retry logic. */
|
|
100
89
|
async connect() {
|
|
101
90
|
return await this.connectWithRetry();
|
|
102
91
|
}
|
|
103
|
-
/** Disconnect the underlying
|
|
92
|
+
/** Disconnect the underlying WebSocket. */
|
|
104
93
|
async disconnect(timeout) {
|
|
105
|
-
return this.
|
|
94
|
+
return this.ws.disconnect(timeout);
|
|
106
95
|
}
|
|
107
|
-
/** Return the current
|
|
96
|
+
/** Return the current Hydra head status (uppercase). */
|
|
108
97
|
getStatus() {
|
|
109
|
-
return this.
|
|
98
|
+
return this.ws.getStatus();
|
|
110
99
|
}
|
|
111
|
-
/** Register a callback for
|
|
100
|
+
/** Register a callback for head status changes. */
|
|
112
101
|
onStatusChange(callback) {
|
|
113
|
-
return this.
|
|
102
|
+
return this.ws.onStatusChange(callback);
|
|
114
103
|
}
|
|
115
104
|
/** Begin the head-opening sequence: init, commit, and listen for state changes. */
|
|
116
105
|
async startHead(commitArgs) {
|
|
117
106
|
this.mode = 'start';
|
|
118
107
|
await this.connectWithRetry();
|
|
119
|
-
this.
|
|
108
|
+
this.ws.on('message', (msg) => this.handleIncoming(msg, commitArgs));
|
|
120
109
|
}
|
|
121
110
|
/** Begin the head-closing sequence: close, fanout, and finalize. */
|
|
122
111
|
async shutdownHead() {
|
|
123
112
|
this.mode = 'shutdown';
|
|
124
113
|
await this.connectWithRetry();
|
|
125
|
-
this.
|
|
114
|
+
this.ws.on('message', (msg) => this.handleIncoming(msg));
|
|
126
115
|
}
|
|
127
116
|
/**
|
|
128
117
|
* Wait for the Hydra head to fully close and finalize.
|
|
@@ -137,7 +126,7 @@ export class Wrangler {
|
|
|
137
126
|
resolve();
|
|
138
127
|
break;
|
|
139
128
|
case 'ReadyToFanout':
|
|
140
|
-
this.
|
|
129
|
+
this.ws.send({ tag: 'Fanout' });
|
|
141
130
|
break;
|
|
142
131
|
case 'Greetings':
|
|
143
132
|
this.onGreetings(message.headStatus).catch((err) => reject(new Error(`Greetings handler failed: ${String(err)}`)));
|
|
@@ -179,29 +168,32 @@ export class Wrangler {
|
|
|
179
168
|
}, timeoutMs, 'Timeout waiting for head status');
|
|
180
169
|
}
|
|
181
170
|
async doCommit(commitArgs) {
|
|
182
|
-
let
|
|
171
|
+
let cborHex;
|
|
183
172
|
if (commitArgs.blueprintTx) {
|
|
184
|
-
|
|
173
|
+
const utxos = await this.fetchUtxos(commitArgs.utxos);
|
|
174
|
+
const hydraUtxos = toHydraUTxOs(utxos);
|
|
175
|
+
cborHex = await this.http.buildCommit({ blueprintTx: commitArgs.blueprintTx, utxo: hydraUtxos });
|
|
185
176
|
}
|
|
186
177
|
else if (commitArgs.utxos.length === 0) {
|
|
187
|
-
|
|
178
|
+
cborHex = await this.http.buildCommit({});
|
|
188
179
|
}
|
|
189
180
|
else if (commitArgs.utxos.length === 1) {
|
|
190
181
|
const { txHash, outputIndex } = commitArgs.utxos[0];
|
|
191
|
-
|
|
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 });
|
|
192
187
|
}
|
|
193
188
|
else {
|
|
194
189
|
throw new Error('Multiple UTxOs without a blueprintTx require a blueprint transaction');
|
|
195
190
|
}
|
|
196
|
-
return await this.blockfrost.submitTx(
|
|
191
|
+
return await this.blockfrost.submitTx(cborHex);
|
|
197
192
|
}
|
|
198
193
|
/**
|
|
199
194
|
* Decommit funds from an open Hydra head back to L1.
|
|
200
195
|
*
|
|
201
|
-
* Posts the decommit transaction via
|
|
202
|
-
* instead of `provider.decommit()` to avoid overwriting the Wrangler's
|
|
203
|
-
* `onMessage` handler (single-callback replacement pattern).
|
|
204
|
-
*
|
|
196
|
+
* Posts the decommit transaction via HTTP to avoid overwriting message handlers.
|
|
205
197
|
* Resolves on `DecommitApproved` — L1 settlement happens asynchronously.
|
|
206
198
|
*
|
|
207
199
|
* @param transaction - The decommit transaction (CBOR-encoded).
|
|
@@ -220,7 +212,7 @@ export class Wrangler {
|
|
|
220
212
|
reject(new Error(`Decommit invalid: ${JSON.stringify(message.decommitInvalidReason)}`));
|
|
221
213
|
}
|
|
222
214
|
}, timeoutMs, 'Timeout waiting for decommit approval');
|
|
223
|
-
await this.
|
|
215
|
+
await this.http.publishDecommit(transaction);
|
|
224
216
|
return result;
|
|
225
217
|
}
|
|
226
218
|
/**
|
|
@@ -251,14 +243,32 @@ export class Wrangler {
|
|
|
251
243
|
throw new Error('Incremental commit requires exactly one UTxO');
|
|
252
244
|
}
|
|
253
245
|
const { txHash, outputIndex } = commitArgs.utxos[0];
|
|
254
|
-
|
|
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;
|
|
255
251
|
if (commitArgs.blueprintTx) {
|
|
256
|
-
|
|
252
|
+
cborHex = await this.http.buildCommit({
|
|
253
|
+
blueprintTx: commitArgs.blueprintTx,
|
|
254
|
+
utxo: { [`${txHash}#${outputIndex}`]: hydraUtxo },
|
|
255
|
+
});
|
|
257
256
|
}
|
|
258
257
|
else {
|
|
259
|
-
|
|
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);
|
|
260
270
|
}
|
|
261
|
-
return
|
|
271
|
+
return results;
|
|
262
272
|
}
|
|
263
273
|
async handleIncoming(message, commitArgs) {
|
|
264
274
|
if (message.tag === 'Greetings') {
|
|
@@ -285,7 +295,7 @@ export class Wrangler {
|
|
|
285
295
|
break;
|
|
286
296
|
case 'shutdown':
|
|
287
297
|
if (message.tag === 'ReadyToFanout') {
|
|
288
|
-
|
|
298
|
+
this.ws.send({ tag: 'Fanout' });
|
|
289
299
|
}
|
|
290
300
|
break;
|
|
291
301
|
}
|
|
@@ -297,7 +307,7 @@ export class Wrangler {
|
|
|
297
307
|
switch (status) {
|
|
298
308
|
case 'Idle':
|
|
299
309
|
console.log('Idle → init()');
|
|
300
|
-
|
|
310
|
+
this.ws.send({ tag: 'Init' });
|
|
301
311
|
break;
|
|
302
312
|
case 'Initializing':
|
|
303
313
|
console.log('Initializing -> commit()');
|
|
@@ -318,11 +328,11 @@ export class Wrangler {
|
|
|
318
328
|
switch (status) {
|
|
319
329
|
case 'Open':
|
|
320
330
|
console.log('Shutting down: closing head…');
|
|
321
|
-
|
|
331
|
+
this.ws.send({ tag: 'Close' });
|
|
322
332
|
break;
|
|
323
333
|
case 'FanoutPossible':
|
|
324
334
|
console.log('Fanout now possible: fanning out…');
|
|
325
|
-
|
|
335
|
+
this.ws.send({ tag: 'Fanout' });
|
|
326
336
|
break;
|
|
327
337
|
default:
|
|
328
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"
|