@hardkas/kaspa-rpc 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Javier Rodriguez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,293 @@
1
+ import { NetworkId } from '@hardkas/core';
2
+
3
+ type RpcHealthState = "healthy" | "degraded" | "stale" | "unreachable";
4
+ type RpcConfidence = "high" | "medium" | "low";
5
+ interface RpcTrace {
6
+ endpoint: string;
7
+ method: string;
8
+ latencyMs: number;
9
+ retries: number;
10
+ timedOut: boolean;
11
+ errorClass?: string;
12
+ }
13
+ interface ResilienceReport {
14
+ state: RpcHealthState;
15
+ confidence: RpcConfidence;
16
+ score: number;
17
+ issues: string[];
18
+ }
19
+ /**
20
+ * Calculates RPC confidence based on health metrics.
21
+ */
22
+ declare function calculateConfidence(metrics: {
23
+ latencyMs: number | null;
24
+ successRate: number;
25
+ retries: number;
26
+ stale: boolean;
27
+ reachable: boolean;
28
+ circuitOpen: boolean;
29
+ }): ResilienceReport;
30
+ /**
31
+ * Classifies an error as transient (retriable) or permanent.
32
+ */
33
+ declare function classifyRpcError(error: any): {
34
+ retriable: boolean;
35
+ category: string;
36
+ };
37
+
38
+ declare enum CircuitState {
39
+ CLOSED = "CLOSED",
40
+ OPEN = "OPEN",
41
+ HALF_OPEN = "HALF_OPEN"
42
+ }
43
+ interface RetryOptions {
44
+ maxRetries: number;
45
+ baseDelayMs: number;
46
+ maxDelayMs: number;
47
+ }
48
+ interface CircuitBreakerOptions {
49
+ failureThreshold: number;
50
+ resetTimeoutMs: number;
51
+ }
52
+ interface RpcClientOptions {
53
+ url: string;
54
+ timeoutMs?: number | undefined;
55
+ retry?: Partial<RetryOptions>;
56
+ circuitBreaker?: Partial<CircuitBreakerOptions>;
57
+ fetcher?: typeof fetch;
58
+ }
59
+ declare class KaspaJsonRpcClient implements KaspaRpcClient {
60
+ readonly url: string;
61
+ private readonly timeoutMs;
62
+ private readonly retry;
63
+ private readonly circuitBreaker;
64
+ private readonly fetcher;
65
+ private circuitState;
66
+ private failureCount;
67
+ private lastFailureTime;
68
+ private lastError;
69
+ private lastLatencyMs;
70
+ private totalRequests;
71
+ private successfulRequests;
72
+ private lastDaaScore;
73
+ private lastDaaCheckTime;
74
+ private retriesCount;
75
+ constructor(options: RpcClientOptions);
76
+ healthCheck(): Promise<KaspaRpcHealth>;
77
+ getInfo(): Promise<KaspaNodeInfo>;
78
+ getBlockDagInfo(): Promise<BlockDagInfo>;
79
+ getUtxosByAddress(address: string): Promise<KaspaRpcUtxo[]>;
80
+ getBalanceByAddress(address: string): Promise<KaspaAddressBalance>;
81
+ getMempoolEntry(txId: string): Promise<MempoolEntry | null>;
82
+ getTransaction(txId: string): Promise<unknown | null>;
83
+ submitTransaction(rawTx: unknown): Promise<{
84
+ transactionId: string;
85
+ }>;
86
+ getServerInfo(): Promise<ServerInfo>;
87
+ close(): Promise<void>;
88
+ private callRpc;
89
+ private withResilience;
90
+ private internalCall;
91
+ private checkCircuit;
92
+ private onSuccess;
93
+ private onFailure;
94
+ private isDeterministicError;
95
+ private getSuccessRate;
96
+ }
97
+
98
+ interface RpcHealthCheckOptions {
99
+ readonly url?: string | undefined;
100
+ readonly timeoutMs?: number | undefined;
101
+ }
102
+ interface RpcReadinessWaitOptions extends RpcHealthCheckOptions {
103
+ readonly wait?: boolean | undefined;
104
+ readonly intervalMs?: number | undefined;
105
+ readonly maxWaitMs?: number | undefined;
106
+ }
107
+ interface RpcHealthResult {
108
+ readonly endpoint: string;
109
+ readonly status: RpcHealthState;
110
+ readonly ready: boolean;
111
+ readonly checkedAt: string;
112
+ readonly latencyMs?: number | undefined;
113
+ readonly networkId?: string | undefined;
114
+ readonly virtualDaaScore?: string | undefined;
115
+ readonly serverVersion?: string | undefined;
116
+ readonly isSynced?: boolean | undefined;
117
+ readonly error?: string | undefined;
118
+ readonly lastError?: string | null | undefined;
119
+ readonly retries?: number | undefined;
120
+ readonly circuitState?: string | undefined;
121
+ readonly stale?: boolean | undefined;
122
+ }
123
+
124
+ declare function checkKaspaRpcHealth(options?: RpcHealthCheckOptions): Promise<RpcHealthResult>;
125
+ declare function waitForKaspaRpcReady(options?: RpcReadinessWaitOptions): Promise<RpcHealthResult>;
126
+
127
+ declare class RpcError extends Error {
128
+ readonly code?: number | undefined;
129
+ readonly data?: unknown | undefined;
130
+ readonly isRetriable: boolean;
131
+ constructor(message: string, code?: number | undefined, data?: unknown | undefined, isRetriable?: boolean);
132
+ }
133
+ declare class RpcTimeoutError extends RpcError {
134
+ constructor(message?: string);
135
+ }
136
+ declare class RpcUnavailableError extends RpcError {
137
+ constructor(message?: string, code?: number);
138
+ }
139
+ declare class RpcCircuitOpenError extends RpcError {
140
+ constructor(message?: string);
141
+ }
142
+ declare class RpcRateLimitError extends RpcError {
143
+ readonly retryAfterMs?: number | undefined;
144
+ constructor(message?: string, retryAfterMs?: number | undefined);
145
+ }
146
+ /**
147
+ * Errors that should NOT be retried (Deterministic/Validation)
148
+ */
149
+ declare class RpcValidationError extends RpcError {
150
+ constructor(message: string, code?: number, data?: unknown);
151
+ }
152
+
153
+ interface LoadBalancerOptions {
154
+ strategy: "round-robin" | "failover";
155
+ }
156
+ declare class LoadBalancedRpcProvider implements KaspaRpcClient {
157
+ private readonly clients;
158
+ private readonly options;
159
+ private currentIndex;
160
+ constructor(clients: KaspaRpcClient[], options?: LoadBalancerOptions);
161
+ getInfo(): Promise<KaspaNodeInfo>;
162
+ healthCheck(): Promise<KaspaRpcHealth>;
163
+ getBalanceByAddress(address: string): Promise<KaspaAddressBalance>;
164
+ getUtxosByAddress(address: string): Promise<KaspaRpcUtxo[]>;
165
+ submitTransaction(rawTransaction: string): Promise<KaspaSubmitTransactionResult>;
166
+ getMempoolEntry(txId: string): Promise<MempoolEntry | null>;
167
+ getTransaction(txId: string): Promise<unknown | null>;
168
+ getBlockDagInfo(): Promise<BlockDagInfo>;
169
+ getServerInfo(): Promise<ServerInfo>;
170
+ close(): Promise<void>;
171
+ private withFailover;
172
+ }
173
+
174
+ interface KaspaNodeInfo {
175
+ serverVersion?: string | undefined;
176
+ isSynced?: boolean | undefined;
177
+ isUtxoIndexed?: boolean | undefined;
178
+ p2pId?: string | undefined;
179
+ mempoolSize?: number | undefined;
180
+ virtualDaaScore?: bigint | undefined;
181
+ networkId?: string | undefined;
182
+ raw?: unknown | undefined;
183
+ }
184
+ interface KaspaRpcHealth {
185
+ readonly endpoint: string;
186
+ readonly status: RpcHealthState;
187
+ readonly confidence?: RpcConfidence;
188
+ readonly score?: number;
189
+ readonly latencyMs?: number | undefined;
190
+ readonly lastError?: string | null | undefined;
191
+ readonly retries?: number | undefined;
192
+ readonly circuitState?: string | undefined;
193
+ readonly stale?: boolean | undefined;
194
+ readonly info?: KaspaNodeInfo | undefined;
195
+ readonly reachable?: boolean | undefined;
196
+ readonly successRate?: number | undefined;
197
+ }
198
+
199
+ interface KaspaAddressBalance {
200
+ address: string;
201
+ balanceSompi: bigint;
202
+ raw?: unknown;
203
+ }
204
+ interface KaspaRpcOutpoint {
205
+ transactionId: string;
206
+ index: number;
207
+ }
208
+ interface KaspaRpcUtxo {
209
+ outpoint: KaspaRpcOutpoint;
210
+ address: string;
211
+ amountSompi: bigint;
212
+ scriptPublicKey?: string;
213
+ blockDaaScore?: bigint | string;
214
+ isCoinbase?: boolean;
215
+ raw?: unknown;
216
+ }
217
+ interface JsonWrpcKaspaClientOptions {
218
+ rpcUrl: string;
219
+ timeoutMs?: number;
220
+ }
221
+ interface BlockDagInfo {
222
+ readonly networkId: NetworkId;
223
+ readonly virtualDaaScore?: bigint;
224
+ readonly tipHashes?: readonly string[];
225
+ }
226
+ interface ServerInfo {
227
+ readonly networkId: NetworkId;
228
+ readonly serverVersion?: string;
229
+ readonly isSynced?: boolean;
230
+ }
231
+ interface MempoolEntry {
232
+ readonly txId: string;
233
+ readonly acceptedAt?: string | undefined;
234
+ }
235
+ interface KaspaSubmitTransactionResult {
236
+ transactionId?: string;
237
+ accepted?: boolean;
238
+ raw?: unknown;
239
+ }
240
+ interface KaspaRpcClient {
241
+ getInfo(): Promise<KaspaNodeInfo>;
242
+ healthCheck(): Promise<KaspaRpcHealth>;
243
+ getBalanceByAddress(address: string): Promise<KaspaAddressBalance>;
244
+ getUtxosByAddress(address: string): Promise<KaspaRpcUtxo[]>;
245
+ submitTransaction(rawTransaction: string): Promise<KaspaSubmitTransactionResult>;
246
+ getMempoolEntry(txId: string): Promise<MempoolEntry | null>;
247
+ getTransaction(txId: string): Promise<unknown | null>;
248
+ getBlockDagInfo(): Promise<BlockDagInfo>;
249
+ getServerInfo(): Promise<ServerInfo>;
250
+ close(): void | Promise<void>;
251
+ }
252
+ declare class JsonWrpcKaspaClient implements KaspaRpcClient {
253
+ private socket;
254
+ private readonly rpcUrl;
255
+ private readonly timeoutMs;
256
+ private requestId;
257
+ constructor(options: JsonWrpcKaspaClientOptions);
258
+ getInfo(): Promise<KaspaNodeInfo>;
259
+ healthCheck(): Promise<KaspaRpcHealth>;
260
+ getBalanceByAddress(address: string): Promise<KaspaAddressBalance>;
261
+ getUtxosByAddress(address: string): Promise<KaspaRpcUtxo[]>;
262
+ submitTransaction(rawTransaction: string): Promise<KaspaSubmitTransactionResult>;
263
+ getMempoolEntry(txId: string): Promise<MempoolEntry | null>;
264
+ getTransaction(txId: string): Promise<unknown | null>;
265
+ getBlockDagInfo(): Promise<BlockDagInfo>;
266
+ getServerInfo(): Promise<ServerInfo>;
267
+ close(): Promise<void>;
268
+ private safeRequest;
269
+ private request;
270
+ private connect;
271
+ }
272
+ declare function mapKaspaNodeInfo(result: any): KaspaNodeInfo;
273
+ declare function mapKaspaAddressBalance(result: any, address: string): KaspaAddressBalance;
274
+ declare function mapKaspaRpcUtxos(result: any, address: string): KaspaRpcUtxo[];
275
+ declare function mapKaspaSubmitTransactionResult(result: any): KaspaSubmitTransactionResult;
276
+ declare class MockKaspaRpcClient implements KaspaRpcClient {
277
+ private readonly networkId;
278
+ private utxosByAddress;
279
+ constructor(networkId?: NetworkId);
280
+ getInfo(): Promise<KaspaNodeInfo>;
281
+ healthCheck(): Promise<KaspaRpcHealth>;
282
+ getBalanceByAddress(address: string): Promise<KaspaAddressBalance>;
283
+ getUtxosByAddress(address: string): Promise<KaspaRpcUtxo[]>;
284
+ setUtxos(address: string, utxos: KaspaRpcUtxo[]): void;
285
+ submitTransaction(rawTransaction: string): Promise<KaspaSubmitTransactionResult>;
286
+ getMempoolEntry(_txId: string): Promise<MempoolEntry | null>;
287
+ getTransaction(_txId: string): Promise<unknown | null>;
288
+ getBlockDagInfo(): Promise<BlockDagInfo>;
289
+ getServerInfo(): Promise<ServerInfo>;
290
+ close(): Promise<void>;
291
+ }
292
+
293
+ export { type BlockDagInfo, type CircuitBreakerOptions, CircuitState, JsonWrpcKaspaClient, type JsonWrpcKaspaClientOptions, type KaspaAddressBalance, KaspaJsonRpcClient, type KaspaNodeInfo, type KaspaRpcClient, type KaspaRpcHealth, type KaspaRpcOutpoint, type KaspaRpcUtxo, type KaspaSubmitTransactionResult, LoadBalancedRpcProvider, type LoadBalancerOptions, type MempoolEntry, MockKaspaRpcClient, type ResilienceReport, type RetryOptions, RpcCircuitOpenError, type RpcClientOptions, type RpcConfidence, RpcError, type RpcHealthCheckOptions, type RpcHealthResult, type RpcHealthState, RpcRateLimitError, type RpcReadinessWaitOptions, RpcTimeoutError, type RpcTrace, RpcUnavailableError, RpcValidationError, type ServerInfo, calculateConfidence, checkKaspaRpcHealth, classifyRpcError, mapKaspaAddressBalance, mapKaspaNodeInfo, mapKaspaRpcUtxos, mapKaspaSubmitTransactionResult, waitForKaspaRpcReady };
package/dist/index.js ADDED
@@ -0,0 +1,935 @@
1
+ // src/index.ts
2
+ import { WebSocket } from "ws";
3
+
4
+ // src/errors.ts
5
+ var RpcError = class extends Error {
6
+ constructor(message, code, data, isRetriable = true) {
7
+ super(message);
8
+ this.code = code;
9
+ this.data = data;
10
+ this.isRetriable = isRetriable;
11
+ this.name = "RpcError";
12
+ }
13
+ code;
14
+ data;
15
+ isRetriable;
16
+ };
17
+ var RpcTimeoutError = class extends RpcError {
18
+ constructor(message = "RPC request timed out") {
19
+ super(message, void 0, void 0, true);
20
+ this.name = "RpcTimeoutError";
21
+ }
22
+ };
23
+ var RpcUnavailableError = class extends RpcError {
24
+ constructor(message = "RPC service unavailable", code) {
25
+ super(message, code, void 0, true);
26
+ this.name = "RpcUnavailableError";
27
+ }
28
+ };
29
+ var RpcCircuitOpenError = class extends RpcError {
30
+ constructor(message = "RPC circuit is open (too many failures)") {
31
+ super(message, void 0, void 0, false);
32
+ this.name = "RpcCircuitOpenError";
33
+ }
34
+ };
35
+ var RpcRateLimitError = class extends RpcError {
36
+ constructor(message = "RPC rate limit exceeded", retryAfterMs) {
37
+ super(message, 429, void 0, true);
38
+ this.retryAfterMs = retryAfterMs;
39
+ this.name = "RpcRateLimitError";
40
+ }
41
+ retryAfterMs;
42
+ };
43
+ var RpcValidationError = class extends RpcError {
44
+ constructor(message, code, data) {
45
+ super(message, code, data, false);
46
+ this.name = "RpcValidationError";
47
+ }
48
+ };
49
+
50
+ // src/resilience.ts
51
+ function calculateConfidence(metrics) {
52
+ const issues = [];
53
+ let score = 100;
54
+ if (!metrics.reachable) {
55
+ return { state: "unreachable", confidence: "low", score: 0, issues: ["Endpoint is unreachable"] };
56
+ }
57
+ if (metrics.circuitOpen) {
58
+ score -= 50;
59
+ issues.push("Circuit breaker is OPEN");
60
+ }
61
+ if (metrics.stale) {
62
+ score -= 40;
63
+ issues.push("Node appears to be STALE (DAA score not advancing)");
64
+ }
65
+ if (metrics.latencyMs !== null) {
66
+ if (metrics.latencyMs > 1e3) {
67
+ score -= 20;
68
+ issues.push(`High latency: ${metrics.latencyMs}ms`);
69
+ } else if (metrics.latencyMs > 500) {
70
+ score -= 10;
71
+ issues.push(`Slightly high latency: ${metrics.latencyMs}ms`);
72
+ }
73
+ }
74
+ if (metrics.successRate < 95) {
75
+ score -= (100 - metrics.successRate) * 2;
76
+ issues.push(`Low success rate: ${metrics.successRate.toFixed(1)}%`);
77
+ }
78
+ if (metrics.retries > 0) {
79
+ score -= Math.min(metrics.retries * 5, 20);
80
+ issues.push(`Request stability issues: ${metrics.retries} retries detected`);
81
+ }
82
+ score = Math.max(0, Math.min(100, score));
83
+ let state = "healthy";
84
+ if (metrics.stale) state = "stale";
85
+ else if (score < 50) state = "degraded";
86
+ else if (score < 90) state = "degraded";
87
+ let confidence = "high";
88
+ if (score < 40) confidence = "low";
89
+ else if (score <= 80) confidence = "medium";
90
+ return { state, confidence, score, issues };
91
+ }
92
+ function classifyRpcError(error) {
93
+ const msg = (error.message || String(error)).toLowerCase();
94
+ const permanentMarkers = [
95
+ "invalid address",
96
+ "insufficient funds",
97
+ "dust",
98
+ "method not found",
99
+ "already spent",
100
+ "invalid transaction",
101
+ "schema validation"
102
+ ];
103
+ if (permanentMarkers.some((m) => msg.includes(m))) {
104
+ return { retriable: false, category: "validation" };
105
+ }
106
+ const transientMarkers = [
107
+ "timeout",
108
+ "timed out",
109
+ "abort",
110
+ "connection refused",
111
+ "fetch failed",
112
+ "429",
113
+ "503",
114
+ "too many requests",
115
+ "circuit open"
116
+ ];
117
+ if (transientMarkers.some((m) => msg.includes(m))) {
118
+ return { retriable: true, category: "network" };
119
+ }
120
+ return { retriable: true, category: "unknown" };
121
+ }
122
+
123
+ // src/json-rpc-client.ts
124
+ import { coreEvents } from "@hardkas/core";
125
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
126
+ CircuitState2["CLOSED"] = "CLOSED";
127
+ CircuitState2["OPEN"] = "OPEN";
128
+ CircuitState2["HALF_OPEN"] = "HALF_OPEN";
129
+ return CircuitState2;
130
+ })(CircuitState || {});
131
+ var KaspaJsonRpcClient = class {
132
+ url;
133
+ timeoutMs;
134
+ retry;
135
+ circuitBreaker;
136
+ fetcher;
137
+ // State & Metrics
138
+ circuitState = "CLOSED" /* CLOSED */;
139
+ failureCount = 0;
140
+ lastFailureTime = 0;
141
+ lastError = null;
142
+ lastLatencyMs = null;
143
+ totalRequests = 0;
144
+ successfulRequests = 0;
145
+ lastDaaScore = null;
146
+ lastDaaCheckTime = 0;
147
+ retriesCount = 0;
148
+ constructor(options) {
149
+ this.url = options.url || "http://127.0.0.1:18210";
150
+ this.timeoutMs = options.timeoutMs || 1e4;
151
+ this.retry = {
152
+ maxRetries: options.retry?.maxRetries ?? 3,
153
+ baseDelayMs: options.retry?.baseDelayMs ?? 500,
154
+ maxDelayMs: options.retry?.maxDelayMs ?? 5e3
155
+ };
156
+ this.circuitBreaker = {
157
+ failureThreshold: options.circuitBreaker?.failureThreshold ?? 5,
158
+ resetTimeoutMs: options.circuitBreaker?.resetTimeoutMs ?? 3e4
159
+ };
160
+ this.fetcher = options.fetcher || globalThis.fetch;
161
+ }
162
+ async healthCheck() {
163
+ this.checkCircuit();
164
+ const start = Date.now();
165
+ try {
166
+ const info = await this.getInfo();
167
+ const latency = Date.now() - start;
168
+ let stale = false;
169
+ const now = Date.now();
170
+ if (this.lastDaaScore !== null && info.virtualDaaScore !== void 0) {
171
+ if (info.virtualDaaScore <= this.lastDaaScore && now - this.lastDaaCheckTime > 3e4) {
172
+ stale = true;
173
+ }
174
+ }
175
+ if (info.virtualDaaScore !== void 0) {
176
+ this.lastDaaScore = info.virtualDaaScore;
177
+ this.lastDaaCheckTime = now;
178
+ }
179
+ const resilience = calculateConfidence({
180
+ latencyMs: latency,
181
+ successRate: this.getSuccessRate(),
182
+ retries: this.retriesCount,
183
+ stale,
184
+ reachable: true,
185
+ circuitOpen: this.circuitState === "OPEN" /* OPEN */
186
+ });
187
+ coreEvents.normalizeAndEmit({
188
+ kind: "rpc.health",
189
+ endpoint: this.url,
190
+ state: resilience.state,
191
+ score: resilience.score,
192
+ latencyMs: latency,
193
+ issues: resilience.issues
194
+ });
195
+ return {
196
+ reachable: true,
197
+ rpcUrl: this.url,
198
+ status: resilience.state,
199
+ info,
200
+ latencyMs: latency,
201
+ lastError: this.lastError,
202
+ successRate: this.getSuccessRate(),
203
+ circuitState: this.circuitState,
204
+ score: resilience.score,
205
+ confidence: resilience.confidence,
206
+ retries: this.retriesCount,
207
+ stale
208
+ };
209
+ } catch (e) {
210
+ const resilience = calculateConfidence({
211
+ latencyMs: null,
212
+ successRate: this.getSuccessRate(),
213
+ retries: this.retriesCount,
214
+ stale: false,
215
+ reachable: false,
216
+ circuitOpen: this.circuitState === "OPEN" /* OPEN */
217
+ });
218
+ coreEvents.normalizeAndEmit({
219
+ kind: "rpc.health",
220
+ endpoint: this.url,
221
+ state: resilience.state,
222
+ score: resilience.score,
223
+ latencyMs: -1,
224
+ issues: resilience.issues
225
+ });
226
+ return {
227
+ reachable: false,
228
+ rpcUrl: this.url,
229
+ status: "unavailable",
230
+ error: e.message,
231
+ lastError: this.lastError || e.message,
232
+ successRate: this.getSuccessRate(),
233
+ circuitState: this.circuitState,
234
+ confidence: resilience.confidence,
235
+ score: resilience.score,
236
+ retries: this.retriesCount
237
+ };
238
+ }
239
+ }
240
+ async getInfo() {
241
+ const data = await this.callRpc("getInfoRequest");
242
+ const info = {
243
+ serverVersion: String(data.serverVersion),
244
+ networkId: String(data.networkId),
245
+ isSynced: Boolean(data.isSynced)
246
+ };
247
+ if (data.virtualDaaScore !== void 0) info.virtualDaaScore = BigInt(data.virtualDaaScore);
248
+ if (data.mempoolSize !== void 0) info.mempoolSize = Number(data.mempoolSize);
249
+ return info;
250
+ }
251
+ async getBlockDagInfo() {
252
+ const data = await this.callRpc("getBlockDagInfoRequest");
253
+ const dagInfo = {
254
+ networkId: data.networkId,
255
+ tipHashes: data.tipHashes,
256
+ ...data.virtualDaaScore !== void 0 ? { virtualDaaScore: BigInt(data.virtualDaaScore) } : {}
257
+ };
258
+ return dagInfo;
259
+ }
260
+ async getUtxosByAddress(address) {
261
+ const data = await this.callRpc("getUtxosByAddressesRequest", { addresses: [address] });
262
+ const entries = data.entries || [];
263
+ return entries.map((e) => ({
264
+ address: e.address,
265
+ outpoint: {
266
+ transactionId: e.outpoint.transactionId,
267
+ index: e.outpoint.index
268
+ },
269
+ amountSompi: BigInt(e.utxoEntry.amount),
270
+ scriptPublicKey: e.utxoEntry.scriptPublicKey,
271
+ blockDaaScore: BigInt(e.utxoEntry.blockDaaScore),
272
+ isCoinbase: e.utxoEntry.isCoinbase
273
+ }));
274
+ }
275
+ async getBalanceByAddress(address) {
276
+ const data = await this.callRpc("getBalanceByAddressRequest", { address });
277
+ return {
278
+ address: data.address,
279
+ balanceSompi: BigInt(data.balance)
280
+ };
281
+ }
282
+ async getMempoolEntry(txId) {
283
+ try {
284
+ const result = await this.callRpc("getMempoolEntryRequest", { txId, includeOrphanPool: true });
285
+ return {
286
+ txId,
287
+ acceptedAt: String(result.entry.acceptedAt)
288
+ };
289
+ } catch (e) {
290
+ return null;
291
+ }
292
+ }
293
+ async getTransaction(txId) {
294
+ try {
295
+ const result = await this.callRpc("getTransactionRequest", { transactionId: txId });
296
+ return result;
297
+ } catch (e) {
298
+ return null;
299
+ }
300
+ }
301
+ async submitTransaction(rawTx) {
302
+ const result = await this.callRpc("submitTransactionRequest", { transaction: rawTx });
303
+ return { transactionId: result.transactionId };
304
+ }
305
+ async getServerInfo() {
306
+ const info = await this.getInfo();
307
+ const result = {
308
+ networkId: info.networkId
309
+ };
310
+ if (info.serverVersion !== void 0) result.serverVersion = info.serverVersion;
311
+ if (info.isSynced !== void 0) result.isSynced = info.isSynced;
312
+ return result;
313
+ }
314
+ async close() {
315
+ }
316
+ async callRpc(method, params = {}) {
317
+ return this.withResilience(() => this.internalCall(method, params));
318
+ }
319
+ async withResilience(fn) {
320
+ this.checkCircuit();
321
+ if (this.circuitState === "OPEN" /* OPEN */) {
322
+ throw new RpcCircuitOpenError();
323
+ }
324
+ let lastErr;
325
+ for (let attempt = 0; attempt <= this.retry.maxRetries; attempt++) {
326
+ const start = Date.now();
327
+ try {
328
+ this.totalRequests++;
329
+ const result = await fn();
330
+ this.onSuccess(Date.now() - start);
331
+ return result;
332
+ } catch (e) {
333
+ this.onFailure(e);
334
+ lastErr = e;
335
+ const isRetriable = e instanceof RpcError ? e.isRetriable : true;
336
+ coreEvents.normalizeAndEmit({
337
+ kind: "rpc.error",
338
+ endpoint: this.url,
339
+ error: e.message,
340
+ retriable: isRetriable
341
+ });
342
+ if (attempt < this.retry.maxRetries && isRetriable) {
343
+ this.retriesCount++;
344
+ }
345
+ if (e instanceof RpcError && !e.isRetriable) {
346
+ throw e;
347
+ }
348
+ if (this.isDeterministicError(e)) {
349
+ throw new RpcValidationError(e.message, e.code, e.data);
350
+ }
351
+ if (attempt === this.retry.maxRetries) break;
352
+ const delay = Math.min(
353
+ this.retry.baseDelayMs * Math.pow(2, attempt),
354
+ this.retry.maxDelayMs
355
+ );
356
+ const jitter = Math.random() * 0.1 * delay;
357
+ await new Promise((resolve) => setTimeout(resolve, delay + jitter));
358
+ }
359
+ }
360
+ throw lastErr;
361
+ }
362
+ async internalCall(method, params) {
363
+ const controller = new AbortController();
364
+ const id = setTimeout(() => controller.abort(), this.timeoutMs);
365
+ try {
366
+ const response = await this.fetcher(this.url, {
367
+ method: "POST",
368
+ headers: { "Content-Type": "application/json" },
369
+ body: JSON.stringify({
370
+ jsonrpc: "2.0",
371
+ id: Date.now(),
372
+ method,
373
+ params
374
+ }),
375
+ signal: controller.signal
376
+ });
377
+ clearTimeout(id);
378
+ if (response.status === 429) {
379
+ throw new RpcRateLimitError();
380
+ }
381
+ if (!response.ok) {
382
+ throw new RpcUnavailableError(`HTTP Error ${response.status}`, response.status);
383
+ }
384
+ const body = await response.json();
385
+ if (body.error) {
386
+ throw new RpcError(body.error.message, body.error.code, body.error.data);
387
+ }
388
+ return body.result;
389
+ } catch (e) {
390
+ clearTimeout(id);
391
+ if (e.name === "AbortError") throw new RpcTimeoutError();
392
+ throw e;
393
+ }
394
+ }
395
+ checkCircuit() {
396
+ if (this.circuitState === "OPEN" /* OPEN */) {
397
+ const now = Date.now();
398
+ if (now - this.lastFailureTime > this.circuitBreaker.resetTimeoutMs) {
399
+ this.circuitState = "HALF_OPEN" /* HALF_OPEN */;
400
+ }
401
+ }
402
+ }
403
+ onSuccess(latency) {
404
+ this.lastLatencyMs = latency;
405
+ this.successfulRequests++;
406
+ this.failureCount = 0;
407
+ this.circuitState = "CLOSED" /* CLOSED */;
408
+ }
409
+ onFailure(e) {
410
+ this.lastError = e.message;
411
+ if (e instanceof RpcValidationError || e instanceof RpcError && !e.isRetriable) {
412
+ return;
413
+ }
414
+ this.failureCount++;
415
+ this.lastFailureTime = Date.now();
416
+ if (this.failureCount >= this.circuitBreaker.failureThreshold) {
417
+ this.circuitState = "OPEN" /* OPEN */;
418
+ }
419
+ }
420
+ isDeterministicError(e) {
421
+ const msg = (e.message || "").toLowerCase();
422
+ const deterministicMarkers = [
423
+ "invalid address",
424
+ "insufficient funds",
425
+ "schema validation",
426
+ "artifact hash mismatch",
427
+ "simulation error",
428
+ "dust",
429
+ "missing required",
430
+ "outpoint already spent",
431
+ "method not found"
432
+ ];
433
+ return deterministicMarkers.some((marker) => msg.includes(marker));
434
+ }
435
+ getSuccessRate() {
436
+ if (this.totalRequests === 0) return 100;
437
+ return this.successfulRequests / this.totalRequests * 100;
438
+ }
439
+ };
440
+
441
+ // src/health.ts
442
+ async function checkKaspaRpcHealth(options) {
443
+ const url = options?.url || "http://127.0.0.1:18210";
444
+ const client = new KaspaJsonRpcClient({ url, timeoutMs: options?.timeoutMs });
445
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
446
+ try {
447
+ const health = await client.healthCheck();
448
+ return {
449
+ endpoint: url,
450
+ status: health.status,
451
+ ready: health.status === "healthy" || health.status === "degraded",
452
+ checkedAt,
453
+ ...health.latencyMs !== void 0 ? { latencyMs: health.latencyMs } : {},
454
+ ...health.info?.networkId !== void 0 ? { networkId: health.info.networkId } : {},
455
+ ...health.info?.virtualDaaScore !== void 0 ? { virtualDaaScore: health.info.virtualDaaScore.toString() } : {},
456
+ ...health.info?.serverVersion !== void 0 ? { serverVersion: health.info.serverVersion } : {},
457
+ ...health.info?.isSynced !== void 0 ? { isSynced: health.info.isSynced } : {},
458
+ lastError: health.lastError,
459
+ ...health.retries !== void 0 ? { retries: health.retries } : {},
460
+ ...health.circuitState !== void 0 ? { circuitState: health.circuitState } : {},
461
+ stale: health.stale
462
+ };
463
+ } catch (e) {
464
+ return {
465
+ endpoint: url,
466
+ status: "unreachable",
467
+ ready: false,
468
+ checkedAt,
469
+ error: e.message,
470
+ lastError: e.message
471
+ };
472
+ }
473
+ }
474
+ async function waitForKaspaRpcReady(options) {
475
+ const intervalMs = options?.intervalMs || 1e3;
476
+ const maxWaitMs = options?.maxWaitMs || 6e4;
477
+ const start = Date.now();
478
+ let lastResult;
479
+ while (Date.now() - start < maxWaitMs) {
480
+ lastResult = await checkKaspaRpcHealth({
481
+ url: options?.url,
482
+ timeoutMs: options?.timeoutMs
483
+ });
484
+ if (lastResult.ready) {
485
+ return lastResult;
486
+ }
487
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
488
+ }
489
+ return lastResult || {
490
+ endpoint: options?.url || "http://127.0.0.1:18210",
491
+ status: "unreachable",
492
+ ready: false,
493
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
494
+ error: "Timed out waiting for RPC to be ready"
495
+ };
496
+ }
497
+
498
+ // src/provider.ts
499
+ var LoadBalancedRpcProvider = class {
500
+ constructor(clients, options = { strategy: "failover" }) {
501
+ this.clients = clients;
502
+ this.options = options;
503
+ if (clients.length === 0) {
504
+ throw new Error("LoadBalancedRpcProvider requires at least one client");
505
+ }
506
+ }
507
+ clients;
508
+ options;
509
+ currentIndex = 0;
510
+ async getInfo() {
511
+ return this.withFailover((c) => c.getInfo());
512
+ }
513
+ async healthCheck() {
514
+ const healths = await Promise.all(this.clients.map((c) => c.healthCheck()));
515
+ const primaryHealth = healths[this.currentIndex];
516
+ const allHealthy = healths.every((h) => h.status === "healthy");
517
+ const anyHealthy = healths.some((h) => h.status === "healthy" || h.status === "degraded");
518
+ return {
519
+ endpoint: `LoadBalancedProvider(${this.clients.length} nodes)`,
520
+ status: allHealthy ? "healthy" : anyHealthy ? "degraded" : "unreachable",
521
+ latencyMs: primaryHealth.latencyMs,
522
+ lastError: primaryHealth.lastError,
523
+ retries: healths.reduce((sum, h) => sum + (h.retries || 0), 0),
524
+ circuitState: primaryHealth.circuitState,
525
+ stale: healths.some((h) => h.stale),
526
+ info: primaryHealth.info,
527
+ reachable: anyHealthy
528
+ };
529
+ }
530
+ async getBalanceByAddress(address) {
531
+ return this.withFailover((c) => c.getBalanceByAddress(address));
532
+ }
533
+ async getUtxosByAddress(address) {
534
+ return this.withFailover((c) => c.getUtxosByAddress(address));
535
+ }
536
+ async submitTransaction(rawTransaction) {
537
+ return this.withFailover((c) => c.submitTransaction(rawTransaction));
538
+ }
539
+ async getMempoolEntry(txId) {
540
+ return this.withFailover((c) => c.getMempoolEntry(txId));
541
+ }
542
+ async getTransaction(txId) {
543
+ return this.withFailover((c) => c.getTransaction(txId));
544
+ }
545
+ async getBlockDagInfo() {
546
+ return this.withFailover((c) => c.getBlockDagInfo());
547
+ }
548
+ async getServerInfo() {
549
+ return this.withFailover((c) => c.getServerInfo());
550
+ }
551
+ async close() {
552
+ await Promise.all(this.clients.map((c) => c.close()));
553
+ }
554
+ async withFailover(fn) {
555
+ let lastError;
556
+ for (let i = 0; i < this.clients.length; i++) {
557
+ const index = (this.currentIndex + i) % this.clients.length;
558
+ const client = this.clients[index];
559
+ try {
560
+ const result = await fn(client);
561
+ if (this.options.strategy === "round-robin") {
562
+ this.currentIndex = (index + 1) % this.clients.length;
563
+ } else {
564
+ this.currentIndex = index;
565
+ }
566
+ return result;
567
+ } catch (error) {
568
+ lastError = error;
569
+ }
570
+ }
571
+ throw lastError || new RpcUnavailableError("All RPC endpoints failed");
572
+ }
573
+ };
574
+
575
+ // src/index.ts
576
+ var JsonWrpcKaspaClient = class {
577
+ socket = null;
578
+ rpcUrl;
579
+ timeoutMs;
580
+ requestId = 1;
581
+ constructor(options) {
582
+ this.rpcUrl = options.rpcUrl;
583
+ this.timeoutMs = options.timeoutMs ?? 1e4;
584
+ }
585
+ async getInfo() {
586
+ const response = await this.safeRequest(["GetInfo", "getInfo", "get_info", "getInfoRequest"]);
587
+ const info = mapKaspaNodeInfo(response);
588
+ if (info.virtualDaaScore === void 0) {
589
+ try {
590
+ const dagResponse = await this.safeRequest(["GetBlockDagInfo", "getBlockDagInfo", "get_block_dag_info"]);
591
+ const dagData = dagResponse?.params || dagResponse;
592
+ if (dagData?.virtualDaaScore !== void 0) {
593
+ info.virtualDaaScore = BigInt(dagData.virtualDaaScore);
594
+ }
595
+ } catch (e) {
596
+ }
597
+ }
598
+ return info;
599
+ }
600
+ async healthCheck() {
601
+ try {
602
+ const info = await this.getInfo();
603
+ return {
604
+ endpoint: this.rpcUrl,
605
+ status: "healthy",
606
+ info,
607
+ reachable: true
608
+ };
609
+ } catch (error) {
610
+ return {
611
+ endpoint: this.rpcUrl,
612
+ status: "unreachable",
613
+ lastError: error instanceof Error ? error.message : String(error),
614
+ reachable: false
615
+ };
616
+ }
617
+ }
618
+ async getBalanceByAddress(address) {
619
+ const response = await this.safeRequest(
620
+ ["GetBalancesByAddresses", "getBalancesByAddresses", "get_balances_by_addresses", "GetBalanceByAddress", "getBalanceByAddress", "get_balance_by_address"],
621
+ { addresses: [address], address }
622
+ );
623
+ return mapKaspaAddressBalance(response, address);
624
+ }
625
+ async getUtxosByAddress(address) {
626
+ const response = await this.safeRequest(
627
+ ["GetUtxosByAddresses", "getUtxosByAddresses", "get_utxos_by_addresses", "GetUtxosByAddress", "getUtxosByAddress", "get_utxos_by_address"],
628
+ { addresses: [address], address }
629
+ );
630
+ return mapKaspaRpcUtxos(response, address);
631
+ }
632
+ async submitTransaction(rawTransaction) {
633
+ const response = await this.safeRequest(
634
+ ["SubmitTransaction", "submitTransaction", "submit_transaction"],
635
+ { transaction: rawTransaction, transactionHex: rawTransaction, rawTransaction }
636
+ );
637
+ return mapKaspaSubmitTransactionResult(response);
638
+ }
639
+ async getMempoolEntry(txId) {
640
+ try {
641
+ const response = await this.safeRequest(
642
+ ["getMempoolEntryRequest", "getMempoolEntry"],
643
+ { txId, transactionId: txId }
644
+ );
645
+ if (!response) return null;
646
+ return {
647
+ txId,
648
+ acceptedAt: response.acceptedAt || response.accepted_at
649
+ };
650
+ } catch (e) {
651
+ return null;
652
+ }
653
+ }
654
+ async getTransaction(txId) {
655
+ try {
656
+ const response = await this.safeRequest(
657
+ ["getTransactionRequest", "getTransaction"],
658
+ { txId, transactionId: txId }
659
+ );
660
+ return response;
661
+ } catch (e) {
662
+ return null;
663
+ }
664
+ }
665
+ async getBlockDagInfo() {
666
+ const info = await this.getInfo();
667
+ const result = {
668
+ networkId: info.networkId || "unknown",
669
+ tipHashes: []
670
+ };
671
+ if (info.virtualDaaScore !== void 0) {
672
+ result.virtualDaaScore = BigInt(info.virtualDaaScore);
673
+ }
674
+ return result;
675
+ }
676
+ async getServerInfo() {
677
+ const info = await this.getInfo();
678
+ const result = {
679
+ networkId: info.networkId || "unknown"
680
+ };
681
+ if (info.serverVersion !== void 0) result.serverVersion = info.serverVersion;
682
+ if (info.isSynced !== void 0) result.isSynced = info.isSynced;
683
+ return result;
684
+ }
685
+ async close() {
686
+ if (this.socket) {
687
+ this.socket.close();
688
+ this.socket = null;
689
+ }
690
+ }
691
+ async safeRequest(methods, params = {}) {
692
+ let lastError = null;
693
+ for (const method of methods) {
694
+ try {
695
+ let actualParams = params;
696
+ const lowerMethod = method.toLowerCase();
697
+ if (lowerMethod.includes("addresses") || lowerMethod.endsWith("s")) {
698
+ if (params.address && !params.addresses) {
699
+ actualParams = { addresses: [params.address] };
700
+ } else if (params.addresses) {
701
+ actualParams = { addresses: params.addresses };
702
+ }
703
+ } else if (params.addresses && !params.address) {
704
+ actualParams = { address: params.addresses[0] };
705
+ } else if (params.address) {
706
+ actualParams = { address: params.address };
707
+ }
708
+ try {
709
+ return await this.request(method, actualParams);
710
+ } catch (e) {
711
+ if (e.message?.includes("deserialization")) {
712
+ const arrayParams = Object.values(actualParams);
713
+ return await this.request(method, arrayParams);
714
+ }
715
+ throw e;
716
+ }
717
+ } catch (error) {
718
+ lastError = error;
719
+ continue;
720
+ }
721
+ }
722
+ throw lastError ?? new Error(`Methods failed: ${methods.join(", ")}`);
723
+ }
724
+ async request(method, params = {}) {
725
+ const ws = await this.connect();
726
+ const id = this.requestId++;
727
+ const payload = JSON.stringify({
728
+ id,
729
+ method,
730
+ params
731
+ });
732
+ return new Promise((resolve, reject) => {
733
+ const timeout = setTimeout(() => {
734
+ cleanup();
735
+ reject(new Error(`RPC request timed out after ${this.timeoutMs}ms`));
736
+ }, this.timeoutMs);
737
+ const onMessage = (data) => {
738
+ try {
739
+ const raw = data.toString();
740
+ const response = JSON.parse(raw);
741
+ if (String(response.id) === String(id)) {
742
+ cleanup();
743
+ if (response.error) {
744
+ const err = response.error;
745
+ const msg = err.message || (typeof err === "string" ? err : JSON.stringify(err));
746
+ reject(new Error(msg));
747
+ } else {
748
+ resolve(response.result !== void 0 ? response.result : response.params);
749
+ }
750
+ }
751
+ } catch (e) {
752
+ }
753
+ };
754
+ const onError = (err) => {
755
+ cleanup();
756
+ reject(err);
757
+ };
758
+ const cleanup = () => {
759
+ clearTimeout(timeout);
760
+ ws.removeListener("message", onMessage);
761
+ ws.removeListener("error", onError);
762
+ };
763
+ ws.on("message", onMessage);
764
+ ws.on("error", onError);
765
+ ws.send(payload);
766
+ });
767
+ }
768
+ async connect() {
769
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
770
+ return this.socket;
771
+ }
772
+ return new Promise((resolve, reject) => {
773
+ const ws = new WebSocket(this.rpcUrl);
774
+ const timeout = setTimeout(() => {
775
+ ws.close();
776
+ reject(new Error(`Cannot connect to Kaspa RPC at ${this.rpcUrl}. Connection timed out.`));
777
+ }, this.timeoutMs);
778
+ ws.on("open", () => {
779
+ clearTimeout(timeout);
780
+ this.socket = ws;
781
+ resolve(ws);
782
+ });
783
+ ws.on("error", (err) => {
784
+ clearTimeout(timeout);
785
+ let message = `Cannot connect to Kaspa RPC at ${this.rpcUrl}. Is kaspad running with --rpclisten-json?`;
786
+ if (err.code === "ECONNREFUSED") {
787
+ message = `Connection refused at ${this.rpcUrl}. Ensure kaspad is running and --rpclisten-json is enabled.`;
788
+ }
789
+ reject(new Error(message));
790
+ });
791
+ });
792
+ }
793
+ };
794
+ function mapKaspaNodeInfo(result) {
795
+ if (!result) return { raw: result };
796
+ const info = {
797
+ serverVersion: result.serverVersion || result.server_version,
798
+ isSynced: result.isSynced !== void 0 ? result.isSynced : result.is_synced,
799
+ isUtxoIndexed: result.isUtxoIndexed !== void 0 ? result.isUtxoIndexed : result.is_utxo_indexed,
800
+ p2pId: result.p2pId || result.p2p_id,
801
+ mempoolSize: result.mempoolSize !== void 0 ? result.mempoolSize : result.mempool_size,
802
+ networkId: result.networkId || result.network_id,
803
+ raw: result
804
+ };
805
+ const score = result.virtualDaaScore !== void 0 ? result.virtualDaaScore : result.virtual_daa_score !== void 0 ? result.virtual_daa_score : result.params?.virtualDaaScore;
806
+ if (score !== void 0) {
807
+ info.virtualDaaScore = BigInt(score);
808
+ }
809
+ return info;
810
+ }
811
+ function mapKaspaAddressBalance(result, address) {
812
+ if (!result) return { address, balanceSompi: 0n, raw: result };
813
+ let entry = result;
814
+ if (Array.isArray(result)) {
815
+ entry = result.find((e) => (e.address || e.addressString || e.address_string) === address) || result[0];
816
+ } else if (result.entries && Array.isArray(result.entries)) {
817
+ entry = result.entries.find((e) => (e.address || e.addressString || e.address_string) === address) || result.entries[0];
818
+ }
819
+ const balance = entry.balance !== void 0 ? entry.balance : entry.balanceSompi !== void 0 ? entry.balanceSompi : entry.amount;
820
+ const balanceSompi = balance !== void 0 ? BigInt(balance) : 0n;
821
+ return {
822
+ address,
823
+ balanceSompi,
824
+ raw: result
825
+ };
826
+ }
827
+ function mapKaspaRpcUtxos(result, address) {
828
+ if (!result) return [];
829
+ let entries = null;
830
+ if (Array.isArray(result)) {
831
+ entries = result;
832
+ } else if (result.result && Array.isArray(result.result)) {
833
+ entries = result.result;
834
+ } else if (result.result && (result.result.entries || result.result.utxos)) {
835
+ entries = result.result.entries || result.result.utxos;
836
+ } else {
837
+ entries = result.entries || result.utxos || result;
838
+ }
839
+ if (!Array.isArray(entries)) return [];
840
+ return entries.map((entry) => {
841
+ const utxoEntry = entry.utxoEntry || entry.utxo_entry || entry.utxo || entry;
842
+ const outpoint = entry.outpoint || entry;
843
+ return {
844
+ outpoint: {
845
+ transactionId: outpoint.transactionId || outpoint.transaction_id || outpoint.txId || outpoint.tx_id || outpoint.transaction_hash || "",
846
+ index: Number(outpoint.index !== void 0 ? outpoint.index : outpoint.outputIndex !== void 0 ? outpoint.outputIndex : outpoint.output_index)
847
+ },
848
+ address: entry.address || address,
849
+ amountSompi: BigInt(utxoEntry.amount || utxoEntry.amountSompi || utxoEntry.amount_sompi || 0),
850
+ scriptPublicKey: utxoEntry.scriptPublicKey || utxoEntry.script_public_key,
851
+ blockDaaScore: utxoEntry.blockDaaScore || utxoEntry.block_daa_score,
852
+ isCoinbase: utxoEntry.isCoinbase || utxoEntry.is_coinbase,
853
+ raw: entry
854
+ };
855
+ });
856
+ }
857
+ function mapKaspaSubmitTransactionResult(result) {
858
+ if (!result) return { raw: result };
859
+ return {
860
+ transactionId: result.transactionId || result.transaction_id || result.txId || result.tx_id,
861
+ accepted: result.accepted !== void 0 ? result.accepted : result.isAccepted || result.success,
862
+ raw: result
863
+ };
864
+ }
865
+ var MockKaspaRpcClient = class {
866
+ constructor(networkId = "simnet") {
867
+ this.networkId = networkId;
868
+ }
869
+ networkId;
870
+ utxosByAddress = /* @__PURE__ */ new Map();
871
+ async getInfo() {
872
+ return { networkId: this.networkId, serverVersion: "mock", isSynced: true, virtualDaaScore: 0n, raw: {} };
873
+ }
874
+ async healthCheck() {
875
+ return {
876
+ endpoint: "mock://local",
877
+ status: "healthy",
878
+ info: await this.getInfo(),
879
+ reachable: true
880
+ };
881
+ }
882
+ async getBalanceByAddress(address) {
883
+ const utxos = this.utxosByAddress.get(address) || [];
884
+ const balanceSompi = utxos.reduce((acc, u) => acc + u.amountSompi, 0n);
885
+ return { address, balanceSompi };
886
+ }
887
+ async getUtxosByAddress(address) {
888
+ return this.utxosByAddress.get(address) || [];
889
+ }
890
+ setUtxos(address, utxos) {
891
+ this.utxosByAddress.set(address, utxos);
892
+ }
893
+ async submitTransaction(rawTransaction) {
894
+ return {
895
+ transactionId: "mock-txid",
896
+ accepted: true,
897
+ raw: { rawTransaction }
898
+ };
899
+ }
900
+ async getMempoolEntry(_txId) {
901
+ return null;
902
+ }
903
+ async getTransaction(_txId) {
904
+ return null;
905
+ }
906
+ async getBlockDagInfo() {
907
+ return { networkId: this.networkId, virtualDaaScore: 0n };
908
+ }
909
+ async getServerInfo() {
910
+ return { networkId: this.networkId, serverVersion: "mock", isSynced: true };
911
+ }
912
+ async close() {
913
+ }
914
+ };
915
+ export {
916
+ CircuitState,
917
+ JsonWrpcKaspaClient,
918
+ KaspaJsonRpcClient,
919
+ LoadBalancedRpcProvider,
920
+ MockKaspaRpcClient,
921
+ RpcCircuitOpenError,
922
+ RpcError,
923
+ RpcRateLimitError,
924
+ RpcTimeoutError,
925
+ RpcUnavailableError,
926
+ RpcValidationError,
927
+ calculateConfidence,
928
+ checkKaspaRpcHealth,
929
+ classifyRpcError,
930
+ mapKaspaAddressBalance,
931
+ mapKaspaNodeInfo,
932
+ mapKaspaRpcUtxos,
933
+ mapKaspaSubmitTransactionResult,
934
+ waitForKaspaRpcReady
935
+ };
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@hardkas/kaspa-rpc",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "dependencies": {
11
+ "ws": "^8.18.0",
12
+ "@hardkas/core": "0.1.0",
13
+ "@hardkas/tx-builder": "0.1.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/ws": "^8.5.13",
17
+ "tsup": "^8.3.5",
18
+ "typescript": "^5.7.2",
19
+ "vitest": "^2.1.8"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Javier Rodriguez",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/jrodrg92/Hardkas.git",
26
+ "directory": "packages/kaspa-rpc"
27
+ },
28
+ "bugs": {
29
+ "url": "https://github.com/jrodrg92/Hardkas/issues"
30
+ },
31
+ "homepage": "https://github.com/jrodrg92/Hardkas/tree/main/packages/kaspa-rpc#readme",
32
+ "files": [
33
+ "dist",
34
+ "LICENSE",
35
+ "README.md"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsup src/index.ts --format esm --dts --clean",
39
+ "test": "vitest run",
40
+ "typecheck": "tsc --noEmit",
41
+ "lint": "eslint ."
42
+ }
43
+ }