@lerna-labs/hydra-sdk 1.0.0-beta.9 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +159 -0
- package/dist/cache/disk-cache.d.ts +37 -0
- package/dist/cache/disk-cache.js +84 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.js +12 -0
- package/dist/hydra/hydra-http-client.d.ts +36 -0
- package/dist/hydra/hydra-http-client.js +66 -0
- package/dist/hydra/hydra-monitor.d.ts +88 -0
- package/dist/hydra/hydra-monitor.js +270 -0
- package/dist/hydra/hydra-websocket.d.ts +46 -0
- package/dist/hydra/hydra-websocket.js +181 -0
- package/dist/hydra/messages.d.ts +14 -0
- package/dist/hydra/messages.js +1 -0
- package/dist/hydra/types.d.ts +486 -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/hydra/utxo.d.ts +25 -5
- package/dist/hydra/utxo.js +37 -31
- package/dist/index.d.ts +15 -7
- package/dist/index.js +11 -7
- package/dist/ipfs/ipfs.d.ts +22 -0
- package/dist/ipfs/ipfs.js +90 -0
- package/dist/mesh/get-admin.d.ts +13 -1
- package/dist/mesh/get-admin.js +37 -7
- package/dist/mesh/native-script.d.ts +30 -5
- package/dist/mesh/native-script.js +38 -10
- package/dist/test.js +3 -3
- package/dist/tx3/submit-tx.d.ts +8 -0
- package/dist/tx3/submit-tx.js +8 -0
- package/dist/utils/chunk-string.d.ts +7 -0
- package/dist/utils/chunk-string.js +7 -0
- package/dist/utils/verify-signature.d.ts +28 -5
- package/dist/utils/verify-signature.js +39 -18
- package/dist/wrangler.d.ts +179 -0
- package/dist/wrangler.js +452 -0
- package/package.json +25 -6
- package/dist/mesh/wrangler.d.ts +0 -29
- package/dist/mesh/wrangler.js +0 -277
package/README.md
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# @lerna-labs/hydra-sdk
|
|
2
|
+
|
|
3
|
+
Core TypeScript SDK for managing [Cardano Hydra](https://hydra.family/) Heads — connect, open, transact, and close Hydra Heads with a high-level API.
|
|
4
|
+
|
|
5
|
+
> **Beta** — APIs may change between releases. Currently at `1.0.0-beta.x`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @lerna-labs/hydra-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Environment Variables
|
|
14
|
+
|
|
15
|
+
The SDK reads configuration from environment variables at the point of use (never at import time):
|
|
16
|
+
|
|
17
|
+
| Variable | Required | Description |
|
|
18
|
+
|----------|----------|-------------|
|
|
19
|
+
| `BLOCKFROST_API_KEY` | Yes (Wrangler) | Blockfrost project ID for L1 chain queries |
|
|
20
|
+
| `HYDRA_API_URL` | Yes (UTxO queries) | Hydra node HTTP API endpoint (e.g. `http://localhost:4001`) |
|
|
21
|
+
| `HYDRA_WS_URL` | Yes (Wrangler) | Hydra node WebSocket endpoint (e.g. `ws://localhost:4001`) |
|
|
22
|
+
| `HYDRA_ADMIN_KEY_FILE` | One required | Path to a Cardano `.sk` signing key file |
|
|
23
|
+
| `HYDRA_ADMIN_CARDANO_PK` | One required | Cardano private key hex string (fallback if no key file) |
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Wrangler — Head Lifecycle Management
|
|
28
|
+
|
|
29
|
+
The `Wrangler` class manages the full Hydra Head lifecycle: connecting, opening, monitoring, and closing.
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { Wrangler } from "@lerna-labs/hydra-sdk";
|
|
33
|
+
|
|
34
|
+
const wrangler = new Wrangler();
|
|
35
|
+
|
|
36
|
+
// Connect to the Hydra node with automatic retry
|
|
37
|
+
await wrangler.connect();
|
|
38
|
+
|
|
39
|
+
// Open the Head (commits UTxO from L1 tx)
|
|
40
|
+
await wrangler.waitForHeadOpen({ txHash: "abc123...", txIndex: 0 });
|
|
41
|
+
|
|
42
|
+
// Monitor status
|
|
43
|
+
const status = await wrangler.getHeadStatus();
|
|
44
|
+
wrangler.onStatusChange((status) => console.log("Status:", status));
|
|
45
|
+
|
|
46
|
+
// Close and finalize
|
|
47
|
+
await wrangler.waitForHeadClose();
|
|
48
|
+
|
|
49
|
+
// Disconnect when done
|
|
50
|
+
await wrangler.disconnect();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### UTxO Queries
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { getUtxoSet, queryUtxoByAddress } from "@lerna-labs/hydra-sdk";
|
|
57
|
+
|
|
58
|
+
// Get all UTxOs in the Hydra Head
|
|
59
|
+
const utxos = await getUtxoSet();
|
|
60
|
+
|
|
61
|
+
// Query UTxOs for a specific address
|
|
62
|
+
const myUtxos = await queryUtxoByAddress("addr_test1...");
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Wallet & Admin
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { getAdmin, createMultisigAddress } from "@lerna-labs/hydra-sdk";
|
|
69
|
+
|
|
70
|
+
// Get a MeshWallet instance from env-configured signing key
|
|
71
|
+
const adminWallet = await getAdmin();
|
|
72
|
+
|
|
73
|
+
// Create a multisig address from two participant addresses
|
|
74
|
+
const { address, scriptCbor, scriptHash } = createMultisigAddress(
|
|
75
|
+
"addr_test1...",
|
|
76
|
+
"addr_test2...",
|
|
77
|
+
);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Signature Verification
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { verifySignature } from "@lerna-labs/hydra-sdk";
|
|
84
|
+
|
|
85
|
+
const { isValid, sigMeta, pubKeyHex } = verifySignature(
|
|
86
|
+
signature,
|
|
87
|
+
message,
|
|
88
|
+
signingAddress,
|
|
89
|
+
signatureKey,
|
|
90
|
+
);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Transaction Submission
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { submitTx } from "@lerna-labs/hydra-sdk";
|
|
97
|
+
|
|
98
|
+
const response = await submitTx(submitEndpoint, cborPayload, txId);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Config Helpers
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { requireEnv, optionalEnv } from "@lerna-labs/hydra-sdk";
|
|
105
|
+
|
|
106
|
+
// Throws with a clear message if missing
|
|
107
|
+
const apiKey = requireEnv("BLOCKFROST_API_KEY");
|
|
108
|
+
|
|
109
|
+
// Returns fallback if not set
|
|
110
|
+
const url = optionalEnv("HYDRA_API_URL", "http://localhost:4001");
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Utilities
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { chunkString, bufferToHex, bufferToAscii } from "@lerna-labs/hydra-sdk";
|
|
117
|
+
|
|
118
|
+
const chunks = chunkString("abcdef", 2); // ["ab", "cd", "ef"]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## API Reference
|
|
122
|
+
|
|
123
|
+
### Functions
|
|
124
|
+
|
|
125
|
+
| Export | Description |
|
|
126
|
+
|--------|-------------|
|
|
127
|
+
| `getAdmin()` | Create a `MeshWallet` from env-configured signing key |
|
|
128
|
+
| `createMultisigAddress(addr1, addr2, networkId?, scriptType?)` | Build a multisig address from two participant addresses |
|
|
129
|
+
| `createNativeScript(addr, opts?)` | Build a native script policy — bare `sig` by default, or time-bound `all:[sig, before]` when `opts.invalidHereafter` is set |
|
|
130
|
+
| `getUtxoSet()` | Fetch all UTxOs in the Hydra Head |
|
|
131
|
+
| `queryUtxoByAddress(address)` | Fetch UTxOs for a specific address |
|
|
132
|
+
| `submitTx(endpoint, payload, id)` | Submit a signed transaction to the Hydra node |
|
|
133
|
+
| `verifySignature(signature, message, address, key)` | Verify a CIP-8 message signature |
|
|
134
|
+
| `requireEnv(name)` | Read a required environment variable (throws if missing) |
|
|
135
|
+
| `optionalEnv(name, fallback)` | Read an optional environment variable with fallback |
|
|
136
|
+
| `chunkString(str, size)` | Split a string into fixed-size chunks |
|
|
137
|
+
| `bufferToHex(buffer)` | Convert a buffer to a hex string |
|
|
138
|
+
| `bufferToAscii(buffer)` | Convert a buffer to an ASCII string |
|
|
139
|
+
|
|
140
|
+
### Classes
|
|
141
|
+
|
|
142
|
+
| Export | Description |
|
|
143
|
+
|--------|-------------|
|
|
144
|
+
| `Wrangler` | Hydra Head lifecycle manager — connect, open, monitor, close |
|
|
145
|
+
|
|
146
|
+
### Types
|
|
147
|
+
|
|
148
|
+
| Export | Description |
|
|
149
|
+
|--------|-------------|
|
|
150
|
+
| `CommitArgs` | Arguments for committing UTxOs when opening a Head |
|
|
151
|
+
| `HeadStatus` | Hydra Head status identifier |
|
|
152
|
+
| `HydraMessage` | Typed Hydra protocol message |
|
|
153
|
+
| `HydraWsMessage` | Raw WebSocket message from Hydra node |
|
|
154
|
+
| `ParsedUtxo` | Parsed UTxO with address, value, and datum |
|
|
155
|
+
| `ServerOutput` | Hydra node server output message type |
|
|
156
|
+
|
|
157
|
+
## License
|
|
158
|
+
|
|
159
|
+
[Apache-2.0](../../LICENSE)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface DiskCacheConfig {
|
|
2
|
+
/** Root directory for all cache storage (e.g. "/ipfs-staging"). */
|
|
3
|
+
stagingDir: string;
|
|
4
|
+
/**
|
|
5
|
+
* Subdirectory name for the full document payloads.
|
|
6
|
+
* @default "documents"
|
|
7
|
+
*/
|
|
8
|
+
documentsSubdir?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Subdirectory name for the latest-entry lookup files.
|
|
11
|
+
* @default "latest"
|
|
12
|
+
*/
|
|
13
|
+
latestSubdir?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A generic disk-backed in-memory cache keyed by a string identifier.
|
|
17
|
+
*
|
|
18
|
+
* Each entry is persisted to disk in two places:
|
|
19
|
+
* - `documents/` — the full payload (intended for IPFS pinning)
|
|
20
|
+
* - `latest/` — a lightweight lookup file keyed by id (for fast rehydration)
|
|
21
|
+
*
|
|
22
|
+
* On startup, call {@link rehydrate} to rebuild the in-memory `Map` from the
|
|
23
|
+
* `latest/` directory so the cache survives service restarts.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam E - The shape of a cache entry (must include a string `id` field
|
|
26
|
+
* used as the Map key and the latest/ filename).
|
|
27
|
+
*/
|
|
28
|
+
export declare function createDiskCache<E extends object>(config: DiskCacheConfig,
|
|
29
|
+
/** Extract the cache key from an entry. */
|
|
30
|
+
keyFn: (entry: E) => string): {
|
|
31
|
+
rehydrate: () => Promise<number>;
|
|
32
|
+
put: (entry: E, filename: string, fullPayload: unknown) => Promise<string>;
|
|
33
|
+
get: (key: string) => E | undefined;
|
|
34
|
+
getAll: () => E[];
|
|
35
|
+
getDocumentsDir: () => string;
|
|
36
|
+
};
|
|
37
|
+
export type DiskCache<E extends object> = ReturnType<typeof createDiskCache<E>>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* A generic disk-backed in-memory cache keyed by a string identifier.
|
|
5
|
+
*
|
|
6
|
+
* Each entry is persisted to disk in two places:
|
|
7
|
+
* - `documents/` — the full payload (intended for IPFS pinning)
|
|
8
|
+
* - `latest/` — a lightweight lookup file keyed by id (for fast rehydration)
|
|
9
|
+
*
|
|
10
|
+
* On startup, call {@link rehydrate} to rebuild the in-memory `Map` from the
|
|
11
|
+
* `latest/` directory so the cache survives service restarts.
|
|
12
|
+
*
|
|
13
|
+
* @typeParam E - The shape of a cache entry (must include a string `id` field
|
|
14
|
+
* used as the Map key and the latest/ filename).
|
|
15
|
+
*/
|
|
16
|
+
export function createDiskCache(config,
|
|
17
|
+
/** Extract the cache key from an entry. */
|
|
18
|
+
keyFn) {
|
|
19
|
+
const docsDir = path.join(config.stagingDir, config.documentsSubdir ?? 'documents');
|
|
20
|
+
const latestDir = path.join(config.stagingDir, config.latestSubdir ?? 'latest');
|
|
21
|
+
const cache = new Map();
|
|
22
|
+
/** Ensure the storage directories exist. */
|
|
23
|
+
async function ensureDirs() {
|
|
24
|
+
await fs.mkdir(docsDir, { recursive: true });
|
|
25
|
+
await fs.mkdir(latestDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Rebuild the in-memory cache from the `latest/` directory on disk.
|
|
29
|
+
* Call once before the service starts accepting requests.
|
|
30
|
+
*
|
|
31
|
+
* @returns The number of entries loaded.
|
|
32
|
+
*/
|
|
33
|
+
async function rehydrate() {
|
|
34
|
+
await ensureDirs();
|
|
35
|
+
const files = await fs.readdir(latestDir);
|
|
36
|
+
let count = 0;
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
if (!file.endsWith('.json'))
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
const raw = await fs.readFile(path.join(latestDir, file), 'utf-8');
|
|
42
|
+
const entry = JSON.parse(raw);
|
|
43
|
+
cache.set(keyFn(entry), entry);
|
|
44
|
+
count++;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Skip corrupt / unparseable files
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return count;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Store an entry in both the in-memory cache and on disk.
|
|
54
|
+
*
|
|
55
|
+
* @param entry - The cache entry.
|
|
56
|
+
* @param filename - Filename for the full payload in `documents/`.
|
|
57
|
+
* @param fullPayload - The complete payload to persist (may differ from
|
|
58
|
+
* the lightweight entry stored in `latest/`).
|
|
59
|
+
* @returns Absolute path to the written document file.
|
|
60
|
+
*/
|
|
61
|
+
async function put(entry, filename, fullPayload) {
|
|
62
|
+
await ensureDirs();
|
|
63
|
+
const key = keyFn(entry);
|
|
64
|
+
cache.set(key, entry);
|
|
65
|
+
const docPath = path.join(docsDir, filename);
|
|
66
|
+
await fs.writeFile(docPath, JSON.stringify(fullPayload, null, 2));
|
|
67
|
+
const latestPath = path.join(latestDir, `${key}.json`);
|
|
68
|
+
await fs.writeFile(latestPath, JSON.stringify(entry));
|
|
69
|
+
return docPath;
|
|
70
|
+
}
|
|
71
|
+
/** Retrieve an entry by key from the in-memory cache. */
|
|
72
|
+
function get(key) {
|
|
73
|
+
return cache.get(key);
|
|
74
|
+
}
|
|
75
|
+
/** Return all cached entries. */
|
|
76
|
+
function getAll() {
|
|
77
|
+
return Array.from(cache.values());
|
|
78
|
+
}
|
|
79
|
+
/** Return the absolute path to the documents directory (for IPFS pinning). */
|
|
80
|
+
function getDocumentsDir() {
|
|
81
|
+
return docsDir;
|
|
82
|
+
}
|
|
83
|
+
return { rehydrate, put, get, getAll, getDocumentsDir };
|
|
84
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Read a required environment variable or throw with a clear message. */
|
|
2
|
+
export declare function requireEnv(name: string): string;
|
|
3
|
+
/** Read an optional environment variable with a fallback default. */
|
|
4
|
+
export declare function optionalEnv(name: string, fallback: string): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Read a required environment variable or throw with a clear message. */
|
|
2
|
+
export function requireEnv(name) {
|
|
3
|
+
const value = process.env[name];
|
|
4
|
+
if (!value) {
|
|
5
|
+
throw new Error(`Missing required environment variable: ${name}`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
/** Read an optional environment variable with a fallback default. */
|
|
10
|
+
export function optionalEnv(name, fallback) {
|
|
11
|
+
return process.env[name] || fallback;
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
/**
|
|
25
|
+
* List the transaction ids of deposits that are pending (observed but not yet
|
|
26
|
+
* incremented into the head). `GET /commits`
|
|
27
|
+
*
|
|
28
|
+
* A deposit can linger here if an L1 rollback cancelled its increment — see
|
|
29
|
+
* {@link recoverable deposits}. Used to detect a stuck deposit and retry.
|
|
30
|
+
*/
|
|
31
|
+
getPendingCommits(): Promise<string[]>;
|
|
32
|
+
/** Fetch protocol parameters. `GET /protocol-parameters` */
|
|
33
|
+
getProtocolParameters(): Promise<unknown>;
|
|
34
|
+
private post;
|
|
35
|
+
private get;
|
|
36
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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', transaction);
|
|
27
|
+
}
|
|
28
|
+
/** Fetch the current UTxO snapshot. `GET /snapshot/utxo` */
|
|
29
|
+
async getSnapshotUtxo() {
|
|
30
|
+
return this.get('/snapshot/utxo');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* List the transaction ids of deposits that are pending (observed but not yet
|
|
34
|
+
* incremented into the head). `GET /commits`
|
|
35
|
+
*
|
|
36
|
+
* A deposit can linger here if an L1 rollback cancelled its increment — see
|
|
37
|
+
* {@link recoverable deposits}. Used to detect a stuck deposit and retry.
|
|
38
|
+
*/
|
|
39
|
+
async getPendingCommits() {
|
|
40
|
+
return this.get('/commits');
|
|
41
|
+
}
|
|
42
|
+
/** Fetch protocol parameters. `GET /protocol-parameters` */
|
|
43
|
+
async getProtocolParameters() {
|
|
44
|
+
return this.get('/protocol-parameters');
|
|
45
|
+
}
|
|
46
|
+
async post(path, payload) {
|
|
47
|
+
const response = await fetch(`${this.baseUrl}${path}`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify(payload),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok && response.status !== 202) {
|
|
53
|
+
const body = await response.text().catch(() => '');
|
|
54
|
+
throw new Error(`Hydra HTTP ${response.status} on POST ${path}: ${body}`);
|
|
55
|
+
}
|
|
56
|
+
return response.json();
|
|
57
|
+
}
|
|
58
|
+
async get(path) {
|
|
59
|
+
const response = await fetch(`${this.baseUrl}${path}`);
|
|
60
|
+
if (!response.ok && response.status !== 202) {
|
|
61
|
+
const body = await response.text().catch(() => '');
|
|
62
|
+
throw new Error(`Hydra HTTP ${response.status} on GET ${path}: ${body}`);
|
|
63
|
+
}
|
|
64
|
+
return response.json();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { HydraWebSocket } from './hydra-websocket.js';
|
|
3
|
+
import type { GreetingsMessage, HeadStatus, HydraHeadInfo, HydraMessage, HydraMonitorOptions, HydraStatus, ServerOutput, TimestampedEvent } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Persistent WebSocket monitor for a Hydra head.
|
|
6
|
+
*
|
|
7
|
+
* Maintains a single long-lived WebSocket connection with auto-reconnect,
|
|
8
|
+
* real-time head state tracking, and proactive error surfacing.
|
|
9
|
+
*
|
|
10
|
+
* Events emitted:
|
|
11
|
+
* - `'message'` — `(msg: HydraWsMessage)` for every incoming message
|
|
12
|
+
* - `'status'` — `(status: HydraStatus, previous: HydraStatus)` on head-status changes
|
|
13
|
+
* - `'error:tx'` — `(msg)` on PostTxOnChainFailed or TxInvalid
|
|
14
|
+
* - `'error:command'` — `(msg)` on CommandFailed
|
|
15
|
+
* - `'error:decommit'` — `(msg)` on DecommitInvalid
|
|
16
|
+
* - `'connected'` — `()` WebSocket open + Greetings received
|
|
17
|
+
* - `'disconnected'` — `()` WebSocket closed (reconnect may follow)
|
|
18
|
+
* - `'reconnecting'` — `(attempt: number, delayMs: number)`
|
|
19
|
+
* - `'reconnect_failed'` — `()` maxAttempts exhausted
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```ts
|
|
23
|
+
* const monitor = new HydraMonitor({ wsUrl: 'ws://hydra-node:4102' });
|
|
24
|
+
* await monitor.start();
|
|
25
|
+
* console.log(monitor.headStatus); // 'IDLE'
|
|
26
|
+
* monitor.on('status', (s, prev) => console.log(`${prev} → ${s}`));
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export declare class HydraMonitor extends EventEmitter {
|
|
30
|
+
readonly ws: HydraWebSocket;
|
|
31
|
+
private _headStatus;
|
|
32
|
+
private _previousStatus;
|
|
33
|
+
private _headId;
|
|
34
|
+
private _events;
|
|
35
|
+
private _stopped;
|
|
36
|
+
private _reconnecting;
|
|
37
|
+
private readonly reconnectEnabled;
|
|
38
|
+
private readonly baseDelayMs;
|
|
39
|
+
private readonly maxDelayMs;
|
|
40
|
+
private readonly maxAttempts;
|
|
41
|
+
private readonly eventBufferSize;
|
|
42
|
+
private readonly boundOnMessage;
|
|
43
|
+
private readonly boundOnClose;
|
|
44
|
+
constructor(options: HydraMonitorOptions);
|
|
45
|
+
/** Connect to the Hydra node. Resolves after Greetings received. */
|
|
46
|
+
start(): Promise<void>;
|
|
47
|
+
/** Disconnect and stop reconnecting. */
|
|
48
|
+
stop(): Promise<void>;
|
|
49
|
+
/** Whether the monitor is actively connected and listening. */
|
|
50
|
+
get connected(): boolean;
|
|
51
|
+
/** Current head status (uppercase). */
|
|
52
|
+
get headStatus(): HydraStatus;
|
|
53
|
+
/** Current head status (mixed-case, as reported in Greetings). */
|
|
54
|
+
get headStatusMixed(): HeadStatus;
|
|
55
|
+
/** The full Greetings message from the most recent connection. */
|
|
56
|
+
get greetings(): GreetingsMessage | null;
|
|
57
|
+
/**
|
|
58
|
+
* Summary of Hydra head info derived from live state plus the last Greetings.
|
|
59
|
+
*
|
|
60
|
+
* `headStatus` and `headId` reflect the current state tracked from transition
|
|
61
|
+
* messages (`HeadIsOpen`, `HeadIsClosed`, etc.), not the snapshot taken
|
|
62
|
+
* at connection time. The remaining fields (node version, participants,
|
|
63
|
+
* contestation period, network info) come from the cached Greetings since
|
|
64
|
+
* they are static for the life of the head.
|
|
65
|
+
*
|
|
66
|
+
* Excludes the full UTxO snapshot to keep payloads small.
|
|
67
|
+
* Returns `null` if no Greetings has been received yet.
|
|
68
|
+
*/
|
|
69
|
+
get headInfo(): HydraHeadInfo | null;
|
|
70
|
+
/** The last N events (configurable via eventBufferSize). Most recent last. */
|
|
71
|
+
get recentEvents(): readonly TimestampedEvent[];
|
|
72
|
+
/**
|
|
73
|
+
* Wait for headStatus to reach the target. Resolves immediately if already there.
|
|
74
|
+
* @param target - The HydraStatus to wait for.
|
|
75
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
76
|
+
*/
|
|
77
|
+
waitForStatus(target: HydraStatus, timeoutMs?: number): Promise<void>;
|
|
78
|
+
/**
|
|
79
|
+
* Wait for the next message matching the given tag.
|
|
80
|
+
* @param tag - The message tag to wait for.
|
|
81
|
+
* @param timeoutMs - Maximum wait time (default 60s).
|
|
82
|
+
*/
|
|
83
|
+
waitForMessage<T extends ServerOutput['tag']>(tag: T, timeoutMs?: number): Promise<HydraMessage<T>>;
|
|
84
|
+
private onMessage;
|
|
85
|
+
private updateStatus;
|
|
86
|
+
private onClose;
|
|
87
|
+
private reconnectLoop;
|
|
88
|
+
}
|