diodejs 0.4.3 → 0.4.5

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/bindPort.js CHANGED
@@ -403,7 +403,7 @@ class BindPort {
403
403
  logger.info(() => `Port closed for ref: ${dataRef.toString('hex')}`);
404
404
  }
405
405
  } else {
406
- if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request') {
406
+ if (messageType != 'portopen' && messageType != 'portopen2' && messageType != 'ticket_request' && messageType != 'response') {
407
407
  logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
408
408
  }
409
409
  }
@@ -660,6 +660,46 @@ class BindPort {
660
660
  let rpc = null;
661
661
 
662
662
  let ref;
663
+ let clientClosed = false;
664
+ let remoteCleanupStarted = false;
665
+ let tlsSocketWrapper = null;
666
+
667
+ const closeRemoteRef = async () => {
668
+ if (remoteCleanupStarted) {
669
+ return;
670
+ }
671
+
672
+ if (!ref || !connection || !rpc) {
673
+ return;
674
+ }
675
+ remoteCleanupStarted = true;
676
+
677
+ const storedSocket = typeof connection.getClientSocket === 'function'
678
+ ? connection.getClientSocket(ref)
679
+ : null;
680
+ if (storedSocket && storedSocket !== clientSocket && typeof storedSocket.end === 'function') {
681
+ try { storedSocket.end(); } catch {}
682
+ }
683
+ if (typeof connection.deleteClientSocket === 'function') {
684
+ try { connection.deleteClientSocket(ref); } catch {}
685
+ }
686
+
687
+ try {
688
+ await rpc.portClose(ref);
689
+ logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
690
+ } catch (error) {
691
+ logger.error(() => `Error closing port on device: ${error}`);
692
+ }
693
+ };
694
+
695
+ clientSocket.once('close', () => {
696
+ clientClosed = true;
697
+ closeRemoteRef();
698
+ });
699
+ clientSocket.once('error', (err) => {
700
+ logger.error(() => `Client socket error: ${err}`);
701
+ });
702
+
663
703
  if (useNative) {
664
704
  try {
665
705
  connection = await this._resolveConnectionForDevice(deviceId);
@@ -783,6 +823,12 @@ class BindPort {
783
823
  return;
784
824
  }
785
825
 
826
+ if (clientClosed || clientSocket.destroyed) {
827
+ logger.warn(() => `Local client disconnected before port ${formattedTargetPort} opened; closing ref ${ref.toString('hex')}`);
828
+ await closeRemoteRef();
829
+ return;
830
+ }
831
+
786
832
  if (protocol === 'tls') {
787
833
  // For tls protocol, create a proper tls connection
788
834
  try {
@@ -829,16 +875,27 @@ class BindPort {
829
875
  diodeSocket,
830
876
  tlsSocket,
831
877
  end: () => {
832
- tlsSocket.end();
833
- diodeSocket._destroy(null, () => {});
878
+ try { tlsSocket.end(); } catch {}
879
+ try { diodeSocket._destroy(null, () => {}); } catch {}
834
880
  }
835
881
  };
882
+ tlsSocketWrapper = socketWrapper;
836
883
 
837
884
  // Store the socket wrapper
838
885
  connection.addClientSocket(ref, socketWrapper);
886
+ tlsSocket.once('close', () => {
887
+ if (!clientSocket.destroyed) {
888
+ clientSocket.end();
889
+ }
890
+ closeRemoteRef();
891
+ });
839
892
 
840
893
  } catch (error) {
841
894
  logger.error(() => `Error setting up tls connection: ${error}`);
895
+ if (tlsSocketWrapper && typeof tlsSocketWrapper.end === 'function') {
896
+ tlsSocketWrapper.end();
897
+ }
898
+ await closeRemoteRef();
842
899
  clientSocket.destroy();
843
900
  return;
844
901
  }
@@ -847,9 +904,19 @@ class BindPort {
847
904
  connection.addClientSocket(ref, clientSocket);
848
905
 
849
906
  // Handle data from client to device
850
- clientSocket.on('data', async (data) => {
907
+ clientSocket.on('data', (data) => {
908
+ clientSocket.pause();
851
909
  try {
852
- await rpc.portSend(ref, data);
910
+ rpc.portSend(ref, data)
911
+ .then(() => {
912
+ if (!clientSocket.destroyed) {
913
+ clientSocket.resume();
914
+ }
915
+ })
916
+ .catch((error) => {
917
+ logger.error(() => `Error sending data to device: ${error}`);
918
+ clientSocket.destroy();
919
+ });
853
920
  } catch (error) {
854
921
  logger.error(() => `Error sending data to device: ${error}`);
855
922
  clientSocket.destroy();
@@ -858,24 +925,9 @@ class BindPort {
858
925
  }
859
926
 
860
927
  // Handle client socket closure (common for all protocols)
861
- clientSocket.on('end', async () => {
928
+ clientSocket.once('end', () => {
862
929
  logger.info(() => 'Client disconnected');
863
- if (ref && connection.hasClientSocket(ref)) {
864
- try {
865
- await rpc.portClose(ref);
866
- logger.info(() => `Port closed on device for ref: ${ref.toString('hex')}`);
867
- connection.deleteClientSocket(ref);
868
- } catch (error) {
869
- logger.error(() => `Error closing port on device: ${error}`);
870
- }
871
- } else {
872
- logger.warn(() => 'Ref is invalid or no longer in clientSockets.');
873
- }
874
- });
875
-
876
- // Handle client socket errors
877
- clientSocket.on('error', (err) => {
878
- logger.error(() => `Client socket error: ${err}`);
930
+ closeRemoteRef();
879
931
  });
880
932
  });
881
933
 
package/connection.js CHANGED
@@ -91,9 +91,13 @@ class DiodeConnection extends EventEmitter {
91
91
  // Add ticket batching configuration
92
92
  this.lastTicketUpdate = Date.now();
93
93
  this.accumulatedBytes = 0;
94
- this.ticketUpdateThreshold = parseInt(process.env.DIODE_TICKET_BYTES_THRESHOLD, 10) || 512000; // 512KB default
94
+ this.ticketUpdateThreshold = parseInt(process.env.DIODE_TICKET_BYTES_THRESHOLD, 10) || 4 * 1024 * 1024; // 4MB default
95
95
  this.ticketUpdateInterval = parseInt(process.env.DIODE_TICKET_UPDATE_INTERVAL, 10) || 30000; // 30 seconds default
96
96
  this.ticketUpdateTimer = null;
97
+ this.ticketUpdateInFlight = false;
98
+ this.pendingTicketUpdateForce = false;
99
+ this.lastAcceptedTicketBytes = 0;
100
+ this.lastRelayMeasuredBytes = 0;
97
101
 
98
102
  // Log the ticket batching settings
99
103
  logger.info(() => `Ticket batching settings - Bytes Threshold: ${this.ticketUpdateThreshold} bytes, Update Interval: ${this.ticketUpdateInterval}ms`);
@@ -144,6 +148,7 @@ class DiodeConnection extends EventEmitter {
144
148
  this._startTicketUpdateTimer();
145
149
  // Send the ticketv2 command
146
150
  try {
151
+ await this._syncMeasuredBytesWithRelay();
147
152
  const ticketCommand = await this.createTicketCommand();
148
153
  const response = await this.sendCommand(ticketCommand).catch(reject);
149
154
  resolve();
@@ -155,6 +160,7 @@ class DiodeConnection extends EventEmitter {
155
160
 
156
161
  this.socket.on('data', (data) => {
157
162
  // logger.debug(() => `Received data: ${data.toString('hex')}`);
163
+ this._recordTrafficBytes(data.length);
158
164
  try {
159
165
  this._handleData(data);
160
166
  } catch (error) {
@@ -371,17 +377,35 @@ class DiodeConnection extends EventEmitter {
371
377
  logger.debug(() => `Received response for requestId: ${requestId}`);
372
378
  logger.debug(() => `Response Type: '${responseType}'`);
373
379
 
374
- const { resolve, reject } = this.pendingRequests.get(requestId);
380
+ const pending = this.pendingRequests.get(requestId);
381
+ const { resolve, reject } = pending;
375
382
  try{
376
383
  if (responseType === 'response') {
377
384
  if (!Array.isArray(responseRaw) && makeReadable(responseRaw) === 'too_low') {
378
- this.fixResponse(responseData);
379
- // Re-send the ticket command
380
- this.createTicketCommand().then((ticketCommand) => {
381
- this.sendCommand(ticketCommand).then(resolve).catch(reject);
382
- }).catch(reject);
385
+ const originalCommand = pending && pending.commandArray;
386
+ const retryCount = pending && pending.ticketRetryCount ? pending.ticketRetryCount : 0;
387
+ const isTicketCommand = Array.isArray(originalCommand) &&
388
+ (originalCommand[0] === 'ticket' || originalCommand[0] === 'ticketv2');
389
+
390
+ if (isTicketCommand && retryCount < 1) {
391
+ this.fixResponse(responseData);
392
+ this.pendingRequests.delete(requestId);
393
+ this._syncMeasuredBytesWithRelay()
394
+ .catch((error) => {
395
+ logger.debug(() => `Unable to sync relay measured bytes after too_low: ${error}`);
396
+ })
397
+ .then(() => this.createTicketCommand())
398
+ .then((ticketCommand) => this.sendCommand(ticketCommand, { ticketRetryCount: retryCount + 1 }))
399
+ .then(resolve)
400
+ .catch(reject);
401
+ return;
402
+ }
403
+ this._recordTicketResponse(originalCommand, responseData);
383
404
  resolve(responseData);
405
+ this.pendingRequests.delete(requestId);
406
+ return;
384
407
  }
408
+ this._recordTicketResponse(pending && pending.commandArray, responseData);
385
409
  resolve(responseData);
386
410
  } else if (responseType === 'error') {
387
411
  if (responseData.length > 1) {
@@ -433,11 +457,23 @@ class DiodeConnection extends EventEmitter {
433
457
  }
434
458
 
435
459
  // Send a fresh ticket promptly to avoid disconnect
436
- this.createTicketCommand()
437
- .then((ticketCommand) => this.sendCommand(ticketCommand))
438
- .then(() => {
439
- this.accumulatedBytes = 0;
440
- this.lastTicketUpdate = Date.now();
460
+ this._syncMeasuredBytesWithRelay()
461
+ .catch((error) => {
462
+ logger.debug(() => `Unable to sync relay measured bytes for ticket_request: ${error}`);
463
+ })
464
+ .then(() => this.createTicketCommand())
465
+ .then((ticketCommand) => {
466
+ const ticketTotalBytes = parseUInt(ticketCommand[5]);
467
+ return this.sendCommand(ticketCommand).then((responseData) => ({ responseData, ticketTotalBytes }));
468
+ })
469
+ .then(({ responseData, ticketTotalBytes }) => {
470
+ const status = responseData && responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
471
+ if (status === 'thanks!') {
472
+ if (Number.isFinite(ticketTotalBytes)) {
473
+ this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
474
+ }
475
+ this.lastTicketUpdate = Date.now();
476
+ }
441
477
  })
442
478
  .catch((error) => {
443
479
  logger.error(() => `Error handling ticket_request: ${error}`);
@@ -446,26 +482,141 @@ class DiodeConnection extends EventEmitter {
446
482
  }
447
483
 
448
484
  fixResponse(response) {
449
- /* response is :
450
- [
451
- 'too_low',
452
- 1284,
453
- 666,
454
- 11,
455
- 135591,
456
- 'test',
457
- '0x01eb1726dd7286d2dab222ea5dfef7c820cd01c30936240f5780a6e468e731f3b55d4c963b3eb768663263b396555aa52be49d7d3ae2a9173732fa410ad46434f3'
458
- ]
459
- [3] is last totalConnections
460
- [4] is last totalBytes
485
+ /*
486
+ ticketv2 too_low responses are:
487
+ [
488
+ 'too_low',
489
+ chain_id,
490
+ epoch,
491
+ last_total_connections,
492
+ last_total_bytes,
493
+ local_address,
494
+ device_signature
495
+ ]
496
+ The byte value is the relay's last accepted paid floor. The live
497
+ unpaid measurement still comes from the relay "bytes" command.
461
498
  */
462
- const totalConnectionsBuffer = Buffer.from(response[3]);
463
- const totalBytesBuffer = Buffer.from(response[4]);
464
- this.totalConnections = parseInt(totalConnectionsBuffer.readUIntBE(0, totalConnectionsBuffer.length), 10) +1;
465
- this.totalBytes = parseInt(totalBytesBuffer.readUIntBE(0, totalBytesBuffer.length), 10) + 128000;
499
+ const lastTicket = this._parseTooLowTicketSummary(response);
500
+ if (Number.isFinite(lastTicket.totalConnections)) {
501
+ this.totalConnections = Math.max(this.totalConnections, lastTicket.totalConnections);
502
+ }
503
+ if (Number.isFinite(lastTicket.totalBytes)) {
504
+ this.lastAcceptedTicketBytes = Math.max(this.lastAcceptedTicketBytes, lastTicket.totalBytes);
505
+ const relayMeasuredBytes = Number.isFinite(this.lastRelayMeasuredBytes) ? this.lastRelayMeasuredBytes : 0;
506
+ const pendingBytes = Math.max(this.accumulatedBytes, relayMeasuredBytes, 0);
507
+ this.totalBytes = Math.max(this.totalBytes, lastTicket.totalBytes + pendingBytes + 1024);
508
+ this.accumulatedBytes = Math.max(this.accumulatedBytes, this.totalBytes - lastTicket.totalBytes);
509
+ }
510
+ }
511
+
512
+ _parseTooLowTicketSummary(response) {
513
+ if (!Array.isArray(response)) {
514
+ return { totalConnections: null, totalBytes: null };
515
+ }
516
+
517
+ const firstItem = response[0];
518
+ const firstItemType = typeof firstItem === 'string' || Buffer.isBuffer(firstItem) || firstItem instanceof Uint8Array
519
+ ? parseResponseType(firstItem)
520
+ : '';
521
+ let normalized = response;
522
+ if (firstItemType === 'response') {
523
+ const secondItem = response[1];
524
+ const secondItemType = typeof secondItem === 'string' || Buffer.isBuffer(secondItem) || secondItem instanceof Uint8Array
525
+ ? parseResponseType(secondItem)
526
+ : '';
527
+ normalized = secondItemType === 'too_low' ? response.slice(2) : response;
528
+ } else if (firstItemType === 'too_low') {
529
+ normalized = response.slice(1);
530
+ }
531
+
532
+ if (normalized.length >= 6) {
533
+ return {
534
+ version: 2,
535
+ chainId: parseUInt(normalized[0]),
536
+ epoch: parseUInt(normalized[1]),
537
+ totalConnections: parseUInt(normalized[2]),
538
+ totalBytes: parseUInt(normalized[3]),
539
+ localAddress: normalized[4],
540
+ deviceSignature: normalized[5],
541
+ };
542
+ }
543
+
544
+ if (normalized.length >= 5) {
545
+ return {
546
+ version: 1,
547
+ blockHash: normalized[0],
548
+ totalConnections: parseUInt(normalized[1]),
549
+ totalBytes: parseUInt(normalized[2]),
550
+ localAddress: normalized[3],
551
+ deviceSignature: normalized[4],
552
+ };
553
+ }
554
+
555
+ return { totalConnections: null, totalBytes: null };
556
+ }
557
+
558
+ _isTicketCommand(commandArray) {
559
+ return Array.isArray(commandArray) &&
560
+ (commandArray[0] === 'ticket' || commandArray[0] === 'ticketv2');
561
+ }
562
+
563
+ _ticketTotalBytes(commandArray) {
564
+ if (!this._isTicketCommand(commandArray)) return null;
565
+ const index = commandArray[0] === 'ticketv2' ? 5 : 4;
566
+ return parseUInt(commandArray[index]);
567
+ }
568
+
569
+ _recordTicketResponse(commandArray, responseData) {
570
+ if (!this._isTicketCommand(commandArray) || !Array.isArray(responseData)) return;
571
+ const status = responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
572
+ if (status !== 'thanks!') return;
573
+ const ticketTotalBytes = this._ticketTotalBytes(commandArray);
574
+ if (!Number.isFinite(ticketTotalBytes)) return;
575
+ this.lastAcceptedTicketBytes = Math.max(this.lastAcceptedTicketBytes, ticketTotalBytes);
576
+ this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
577
+ this.lastTicketUpdate = Date.now();
578
+ }
579
+
580
+ _recordTrafficBytes(bytesCount) {
581
+ if (!Number.isFinite(bytesCount) || bytesCount <= 0) return;
582
+ this.totalBytes += bytesCount;
583
+ this.accumulatedBytes += bytesCount;
584
+
585
+ if (this.accumulatedBytes >= this.ticketUpdateThreshold) {
586
+ this._updateTicketIfNeeded();
587
+ }
588
+ }
589
+
590
+ _parseRelaySignedInt(valueRaw) {
591
+ const encoded = parseUInt(valueRaw);
592
+ if (!Number.isFinite(encoded)) return null;
593
+ if (encoded % 2 === 0) return encoded / 2;
594
+ return -((encoded - 1) / 2);
595
+ }
596
+
597
+ async _syncMeasuredBytesWithRelay() {
598
+ if (!this.socket || this.socket.destroyed) return null;
599
+ const responseData = await this.sendCommand(['bytes']);
600
+ const measuredBytes = responseData && responseData[0] !== undefined
601
+ ? this._parseRelaySignedInt(responseData[0])
602
+ : null;
603
+ if (!Number.isFinite(measuredBytes) || measuredBytes <= 0) {
604
+ return measuredBytes;
605
+ }
606
+
607
+ this.lastRelayMeasuredBytes = measuredBytes;
608
+ const baseBytes = Number.isFinite(this.lastAcceptedTicketBytes) && this.lastAcceptedTicketBytes > 0
609
+ ? this.lastAcceptedTicketBytes
610
+ : 128000;
611
+ const targetTotalBytes = baseBytes + measuredBytes + 1024;
612
+ if (targetTotalBytes > this.totalBytes) {
613
+ this.totalBytes = targetTotalBytes;
614
+ this.accumulatedBytes = Math.max(this.accumulatedBytes, this.totalBytes - baseBytes);
615
+ }
616
+ return measuredBytes;
466
617
  }
467
618
 
468
- sendCommand(commandArray) {
619
+ sendCommand(commandArray, options = {}) {
469
620
  return new Promise((resolve, reject) => {
470
621
  this._ensureConnected().then(() => {
471
622
  const requestId = this._getNextRequestId();
@@ -473,7 +624,12 @@ class DiodeConnection extends EventEmitter {
473
624
  const commandWithId = [requestId, commandArray];
474
625
 
475
626
  // Store the promise callbacks to resolve/reject later
476
- this.pendingRequests.set(requestId, { resolve, reject });
627
+ this.pendingRequests.set(requestId, {
628
+ resolve,
629
+ reject,
630
+ commandArray,
631
+ ticketRetryCount: options.ticketRetryCount || 0,
632
+ });
477
633
 
478
634
  const commandBuffer = RLP.encode(commandWithId);
479
635
  const byteLength = commandBuffer.length; // Buffer/Uint8Array length is bytes
@@ -488,6 +644,7 @@ class DiodeConnection extends EventEmitter {
488
644
  // logger.debug(() => `Command buffer: ${message.toString('hex')}`);
489
645
 
490
646
  this.socket.write(message);
647
+ this._recordTrafficBytes(message.length);
491
648
  }).catch(reject);
492
649
  });
493
650
  }
@@ -499,9 +656,6 @@ class DiodeConnection extends EventEmitter {
499
656
  // Build the message as [requestId, [commandArray]]
500
657
  const commandWithId = [requestId, commandArray];
501
658
 
502
- // Store the promise callbacks to resolve/reject later
503
- this.pendingRequests.set(requestId, { resolve, reject });
504
-
505
659
  const commandBuffer = RLP.encode(commandWithId);
506
660
  const byteLength = commandBuffer.length; // Buffer/Uint8Array length is bytes
507
661
 
@@ -514,8 +668,8 @@ class DiodeConnection extends EventEmitter {
514
668
  logger.debug(() => `Sending command with requestId ${requestId}: ${commandArray}`);
515
669
  // logger.debug(() => `Command buffer: ${message.toString('hex')}`);
516
670
 
517
- this.socket.write(message);
518
- resolve();
671
+ this.socket.write(message, resolve);
672
+ this._recordTrafficBytes(message.length);
519
673
  }).catch(reject);
520
674
  });
521
675
  }
@@ -765,7 +919,7 @@ class DiodeConnection extends EventEmitter {
765
919
  }
766
920
 
767
921
  this.ticketUpdateTimer = setTimeout(() => {
768
- this._updateTicketIfNeeded(true);
922
+ this._updateTicketIfNeeded();
769
923
  }, this.ticketUpdateInterval);
770
924
  }
771
925
 
@@ -775,6 +929,11 @@ class DiodeConnection extends EventEmitter {
775
929
  if (!this.socket || this.socket.destroyed) {
776
930
  return;
777
931
  }
932
+
933
+ if (this.ticketUpdateInFlight) {
934
+ this.pendingTicketUpdateForce = this.pendingTicketUpdateForce || force;
935
+ return;
936
+ }
778
937
 
779
938
  const timeSinceLastUpdate = Date.now() - this.lastTicketUpdate;
780
939
 
@@ -783,18 +942,38 @@ class DiodeConnection extends EventEmitter {
783
942
  (this.accumulatedBytes >= this.ticketUpdateThreshold ||
784
943
  timeSinceLastUpdate >= this.ticketUpdateInterval))) {
785
944
 
945
+ this.ticketUpdateInFlight = true;
946
+ let accepted = false;
786
947
  try {
787
- if (this.accumulatedBytes > 0 || force) {
788
- logger.debug(() => `Updating ticket: accumulated ${this.accumulatedBytes} bytes, ${timeSinceLastUpdate}ms since last update`);
789
- const ticketCommand = await this.createTicketCommand();
790
- await this.sendCommand(ticketCommand);
791
-
792
- // Reset counters
793
- this.accumulatedBytes = 0;
794
- this.lastTicketUpdate = Date.now();
795
- }
948
+ if (this.accumulatedBytes > 0 || force) {
949
+ logger.debug(() => `Updating ticket: accumulated ${this.accumulatedBytes} bytes, ${timeSinceLastUpdate}ms since last update`);
950
+ await this._syncMeasuredBytesWithRelay().catch((error) => {
951
+ logger.debug(() => `Unable to sync relay measured bytes before ticket update: ${error}`);
952
+ });
953
+ const ticketCommand = await this.createTicketCommand();
954
+ const ticketTotalBytes = parseUInt(ticketCommand[5]);
955
+ const responseData = await this.sendCommand(ticketCommand);
956
+ const status = responseData && responseData[0] !== undefined ? parseResponseType(responseData[0]) : '';
957
+ if (status === 'thanks!') {
958
+ accepted = true;
959
+ if (Number.isFinite(ticketTotalBytes)) {
960
+ this.accumulatedBytes = Math.max(0, this.totalBytes - ticketTotalBytes);
961
+ }
962
+ this.lastTicketUpdate = Date.now();
963
+ }
964
+ }
796
965
  } catch (error) {
797
- logger.error(() => `Error updating ticket: ${error}`);
966
+ logger.error(() => `Error updating ticket: ${error}`);
967
+ } finally {
968
+ this.ticketUpdateInFlight = false;
969
+ }
970
+
971
+ if (accepted && (this.pendingTicketUpdateForce || this.accumulatedBytes >= this.ticketUpdateThreshold)) {
972
+ const pendingForce = this.pendingTicketUpdateForce;
973
+ this.pendingTicketUpdateForce = false;
974
+ setImmediate(() => this._updateTicketIfNeeded(pendingForce));
975
+ } else {
976
+ this.pendingTicketUpdateForce = false;
798
977
  }
799
978
  }
800
979
 
@@ -804,13 +983,7 @@ class DiodeConnection extends EventEmitter {
804
983
 
805
984
  // Add method to track bytes without immediate ticket update
806
985
  addBytes(bytesCount) {
807
- this.totalBytes += bytesCount;
808
- this.accumulatedBytes += bytesCount;
809
-
810
- // Optionally check if we should update ticket now
811
- if (this.accumulatedBytes >= this.ticketUpdateThreshold) {
812
- this._updateTicketIfNeeded();
813
- }
986
+ this._recordTrafficBytes(bytesCount);
814
987
  }
815
988
 
816
989
  // Method to set ticket batching options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "diodejs",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "A JavaScript client for interacting with the Diode network. It provides functionalities to bind and publish ports, send RPC commands, and handle responses.",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/publishPort.js CHANGED
@@ -231,7 +231,7 @@ class PublishPort extends EventEmitter {
231
231
  } else if (messageType === 'portclose') {
232
232
  this.handlePortClose(sessionIdRaw, messageContent, connection);
233
233
  } else {
234
- if (messageType !== 'ticket_request') {
234
+ if (messageType !== 'ticket_request' && messageType !== 'response') {
235
235
  logger.warn(() => `Unknown unsolicited message type: ${messageType}`);
236
236
  }
237
237
  }
@@ -736,7 +736,17 @@ class PublishPort extends EventEmitter {
736
736
  } else {
737
737
  localSocket.on('data', (data) => {
738
738
  // When data is received from the local service, send it back via Diode
739
- rpc.portSend(ref, data);
739
+ localSocket.pause();
740
+ rpc.portSend(ref, data)
741
+ .then(() => {
742
+ if (!localSocket.destroyed) {
743
+ localSocket.resume();
744
+ }
745
+ })
746
+ .catch((error) => {
747
+ logger.error(() => `Error sending data to device: ${error}`);
748
+ localSocket.destroy();
749
+ });
740
750
  });
741
751
 
742
752
  localSocket.on('end', () => {
package/rpc.js CHANGED
@@ -1,5 +1,12 @@
1
1
  //rpc.js
2
- const { makeReadable, parseRequestId, parseResponseType, parseReason, parseUInt, toBufferView } = require('./utils');
2
+ const {
3
+ makeReadable,
4
+ parseRequestId,
5
+ parseResponseType,
6
+ parseReason,
7
+ parseUInt,
8
+ toBufferView,
9
+ } = require('./utils');
3
10
  const logger = require('./logger');
4
11
 
5
12
  function decodeToken(value) {
@@ -89,6 +96,80 @@ class DiodeRPC {
89
96
  expiry: null,
90
97
  };
91
98
  }
99
+
100
+ async dioTraffic(options = {}) {
101
+ const chainId = options.chainId === undefined ? 1284 : options.chainId;
102
+ const params = options.epoch === undefined ? [chainId] : [chainId, options.epoch];
103
+
104
+ return this.nodeRpc('dio_traffic', params, options);
105
+ }
106
+
107
+ dio_traffic(options = {}) {
108
+ return this.dioTraffic(options);
109
+ }
110
+
111
+ async nodeRpc(method, params = [], options = {}) {
112
+ if (typeof fetch !== 'function') {
113
+ throw new Error('global fetch is required for node RPC calls');
114
+ }
115
+
116
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0
117
+ ? Math.floor(options.timeoutMs)
118
+ : 30000;
119
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
120
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
121
+
122
+ try {
123
+ const response = await fetch(this._nodeRpcEndpoint(options), {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ body: JSON.stringify({
127
+ jsonrpc: '2.0',
128
+ id: options.id || 1,
129
+ method,
130
+ params,
131
+ }),
132
+ signal: controller ? controller.signal : undefined,
133
+ });
134
+
135
+ let payload = {};
136
+ try {
137
+ payload = await response.json();
138
+ } catch (_) {
139
+ payload = {};
140
+ }
141
+
142
+ if (!response.ok || payload.error) {
143
+ const error = new Error(payload?.error?.message || `${method} failed with status ${response.status}`);
144
+ error.status = response.status;
145
+ error.response = payload;
146
+ throw error;
147
+ }
148
+
149
+ return payload.result;
150
+ } finally {
151
+ if (timer) {
152
+ clearTimeout(timer);
153
+ }
154
+ }
155
+ }
156
+
157
+ _nodeRpcEndpoint(options = {}) {
158
+ const protocol = options.protocol || 'https';
159
+ const rpcPort = Number.isFinite(options.rpcPort) && options.rpcPort > 0 ? options.rpcPort : 8443;
160
+ let host = options.host || '';
161
+ if (!host && this.connection) {
162
+ host = this.connection.host;
163
+ }
164
+ if (!host && this.connection && typeof this.connection.getServerRelayHost === 'function') {
165
+ host = this.connection.getServerRelayHost();
166
+ }
167
+ if (!host) {
168
+ throw new Error('No relay host available for node RPC call');
169
+ }
170
+ const formattedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
171
+ return `${protocol}://${formattedHost}:${rpcPort}/`;
172
+ }
92
173
 
93
174
  getBlockPeak() {
94
175
  return this.connection.sendCommand(['getblockpeak']).then((responseData) => {
@@ -223,10 +304,6 @@ class DiodeRPC {
223
304
  }
224
305
 
225
306
  async portSend(ref, data) {
226
- // Update bytes count but don't update ticket yet
227
- const bytesToSend = data.length;
228
- this.connection.addBytes(bytesToSend);
229
-
230
307
  // Maximum size that can be sent in a single message (less than 65535 to be safe)
231
308
  const MAX_CHUNK_SIZE = 65000;
232
309
 
@@ -1,6 +1,8 @@
1
1
  const test = require('node:test');
2
2
  const assert = require('node:assert/strict');
3
3
  const EventEmitter = require('events');
4
+ const net = require('net');
5
+ const { once } = require('events');
4
6
 
5
7
  const BindPort = require('../bindPort');
6
8
 
@@ -9,21 +11,61 @@ function makeRef(hex) {
9
11
  }
10
12
 
11
13
  function makeRelay(hostKey, result, calls) {
14
+ const clientSockets = new Map();
15
+ const portCloseCalls = [];
12
16
  return {
13
17
  _managerHostKey: hostKey,
14
18
  socket: { destroyed: false },
19
+ clientSockets,
20
+ portCloseCalls,
15
21
  RPC: {
16
22
  portOpen: async (_deviceId, port, flags) => {
17
23
  calls.push({ hostKey, port, flags });
18
24
  if (result instanceof Error) {
19
25
  throw result;
20
26
  }
21
- return result;
27
+ return typeof result === 'function' ? result() : result;
22
28
  },
29
+ portClose: async (ref) => {
30
+ portCloseCalls.push(ref);
31
+ },
32
+ },
33
+ addClientSocket(ref, socket) {
34
+ clientSockets.set(ref.toString('hex'), socket);
35
+ },
36
+ getClientSocket(ref) {
37
+ return clientSockets.get(ref.toString('hex'));
38
+ },
39
+ deleteClientSocket(ref) {
40
+ return clientSockets.delete(ref.toString('hex'));
41
+ },
42
+ hasClientSocket(ref) {
43
+ return clientSockets.has(ref.toString('hex'));
23
44
  },
24
45
  };
25
46
  }
26
47
 
48
+ function deferred() {
49
+ let resolve;
50
+ let reject;
51
+ const promise = new Promise((res, rej) => {
52
+ resolve = res;
53
+ reject = rej;
54
+ });
55
+ return { promise, resolve, reject };
56
+ }
57
+
58
+ async function waitFor(predicate, timeoutMs = 1000) {
59
+ const startedAt = Date.now();
60
+ while (Date.now() - startedAt < timeoutMs) {
61
+ if (predicate()) {
62
+ return;
63
+ }
64
+ await new Promise((resolve) => setTimeout(resolve, 10));
65
+ }
66
+ assert.equal(predicate(), true);
67
+ }
68
+
27
69
  class FakeManager extends EventEmitter {
28
70
  constructor({ relays, resolvedRelay, nearestRelay, resolveError = null }) {
29
71
  super();
@@ -123,3 +165,125 @@ test('multiple BindPort instances share one manager listener set', () => {
123
165
  assert.equal(manager.listenerCount('end'), 1);
124
166
  assert.equal(manager.listenerCount('error'), 1);
125
167
  });
168
+
169
+ test('API bind closes ref when local TCP client disconnects before portopen returns', async () => {
170
+ const calls = [];
171
+ const opened = deferred();
172
+ const ref = makeRef('0a0b0c0d');
173
+ const relay = makeRelay('relay.example:41046', () => opened.promise, calls);
174
+ const manager = new FakeManager({
175
+ relays: [relay],
176
+ resolvedRelay: relay,
177
+ nearestRelay: relay,
178
+ });
179
+
180
+ const bind = new BindPort(manager, {
181
+ 0: {
182
+ targetPort: 8088,
183
+ deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
184
+ protocol: 'tcp',
185
+ }
186
+ });
187
+ bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
188
+ const server = bind.servers.get(0);
189
+
190
+ try {
191
+ await once(server, 'listening');
192
+ const client = net.connect(server.address().port, '127.0.0.1');
193
+ await once(client, 'connect');
194
+ client.destroy();
195
+ await once(client, 'close');
196
+
197
+ opened.resolve(ref);
198
+ await waitFor(() => relay.portCloseCalls.length === 1);
199
+
200
+ assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
201
+ assert.equal(relay.hasClientSocket(ref), false);
202
+ } finally {
203
+ server.close();
204
+ }
205
+ });
206
+
207
+ test('API bind closes ref when local TCP client disconnects after portopen returns', async () => {
208
+ const calls = [];
209
+ const ref = makeRef('0e0f1011');
210
+ const relay = makeRelay('relay.example:41046', ref, calls);
211
+ const manager = new FakeManager({
212
+ relays: [relay],
213
+ resolvedRelay: relay,
214
+ nearestRelay: relay,
215
+ });
216
+
217
+ const bind = new BindPort(manager, {
218
+ 0: {
219
+ targetPort: 8088,
220
+ deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
221
+ protocol: 'tcp',
222
+ }
223
+ });
224
+ bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
225
+ const server = bind.servers.get(0);
226
+
227
+ try {
228
+ await once(server, 'listening');
229
+ const client = net.connect(server.address().port, '127.0.0.1');
230
+ await once(client, 'connect');
231
+ await waitFor(() => relay.hasClientSocket(ref));
232
+
233
+ client.end();
234
+ await once(client, 'close');
235
+ await waitFor(() => relay.portCloseCalls.length === 1);
236
+
237
+ assert.equal(relay.portCloseCalls[0].toString('hex'), ref.toString('hex'));
238
+ assert.equal(relay.hasClientSocket(ref), false);
239
+ } finally {
240
+ server.close();
241
+ }
242
+ });
243
+
244
+ test('API bind pauses local TCP socket while portSend is in flight', async () => {
245
+ const calls = [];
246
+ const send = deferred();
247
+ const ref = makeRef('12131415');
248
+ const relay = makeRelay('relay.example:41046', ref, calls);
249
+ const portSendCalls = [];
250
+ relay.RPC.portSend = async (...args) => {
251
+ portSendCalls.push(args);
252
+ return send.promise;
253
+ };
254
+ const manager = new FakeManager({
255
+ relays: [relay],
256
+ resolvedRelay: relay,
257
+ nearestRelay: relay,
258
+ });
259
+
260
+ const bind = new BindPort(manager, {
261
+ 0: {
262
+ targetPort: 8088,
263
+ deviceIdHex: '8a72468957504d50247a260deb0218d504dd091b',
264
+ protocol: 'tcp',
265
+ }
266
+ });
267
+ bind.addPort(0, 8088, '8a72468957504d50247a260deb0218d504dd091b', 'tcp');
268
+ const server = bind.servers.get(0);
269
+
270
+ try {
271
+ await once(server, 'listening');
272
+ const client = net.connect(server.address().port, '127.0.0.1');
273
+ await once(client, 'connect');
274
+ await waitFor(() => relay.hasClientSocket(ref));
275
+
276
+ client.write(Buffer.from('hello'));
277
+ await waitFor(() => portSendCalls.length === 1);
278
+
279
+ const acceptedSocket = relay.getClientSocket(ref);
280
+ assert.equal(acceptedSocket.isPaused(), true);
281
+
282
+ send.resolve();
283
+ await waitFor(() => acceptedSocket.isPaused() === false);
284
+
285
+ client.destroy();
286
+ } finally {
287
+ server.close();
288
+ }
289
+ });
@@ -0,0 +1,162 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { RLP } = require('@ethereumjs/rlp');
7
+
8
+ const DiodeConnection = require('../connection');
9
+
10
+ function makeConnection() {
11
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'diode-ticket-retry-test-'));
12
+ return new DiodeConnection('relay.example', 41046, path.join(tempDir, 'keys.json'));
13
+ }
14
+
15
+ function encodeResponse(requestId, response) {
16
+ const payload = RLP.encode([requestId, response]);
17
+ const length = Buffer.alloc(2);
18
+ length.writeUInt16BE(payload.length, 0);
19
+ return Buffer.concat([length, Buffer.from(payload)]);
20
+ }
21
+
22
+ function tooLowResponse(overrides = {}) {
23
+ const totalConnections = overrides.totalConnections === undefined ? 11 : overrides.totalConnections;
24
+ const totalBytes = overrides.totalBytes === undefined ? 135591 : overrides.totalBytes;
25
+
26
+ return [
27
+ 'response',
28
+ 'too_low',
29
+ 1284,
30
+ 687,
31
+ totalConnections,
32
+ totalBytes,
33
+ Buffer.from([0]),
34
+ Buffer.from([1]),
35
+ ];
36
+ }
37
+
38
+ test('ticket too_low response retries once with repaired ticket', async () => {
39
+ const connection = makeConnection();
40
+ let retryCount = 0;
41
+
42
+ connection.fixResponse = () => {};
43
+ connection.createTicketCommand = async () => ['ticketv2'];
44
+ connection.sendCommand = async (command, options) => {
45
+ retryCount += 1;
46
+ assert.deepEqual(command, ['ticketv2']);
47
+ assert.equal(options.ticketRetryCount, 1);
48
+ return ['thanks!', 1];
49
+ };
50
+
51
+ const resultPromise = new Promise((resolve, reject) => {
52
+ connection.pendingRequests.set(1, {
53
+ resolve,
54
+ reject,
55
+ commandArray: ['ticketv2'],
56
+ ticketRetryCount: 0,
57
+ });
58
+ });
59
+
60
+ connection._handleData(encodeResponse(1, tooLowResponse()));
61
+ const result = await resultPromise;
62
+
63
+ assert.deepEqual(result, ['thanks!', 1]);
64
+ assert.equal(retryCount, 1);
65
+ });
66
+
67
+ test('ticket too_low response does not retry recursively', async () => {
68
+ const connection = makeConnection();
69
+ let retryCount = 0;
70
+
71
+ connection.fixResponse = () => {};
72
+ connection.createTicketCommand = async () => {
73
+ retryCount += 1;
74
+ return ['ticketv2'];
75
+ };
76
+
77
+ const resultPromise = new Promise((resolve, reject) => {
78
+ connection.pendingRequests.set(1, {
79
+ resolve,
80
+ reject,
81
+ commandArray: ['ticketv2'],
82
+ ticketRetryCount: 1,
83
+ });
84
+ });
85
+
86
+ connection._handleData(encodeResponse(1, tooLowResponse()));
87
+ const result = await resultPromise;
88
+
89
+ assert.equal(retryCount, 0);
90
+ assert.equal(Buffer.from(result[0]).toString('utf8'), 'too_low');
91
+ });
92
+
93
+ test('session responses do not swallow later unsolicited messages on the same session', async () => {
94
+ const connection = makeConnection();
95
+ const writes = [];
96
+
97
+ connection._ensureConnected = async () => {};
98
+ connection.socket = {
99
+ write(message, callback) {
100
+ writes.push(message);
101
+ callback();
102
+ },
103
+ };
104
+
105
+ await connection.sendCommandWithSessionId(['response', Buffer.from([1]), 'ok'], 7);
106
+
107
+ assert.equal(connection.pendingRequests.has(7), false);
108
+ assert.equal(writes.length, 1);
109
+
110
+ const unsolicited = new Promise((resolve) => {
111
+ connection.once('unsolicited', resolve);
112
+ });
113
+ connection._handleData(encodeResponse(7, ['portsend', Buffer.from([1]), Buffer.from('hello')]));
114
+
115
+ const message = await unsolicited;
116
+ assert.equal(Buffer.from(message[0]).readUIntBE(0, Buffer.from(message[0]).length), 7);
117
+ assert.equal(Buffer.from(message[1][0]).toString('utf8'), 'portsend');
118
+ });
119
+
120
+ test('too_low repair does not reduce local byte counters', () => {
121
+ const connection = makeConnection();
122
+ connection.totalConnections = 40;
123
+ connection.totalBytes = 1304576;
124
+
125
+ connection.fixResponse(tooLowResponse());
126
+
127
+ assert.equal(connection.totalConnections, 40);
128
+ assert.equal(connection.totalBytes, 1304576);
129
+ });
130
+
131
+ test('too_low repair uses node-reported paid byte floor', () => {
132
+ const connection = makeConnection();
133
+ connection.totalConnections = 4;
134
+ connection.totalBytes = 128000;
135
+ connection.accumulatedBytes = 2048;
136
+ connection.lastRelayMeasuredBytes = 4096;
137
+
138
+ connection.fixResponse(tooLowResponse({
139
+ totalConnections: 7,
140
+ totalBytes: 135591,
141
+ }));
142
+
143
+ assert.equal(connection.totalConnections, 7);
144
+ assert.equal(connection.lastAcceptedTicketBytes, 135591);
145
+ assert.equal(connection.totalBytes, 140711);
146
+ assert.equal(connection.accumulatedBytes, 5120);
147
+ });
148
+
149
+ test('ticket update keeps accumulated bytes when ticket is rejected', async () => {
150
+ const connection = makeConnection();
151
+ connection.socket = { destroyed: false };
152
+ connection.accumulatedBytes = 1000;
153
+ connection.lastTicketUpdate = 123;
154
+ connection.createTicketCommand = async () => ['ticketv2'];
155
+ connection.sendCommand = async () => ['too_low'];
156
+ connection._startTicketUpdateTimer = () => {};
157
+
158
+ await connection._updateTicketIfNeeded(true);
159
+
160
+ assert.equal(connection.accumulatedBytes, 1000);
161
+ assert.equal(connection.lastTicketUpdate, 123);
162
+ });
@@ -15,10 +15,17 @@ class FakeStreamSocket extends EventEmitter {
15
15
  this.remoteAddress = undefined;
16
16
  this.remotePort = undefined;
17
17
  this.writes = [];
18
+ this.pauseCalls = 0;
19
+ this.resumeCalls = 0;
18
20
  }
19
21
 
20
22
  setNoDelay() {}
21
- pause() {}
23
+ pause() {
24
+ this.pauseCalls += 1;
25
+ }
26
+ resume() {
27
+ this.resumeCalls += 1;
28
+ }
22
29
  write(data) {
23
30
  this.writes.push(data);
24
31
  }
@@ -145,6 +152,27 @@ function makeDeviceId(hexByte) {
145
152
  return Buffer.from(byte.repeat(20), 'hex');
146
153
  }
147
154
 
155
+ function deferred() {
156
+ let resolve;
157
+ let reject;
158
+ const promise = new Promise((res, rej) => {
159
+ resolve = res;
160
+ reject = rej;
161
+ });
162
+ return { promise, resolve, reject };
163
+ }
164
+
165
+ async function waitFor(predicate, timeoutMs = 1000) {
166
+ const startedAt = Date.now();
167
+ while (Date.now() - startedAt < timeoutMs) {
168
+ if (predicate()) {
169
+ return;
170
+ }
171
+ await new Promise((resolve) => setTimeout(resolve, 10));
172
+ }
173
+ assert.equal(predicate(), true);
174
+ }
175
+
148
176
  test('PublishPort array input defaults host to 127.0.0.1', () => {
149
177
  const connection = new FakeConnection();
150
178
  const publishPort = new PublishPort(connection, [8080]);
@@ -211,6 +239,29 @@ test('TCP publish connects to configured host', () => {
211
239
  assert.deepEqual(connectCalls[0], { port: 8080, host: '192.168.1.10' });
212
240
  });
213
241
 
242
+ test('TCP publish pauses local service socket while portSend is in flight', async () => {
243
+ const connection = new FakeConnection();
244
+ const publishPort = new PublishPort(connection, [8080]);
245
+ const localSocket = new FakeStreamSocket();
246
+ const send = deferred();
247
+ connection.RPC.portSend = async (...args) => {
248
+ connection.portSendCalls.push(args);
249
+ return send.promise;
250
+ };
251
+
252
+ publishPort.setupLocalSocketHandlers(localSocket, makeRef('04'), 'tcp', connection.RPC, connection);
253
+ localSocket.emit('data', Buffer.from('hello'));
254
+
255
+ await waitFor(() => connection.portSendCalls.length === 1);
256
+ assert.equal(localSocket.pauseCalls, 1);
257
+ assert.equal(localSocket.resumeCalls, 0);
258
+
259
+ send.resolve();
260
+ await waitFor(() => localSocket.resumeCalls === 1);
261
+
262
+ publishPort.stopListening();
263
+ });
264
+
214
265
  test('TLS publish backend socket connects to configured host', () => {
215
266
  const connection = new FakeConnection();
216
267
  const publishPort = new PublishPort(connection, {
@@ -0,0 +1,124 @@
1
+ const test = require('node:test');
2
+ const assert = require('node:assert/strict');
3
+ const http = require('http');
4
+
5
+ const DiodeRPC = require('../rpc');
6
+
7
+ function withServer(handler) {
8
+ return new Promise((resolve, reject) => {
9
+ const server = http.createServer(handler);
10
+ server.once('error', reject);
11
+ server.listen(0, '127.0.0.1', () => {
12
+ resolve({
13
+ port: server.address().port,
14
+ close: () => new Promise((done) => server.close(done)),
15
+ });
16
+ });
17
+ });
18
+ }
19
+
20
+ async function readJson(req) {
21
+ const chunks = [];
22
+ for await (const chunk of req) {
23
+ chunks.push(chunk);
24
+ }
25
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
26
+ }
27
+
28
+ function makeRpc(port) {
29
+ return new DiodeRPC({ host: '127.0.0.1' }).dioTraffic({
30
+ protocol: 'http',
31
+ rpcPort: port,
32
+ chainId: 1284,
33
+ epoch: 687,
34
+ });
35
+ }
36
+
37
+ test('dioTraffic posts dio_traffic to the connected relay node RPC endpoint', async () => {
38
+ let requestBody = null;
39
+ const result = {
40
+ chain_id: '0x504',
41
+ epoch: '0x2af',
42
+ fleets: {},
43
+ };
44
+ const server = await withServer(async (req, res) => {
45
+ requestBody = await readJson(req);
46
+ res.setHeader('content-type', 'application/json');
47
+ res.end(JSON.stringify({
48
+ jsonrpc: '2.0',
49
+ id: requestBody.id,
50
+ result,
51
+ }));
52
+ });
53
+
54
+ try {
55
+ const response = await makeRpc(server.port);
56
+
57
+ assert.deepEqual(response, result);
58
+ assert.equal(requestBody.method, 'dio_traffic');
59
+ assert.deepEqual(requestBody.params, [1284, 687]);
60
+ } finally {
61
+ await server.close();
62
+ }
63
+ });
64
+
65
+ test('dioTraffic defaults to chain 1284 and omits epoch when not supplied', async () => {
66
+ let requestBody = null;
67
+ const server = await withServer(async (req, res) => {
68
+ requestBody = await readJson(req);
69
+ res.setHeader('content-type', 'application/json');
70
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: requestBody.id, result: { fleets: {} } }));
71
+ });
72
+
73
+ try {
74
+ const rpc = new DiodeRPC({ host: '127.0.0.1' });
75
+
76
+ await rpc.dioTraffic({
77
+ protocol: 'http',
78
+ rpcPort: server.port,
79
+ });
80
+
81
+ assert.deepEqual(requestBody.params, [1284]);
82
+ } finally {
83
+ await server.close();
84
+ }
85
+ });
86
+
87
+ test('dio_traffic aliases dioTraffic', async () => {
88
+ let requestBody = null;
89
+ const server = await withServer(async (req, res) => {
90
+ requestBody = await readJson(req);
91
+ res.setHeader('content-type', 'application/json');
92
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: requestBody.id, result: { fleets: {} } }));
93
+ });
94
+
95
+ try {
96
+ const rpc = new DiodeRPC({ host: '127.0.0.1' });
97
+
98
+ await rpc.dio_traffic({
99
+ protocol: 'http',
100
+ rpcPort: server.port,
101
+ chainId: 1284,
102
+ });
103
+
104
+ assert.deepEqual(requestBody.params, [1284]);
105
+ } finally {
106
+ await server.close();
107
+ }
108
+ });
109
+
110
+ test('dioTraffic throws JSON-RPC errors', async () => {
111
+ const server = await withServer(async (_req, res) => {
112
+ res.setHeader('content-type', 'application/json');
113
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: 1, error: { message: 'bad params' } }));
114
+ });
115
+
116
+ try {
117
+ await assert.rejects(
118
+ () => makeRpc(server.port),
119
+ /bad params/,
120
+ );
121
+ } finally {
122
+ await server.close();
123
+ }
124
+ });