@hardkas/node-runner 0.2.2-alpha.1 → 0.3.0-alpha
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/index.d.ts +23 -1
- package/dist/index.js +125 -8
- package/package.json +5 -3
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ interface DockerKaspadOptions {
|
|
|
12
12
|
readonly ports?: Partial<KaspadPorts>;
|
|
13
13
|
readonly dataDir?: string;
|
|
14
14
|
readonly detach?: boolean;
|
|
15
|
+
readonly allowFloatingImage?: boolean;
|
|
15
16
|
}
|
|
16
17
|
interface KaspadNodeStatus {
|
|
17
18
|
readonly containerName: string;
|
|
@@ -21,9 +22,27 @@ interface KaspadNodeStatus {
|
|
|
21
22
|
readonly statusText?: string;
|
|
22
23
|
readonly ports: KaspadPorts;
|
|
23
24
|
readonly dataDir: string;
|
|
25
|
+
readonly rpcUrl: string;
|
|
26
|
+
readonly rpcReady: boolean;
|
|
27
|
+
readonly transports: {
|
|
28
|
+
readonly grpc: {
|
|
29
|
+
readonly port: number;
|
|
30
|
+
readonly ready: boolean;
|
|
31
|
+
};
|
|
32
|
+
readonly borsh: {
|
|
33
|
+
readonly port: number;
|
|
34
|
+
readonly ready: boolean;
|
|
35
|
+
};
|
|
36
|
+
readonly json: {
|
|
37
|
+
readonly port: number;
|
|
38
|
+
readonly ready: boolean;
|
|
39
|
+
readonly url: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
readonly lastError?: string | null;
|
|
24
43
|
}
|
|
25
44
|
|
|
26
|
-
declare const DEFAULT_IMAGE = "kaspanet/rusty-kaspad:
|
|
45
|
+
declare const DEFAULT_IMAGE = "kaspanet/rusty-kaspad:v1.1.0";
|
|
27
46
|
declare const DEFAULT_CONTAINER_NAME = "hardkas-kaspad-simnet";
|
|
28
47
|
declare const DEFAULT_NETWORK = "simnet";
|
|
29
48
|
declare const DEFAULT_PORTS: KaspadPorts;
|
|
@@ -31,7 +50,10 @@ declare class DockerKaspadRunner {
|
|
|
31
50
|
private readonly options;
|
|
32
51
|
constructor(options?: DockerKaspadOptions);
|
|
33
52
|
start(): Promise<KaspadNodeStatus>;
|
|
53
|
+
private ensurePortsAvailable;
|
|
54
|
+
private isPortAvailable;
|
|
34
55
|
stop(): Promise<KaspadNodeStatus>;
|
|
56
|
+
private checkTransportReady;
|
|
35
57
|
status(): Promise<KaspadNodeStatus>;
|
|
36
58
|
restart(): Promise<KaspadNodeStatus>;
|
|
37
59
|
reset(options?: {
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,12 @@ import { execa } from "execa";
|
|
|
3
3
|
import fs from "fs/promises";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
|
-
|
|
6
|
+
import net from "net";
|
|
7
|
+
import {
|
|
8
|
+
checkKaspaRpcHealth,
|
|
9
|
+
waitForKaspaRpcReady
|
|
10
|
+
} from "@hardkas/kaspa-rpc";
|
|
11
|
+
var DEFAULT_IMAGE = "kaspanet/rusty-kaspad:v1.1.0";
|
|
7
12
|
var DEFAULT_CONTAINER_NAME = "hardkas-kaspad-simnet";
|
|
8
13
|
var DEFAULT_NETWORK = "simnet";
|
|
9
14
|
var DEFAULT_PORTS = {
|
|
@@ -25,7 +30,8 @@ var DockerKaspadRunner = class {
|
|
|
25
30
|
...DEFAULT_PORTS,
|
|
26
31
|
...options?.ports || {}
|
|
27
32
|
},
|
|
28
|
-
detach: options?.detach ?? true
|
|
33
|
+
detach: options?.detach ?? true,
|
|
34
|
+
allowFloatingImage: options?.allowFloatingImage ?? false
|
|
29
35
|
};
|
|
30
36
|
}
|
|
31
37
|
async start() {
|
|
@@ -38,6 +44,25 @@ var DockerKaspadRunner = class {
|
|
|
38
44
|
} catch (e) {
|
|
39
45
|
throw new Error("Docker is not available. Please install Docker to run a real Kaspa node.");
|
|
40
46
|
}
|
|
47
|
+
if (this.options.image.endsWith(":latest") && !this.options.allowFloatingImage) {
|
|
48
|
+
console.warn("\n \u26A0\uFE0F WARNING: Using a floating Docker tag (:latest) reduces reproducibility.\n");
|
|
49
|
+
}
|
|
50
|
+
const network = this.options.network;
|
|
51
|
+
let networkFlag = "";
|
|
52
|
+
if (network === "simnet") {
|
|
53
|
+
networkFlag = "--simnet";
|
|
54
|
+
} else if (network.startsWith("testnet")) {
|
|
55
|
+
networkFlag = "--testnet";
|
|
56
|
+
} else if (network === "devnet") {
|
|
57
|
+
networkFlag = "--devnet";
|
|
58
|
+
} else if (network === "mainnet") {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Local Docker node for 'mainnet' is currently unsupported by HardKAS.\nPlease use a remote RPC provider or a manual kaspad setup for mainnet operations."
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error(`Unsupported network for Docker runner: ${network}`);
|
|
64
|
+
}
|
|
65
|
+
await this.ensurePortsAvailable();
|
|
41
66
|
const absoluteDataDir = path.isAbsolute(this.options.dataDir) ? this.options.dataDir : path.resolve(this.options.cwd, this.options.dataDir);
|
|
42
67
|
if (!existsSync(absoluteDataDir)) {
|
|
43
68
|
await fs.mkdir(absoluteDataDir, { recursive: true });
|
|
@@ -52,11 +77,11 @@ var DockerKaspadRunner = class {
|
|
|
52
77
|
"--name",
|
|
53
78
|
this.options.containerName,
|
|
54
79
|
"-p",
|
|
55
|
-
|
|
80
|
+
`127.0.0.1:${this.options.ports.rpc}:${this.options.ports.rpc}`,
|
|
56
81
|
"-p",
|
|
57
|
-
|
|
82
|
+
`127.0.0.1:${this.options.ports.borshRpc}:${this.options.ports.borshRpc}`,
|
|
58
83
|
"-p",
|
|
59
|
-
|
|
84
|
+
`127.0.0.1:${this.options.ports.jsonRpc}:${this.options.ports.jsonRpc}`,
|
|
60
85
|
"-v",
|
|
61
86
|
`${absoluteDataDir}:/app/data`,
|
|
62
87
|
this.options.image,
|
|
@@ -65,14 +90,56 @@ var DockerKaspadRunner = class {
|
|
|
65
90
|
"--nologfiles",
|
|
66
91
|
"--disable-upnp",
|
|
67
92
|
"--utxoindex",
|
|
68
|
-
|
|
93
|
+
...networkFlag ? [networkFlag] : [],
|
|
69
94
|
`--rpclisten=0.0.0.0:${this.options.ports.rpc}`,
|
|
70
95
|
`--rpclisten-borsh=0.0.0.0:${this.options.ports.borshRpc}`,
|
|
71
96
|
`--rpclisten-json=0.0.0.0:${this.options.ports.jsonRpc}`
|
|
72
97
|
];
|
|
73
98
|
await execa("docker", args);
|
|
99
|
+
const rpcUrl = `http://127.0.0.1:${this.options.ports.jsonRpc}`;
|
|
100
|
+
const health = await waitForKaspaRpcReady({
|
|
101
|
+
url: rpcUrl,
|
|
102
|
+
maxWaitMs: 6e4,
|
|
103
|
+
intervalMs: 1e3
|
|
104
|
+
});
|
|
105
|
+
if (!health.ready) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Kaspad RPC failed to become ready within 60s.
|
|
108
|
+
Container: ${this.options.containerName}
|
|
109
|
+
Image: ${this.options.image}
|
|
110
|
+
RPC: ${rpcUrl}
|
|
111
|
+
Last Error: ${health.lastError || "Timeout"}
|
|
112
|
+
|
|
113
|
+
Try checking logs: hardkas node logs --tail 200`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
74
116
|
return this.status();
|
|
75
117
|
}
|
|
118
|
+
async ensurePortsAvailable() {
|
|
119
|
+
const ports = [this.options.ports.rpc, this.options.ports.borshRpc, this.options.ports.jsonRpc];
|
|
120
|
+
for (const port of ports) {
|
|
121
|
+
const available = await this.isPortAvailable(port);
|
|
122
|
+
if (!available) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Port ${port} is already in use on the host. Cannot start node.
|
|
125
|
+
- Stop any existing process using this port.
|
|
126
|
+
- Or change the port in hardkas.config.ts.
|
|
127
|
+
- Or run 'hardkas node reset --yes' if it's a stale container.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
isPortAvailable(port) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const server = net.createServer();
|
|
135
|
+
server.once("error", () => resolve(false));
|
|
136
|
+
server.once("listening", () => {
|
|
137
|
+
server.close();
|
|
138
|
+
resolve(true);
|
|
139
|
+
});
|
|
140
|
+
server.listen(port, "127.0.0.1");
|
|
141
|
+
});
|
|
142
|
+
}
|
|
76
143
|
async stop() {
|
|
77
144
|
try {
|
|
78
145
|
await execa("docker", ["stop", this.options.containerName]);
|
|
@@ -81,7 +148,27 @@ var DockerKaspadRunner = class {
|
|
|
81
148
|
}
|
|
82
149
|
return this.status();
|
|
83
150
|
}
|
|
151
|
+
async checkTransportReady(port) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
const socket = new net.Socket();
|
|
154
|
+
socket.setTimeout(1e3);
|
|
155
|
+
socket.once("connect", () => {
|
|
156
|
+
socket.destroy();
|
|
157
|
+
resolve(true);
|
|
158
|
+
});
|
|
159
|
+
socket.once("timeout", () => {
|
|
160
|
+
socket.destroy();
|
|
161
|
+
resolve(false);
|
|
162
|
+
});
|
|
163
|
+
socket.once("error", () => {
|
|
164
|
+
socket.destroy();
|
|
165
|
+
resolve(false);
|
|
166
|
+
});
|
|
167
|
+
socket.connect(port, "127.0.0.1");
|
|
168
|
+
});
|
|
169
|
+
}
|
|
84
170
|
async status() {
|
|
171
|
+
const rpcUrl = `http://127.0.0.1:${this.options.ports.jsonRpc}`;
|
|
85
172
|
try {
|
|
86
173
|
const { stdout } = await execa("docker", [
|
|
87
174
|
"inspect",
|
|
@@ -90,6 +177,19 @@ var DockerKaspadRunner = class {
|
|
|
90
177
|
this.options.containerName
|
|
91
178
|
]);
|
|
92
179
|
const running = stdout.trim() === "running";
|
|
180
|
+
let jsonReady = false;
|
|
181
|
+
let grpcReady = false;
|
|
182
|
+
let borshReady = false;
|
|
183
|
+
let lastError = null;
|
|
184
|
+
if (running) {
|
|
185
|
+
const health = await checkKaspaRpcHealth({ url: rpcUrl, timeoutMs: 2e3 });
|
|
186
|
+
jsonReady = health.ready;
|
|
187
|
+
lastError = health.lastError || null;
|
|
188
|
+
[grpcReady, borshReady] = await Promise.all([
|
|
189
|
+
this.checkTransportReady(this.options.ports.rpc),
|
|
190
|
+
this.checkTransportReady(this.options.ports.borshRpc)
|
|
191
|
+
]);
|
|
192
|
+
}
|
|
93
193
|
return {
|
|
94
194
|
containerName: this.options.containerName,
|
|
95
195
|
image: this.options.image,
|
|
@@ -97,7 +197,16 @@ var DockerKaspadRunner = class {
|
|
|
97
197
|
running,
|
|
98
198
|
statusText: stdout.trim(),
|
|
99
199
|
ports: this.options.ports,
|
|
100
|
-
dataDir: this.options.dataDir
|
|
200
|
+
dataDir: this.options.dataDir,
|
|
201
|
+
rpcUrl,
|
|
202
|
+
rpcReady: jsonReady,
|
|
203
|
+
// Unified readiness
|
|
204
|
+
transports: {
|
|
205
|
+
grpc: { port: this.options.ports.rpc, ready: grpcReady },
|
|
206
|
+
borsh: { port: this.options.ports.borshRpc, ready: borshReady },
|
|
207
|
+
json: { port: this.options.ports.jsonRpc, ready: jsonReady, url: rpcUrl }
|
|
208
|
+
},
|
|
209
|
+
lastError
|
|
101
210
|
};
|
|
102
211
|
} catch (e) {
|
|
103
212
|
return {
|
|
@@ -107,7 +216,15 @@ var DockerKaspadRunner = class {
|
|
|
107
216
|
running: false,
|
|
108
217
|
statusText: "not-found",
|
|
109
218
|
ports: this.options.ports,
|
|
110
|
-
dataDir: this.options.dataDir
|
|
219
|
+
dataDir: this.options.dataDir,
|
|
220
|
+
rpcUrl,
|
|
221
|
+
rpcReady: false,
|
|
222
|
+
transports: {
|
|
223
|
+
grpc: { port: this.options.ports.rpc, ready: false },
|
|
224
|
+
borsh: { port: this.options.ports.borshRpc, ready: false },
|
|
225
|
+
json: { port: this.options.ports.jsonRpc, ready: false, url: rpcUrl }
|
|
226
|
+
},
|
|
227
|
+
lastError: "Container not found"
|
|
111
228
|
};
|
|
112
229
|
}
|
|
113
230
|
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hardkas/node-runner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0-alpha",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"execa": "^9.5.2"
|
|
8
|
+
"execa": "^9.5.2",
|
|
9
|
+
"@hardkas/core": "0.3.0-alpha",
|
|
10
|
+
"@hardkas/kaspa-rpc": "0.3.0-alpha"
|
|
9
11
|
},
|
|
10
12
|
"devDependencies": {
|
|
11
13
|
"tsup": "^8.3.5",
|
|
12
14
|
"typescript": "^5.7.2",
|
|
13
15
|
"vitest": "^2.1.8",
|
|
14
|
-
"@hardkas/config": "0.
|
|
16
|
+
"@hardkas/config": "0.3.0-alpha"
|
|
15
17
|
},
|
|
16
18
|
"license": "MIT",
|
|
17
19
|
"author": "Javier Rodriguez",
|