chia-agent 14.3.2 → 14.3.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [14.3.4]
4
+ ### Breaking change
5
+ - Changed `daemon.connect()` API signature from `connect(url?, timeoutMs?)` to `connect(url?, options?)`
6
+ - All connection options are now consolidated into a single `options` parameter
7
+ - Existing code calling `connect()` without parameters remains compatible
8
+ ### Changed
9
+ - Improved logger system
10
+ - Added logger instance caching
11
+ - Added support for multiple loggers with per-instance configuration
12
+ - Added `NullWriter` for complete log suppression
13
+ - Updated `ConsoleWriter` to use proper console methods
14
+ - Added `trace` log level
15
+ - Added environment variable support: `LOG_LEVEL` and `LOG_SUPPRESS`
16
+ - Refactored API to use named loggers
17
+ - Added customizable log formatting with built-in formatters
18
+ - Auto-reconnection is now enabled by default
19
+ ### Fixed
20
+ - Fixed WebSocket message handling issues
21
+ - Added timeout handling for sent messages (default 30s)
22
+ - Fixed connection state check to use actual WebSocket readyState
23
+ - Preserved event listeners on connection close for reconnection support
24
+ - Added proper cleanup of message timeouts on response
25
+ - Fixed RPC agent to properly handle HTTP connections with explicit host/port
26
+ ### Added
27
+ - Added automatic retry/reconnection mechanism for WebSocket connection attempts
28
+ - Exponential backoff with configurable parameters
29
+ - Configurable retry parameters (maxAttempts, initialDelay, maxDelay, backoffMultiplier)
30
+ - Automatic re-subscription to services after reconnection
31
+ - Same retry configuration applies to both initial connection and reconnection
32
+ ### Internal change
33
+ - Removed `TDestination` type in favor of Writer-based approach
34
+
35
+ ## [14.3.3]
36
+ ### Changed
37
+ - The default service name which [`Daemon`](./src/daemon/index.ts) client tries to register is now `wallet_ui`.
38
+ Previously `chia_agent` service and optionally `wallet_ui` service were registered to `chia-blockchain`'s `Daemon`.
39
+
3
40
  ## [14.3.2]
4
41
  ### Changed
5
42
  - Error logs are generated when
@@ -1715,6 +1752,8 @@ daemon.sendMessage(destination, get_block_record_by_height_command, data);
1715
1752
  Initial release.
1716
1753
 
1717
1754
  <!-- [Unreleased]: https://github.com/Chia-Mine/chia-agent/compare/v0.0.1...v0.0.2 -->
1755
+ [14.3.4]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.3...v14.3.4
1756
+ [14.3.3]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.2...v14.3.3
1718
1757
  [14.3.2]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.1...v14.3.2
1719
1758
  [14.3.1]: https://github.com/Chia-Mine/chia-agent/compare/v14.3.0...v14.3.1
1720
1759
  [14.3.0]: https://github.com/Chia-Mine/chia-agent/compare/v14.2.2...v14.3.0
package/README.md CHANGED
@@ -113,6 +113,33 @@ setTimeout(async () => {
113
113
  */
114
114
  ```
115
115
 
116
+ ## Logging
117
+ The logger system supports multiple log levels and formatters:
118
+
119
+ ```js
120
+ const {setLogLevel, getLogger, setDefaultFormatter, simpleLogFormatter} = require("chia-agent");
121
+
122
+ // Set log level (case-insensitive)
123
+ setLogLevel("DEBUG"); // "error", "warning", "info", "debug", "trace", "none"
124
+
125
+ // Use built-in formatters
126
+ setDefaultFormatter(simpleLogFormatter); // Simple format without timestamp
127
+
128
+ // Custom formatter
129
+ const myFormatter = (context) => {
130
+ return `[${context.level}] ${context.message}`;
131
+ };
132
+ setDefaultFormatter(myFormatter);
133
+
134
+ // Named loggers
135
+ const logger = getLogger("MyModule");
136
+ logger.info("Module initialized");
137
+
138
+ // Per-logger configuration
139
+ logger.setLogLevel("debug");
140
+ logger.setFormatter(myFormatter);
141
+ ```
142
+
116
143
  ## API Reference
117
144
  [See Documentation here](https://github.com/Chia-Mine/chia-agent/blob/main/src/api/README.md)
118
145
 
@@ -42,16 +42,16 @@ function open(url, timeoutMs) {
42
42
  reject(err);
43
43
  }
44
44
  };
45
- ws.addEventListener("open", (openEvent) => {
45
+ ws.on("open", (openEvent) => {
46
46
  opened = true;
47
- ws.removeEventListener("error", onOpenError);
47
+ ws.off("error", onOpenError);
48
48
  if (timer !== null) {
49
49
  clearTimeout(timer);
50
50
  timer = null;
51
51
  resolve({ ws, openEvent });
52
52
  }
53
53
  });
54
- ws.addEventListener("error", onOpenError);
54
+ ws.on("error", onOpenError);
55
55
  });
56
56
  }
57
57
  exports.open = open;
package/daemon/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference types="node" />
1
2
  import type { CloseEvent, ErrorEvent, MessageEvent, Event } from "ws";
2
3
  import * as WS from "ws";
3
4
  import { WsMessage } from "../api/ws";
@@ -7,12 +8,27 @@ export type WsEvent = Event | MessageEvent | ErrorEvent | CloseEvent;
7
8
  export type EventListener<T = WsEvent> = (ev: T) => unknown;
8
9
  type EventListenerOf<T> = T extends "open" ? EventListener<Event> : T extends "message" ? EventListener<MessageEvent> : T extends "error" ? EventListener<ErrorEvent> : T extends "close" ? EventListener<CloseEvent> : never;
9
10
  export type MessageListener<D extends WsMessage> = (msg: D) => unknown;
10
- export declare function getDaemon(): Daemon;
11
+ export interface RetryOptions {
12
+ maxAttempts?: number;
13
+ initialDelay?: number;
14
+ maxDelay?: number;
15
+ backoffMultiplier?: number;
16
+ }
17
+ export interface ConnectOptions {
18
+ timeoutMs?: number;
19
+ autoReconnect?: boolean;
20
+ retryOptions?: RetryOptions;
21
+ }
22
+ export declare function getDaemon(serviceName?: string): Daemon;
11
23
  declare class Daemon {
12
24
  protected _socket: WS | null;
13
25
  protected _connectedUrl: string;
14
26
  protected _responseQueue: {
15
- [request_id: string]: (value: unknown) => void;
27
+ [request_id: string]: {
28
+ resolver: (value: unknown) => void;
29
+ rejecter: (error: unknown) => void;
30
+ timeout: NodeJS.Timeout;
31
+ };
16
32
  };
17
33
  protected _openEventListeners: Array<(e: Event) => unknown>;
18
34
  protected _messageEventListeners: Array<(e: MessageEvent) => unknown>;
@@ -22,17 +38,26 @@ declare class Daemon {
22
38
  protected _closing: boolean;
23
39
  protected _onClosePromise: (() => unknown) | undefined;
24
40
  protected _subscriptions: string[];
41
+ protected _serviceName: string;
42
+ protected _autoReconnect: boolean;
43
+ protected _retryOptions: Required<RetryOptions>;
44
+ protected _timeoutMs: number;
45
+ protected _reconnectAttempts: number;
46
+ protected _reconnectTimer: NodeJS.Timeout | null;
47
+ protected _lastConnectionUrl: string;
48
+ protected _isReconnecting: boolean;
25
49
  get connected(): boolean;
26
50
  get closing(): boolean;
27
- constructor();
51
+ constructor(serviceName?: string);
52
+ protected onRejection(e: unknown): null;
28
53
  /**
29
54
  * Connect to local daemon via websocket.
30
- * @param daemonServerURL
31
- * @param timeoutMs
55
+ * @param daemonServerURL - The websocket URL to connect to. If not provided, uses config values.
56
+ * @param options - Connection options including timeout, reconnect settings, and retry settings
32
57
  */
33
- connect(daemonServerURL?: string, timeoutMs?: number): Promise<boolean>;
58
+ connect(daemonServerURL?: string, options?: ConnectOptions): Promise<boolean>;
34
59
  close(): Promise<unknown>;
35
- sendMessage<M = unknown>(destination: string, command: string, data?: Record<string, unknown>): Promise<M>;
60
+ sendMessage<M = unknown>(destination: string, command: string, data?: Record<string, unknown>, timeoutMs?: number): Promise<M>;
36
61
  createMessageTemplate(command: string, destination: string, data: Record<string, unknown>): {
37
62
  command: string;
38
63
  data: Record<string, unknown>;
@@ -59,6 +84,7 @@ declare class Daemon {
59
84
  protected onClose(event: CloseEvent): void;
60
85
  protected onPing(): void;
61
86
  protected onPong(): void;
87
+ protected _attemptReconnection(previousSubscriptions: string[]): void;
62
88
  }
63
89
  export type TDaemon = InstanceType<typeof Daemon>;
64
90
  export {};
package/daemon/index.js CHANGED
@@ -10,17 +10,26 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.getDaemon = void 0;
13
+ const WS = require("ws");
13
14
  const crypto_1 = require("crypto");
14
15
  const logger_1 = require("../logger");
15
16
  const connection_1 = require("./connection");
16
17
  const index_1 = require("../config/index");
17
- const chia_agent_service = "chia_agent";
18
+ const DEFAULT_SERVICE_NAME = "wallet_ui";
19
+ const DEFAULT_AUTO_RECONNECT = true;
20
+ const DEFAULT_TIMEOUT_MS = 30000;
21
+ const DEFAULT_RETRY_OPTIONS = {
22
+ maxAttempts: 5,
23
+ initialDelay: 1000,
24
+ maxDelay: 30000,
25
+ backoffMultiplier: 1.5,
26
+ };
18
27
  let daemon = null;
19
- function getDaemon() {
28
+ function getDaemon(serviceName) {
20
29
  if (daemon) {
21
30
  return daemon;
22
31
  }
23
- return daemon = new Daemon();
32
+ return (daemon = new Daemon(serviceName));
24
33
  }
25
34
  exports.getDaemon = getDaemon;
26
35
  // Gracefully disconnect from remote daemon server on Ctrl+C.
@@ -48,12 +57,14 @@ const onProcessExit = () => {
48
57
  process.addListener("SIGINT", onProcessExit);
49
58
  class Daemon {
50
59
  get connected() {
51
- return Boolean(this._connectedUrl);
60
+ return (Boolean(this._connectedUrl) &&
61
+ this._socket !== null &&
62
+ this._socket.readyState === WS.OPEN);
52
63
  }
53
64
  get closing() {
54
65
  return this._closing;
55
66
  }
56
- constructor() {
67
+ constructor(serviceName) {
57
68
  this._socket = null;
58
69
  this._connectedUrl = "";
59
70
  this._responseQueue = {};
@@ -64,19 +75,51 @@ class Daemon {
64
75
  this._messageListeners = {};
65
76
  this._closing = false;
66
77
  this._subscriptions = [];
78
+ this._serviceName = DEFAULT_SERVICE_NAME;
79
+ this._autoReconnect = DEFAULT_AUTO_RECONNECT;
80
+ this._retryOptions = DEFAULT_RETRY_OPTIONS;
81
+ this._timeoutMs = DEFAULT_TIMEOUT_MS;
82
+ this._reconnectAttempts = 0;
83
+ this._reconnectTimer = null;
84
+ this._lastConnectionUrl = "";
85
+ this._isReconnecting = false;
67
86
  this.onOpen = this.onOpen.bind(this);
68
87
  this.onError = this.onError.bind(this);
69
88
  this.onMessage = this.onMessage.bind(this);
70
89
  this.onClose = this.onClose.bind(this);
71
90
  this.onPing = this.onPing.bind(this);
72
91
  this.onPong = this.onPong.bind(this);
92
+ this.onRejection = this.onRejection.bind(this);
93
+ if (serviceName) {
94
+ this._serviceName = serviceName;
95
+ }
96
+ }
97
+ onRejection(e) {
98
+ if (typeof e === "string") {
99
+ (0, logger_1.getLogger)().error(`Error: ${e}`);
100
+ }
101
+ else if (e instanceof Error) {
102
+ (0, logger_1.getLogger)().error(`Error ${e.name}: ${e.message}`);
103
+ if (e.stack) {
104
+ (0, logger_1.getLogger)().error(e.stack);
105
+ }
106
+ }
107
+ else {
108
+ try {
109
+ (0, logger_1.getLogger)().error(`Error: ${JSON.stringify(e)}`);
110
+ }
111
+ catch (_e) {
112
+ (0, logger_1.getLogger)().error("Unknown error");
113
+ }
114
+ }
115
+ return null;
73
116
  }
74
117
  /**
75
118
  * Connect to local daemon via websocket.
76
- * @param daemonServerURL
77
- * @param timeoutMs
119
+ * @param daemonServerURL - The websocket URL to connect to. If not provided, uses config values.
120
+ * @param options - Connection options including timeout, reconnect settings, and retry settings
78
121
  */
79
- connect(daemonServerURL, timeoutMs) {
122
+ connect(daemonServerURL, options) {
80
123
  return __awaiter(this, void 0, void 0, function* () {
81
124
  if (!daemonServerURL) {
82
125
  const config = (0, index_1.getConfig)();
@@ -84,6 +127,19 @@ class Daemon {
84
127
  const daemonPort = config["/ui/daemon_port"];
85
128
  daemonServerURL = `wss://${daemonHost}:${daemonPort}`;
86
129
  }
130
+ // Extract options with defaults
131
+ const timeoutMs = (options === null || options === void 0 ? void 0 : options.timeoutMs) || this._timeoutMs;
132
+ // Store timeout for reconnection attempts
133
+ if ((options === null || options === void 0 ? void 0 : options.timeoutMs) !== undefined) {
134
+ this._timeoutMs = options.timeoutMs;
135
+ }
136
+ // Update settings from options
137
+ if ((options === null || options === void 0 ? void 0 : options.autoReconnect) !== undefined) {
138
+ this._autoReconnect = options.autoReconnect;
139
+ }
140
+ if ((options === null || options === void 0 ? void 0 : options.retryOptions) !== undefined) {
141
+ this._retryOptions = Object.assign(Object.assign({}, DEFAULT_RETRY_OPTIONS), options.retryOptions);
142
+ }
87
143
  if (this._connectedUrl === daemonServerURL) {
88
144
  return true;
89
145
  }
@@ -91,33 +147,65 @@ class Daemon {
91
147
  (0, logger_1.getLogger)().error("Connection is still active. Please close living connection first");
92
148
  return false;
93
149
  }
94
- (0, logger_1.getLogger)().debug(`Opening websocket connection to ${daemonServerURL}`);
95
- const result = yield (0, connection_1.open)(daemonServerURL, timeoutMs);
96
- this._socket = result.ws;
97
- this._socket.addEventListener("error", this.onError);
98
- this._socket.addEventListener("message", this.onMessage);
99
- this._socket.addEventListener("close", this.onClose);
100
- this._socket.addListener("ping", this.onPing);
101
- this._socket.addListener("pong", this.onPong);
102
- yield this.onOpen(result.openEvent, daemonServerURL);
103
- return true;
150
+ // Store URL for reconnection
151
+ this._lastConnectionUrl = daemonServerURL;
152
+ // Attempt connection with retry logic
153
+ let lastError;
154
+ for (let attempt = 1; attempt <= this._retryOptions.maxAttempts; attempt++) {
155
+ (0, logger_1.getLogger)().debug(`Opening websocket connection to ${daemonServerURL} (attempt ${attempt}/${this._retryOptions.maxAttempts})`);
156
+ const result = yield (0, connection_1.open)(daemonServerURL, timeoutMs).catch((error) => {
157
+ lastError = error;
158
+ return null;
159
+ });
160
+ if (result) {
161
+ this._socket = result.ws;
162
+ this._socket.on("error", this.onError);
163
+ this._socket.addEventListener("message", this.onMessage);
164
+ this._socket.on("close", this.onClose);
165
+ this._socket.on("ping", this.onPing);
166
+ this._socket.on("pong", this.onPong);
167
+ // Call onOpen but don't check result (maintain original behavior)
168
+ yield this.onOpen(result.openEvent, daemonServerURL).catch(this.onRejection);
169
+ return true;
170
+ }
171
+ // If not the last attempt, wait before retrying
172
+ if (attempt < this._retryOptions.maxAttempts) {
173
+ const delay = Math.min(this._retryOptions.initialDelay *
174
+ Math.pow(this._retryOptions.backoffMultiplier, attempt - 1), this._retryOptions.maxDelay);
175
+ (0, logger_1.getLogger)().info(`Connection attempt ${attempt} failed. Retrying in ${delay}ms...`);
176
+ yield new Promise((resolve) => setTimeout(resolve, delay));
177
+ }
178
+ }
179
+ // All attempts failed
180
+ (0, logger_1.getLogger)().error(`Failed to connect after ${this._retryOptions.maxAttempts} attempts`);
181
+ this.onRejection(lastError);
182
+ return false;
104
183
  });
105
184
  }
106
185
  close() {
107
186
  return __awaiter(this, void 0, void 0, function* () {
108
- return new Promise(((resolve) => {
187
+ return new Promise((resolve) => {
109
188
  if (this._closing || !this._socket) {
110
189
  return;
111
190
  }
191
+ // Cancel any pending reconnection
192
+ if (this._reconnectTimer) {
193
+ clearTimeout(this._reconnectTimer);
194
+ this._reconnectTimer = null;
195
+ }
196
+ // Disable reconnection for manual close
197
+ this._autoReconnect = false;
198
+ this._isReconnecting = false;
112
199
  (0, logger_1.getLogger)().debug("Closing web socket connection");
113
200
  this._socket.close();
114
201
  this._closing = true;
202
+ this._connectedUrl = "";
115
203
  this._onClosePromise = resolve; // Resolved in onClose function.
116
- }));
204
+ });
117
205
  });
118
206
  }
119
- sendMessage(destination, command, data) {
120
- return __awaiter(this, void 0, void 0, function* () {
207
+ sendMessage(destination_1, command_1, data_1) {
208
+ return __awaiter(this, arguments, void 0, function* (destination, command, data, timeoutMs = 30000) {
121
209
  return new Promise((resolve, reject) => {
122
210
  if (!this.connected || !this._socket) {
123
211
  (0, logger_1.getLogger)().error("Tried to send message without active connection");
@@ -126,13 +214,32 @@ class Daemon {
126
214
  }
127
215
  const message = this.createMessageTemplate(command, destination, data || {});
128
216
  const reqId = message.request_id;
129
- this._responseQueue[reqId] = resolve;
130
- (0, logger_1.getLogger)().debug(`Sending message. dest=${destination} command=${command} reqId=${reqId}`);
217
+ // Set up timeout
218
+ const timeout = setTimeout(() => {
219
+ const entry = this._responseQueue[reqId];
220
+ if (entry) {
221
+ delete this._responseQueue[reqId];
222
+ entry.rejecter(new Error(`Message timeout after ${timeoutMs}ms. dest=${destination} command=${command} reqId=${reqId}`));
223
+ }
224
+ }, timeoutMs);
225
+ this._responseQueue[reqId] = {
226
+ resolver: resolve,
227
+ rejecter: reject,
228
+ timeout,
229
+ };
230
+ (0, logger_1.getLogger)().debug(`Sending Ws message. dest=${destination} command=${command} reqId=${reqId}`);
131
231
  const messageStr = JSON.stringify(message);
132
232
  this._socket.send(messageStr, (err) => {
133
233
  if (err) {
134
234
  (0, logger_1.getLogger)().error(`Error while sending message: ${messageStr}`);
135
235
  (0, logger_1.getLogger)().error(JSON.stringify(err));
236
+ // Clean up on send error
237
+ const entry = this._responseQueue[reqId];
238
+ if (entry) {
239
+ clearTimeout(entry.timeout);
240
+ delete this._responseQueue[reqId];
241
+ reject(err);
242
+ }
136
243
  }
137
244
  });
138
245
  });
@@ -143,7 +250,7 @@ class Daemon {
143
250
  command,
144
251
  data,
145
252
  ack: false,
146
- origin: chia_agent_service,
253
+ origin: this._serviceName,
147
254
  destination,
148
255
  request_id: (0, crypto_1.randomBytes)(32).toString("hex"),
149
256
  };
@@ -154,23 +261,28 @@ class Daemon {
154
261
  (0, logger_1.getLogger)().error(`Tried to subscribe '${service}' without active connection`);
155
262
  throw new Error("Not connected");
156
263
  }
157
- if (this._subscriptions.findIndex(s => s === service) > -1) {
264
+ if (this._subscriptions.findIndex((s) => s === service) > -1) {
158
265
  return {
159
266
  command: "register_service",
160
267
  data: { success: true },
161
268
  ack: true,
162
269
  origin: "daemon",
163
- destination: chia_agent_service,
270
+ destination: service,
164
271
  request_id: "",
165
272
  };
166
273
  }
167
274
  let error;
168
- const result = yield this.sendMessage("daemon", "register_service", { service }).catch(e => {
275
+ const result = yield this.sendMessage("daemon", "register_service", {
276
+ service,
277
+ }).catch((e) => {
169
278
  error = e;
170
279
  return null;
171
280
  });
172
281
  if (error || !result) {
173
282
  (0, logger_1.getLogger)().error("Failed to register agent service to daemon");
283
+ (0, logger_1.getLogger)().error(error instanceof Error
284
+ ? `${error.name}: ${error.message}`
285
+ : JSON.stringify(error));
174
286
  throw new Error("Subscribe failed");
175
287
  }
176
288
  this._subscriptions.push(service);
@@ -241,7 +353,7 @@ class Daemon {
241
353
  if (!listeners) {
242
354
  return;
243
355
  }
244
- const index = listeners.findIndex(l => l === listener);
356
+ const index = listeners.findIndex((l) => l === listener);
245
357
  if (index > -1) {
246
358
  listeners.splice(index, 1);
247
359
  }
@@ -253,13 +365,13 @@ class Daemon {
253
365
  return __awaiter(this, void 0, void 0, function* () {
254
366
  (0, logger_1.getLogger)().info("ws connection opened");
255
367
  this._connectedUrl = url;
256
- this._openEventListeners.forEach(l => l(event));
257
- return this.subscribe(chia_agent_service);
368
+ this._openEventListeners.forEach((l) => l(event));
369
+ return this.subscribe(this._serviceName);
258
370
  });
259
371
  }
260
372
  onError(error) {
261
- (0, logger_1.getLogger)().error(`ws connection error: ${error.message}`);
262
- this._errorEventListeners.forEach(l => l(error));
373
+ (0, logger_1.getLogger)().error(`ws connection error: ${error.type} ${error.target} ${error.error} ${error.message}`);
374
+ this._errorEventListeners.forEach((l) => l(error));
263
375
  }
264
376
  onMessage(event) {
265
377
  let payload;
@@ -271,46 +383,62 @@ class Daemon {
271
383
  ({ request_id, origin, command } = payload);
272
384
  }
273
385
  catch (err) {
274
- (0, logger_1.getLogger)().error(`Failed to parse message data: ${JSON.stringify(err)}`);
275
- (0, logger_1.getLogger)().error(`payload: ${event.data}`);
386
+ (0, logger_1.getLogger)().error(`Failed to parse ws message data: ${JSON.stringify(err)}`);
387
+ (0, logger_1.getLogger)().error(`ws payload: ${event.data}`);
276
388
  return;
277
389
  }
278
- (0, logger_1.getLogger)().debug(`Arrived message. origin=${origin} command=${command} reqId=${request_id}`);
279
- const resolver = this._responseQueue[request_id];
280
- if (resolver) {
390
+ const entry = this._responseQueue[request_id];
391
+ if (entry) {
392
+ clearTimeout(entry.timeout);
281
393
  delete this._responseQueue[request_id];
282
- resolver(payload);
394
+ (0, logger_1.getLogger)().debug(`Ws response received. origin=${origin} command=${command} reqId=${request_id}`);
395
+ entry.resolver(payload);
283
396
  }
284
- this._messageEventListeners.forEach(l => l(event));
397
+ else {
398
+ (0, logger_1.getLogger)().debug(`Ws message arrived. origin=${origin} command=${command} reqId=${request_id}`);
399
+ }
400
+ (0, logger_1.getLogger)().trace(`Ws message: ${JSON.stringify(payload)}`);
401
+ this._messageEventListeners.forEach((l) => l(event));
285
402
  for (const o in this._messageListeners) {
286
403
  if (!Object.prototype.hasOwnProperty.call(this._messageListeners, o)) {
287
404
  continue;
288
405
  }
289
406
  const listeners = this._messageListeners[o];
290
407
  if (origin === o || o === "all") {
291
- listeners.forEach(l => l(payload));
408
+ listeners.forEach((l) => l(payload));
292
409
  }
293
410
  }
294
411
  }
295
412
  onClose(event) {
413
+ const previousSubscriptions = [...this._subscriptions];
296
414
  if (this._socket) {
297
- this._socket.removeEventListener("error", this.onError);
415
+ this._socket.off("error", this.onError);
298
416
  this._socket.removeEventListener("message", this.onMessage);
299
- this._socket.removeEventListener("close", this.onClose);
300
- this._socket.removeListener("ping", this.onPing);
301
- this._socket.removeListener("pong", this.onPong);
417
+ this._socket.off("close", this.onClose);
418
+ this._socket.off("ping", this.onPing);
419
+ this._socket.off("pong", this.onPong);
302
420
  this._socket = null;
303
421
  }
304
422
  this._closing = false;
305
423
  this._connectedUrl = "";
306
424
  this._subscriptions = [];
307
- this._closeEventListeners.forEach(l => l(event));
308
- this.clearAllEventListeners();
309
- (0, logger_1.getLogger)().info("Closed ws connection");
425
+ this._closeEventListeners.forEach((l) => l(event));
426
+ // Don't clear event listeners - preserve them for reconnection
427
+ // this.clearAllEventListeners();
428
+ (0, logger_1.getLogger)().info(`Closed ws connection. code:${event.code} wasClean:${event.wasClean} reason:${event.reason}`);
310
429
  if (this._onClosePromise) {
311
430
  this._onClosePromise();
312
431
  this._onClosePromise = undefined;
313
432
  }
433
+ // Attempt reconnection if enabled and not manually closed
434
+ if (this._autoReconnect &&
435
+ this._lastConnectionUrl &&
436
+ !this._isReconnecting &&
437
+ event.code !== 1000 // 1000 = normal closure
438
+ ) {
439
+ this._isReconnecting = true;
440
+ this._attemptReconnection(previousSubscriptions);
441
+ }
314
442
  }
315
443
  onPing() {
316
444
  (0, logger_1.getLogger)().debug("Received ping");
@@ -318,4 +446,63 @@ class Daemon {
318
446
  onPong() {
319
447
  (0, logger_1.getLogger)().debug("Received pong");
320
448
  }
449
+ _attemptReconnection(previousSubscriptions) {
450
+ if (this._reconnectAttempts >= this._retryOptions.maxAttempts) {
451
+ (0, logger_1.getLogger)().error(`Max reconnection attempts (${this._retryOptions.maxAttempts}) reached. Giving up.`);
452
+ this._isReconnecting = false;
453
+ this._reconnectAttempts = 0;
454
+ // Emit a custom event for max retries reached
455
+ const errorEvent = {
456
+ type: "error",
457
+ message: "Max reconnection attempts reached",
458
+ error: new Error("Max reconnection attempts reached"),
459
+ target: this._socket,
460
+ };
461
+ this._errorEventListeners.forEach((l) => l(errorEvent));
462
+ return;
463
+ }
464
+ const delay = Math.min(this._retryOptions.initialDelay *
465
+ Math.pow(this._retryOptions.backoffMultiplier, this._reconnectAttempts), this._retryOptions.maxDelay);
466
+ this._reconnectAttempts++;
467
+ (0, logger_1.getLogger)().info(`Attempting reconnection ${this._reconnectAttempts}/${this._retryOptions.maxAttempts} in ${delay}ms...`);
468
+ this._reconnectTimer = setTimeout(() => __awaiter(this, void 0, void 0, function* () {
469
+ this._reconnectTimer = null;
470
+ try {
471
+ const connected = yield this.connect(this._lastConnectionUrl, {
472
+ timeoutMs: this._timeoutMs,
473
+ autoReconnect: this._autoReconnect,
474
+ retryOptions: this._retryOptions,
475
+ });
476
+ if (connected) {
477
+ (0, logger_1.getLogger)().info("Reconnection successful");
478
+ this._reconnectAttempts = 0;
479
+ this._isReconnecting = false;
480
+ // Re-establish previous subscriptions
481
+ for (const service of previousSubscriptions) {
482
+ try {
483
+ yield this.subscribe(service);
484
+ (0, logger_1.getLogger)().debug(`Re-subscribed to ${service}`);
485
+ }
486
+ catch (e) {
487
+ (0, logger_1.getLogger)().error(`Failed to re-subscribe to ${service}: ${e}`);
488
+ }
489
+ }
490
+ // Emit successful reconnection event
491
+ const reconnectedEvent = {
492
+ type: "reconnected",
493
+ target: this._socket,
494
+ };
495
+ this._openEventListeners.forEach((l) => l(reconnectedEvent));
496
+ }
497
+ else {
498
+ // Connection failed, try again
499
+ this._attemptReconnection(previousSubscriptions);
500
+ }
501
+ }
502
+ catch (error) {
503
+ (0, logger_1.getLogger)().error(`Reconnection attempt failed: ${error}`);
504
+ this._attemptReconnection(previousSubscriptions);
505
+ }
506
+ }), delay);
507
+ }
321
508
  }
package/logger.d.ts CHANGED
@@ -1,22 +1,49 @@
1
- export type TLogLevel = "error" | "warning" | "info" | "debug" | "none";
2
- export type TDestination = "console";
3
- export type Writer = {
4
- write: (message: string) => void;
5
- };
6
- export declare function getLogLevel(): TLogLevel;
7
- export declare function setLogLevel(logLevel: TLogLevel): TLogLevel;
8
- export declare function getLogger(writer?: TDestination): Logger;
9
- declare class Logger {
10
- loglevel: TLogLevel;
11
- protected _writer: Writer;
12
- protected constructor(logLevel: TLogLevel, writer?: TDestination | Writer);
13
- static getLogger(logLevel: TLogLevel, writer?: TDestination): Logger;
1
+ export type TLogLevel = "error" | "warning" | "info" | "debug" | "trace" | "none";
2
+ export declare function isValidLogLevel(level: string): level is TLogLevel;
3
+ export declare function normalizeLogLevel(level: string): TLogLevel | null;
4
+ export interface Writer {
5
+ write: (message: string, level?: TLogLevel) => void;
6
+ }
7
+ export interface LogContext {
8
+ timestamp: Date;
9
+ level: TLogLevel;
10
+ loggerName: string;
11
+ message: string;
12
+ }
13
+ export type LogFormatter = (context: LogContext) => string;
14
+ export declare function setDefaultLogLevel(level: TLogLevel): void;
15
+ export declare function getDefaultLogLevel(): TLogLevel;
16
+ export declare function setDefaultWriter(writer: Writer): void;
17
+ export declare function setDefaultFormatter(formatter: LogFormatter): void;
18
+ export declare function getDefaultFormatter(): LogFormatter;
19
+ export declare const DEFAULT_LOGGER_NAME = "default";
20
+ export declare const defaultLogFormatter: LogFormatter;
21
+ export declare const simpleLogFormatter: LogFormatter;
22
+ export declare const jsonLogFormatter: LogFormatter;
23
+ export declare function getLogger(name?: string): Logger;
24
+ export declare function clearLoggerCache(): void;
25
+ export declare function createConsoleWriter(): Writer;
26
+ export declare function createNullWriter(): Writer;
27
+ export declare class Logger {
28
+ private _name;
29
+ private _logLevel;
30
+ private _writer;
31
+ private _formatter;
32
+ constructor(name: string, logLevel: TLogLevel, writer: Writer, formatter?: LogFormatter);
14
33
  setLogLevel(level: TLogLevel): void;
15
- shouldWrite(logLevel: TLogLevel): boolean;
16
- formatMessage(level: TLogLevel, body: string): string;
34
+ getLogLevel(): TLogLevel;
35
+ setWriter(writer: Writer): void;
36
+ getWriter(): Writer;
37
+ getName(): string;
38
+ setFormatter(formatter: LogFormatter): void;
39
+ getFormatter(): LogFormatter;
40
+ private _shouldWrite;
41
+ private _formatMessage;
42
+ trace(msg: any): void;
17
43
  debug(msg: any): void;
18
44
  info(msg: any): void;
19
45
  warning(msg: any): void;
20
46
  error(msg: any): void;
21
47
  }
22
- export {};
48
+ export declare function getLogLevel(): TLogLevel;
49
+ export declare function setLogLevel(level: TLogLevel): void;
package/logger.js CHANGED
@@ -1,23 +1,157 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getLogger = exports.setLogLevel = exports.getLogLevel = void 0;
3
+ exports.setLogLevel = exports.getLogLevel = exports.Logger = exports.createNullWriter = exports.createConsoleWriter = exports.clearLoggerCache = exports.getLogger = exports.jsonLogFormatter = exports.simpleLogFormatter = exports.defaultLogFormatter = exports.DEFAULT_LOGGER_NAME = exports.getDefaultFormatter = exports.setDefaultFormatter = exports.setDefaultWriter = exports.getDefaultLogLevel = exports.setDefaultLogLevel = exports.normalizeLogLevel = exports.isValidLogLevel = void 0;
4
+ const LOG_LEVELS = [
5
+ "error",
6
+ "warning",
7
+ "info",
8
+ "debug",
9
+ "trace",
10
+ "none",
11
+ ];
12
+ function isValidLogLevel(level) {
13
+ return LOG_LEVELS.includes(level.toLowerCase());
14
+ }
15
+ exports.isValidLogLevel = isValidLogLevel;
16
+ function normalizeLogLevel(level) {
17
+ const normalized = level.toLowerCase();
18
+ if (isValidLogLevel(normalized)) {
19
+ return normalized;
20
+ }
21
+ return null;
22
+ }
23
+ exports.normalizeLogLevel = normalizeLogLevel;
4
24
  const logPriority = {
5
25
  none: 9999,
6
- error: 4,
7
- warning: 3,
8
- info: 2,
9
- debug: 1,
26
+ error: 5,
27
+ warning: 4,
28
+ info: 3,
29
+ debug: 2,
30
+ trace: 1,
10
31
  };
11
32
  class ConsoleWriter {
12
- write(message) {
13
- console.log(message);
33
+ write(message, level) {
34
+ switch (level) {
35
+ case "error":
36
+ console.error(message);
37
+ break;
38
+ case "warning":
39
+ console.warn(message);
40
+ break;
41
+ case "info":
42
+ console.info(message);
43
+ break;
44
+ case "debug":
45
+ console.debug(message);
46
+ break;
47
+ case "trace":
48
+ console.trace(message);
49
+ break;
50
+ default:
51
+ console.log(message);
52
+ }
14
53
  }
15
54
  }
16
- let currentLogLevel = "error";
17
- function getLogLevel() { return currentLogLevel; }
18
- exports.getLogLevel = getLogLevel;
19
- function setLogLevel(logLevel) { return currentLogLevel = logLevel; }
20
- exports.setLogLevel = setLogLevel;
55
+ class NullWriter {
56
+ write(_message, _level) {
57
+ // Suppress all output
58
+ }
59
+ }
60
+ // Global defaults
61
+ let defaultLogLevel = "error";
62
+ if (process.env.LOG_LEVEL) {
63
+ const normalizedLevel = normalizeLogLevel(process.env.LOG_LEVEL);
64
+ if (normalizedLevel) {
65
+ defaultLogLevel = normalizedLevel;
66
+ }
67
+ else {
68
+ console.warn(`Invalid LOG_LEVEL environment variable: ${process.env.LOG_LEVEL}. Using default: error`);
69
+ }
70
+ }
71
+ let defaultWriter = process.env.LOG_SUPPRESS === "true" ? new NullWriter() : new ConsoleWriter();
72
+ // Logger instance cache
73
+ const loggerCache = new Map();
74
+ // Configuration functions for global defaults
75
+ function setDefaultLogLevel(level) {
76
+ // Normalize for JavaScript users who might pass uppercase
77
+ const normalized = normalizeLogLevel(level);
78
+ if (!normalized) {
79
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
80
+ }
81
+ defaultLogLevel = normalized;
82
+ }
83
+ exports.setDefaultLogLevel = setDefaultLogLevel;
84
+ function getDefaultLogLevel() {
85
+ return defaultLogLevel;
86
+ }
87
+ exports.getDefaultLogLevel = getDefaultLogLevel;
88
+ function setDefaultWriter(writer) {
89
+ defaultWriter = writer;
90
+ }
91
+ exports.setDefaultWriter = setDefaultWriter;
92
+ function setDefaultFormatter(formatter) {
93
+ defaultFormatter = formatter;
94
+ }
95
+ exports.setDefaultFormatter = setDefaultFormatter;
96
+ function getDefaultFormatter() {
97
+ return defaultFormatter;
98
+ }
99
+ exports.getDefaultFormatter = getDefaultFormatter;
100
+ exports.DEFAULT_LOGGER_NAME = "default";
101
+ // Default formatter
102
+ const defaultLogFormatter = (context) => {
103
+ const timestamp = context.timestamp.toISOString();
104
+ const levelStr = context.level.toUpperCase();
105
+ const nameStr = context.loggerName === exports.DEFAULT_LOGGER_NAME
106
+ ? ""
107
+ : ` [${context.loggerName}]`;
108
+ return `${timestamp} [${levelStr}]${nameStr} - ${context.message}`;
109
+ };
110
+ exports.defaultLogFormatter = defaultLogFormatter;
111
+ // Simple formatter without timestamp
112
+ const simpleLogFormatter = (context) => {
113
+ const levelStr = context.level.toUpperCase();
114
+ const nameStr = context.loggerName === exports.DEFAULT_LOGGER_NAME
115
+ ? ""
116
+ : `[${context.loggerName}] `;
117
+ return `[${levelStr}] ${nameStr}${context.message}`;
118
+ };
119
+ exports.simpleLogFormatter = simpleLogFormatter;
120
+ // JSON formatter
121
+ const jsonLogFormatter = (context) => {
122
+ return JSON.stringify({
123
+ timestamp: context.timestamp.toISOString(),
124
+ level: context.level,
125
+ logger: context.loggerName,
126
+ message: context.message,
127
+ });
128
+ };
129
+ exports.jsonLogFormatter = jsonLogFormatter;
130
+ let defaultFormatter = exports.defaultLogFormatter;
131
+ // Factory function with caching
132
+ function getLogger(name = exports.DEFAULT_LOGGER_NAME) {
133
+ let logger = loggerCache.get(name);
134
+ if (!logger) {
135
+ logger = new Logger(name, defaultLogLevel, defaultWriter, defaultFormatter);
136
+ loggerCache.set(name, logger);
137
+ }
138
+ return logger;
139
+ }
140
+ exports.getLogger = getLogger;
141
+ // Clear logger cache (useful for testing)
142
+ function clearLoggerCache() {
143
+ loggerCache.clear();
144
+ }
145
+ exports.clearLoggerCache = clearLoggerCache;
146
+ // Helper to create writers
147
+ function createConsoleWriter() {
148
+ return new ConsoleWriter();
149
+ }
150
+ exports.createConsoleWriter = createConsoleWriter;
151
+ function createNullWriter() {
152
+ return new NullWriter();
153
+ }
154
+ exports.createNullWriter = createNullWriter;
21
155
  function stringify(obj, indent) {
22
156
  if (typeof obj === "string") {
23
157
  return obj;
@@ -41,7 +175,7 @@ function stringify(obj, indent) {
41
175
  return "[Function]";
42
176
  }
43
177
  const seen = new WeakSet();
44
- return JSON.stringify(obj, (k, v) => {
178
+ return JSON.stringify(obj, (_k, v) => {
45
179
  if (typeof v === "object" && v !== null) {
46
180
  if (seen.has(v)) {
47
181
  return undefined;
@@ -54,60 +188,96 @@ function stringify(obj, indent) {
54
188
  return v;
55
189
  }, indent);
56
190
  }
57
- const loggers = {};
58
- function getLogger(writer) {
59
- const w = writer || "console";
60
- const logger = loggers[w];
61
- if (logger && logger.loglevel === currentLogLevel) {
62
- return logger;
63
- }
64
- return loggers[w] = Logger.getLogger(currentLogLevel, w);
65
- }
66
- exports.getLogger = getLogger;
67
191
  class Logger {
68
- constructor(logLevel, writer) {
69
- this.loglevel = "error";
70
- if (writer === "console") {
71
- this._writer = new ConsoleWriter();
72
- }
73
- else if (writer) {
74
- this._writer = writer;
192
+ constructor(name, logLevel, writer, formatter) {
193
+ // Normalize for JavaScript users who might pass uppercase
194
+ const normalized = normalizeLogLevel(logLevel);
195
+ if (!normalized) {
196
+ throw new Error(`Invalid log level: ${logLevel}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
75
197
  }
76
- else {
77
- this._writer = new ConsoleWriter();
198
+ this._name = name;
199
+ this._logLevel = normalized;
200
+ this._writer = writer;
201
+ this._formatter = formatter || defaultFormatter;
202
+ }
203
+ // Allow changing log level for this specific logger
204
+ setLogLevel(level) {
205
+ // Normalize for JavaScript users who might pass uppercase
206
+ const normalized = normalizeLogLevel(level);
207
+ if (!normalized) {
208
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
78
209
  }
79
- this.loglevel = logLevel;
210
+ this._logLevel = normalized;
80
211
  }
81
- static getLogger(logLevel, writer) {
82
- return new Logger(logLevel, writer);
212
+ getLogLevel() {
213
+ return this._logLevel;
83
214
  }
84
- setLogLevel(level) {
85
- this.loglevel = level;
215
+ // Allow changing writer for this specific logger
216
+ setWriter(writer) {
217
+ this._writer = writer;
86
218
  }
87
- shouldWrite(logLevel) {
88
- return logPriority[this.loglevel] <= logPriority[logLevel];
219
+ getWriter() {
220
+ return this._writer;
89
221
  }
90
- formatMessage(level, body) {
91
- return `${(new Date()).toLocaleString()} [${level.toUpperCase()}] ${body}`;
222
+ getName() {
223
+ return this._name;
224
+ }
225
+ setFormatter(formatter) {
226
+ this._formatter = formatter;
227
+ }
228
+ getFormatter() {
229
+ return this._formatter;
230
+ }
231
+ _shouldWrite(logLevel) {
232
+ return logPriority[this._logLevel] <= logPriority[logLevel];
233
+ }
234
+ _formatMessage(level, body) {
235
+ const context = {
236
+ timestamp: new Date(),
237
+ level,
238
+ loggerName: this._name,
239
+ message: body,
240
+ };
241
+ return this._formatter(context);
242
+ }
243
+ trace(msg) {
244
+ if (this._shouldWrite("trace")) {
245
+ this._writer.write(this._formatMessage("trace", stringify(msg)), "trace");
246
+ }
92
247
  }
93
248
  debug(msg) {
94
- if (this.shouldWrite("debug")) {
95
- this._writer.write(this.formatMessage("debug", stringify(msg)));
249
+ if (this._shouldWrite("debug")) {
250
+ this._writer.write(this._formatMessage("debug", stringify(msg)), "debug");
96
251
  }
97
252
  }
98
253
  info(msg) {
99
- if (this.shouldWrite("info")) {
100
- this._writer.write(this.formatMessage("info", stringify(msg)));
254
+ if (this._shouldWrite("info")) {
255
+ this._writer.write(this._formatMessage("info", stringify(msg)), "info");
101
256
  }
102
257
  }
103
258
  warning(msg) {
104
- if (this.shouldWrite("warning")) {
105
- this._writer.write(this.formatMessage("warning", stringify(msg)));
259
+ if (this._shouldWrite("warning")) {
260
+ this._writer.write(this._formatMessage("warning", stringify(msg)), "warning");
106
261
  }
107
262
  }
108
263
  error(msg) {
109
- if (this.shouldWrite("error")) {
110
- this._writer.write(this.formatMessage("error", stringify(msg)));
264
+ if (this._shouldWrite("error")) {
265
+ this._writer.write(this._formatMessage("error", stringify(msg)), "error");
111
266
  }
112
267
  }
113
268
  }
269
+ exports.Logger = Logger;
270
+ // For backward compatibility
271
+ function getLogLevel() {
272
+ return getDefaultLogLevel();
273
+ }
274
+ exports.getLogLevel = getLogLevel;
275
+ function setLogLevel(level) {
276
+ // Normalize for JavaScript users who might pass uppercase
277
+ const normalized = normalizeLogLevel(level);
278
+ if (!normalized) {
279
+ throw new Error(`Invalid log level: ${level}. Valid levels are: ${LOG_LEVELS.join(", ")}`);
280
+ }
281
+ setDefaultLogLevel(normalized);
282
+ }
283
+ exports.setLogLevel = setLogLevel;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chia-agent",
3
- "version": "14.3.2",
3
+ "version": "14.3.4",
4
4
  "author": "ChiaMineJP <admin@chiamine.jp>",
5
5
  "description": "chia rpc/websocket client library",
6
6
  "license": "MIT",
@@ -10,6 +10,7 @@
10
10
  },
11
11
  "bugs": "https://github.com/Chia-Mine/chia-agent/issues",
12
12
  "main": "./index.js",
13
+ "types": "./index.d.ts",
13
14
  "bin": {
14
15
  "chia-agent": "./bin/cli.js"
15
16
  },
package/rpc/index.d.ts CHANGED
@@ -61,12 +61,16 @@ export type TRPCAgentProps = {
61
61
  skip_hostname_verification?: boolean;
62
62
  } | {
63
63
  httpAgent: HttpAgent;
64
+ host: string;
65
+ port: number;
64
66
  skip_hostname_verification?: boolean;
65
67
  };
66
68
  export declare class RPCAgent implements APIAgent {
67
69
  protected _protocol: "http" | "https";
68
70
  protected _agent: HttpsAgent | HttpAgent;
69
71
  protected _skip_hostname_verification: boolean;
72
+ protected _host: string;
73
+ protected _port: number;
70
74
  constructor(props: TRPCAgentProps);
71
75
  sendMessage<M>(destination: string, command: string, data?: Record<string, unknown>): Promise<M>;
72
76
  request<R>(method: string, path: string, data?: any): Promise<R>;
package/rpc/index.js CHANGED
@@ -70,9 +70,15 @@ function getConf(configPath) {
70
70
  }
71
71
  exports.getConf = getConf;
72
72
  function loadCertFilesFromConfig(config) {
73
- const clientCertPath = (0, index_1.resolveFromChiaRoot)([config["/daemon_ssl/private_crt"]]);
74
- const clientKeyPath = (0, index_1.resolveFromChiaRoot)([config["/daemon_ssl/private_key"]]);
75
- const caCertPath = (0, index_1.resolveFromChiaRoot)([config["/private_ssl_ca/crt"]]);
73
+ const clientCertPath = (0, index_1.resolveFromChiaRoot)([
74
+ config["/daemon_ssl/private_crt"],
75
+ ]);
76
+ const clientKeyPath = (0, index_1.resolveFromChiaRoot)([
77
+ config["/daemon_ssl/private_key"],
78
+ ]);
79
+ const caCertPath = (0, index_1.resolveFromChiaRoot)([
80
+ config["/private_ssl_ca/crt"],
81
+ ]);
76
82
  (0, logger_1.getLogger)().debug(`Loading client cert file from ${clientCertPath}`);
77
83
  (0, logger_1.getLogger)().debug(`Loading client key file from ${clientKeyPath}`);
78
84
  (0, logger_1.getLogger)().debug(`Loading ca cert file from ${caCertPath}`);
@@ -93,15 +99,31 @@ const userAgent = "chia-agent/1.0.0";
93
99
  class RPCAgent {
94
100
  constructor(props) {
95
101
  this._skip_hostname_verification = false;
102
+ this._host = "";
103
+ this._port = 0;
96
104
  if ("httpsAgent" in props) {
97
105
  this._protocol = "https";
98
106
  this._agent = props.httpsAgent;
99
107
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
108
+ // Extract host/port from httpsAgent options
109
+ // Note: TypeScript doesn't expose options property, but it exists at runtime
110
+ const agent = this._agent;
111
+ if (agent.options && agent.options.host && agent.options.port) {
112
+ this._host = agent.options.host;
113
+ this._port = agent.options.port;
114
+ (0, logger_1.getLogger)().debug(`Constructing RPCAgent with httpsAgent: ${this._host}:${this._port}`);
115
+ }
116
+ else {
117
+ (0, logger_1.getLogger)().debug("Constructing RPCAgent with httpsAgent (host/port not available in agent options)");
118
+ }
100
119
  }
101
120
  else if ("httpAgent" in props) {
102
121
  this._protocol = "http";
103
122
  this._agent = props.httpAgent;
123
+ this._host = props.host;
124
+ this._port = props.port;
104
125
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
126
+ (0, logger_1.getLogger)().debug(`Constructing RPCAgent with httpAgent: ${this._host}:${this._port}`);
105
127
  }
106
128
  else if ("protocol" in props) {
107
129
  this._protocol = props.protocol;
@@ -110,12 +132,17 @@ class RPCAgent {
110
132
  let clientKey;
111
133
  let caCert;
112
134
  const keepAlive = props.keepAlive !== false;
113
- const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ?
114
- props.keepAliveMsecs : 1000;
115
- const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ?
116
- props.maxSockets : Infinity;
117
- const timeout = typeof props.timeout === "number" && props.timeout > 0 ?
118
- props.timeout : undefined;
135
+ const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
136
+ ? props.keepAliveMsecs
137
+ : 1000;
138
+ const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
139
+ ? props.maxSockets
140
+ : Infinity;
141
+ const timeout = typeof props.timeout === "number" && props.timeout > 0
142
+ ? props.timeout
143
+ : undefined;
144
+ this._host = host;
145
+ this._port = port;
119
146
  if (props.protocol === "https") {
120
147
  if ("configPath" in props) {
121
148
  const config = getConf(props.configPath);
@@ -126,7 +153,11 @@ class RPCAgent {
126
153
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
127
154
  }
128
155
  else {
129
- ({ client_cert: clientCert, client_key: clientKey, ca_cert: caCert } = props);
156
+ ({
157
+ client_cert: clientCert,
158
+ client_key: clientKey,
159
+ ca_cert: caCert,
160
+ } = props);
130
161
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
131
162
  }
132
163
  this._agent = new https_1.Agent({
@@ -141,6 +172,7 @@ class RPCAgent {
141
172
  maxSockets,
142
173
  timeout,
143
174
  });
175
+ (0, logger_1.getLogger)().debug(`Constructed RPCAgent with httpsAgent: ${host}:${port}`);
144
176
  }
145
177
  else {
146
178
  this._agent = new http_1.Agent({
@@ -156,12 +188,15 @@ class RPCAgent {
156
188
  let host;
157
189
  let port;
158
190
  const keepAlive = props.keepAlive !== false;
159
- const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0 ?
160
- props.keepAliveMsecs : 1000;
161
- const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0 ?
162
- props.maxSockets : Infinity;
163
- const timeout = typeof props.timeout === "number" && props.timeout > 0 ?
164
- props.timeout : undefined;
191
+ const keepAliveMsecs = typeof props.keepAliveMsecs === "number" && props.keepAliveMsecs > 0
192
+ ? props.keepAliveMsecs
193
+ : 1000;
194
+ const maxSockets = typeof props.maxSockets === "number" && props.maxSockets > 0
195
+ ? props.maxSockets
196
+ : Infinity;
197
+ const timeout = typeof props.timeout === "number" && props.timeout > 0
198
+ ? props.timeout
199
+ : undefined;
165
200
  const config = getConf("configPath" in props ? props.configPath : undefined);
166
201
  if (props.host && typeof props.port === "number") {
167
202
  ({ host, port } = props);
@@ -175,6 +210,8 @@ class RPCAgent {
175
210
  const certs = loadCertFilesFromConfig(config);
176
211
  const { clientCert, clientKey, caCert } = certs;
177
212
  this._skip_hostname_verification = Boolean(props.skip_hostname_verification);
213
+ this._host = host;
214
+ this._port = port;
178
215
  this._agent = new https_1.Agent({
179
216
  host: host,
180
217
  port: port,
@@ -192,7 +229,7 @@ class RPCAgent {
192
229
  sendMessage(destination, command, data) {
193
230
  return __awaiter(this, void 0, void 0, function* () {
194
231
  // parameter `destination` is not used because target rpc server is determined by url.
195
- (0, logger_1.getLogger)().debug(`Sending message. dest=${destination} command=${command}`);
232
+ (0, logger_1.getLogger)().debug(`Sending RPC message. dest=${destination} command=${command}`);
196
233
  return this.request("POST", command, data);
197
234
  });
198
235
  }
@@ -206,9 +243,8 @@ class RPCAgent {
206
243
  Accept: "application/json, text/plain, */*",
207
244
  "User-Agent": userAgent,
208
245
  };
209
- if ("options" in this._agent && typeof this._agent.options.host === "string") {
210
- // Assuming `this._agent instanceof HttpsAgent` is true.
211
- headers.Host = this._agent.options.host;
246
+ if (this._host) {
247
+ headers.Host = this._host;
212
248
  }
213
249
  const options = {
214
250
  path: pathname,
@@ -216,6 +252,11 @@ class RPCAgent {
216
252
  agent: this._agent,
217
253
  headers,
218
254
  };
255
+ // For HTTP protocol, we need to explicitly set hostname and port
256
+ if (this._protocol === "http") {
257
+ options.hostname = this._host;
258
+ options.port = this._port;
259
+ }
219
260
  if (this._skip_hostname_verification) {
220
261
  options.checkServerIdentity = () => {
221
262
  return undefined;
@@ -243,7 +284,9 @@ class RPCAgent {
243
284
  }
244
285
  }
245
286
  const transporter = this._protocol === "https" ? https_1.request : http_1.request;
246
- (0, logger_1.getLogger)().debug(`Requesting to ${options.protocol}//${options.hostname}:${options.port}${options.path}`);
287
+ (0, logger_1.getLogger)().debug(`Dispatching RPC ${METHOD} request to ${this._protocol}//${this._host}:${this._port}${options.path}`);
288
+ (0, logger_1.getLogger)().trace(`Request options: ${JSON.stringify(options)}`);
289
+ (0, logger_1.getLogger)().trace(`Request body: ${body}`);
247
290
  const req = transporter(options, (res) => {
248
291
  if (!res.statusCode || res.statusCode < 200 || res.statusCode >= 300) {
249
292
  (0, logger_1.getLogger)().error(`Status not ok: ${res.statusCode}`);
@@ -256,11 +299,12 @@ class RPCAgent {
256
299
  return reject(new Error(`Status not ok: ${res.statusCode}`));
257
300
  }
258
301
  const chunks = [];
259
- res.on("data", chunk => {
302
+ res.on("data", (chunk) => {
260
303
  chunks.push(chunk);
261
304
  if (chunks.length === 0) {
262
305
  (0, logger_1.getLogger)().debug("The first response chunk data arrived");
263
306
  }
307
+ (0, logger_1.getLogger)().trace(`Response chunk #${chunks.length} - ${chunk.length} bytes: ${chunk.toString()}`);
264
308
  });
265
309
  res.on("end", () => {
266
310
  try {
@@ -280,6 +324,8 @@ class RPCAgent {
280
324
  (0, logger_1.getLogger)().info(`API failure: ${d.error}`);
281
325
  return reject(d);
282
326
  }
327
+ (0, logger_1.getLogger)().debug(`RPC response received from ${this._protocol}//${this._host}:${this._port}${options.path}`);
328
+ (0, logger_1.getLogger)().trace(`RPC response data: ${JSON.stringify(d)}`);
283
329
  return resolve(d);
284
330
  }
285
331
  // RPC Server should return response like
@@ -289,20 +335,23 @@ class RPCAgent {
289
335
  reject(new Error("Server responded without expected data"));
290
336
  }
291
337
  catch (e) {
292
- (0, logger_1.getLogger)().error("Failed to parse response data");
338
+ (0, logger_1.getLogger)().error(`Failed to parse response data: ${JSON.stringify(e)}`);
293
339
  try {
294
340
  (0, logger_1.getLogger)().error(Buffer.concat(chunks).toString());
295
341
  }
296
- catch (_) { /* Do nothing */ }
342
+ catch (_e2) {
343
+ /* Do nothing */
344
+ }
297
345
  reject(new Error("Server responded without expected data"));
298
346
  }
299
347
  });
300
348
  });
301
- req.on("error", error => {
349
+ req.on("error", (error) => {
302
350
  (0, logger_1.getLogger)().error(JSON.stringify(error));
303
351
  reject(error);
304
352
  });
305
- if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") && body) {
353
+ if ((METHOD === "POST" || METHOD === "PUT" || METHOD === "DELETE") &&
354
+ body) {
306
355
  req.write(body);
307
356
  }
308
357
  req.end();