trac-msb 0.2.13 → 0.2.15

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.
Files changed (121) hide show
  1. package/.github/workflows/acceptance-tests.yml +38 -0
  2. package/.github/workflows/lint-pr-title.yml +26 -0
  3. package/.github/workflows/publish.yml +33 -0
  4. package/.github/workflows/unit-tests.yml +34 -0
  5. package/package.json +7 -12
  6. package/proto/network.proto +74 -0
  7. package/rpc/rpc_services.js +4 -22
  8. package/scripts/generate-protobufs.js +12 -37
  9. package/src/config/config.js +9 -26
  10. package/src/config/env.js +17 -27
  11. package/src/core/network/Network.js +36 -73
  12. package/src/core/network/protocols/LegacyProtocol.js +11 -21
  13. package/src/core/network/protocols/NetworkMessages.js +17 -38
  14. package/src/core/network/protocols/ProtocolInterface.js +2 -14
  15. package/src/core/network/protocols/ProtocolSession.js +17 -144
  16. package/src/core/network/protocols/V1Protocol.js +18 -37
  17. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +19 -25
  18. package/src/core/network/protocols/legacy/handlers/{LegacyGetRequestHandler.js → GetRequestHandler.js} +6 -6
  19. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +37 -0
  20. package/src/core/network/protocols/{legacy/handlers/LegacyRoleOperationHandler.js → shared/handlers/RoleOperationHandler.js} +11 -18
  21. package/src/core/network/protocols/{legacy/handlers/LegacySubnetworkOperationHandler.js → shared/handlers/SubnetworkOperationHandler.js} +17 -28
  22. package/src/core/network/protocols/{legacy/handlers/LegacyTransferOperationHandler.js → shared/handlers/TransferOperationHandler.js} +11 -17
  23. package/src/core/network/protocols/{legacy/handlers/BaseStateOperationHandler.js → shared/handlers/base/BaseOperationHandler.js} +12 -23
  24. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeploymentValidator.js → PartialBootstrapDeployment.js} +4 -9
  25. package/src/core/network/protocols/shared/validators/{PartialRoleAccessValidator.js → PartialRoleAccess.js} +17 -51
  26. package/src/core/network/protocols/shared/validators/{PartialTransactionValidator.js → PartialTransaction.js} +7 -21
  27. package/src/core/network/protocols/shared/validators/{PartialTransferValidator.js → PartialTransfer.js} +9 -26
  28. package/src/core/network/protocols/shared/validators/{PartialOperationValidator.js → base/PartialOperation.js} +25 -47
  29. package/src/core/network/protocols/v1/NetworkMessageRouter.js +7 -91
  30. package/src/core/network/services/ConnectionManager.js +94 -146
  31. package/src/core/network/services/MessageOrchestrator.js +27 -151
  32. package/src/core/network/services/TransactionPoolService.js +18 -129
  33. package/src/core/network/services/TransactionRateLimiterService.js +34 -52
  34. package/src/core/network/services/ValidatorObserverService.js +26 -18
  35. package/src/core/state/State.js +19 -70
  36. package/src/index.js +8 -6
  37. package/src/messages/network/v1/NetworkMessageBuilder.js +79 -59
  38. package/src/messages/network/v1/NetworkMessageDirector.js +50 -16
  39. package/src/utils/Scheduler.js +8 -0
  40. package/src/utils/constants.js +5 -71
  41. package/src/utils/helpers.js +1 -10
  42. package/src/utils/normalizers.js +0 -38
  43. package/src/utils/protobuf/network.cjs +840 -0
  44. package/src/utils/protobuf/operationHelpers.js +3 -24
  45. package/tests/acceptance/v1/account/account.test.mjs +2 -8
  46. package/tests/acceptance/v1/tx/tx.test.mjs +1 -23
  47. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +6 -34
  48. package/tests/fixtures/assembleMessage.fixtures.js +8 -7
  49. package/tests/fixtures/networkV1.fixtures.js +28 -2
  50. package/tests/helpers/autobaseTestHelpers.js +5 -2
  51. package/tests/helpers/createTestSignature.js +3 -2
  52. package/tests/helpers/transactionPayloads.mjs +2 -2
  53. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +79 -239
  54. package/tests/unit/messages/network/NetworkMessageDirector.test.js +77 -223
  55. package/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js +5 -1
  56. package/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js +5 -1
  57. package/tests/unit/network/ConnectionManager.test.js +191 -0
  58. package/tests/unit/network/networkModule.test.js +1 -4
  59. package/tests/unit/unit.test.js +2 -2
  60. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +2 -2
  61. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +2 -2
  62. package/tests/unit/utils/protobuf/operationHelpers.test.js +4 -2
  63. package/tests/unit/utils/utils.test.js +0 -1
  64. package/proto/network/v1/enums/message_type.proto +0 -16
  65. package/proto/network/v1/enums/result_code.proto +0 -84
  66. package/proto/network/v1/messages/broadcast_transaction_request.proto +0 -9
  67. package/proto/network/v1/messages/broadcast_transaction_response.proto +0 -13
  68. package/proto/network/v1/messages/liveness_request.proto +0 -8
  69. package/proto/network/v1/messages/liveness_response.proto +0 -11
  70. package/proto/network/v1/network_message.proto +0 -22
  71. package/src/core/network/protocols/connectionPolicies.js +0 -88
  72. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +0 -23
  73. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +0 -27
  74. package/src/core/network/protocols/v1/V1ProtocolError.js +0 -91
  75. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +0 -65
  76. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +0 -389
  77. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +0 -87
  78. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +0 -211
  79. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +0 -26
  80. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +0 -276
  81. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +0 -15
  82. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +0 -17
  83. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +0 -210
  84. package/src/core/network/services/PendingRequestService.js +0 -172
  85. package/src/core/network/services/TransactionCommitService.js +0 -149
  86. package/src/core/network/services/ValidatorHealthCheckService.js +0 -127
  87. package/src/utils/deepEqualApplyPayload.js +0 -40
  88. package/src/utils/logger.js +0 -25
  89. package/src/utils/protobuf/networkV1.generated.cjs +0 -2460
  90. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +0 -54
  91. package/tests/unit/network/ProtocolSession.test.js +0 -127
  92. package/tests/unit/network/services/ConnectionManager.test.js +0 -450
  93. package/tests/unit/network/services/MessageOrchestrator.test.js +0 -445
  94. package/tests/unit/network/services/PendingRequestService.test.js +0 -431
  95. package/tests/unit/network/services/TransactionCommitService.test.js +0 -246
  96. package/tests/unit/network/services/TransactionPoolService.test.js +0 -489
  97. package/tests/unit/network/services/TransactionRateLimiterService.test.js +0 -139
  98. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +0 -115
  99. package/tests/unit/network/services/services.test.js +0 -17
  100. package/tests/unit/network/utils/v1TestUtils.js +0 -153
  101. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +0 -151
  102. package/tests/unit/network/v1/V1BaseOperation.test.js +0 -356
  103. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +0 -129
  104. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +0 -53
  105. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +0 -512
  106. package/tests/unit/network/v1/V1LivenessRequest.test.js +0 -32
  107. package/tests/unit/network/v1/V1LivenessResponse.test.js +0 -45
  108. package/tests/unit/network/v1/V1ResultCode.test.js +0 -84
  109. package/tests/unit/network/v1/V1ValidationSchema.test.js +0 -13
  110. package/tests/unit/network/v1/connectionPolicies.test.js +0 -49
  111. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +0 -284
  112. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +0 -794
  113. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +0 -193
  114. package/tests/unit/network/v1/v1.handlers.test.js +0 -15
  115. package/tests/unit/network/v1/v1.test.js +0 -19
  116. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +0 -119
  117. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +0 -136
  118. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +0 -308
  119. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +0 -90
  120. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +0 -133
  121. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +0 -102
@@ -1,154 +1,89 @@
1
1
  import b4a from 'b4a'
2
- import {EventType, ResultCode} from '../../../utils/constants.js';
3
- import {publicKeyToAddress} from "../../../utils/helpers.js";
4
- import {Logger} from "../../../utils/logger.js";
5
- /**
6
- * @typedef {import('hyperswarm').Connection} Connection
7
- */
2
+ import PeerWallet from "trac-wallet"
3
+
4
+
5
+ // -- Debug Mode --
6
+ // TODO: Implement a better debug system in the future. This is just temporary.
7
+ const DEBUG = false;
8
+ const debugLog = (...args) => {
9
+ if (DEBUG) {
10
+ console.log('DEBUG [ConnectionManager] ==> ', ...args);
11
+ }
12
+ };
8
13
 
9
14
  class ConnectionManager {
10
15
  #validators
11
16
  #maxValidators
12
17
  #config
13
- #healthCheckService
14
- #boundedHealthCheckHandler
15
- #logger
18
+
16
19
  // Note: #validators is using publicKey (Buffer) as key
17
20
  // As Buffers are objects, we will rely on internal conversions done by JS to compare them.
18
21
  // It would be better to handle these conversions manually by using hex strings as keys to avoid issues
19
22
  /**
20
23
  * @param {Config} config
21
24
  **/
22
- constructor(config) {
25
+ constructor(config) {
23
26
  this.#validators = new Map();
24
27
  this.#config = config
25
28
  this.#maxValidators = config.maxValidators
26
- this.#boundedHealthCheckHandler = this.#healthCheckHandler.bind(this);
27
- this.#logger = new Logger(config)
28
29
  }
29
30
 
30
31
  /**
31
- * Subscribes to periodic validator health checks.
32
- * @param {ReadyResource} healthCheckService
32
+ * Sends a message to a single randomly selected connected validator.
33
+ * Returns the public key (buffer) of the validator used, or throws
34
+ * if the specified validator is unavailable.
35
+ * @param {Object} message - The message to send to the validator
36
+ * @returns {String} - The public key of the validator used
33
37
  */
34
- // TODO: We should consider moving this to ValidatorObserver instead.
35
- // Keep here only if we forsee having health checks for non-validator connections in the future.
36
- // For now, it seems that it would be better to keep this logic here.
37
- subscribeToHealthChecks(healthCheckService) {
38
- this.#logger.debug('subscribeToHealthChecks: subscribing to health check events');
39
- if (!healthCheckService || typeof healthCheckService.on !== 'function' || typeof healthCheckService.off !== 'function') {
40
- throw new Error('ConnectionManager: health check service must implement on/off');
41
- }
42
-
43
- if (this.#healthCheckService && this.#boundedHealthCheckHandler) {
44
- this.#logger.debug('subscribeToHealthChecks: removing previous health check handler');
45
- // Unsubscribe from previous health check service if already subscribed
46
- // TODO: Maybe we should not allow switching to a new health check service
47
- this.#healthCheckService.off(EventType.VALIDATOR_HEALTH_CHECK, this.#boundedHealthCheckHandler);
48
- }
49
-
50
- this.#healthCheckService = healthCheckService; // TODO: Maybe this should be handled in the constructor directly?
51
- // TODO: declare this method outside this function to avoid redeclaring it every time we subscribe to health checks. We can just bind it to 'this' in the constructor.
52
-
53
- this.#healthCheckService.on(EventType.VALIDATOR_HEALTH_CHECK, this.#boundedHealthCheckHandler);
54
- this.#logger.debug('subscribeToHealthChecks: subscribed to health check events');
55
- }
56
-
57
- async #healthCheckHandler(publicKey, requestId) {
58
- if (typeof publicKey !== 'string' || typeof requestId !== 'string') {
59
- // We can't throw here because this is an event handler, but we should at least log the error and return early to avoid further issues.
60
- this.#logger.error(`healthCheck: malformed event payload. Typeof publicKey = ${typeof publicKey}. Typeof requestId = ${typeof requestId}`);
61
- return;
62
- }
38
+ send(message) {
39
+ const connectedValidators = this.connectedValidators();
63
40
 
64
- const targetAddress = publicKeyToAddress(publicKey, this.#config);
65
-
66
- if (!this.exists(publicKey) || !this.connected(publicKey)) {
67
- this.#logger.debug(`healthCheck: validator not connected, stopping checks. Address = ${targetAddress}; Request ID = ${requestId}`);
68
- this.#stopHealthCheck(publicKey);
69
- return;
70
- }
71
-
72
- const connection = this.getConnection(publicKey);
73
- if (!connection || !connection.protocolSession || typeof connection.protocolSession.sendHealthCheck !== 'function') {
74
- this.#logger.debug(`healthCheck: missing protocol session, removing validator. Address = ${targetAddress}; Request ID = ${requestId}`);
75
- this.#stopHealthCheck(publicKey);
76
- this.remove(publicKey);
77
- return;
78
- }
79
-
80
- let success = false;
81
- try {
82
- this.#logger.debug(`healthCheck: sending liveness request. Address = ${targetAddress}; Request ID = ${requestId}`);
83
-
84
- const resultCode = await connection.protocolSession.sendHealthCheck();
85
- success = resultCode === ResultCode.OK;
86
- if (!success) {
87
- this.#logger.debug(`healthCheck: non-OK result code. Address = ${targetAddress}; Request ID = ${requestId}`);
88
- }
89
- } catch {
90
- success = false;
91
- }
92
-
93
- if (!success) {
94
- this.#logger.debug(`healthCheck: liveness request failed, removing validator. Address = ${targetAddress}; Request ID = ${requestId}`);
95
- this.remove(publicKey);
96
- this.#stopHealthCheck(publicKey);
97
- } else {
98
- this.#logger.debug(`healthCheck: success. Address = ${targetAddress}; Request ID = ${requestId}`);
41
+ if (connectedValidators.length === 0) {
42
+ throw new Error('ConnectionManager: no connected validators available to send message');
99
43
  }
100
- };
101
44
 
102
- #stopHealthCheck(publicKeyHex) {
103
- const targetAddress = publicKeyToAddress(publicKeyHex, this.#config);
45
+ const target = this.pickRandomValidator(connectedValidators);
46
+ const entry = this.#validators.get(target);
47
+ if (!entry || !entry.connection || !entry.connection.protocolSession?.has('legacy')) return null;
104
48
 
105
- if (!this.#healthCheckService) {
106
- this.#logger.debug(`stopHealthCheck: no health check service, cannot stop checks for ${targetAddress}`);
107
- return;
108
- }
109
49
  try {
110
- if (this.#healthCheckService.has(publicKeyHex)) {
111
- this.#logger.debug(`stopHealthCheck: stopping scheduled checks for ${targetAddress}`);
112
- this.#healthCheckService.stop(publicKeyHex);
113
- }
114
- } catch (error) {
115
- this.#logger.debug(`stopHealthCheck: failed to stop health check for validator ${targetAddress}. Error: ${error.message}`);
50
+ entry.connection.protocolSession.send(message);
51
+ entry.sent = (entry.sent || 0) + 1;
52
+ } catch (e) {
53
+ // Swallow individual send errors.
116
54
  }
117
- }
118
55
 
119
- /**
120
- * Retrieves the Hyperswarm connection object for a given validator public key.
121
- * @param {String | Buffer} publicKey - The public key (Buffer or hex string) of the validator.
122
- * @returns {Connection|undefined} - The connection object if found, otherwise undefined.
123
- */
124
- getConnection(publicKey) {
125
- const publicKeyHex = this.#toHexString(publicKey);
126
- const entry = this.#validators.get(publicKeyHex);
127
- return entry ? entry.connection : undefined;
56
+ return target;
128
57
  }
129
58
 
130
59
  /**
131
60
  * Sends a message through a specific validator without increasing sent messages count.
132
- * @param {Object} message - The message to send to the validator.
61
+ * @param {Object} message - The message to send to the validator
133
62
  * @param {String | Buffer} publicKey - A validator public key hex string to be fetched from the pool.
134
- * @returns {Promise<*>} A promise returned by `validator.connection.protocolSession.send(message)`.
135
- * @throws {ConnectionManagerError} If the validator is not connected.
136
- * @throws {ConnectionManagerError} If the validator has no valid connection or protocol session.
63
+ * @returns {Boolean} True if the message was sent, false otherwise.
137
64
  */
138
- async sendSingleMessage(message, publicKey) {
65
+ sendSingleMessage(message, publicKey) {
139
66
  let publicKeyHex = this.#toHexString(publicKey);
140
- if (!this.connected(publicKeyHex)) {
141
- throw new ConnectionManagerError(
142
- `Cannot send message: validator ${publicKeyToAddress(publicKey, this.#config)} is not connected.`
143
- );
144
- }
67
+ if (!this.exists(publicKeyHex) || !this.connected(publicKeyHex)) return false; // Fail silently
68
+
145
69
  const validator = this.#validators.get(publicKeyHex);
146
- if (!validator || !validator.connection || !validator.connection.protocolSession) {
147
- throw new ConnectionManagerError(
148
- `Cannot send message: no valid connection found for validator ${publicKeyToAddress(publicKey, this.#config)}.`
149
- );
70
+ if (!validator || !validator.connection || !validator.connection.protocolSession) return false;
71
+ try {
72
+ validator.connection.protocolSession.send(message);
73
+ } catch (e) {
74
+ // Swallow individual send errors.
150
75
  }
151
- return validator.connection.protocolSession.send(message)
76
+ return true; // TODO: Implement better success/failure reporting
77
+ }
78
+
79
+ /**
80
+ * Creates a blank entry for a validator in the pool without a connection.
81
+ * @param {String | Buffer} publicKey - The public key hex string of the validator to whitelist
82
+ */
83
+ // TODO: Deprecated/Unused - remove if not needed
84
+ whiteList(publicKey) {
85
+ let publicKeyHex = this.#toHexString(publicKey);
86
+ this.#validators.set(publicKeyHex, { connection: null, sent: 0 });
152
87
  }
153
88
 
154
89
  /**
@@ -160,20 +95,20 @@ class ConnectionManager {
160
95
  addValidator(publicKey, connection) {
161
96
  let publicKeyHex = this.#toHexString(publicKey);
162
97
  if (this.maxConnectionsReached()) {
163
- this.#logger.debug('addValidator: max connections reached.');
98
+ debugLog(`addValidator: max connections reached.`);
164
99
  return false;
165
100
  }
166
- this.#logger.debug(`addValidator: adding validator ${publicKeyToAddress(publicKeyHex, this.#config)}`);
101
+ debugLog(`addValidator: adding validator ${this.#toAddress(publicKeyHex)}`);
167
102
  if (!this.exists(publicKeyHex)) {
168
- this.#logger.debug(`addValidator: appending validator ${publicKeyToAddress(publicKeyHex, this.#config)}`);
103
+ debugLog(`addValidator: appending validator ${this.#toAddress(publicKeyHex)}`);
169
104
  this.#append(publicKeyHex, connection);
170
105
  return true;
171
106
  } else if (!this.connected(publicKeyHex)) {
172
- this.#logger.debug(`addValidator: updating validator ${publicKeyToAddress(publicKeyHex, this.#config)}`);
107
+ debugLog(`addValidator: updating validator ${this.#toAddress(publicKeyHex)}`);
173
108
  this.#update(publicKeyHex, connection);
174
109
  return true;
175
110
  }
176
- this.#logger.debug(`addValidator: didn't add validator ${publicKeyToAddress(publicKeyHex, this.#config)}`);
111
+ debugLog(`addValidator: didn't add validator ${this.#toAddress(publicKeyHex)}`);
177
112
  return false; // TODO: Implement better success/failure reporting
178
113
  }
179
114
 
@@ -182,9 +117,8 @@ class ConnectionManager {
182
117
  * @param {String | Buffer} publicKey - The public key hex string of the validator to remove
183
118
  */
184
119
  remove(publicKey) {
185
- this.#logger.debug(`remove: removing validator ${publicKeyToAddress(publicKey, this.#config)}`);
120
+ debugLog(`remove: removing validator ${this.#toAddress(publicKey)}`);
186
121
  const publicKeyHex = this.#toHexString(publicKey);
187
- this.#stopHealthCheck(publicKeyHex);
188
122
  if (this.exists(publicKeyHex)) {
189
123
  // Close the connection socket
190
124
  const entry = this.#validators.get(publicKeyHex);
@@ -193,13 +127,12 @@ class ConnectionManager {
193
127
  entry.connection.end();
194
128
  } catch (e) {
195
129
  // Ignore errors on connection end
196
- this.#logger.debug(`remove: failed to end connection: ${e.message}`);
197
130
  // TODO: Consider logging these errors here in verbose mode
198
131
  }
199
132
  }
200
- this.#logger.debug(`remove: removing validator from map: ${publicKeyToAddress(publicKeyHex, this.#config)}. Map size before removal: ${this.#validators.size}.`);
133
+ debugLog(`remove: removing validator from map: ${this.#toAddress(publicKeyHex)}. Map size before removal: ${this.#validators.size}.`);
201
134
  this.#validators.delete(publicKeyHex);
202
- this.#logger.debug(`remove: validator removed successfully. Map size is now ${this.#validators.size}.`);
135
+ debugLog(`remove: validator removed successfully. Map size is now ${this.#validators.size}.`);
203
136
  }
204
137
  }
205
138
 
@@ -273,12 +206,26 @@ class ConnectionManager {
273
206
  }
274
207
 
275
208
  prettyPrint() {
276
- this.#logger.info(`Connection count: ${this.connectionCount()}`);
277
- this.#logger.info(`Validator map keys count: ${this.#validators.size}`);
278
- this.#logger.info(`Validator map keys:\n${Array.from(this.#validators.entries()).map(([publicKey, val]) => {
279
- const protocols = val.connection?.protocolSession?.preferredProtocol || 'none';
280
- return `${publicKeyToAddress(publicKey, this.#config)}: ${protocols}`;
281
- }).join('\n')}`);
209
+ console.log('Connection count: ', this.connectionCount())
210
+ console.log('Validator map keys count: ', this.#validators.size)
211
+ console.log('Validator map keys: ', Array.from(this.#validators.keys()).map(val => this.#toAddress(val)).join(', '))
212
+ }
213
+
214
+ // Note 1: This method shuffles the whole array (in practice, probably around 50 elements)
215
+ // just to fetch a small subset of it (most times, 1 element).
216
+ // There are more efficient ways to pick a small subset of validators. Consider optimizing.
217
+ // Note 2: This method is unused now, but will be kept here for future reference
218
+ // TODO: Deprecated/Unused - remove if not needed
219
+ pickRandomSubset(validators, maxTargets) {
220
+ const copy = validators.slice();
221
+ const count = Math.min(maxTargets, copy.length);
222
+
223
+ for (let i = copy.length - 1; i > 0; i--) {
224
+ const j = Math.floor(Math.random() * (i + 1));
225
+ [copy[i], copy[j]] = [copy[j], copy[i]];
226
+ }
227
+
228
+ return copy.slice(0, count);
282
229
  }
283
230
 
284
231
  /**
@@ -310,18 +257,18 @@ class ConnectionManager {
310
257
  * @param {Object} connection - The connection object
311
258
  */
312
259
  #append(publicKey, connection) {
313
- this.#logger.debug(`#append: appending validator ${publicKeyToAddress(publicKey, this.#config)}`);
260
+ debugLog(`#append: appending validator ${this.#toAddress(publicKey)}`);
314
261
  const publicKeyHex = this.#toHexString(publicKey);
315
262
  if (this.#validators.has(publicKeyHex)) {
316
263
  // This should never happen, but just in case, we log it
317
- this.#logger.debug(`#append: tried to append existing validator: ${publicKeyToAddress(publicKey, this.#config)}`);
264
+ debugLog(`#append: tried to append existing validator: ${this.#toAddress(publicKey)}`);
318
265
  return;
319
266
  }
320
- this.#validators.set(publicKeyHex, {connection, sent: 0});
267
+ this.#validators.set(publicKeyHex, { connection, sent: 0 });
321
268
  connection.on('close', () => {
322
- this.#logger.debug(`#append: connection closing for validator ${publicKeyToAddress(publicKey, this.#config)}`);
269
+ debugLog(`#append: connection closing for validator ${this.#toAddress(publicKey)}`);
323
270
  this.remove(publicKeyHex);
324
- this.#logger.debug(`#append: connection closed for validator ${publicKeyToAddress(publicKey, this.#config)}`);
271
+ debugLog(`#append: connection closed for validator ${this.#toAddress(publicKey)}`);
325
272
  });
326
273
  }
327
274
 
@@ -336,23 +283,24 @@ class ConnectionManager {
336
283
  // It would be preferable to keep them separated though, but we would need to review all usages to ensure correctness.
337
284
  // Also, we should remove the 'else' branch below if we decide to keep 'update' and 'append' separated.
338
285
  const publicKeyHex = this.#toHexString(publicKey);
339
- this.#logger.debug(`#update: updating validator ${publicKeyToAddress(publicKey, this.#config)}`);
286
+ debugLog(`#update: updating validator ${this.#toAddress(publicKey)}`);
340
287
  if (this.#validators.has(publicKeyHex)) {
341
288
  this.#validators.get(publicKeyHex).connection = connection;
342
289
  } else {
343
- this.#validators.set(publicKeyHex, {connection, sent: 0});
290
+ this.#validators.set(publicKeyHex, { connection, sent: 0 });
344
291
  }
345
292
  }
346
293
 
347
- #toHexString(publicKey) {
348
- return b4a.isBuffer(publicKey) ? publicKey.toString('hex') : publicKey;
294
+ #toAddress(publicKey) {
295
+ const keyHex = b4a.isBuffer(publicKey) ? publicKey : b4a.from(publicKey, 'hex');
296
+ return PeerWallet.encodeBech32m(
297
+ this.#config.addressPrefix,
298
+ keyHex
299
+ );
349
300
  }
350
- }
351
301
 
352
- export class ConnectionManagerError extends Error {
353
- constructor(message) {
354
- super(message);
355
- this.name = this.constructor.name;
302
+ #toHexString(publicKey) {
303
+ return b4a.isBuffer(publicKey) ? publicKey.toString('hex') : publicKey;
356
304
  }
357
305
  }
358
306
 
@@ -1,20 +1,11 @@
1
- import { generateUUID, publicKeyToAddress, sleep } from '../../../utils/helpers.js';
1
+ import { sleep } from '../../../utils/helpers.js';
2
2
  import { operationToPayload } from '../../../utils/applyOperations.js';
3
- import { networkMessageFactory } from "../../../messages/network/v1/networkMessageFactory.js";
4
- import { NETWORK_CAPABILITIES } from "../../../utils/constants.js";
5
- import {
6
- unsafeEncodeApplyOperation
7
- } from "../../../utils/protobuf/operationHelpers.js";
8
- import { normalizeMessageByOperationType } from "../../../utils/normalizers.js";
9
- import { resultToValidatorAction, SENDER_ACTION } from "../protocols/connectionPolicies.js";
10
- import { ConnectionManagerError } from './ConnectionManager.js';
11
3
  /**
12
4
  * MessageOrchestrator coordinates message submission, retry, and validator management.
13
5
  * It works with ConnectionManager and ledger state to ensure reliable message delivery.
14
6
  */
15
7
  class MessageOrchestrator {
16
8
  #config;
17
- #wallet;
18
9
  /**
19
10
  * Attempts to send a message to validators with retries and state checks.
20
11
  * @param {ConnectionManager} connectionManager - The connection manager instance
@@ -25,161 +16,46 @@ class MessageOrchestrator {
25
16
  this.connectionManager = connectionManager;
26
17
  this.state = state;
27
18
  this.#config = config;
28
- this.#wallet = null;
29
- }
30
-
31
- setWallet(wallet) {
32
- this.#wallet = wallet;
33
- }
34
-
35
- /**
36
- * Picks a validator for an outgoing message while avoiding requester self-validation.
37
- *
38
- * ValidatorObserverService already prevents connecting to the local node itself.
39
- * This method handles a different case: for a given message, we avoid selecting
40
- * a validator whose address equals `message.address` (requester), because
41
- * validator-side checks reject that flow.
42
- *
43
- * @param {object} [message] Outgoing operation payload.
44
- * @param {string} [message.address] Requester address (bech32m).
45
- * @returns {string|null} Selected validator public key hex, or null when unavailable.
46
- */
47
- #pickValidatorForMessage(message) {
48
- const requesterAddress = message?.address;
49
- if (!requesterAddress || typeof this.connectionManager.connectedValidators !== 'function') {
50
- return this.connectionManager.pickRandomConnectedValidator();
51
- }
52
-
53
- const connected = this.connectionManager.connectedValidators();
54
- if (!Array.isArray(connected) || connected.length === 0) {
55
- return null;
56
- }
57
-
58
- const eligible = connected.filter((publicKey) => {
59
- return publicKeyToAddress(publicKey, this.#config) !== requesterAddress;
60
- });
61
-
62
- const pool = eligible.length > 0 ? eligible : connected;
63
- if (typeof this.connectionManager.pickRandomValidator === 'function') {
64
- return this.connectionManager.pickRandomValidator(pool);
65
- }
66
-
67
- const index = Math.floor(Math.random() * pool.length);
68
- return pool[index] ?? null;
69
19
  }
70
20
 
71
21
  /**
72
22
  * Sends a message to a single randomly selected connected validator.
73
23
  * @param {object} message - The message object to be sent
74
- * @param retries - The current retry count for this message
75
24
  * @returns {Promise<boolean>} - true if successful, false otherwise
76
25
  */
77
- async send(message, retries = 0) {
78
- if (retries > this.#config.maxRetries) {
79
- console.warn(`MessageOrchestrator: Max retries reached for message ${JSON.stringify(message)}. Aborting send.`);
80
- return false;
81
- }
82
-
83
- const validatorPublicKey = this.#pickValidatorForMessage(message);
84
- if (!validatorPublicKey) return false;
85
- console.log("Sending message to validator:", publicKeyToAddress(validatorPublicKey, this.#config));
86
-
87
- /* NOTE: Since the retry logic for Legacy is handled here, and is very unique to the protocol,
88
- * it was decided to not change MessageOrchestrator send method in the refactor to make protocols transparent.
89
- * As the Legacy protocol is going to be deprecated soon, it was decided to keep the retry logic
90
- * here instead of abstracting it in the protocol implementation.
91
- * If we were to abstract it, we would need to add protocol-specific logic in the ProtocolSession
92
- * or ProtocolInterface, which would make them less clean and more coupled with the specifics of the protocols.
93
- * The parts to be refactored in the future are marked with TODO comments.
94
- */
95
-
96
- // TODO: After Legacy is deprecated, we don't need to check preferred protocol here.
97
- const validatorConnection = this.connectionManager.getConnection(validatorPublicKey);
98
- const preferredProtocol = validatorConnection.protocolSession.preferredProtocol;
99
- let success = false;
100
- if (preferredProtocol === validatorConnection.protocolSession.supportedProtocols.LEGACY) {
101
-
102
- try {
103
- success = await this.#attemptSendMessageForLegacy(validatorPublicKey, message);
104
- } catch (error) {
105
- success = await this.send(message, retries + 1);
26
+ async send(message) {
27
+ const startTime = Date.now();
28
+ while (Date.now() - startTime < this.#config.messageValidatorResponseTimeout) {
29
+ const validator = this.connectionManager.pickRandomConnectedValidator();
30
+ if (!validator) return false;
31
+
32
+ const success = await this.#attemptSendMessage(validator, message);
33
+ if (success) {
34
+ return true;
106
35
  }
107
- if (!success) {
108
- // Remove validator and retry
109
- this.connectionManager.remove(validatorPublicKey);
110
- success = await this.send(message, retries + 1);
111
- }
112
- } else if (preferredProtocol === validatorConnection.protocolSession.supportedProtocols.V1) {
113
- // TODO: This is probably better placed inside the V1 protocol definition.
114
- // Both protocols should receive a 'canonical' message and solve the encodings internally
115
- // Refactor
116
- const normalizedMessage = normalizeMessageByOperationType(message, this.#config)
117
- const encodedTransaction = unsafeEncodeApplyOperation(normalizedMessage)
118
- const v1Message = await networkMessageFactory(this.#wallet, this.#config)
119
- .buildBroadcastTransactionRequest(
120
- generateUUID(),
121
- encodedTransaction,
122
- NETWORK_CAPABILITIES
123
- );
124
-
125
- await this.connectionManager.sendSingleMessage(v1Message, validatorPublicKey)
126
- .then(
127
- (resultCode) => {
128
- // TODO: When we will deprecate the legacy protocol, we should refactor this scope, to propagate domain-error with result code.
129
- const action = resultToValidatorAction(resultCode);
130
- switch (action) {
131
- case SENDER_ACTION.SUCCESS:
132
- success = true;
133
- //TODO: Create a function for action below, and replace it also in legacy flow.
134
- this.incrementSentCount(validatorPublicKey);
135
- if (this.shouldRemove(validatorPublicKey)) {
136
- this.connectionManager.remove(validatorPublicKey);
137
- }
138
- break;
139
- case SENDER_ACTION.ROTATE:
140
- this.connectionManager.remove(validatorPublicKey);
141
- break;
142
- case SENDER_ACTION.NO_ROTATE:
143
- // ignore
144
- break;
145
- default:
146
- this.connectionManager.remove(validatorPublicKey);
147
- console.warn(
148
- `MessageOrchestrator: Unrecognized action from connectionPolicies: ${action}.
149
- ResultCode was: ${resultCode}. Removing validator ${publicKeyToAddress(validatorPublicKey, this.#config)}`
150
- );
151
- break;
152
- }
153
- }
154
- )
155
- .catch(
156
- async (err) => {
157
- if (err instanceof ConnectionManagerError) {
158
- success = await this.send(message, retries + 1);
159
- console.warn(`MessageOrchestrator: Connection Error: ${err.message}`);
160
- } else {
161
- this.connectionManager.remove(validatorPublicKey);
162
- success = await this.send(message, retries + 1);
163
- }
164
- }
165
- )
166
-
167
36
  }
168
- return success;
37
+ return false;
169
38
  }
170
39
 
171
- // TODO: Delete this function after legacy protocol is deprecated
172
- async #attemptSendMessageForLegacy(validatorPublicKey, message) {
40
+ async #attemptSendMessage(validator, message) {
41
+ let attempts = 0;
173
42
  const deductedTxType = operationToPayload(message.type);
174
- await this.connectionManager.sendSingleMessage(message, validatorPublicKey);
175
- const appeared = await this.waitForUnsignedState(message[deductedTxType].tx, this.#config.messageValidatorResponseTimeout);
176
- if (appeared) {
177
- this.incrementSentCount(validatorPublicKey);
178
- if (this.shouldRemove(validatorPublicKey)) {
179
- this.connectionManager.remove(validatorPublicKey);
43
+ while (attempts <= this.#config.maxRetries) {
44
+ this.connectionManager.sendSingleMessage(message, validator);
45
+
46
+ const appeared = await this.waitForUnsignedState(message[deductedTxType].tx, this.#config.messageValidatorRetryDelay);
47
+ if (appeared) {
48
+ this.incrementSentCount(validator);
49
+ if (this.shouldRemove(validator)) {
50
+ this.connectionManager.remove(validator);
51
+ }
52
+ return true;
180
53
  }
181
- return true;
54
+ attempts++;
182
55
  }
56
+
57
+ // If all retries fail, remove validator from pool
58
+ this.connectionManager.remove(validator);
183
59
  return false;
184
60
  }
185
61