trac-msb 0.2.12 → 0.2.13

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 (114) hide show
  1. package/package.json +9 -4
  2. package/proto/network/v1/enums/message_type.proto +16 -0
  3. package/proto/network/v1/enums/result_code.proto +84 -0
  4. package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
  5. package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
  6. package/proto/network/v1/messages/liveness_request.proto +8 -0
  7. package/proto/network/v1/messages/liveness_response.proto +11 -0
  8. package/proto/network/v1/network_message.proto +22 -0
  9. package/rpc/rpc_services.js +22 -4
  10. package/scripts/generate-protobufs.js +37 -12
  11. package/src/config/config.js +26 -5
  12. package/src/config/env.js +25 -11
  13. package/src/core/network/Network.js +73 -36
  14. package/src/core/network/protocols/LegacyProtocol.js +21 -11
  15. package/src/core/network/protocols/NetworkMessages.js +38 -17
  16. package/src/core/network/protocols/ProtocolInterface.js +14 -2
  17. package/src/core/network/protocols/ProtocolSession.js +144 -17
  18. package/src/core/network/protocols/V1Protocol.js +37 -18
  19. package/src/core/network/protocols/connectionPolicies.js +88 -0
  20. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +25 -19
  21. package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +23 -12
  22. package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
  23. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
  24. package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +18 -11
  25. package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +28 -17
  26. package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +17 -11
  27. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
  28. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
  29. package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
  30. package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
  31. package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
  32. package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
  33. package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
  34. package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
  35. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
  36. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
  37. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
  38. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
  39. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
  40. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
  41. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
  42. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
  43. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
  44. package/src/core/network/services/ConnectionManager.js +146 -94
  45. package/src/core/network/services/MessageOrchestrator.js +151 -27
  46. package/src/core/network/services/PendingRequestService.js +172 -0
  47. package/src/core/network/services/TransactionCommitService.js +149 -0
  48. package/src/core/network/services/TransactionPoolService.js +129 -18
  49. package/src/core/network/services/TransactionRateLimiterService.js +52 -34
  50. package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
  51. package/src/core/network/services/ValidatorObserverService.js +18 -26
  52. package/src/core/state/State.js +70 -19
  53. package/src/index.js +5 -4
  54. package/src/messages/network/v1/NetworkMessageBuilder.js +59 -79
  55. package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
  56. package/src/utils/Scheduler.js +0 -8
  57. package/src/utils/constants.js +71 -5
  58. package/src/utils/deepEqualApplyPayload.js +40 -0
  59. package/src/utils/helpers.js +10 -1
  60. package/src/utils/logger.js +25 -0
  61. package/src/utils/normalizers.js +38 -0
  62. package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
  63. package/src/utils/protobuf/operationHelpers.js +24 -3
  64. package/tests/acceptance/v1/account/account.test.mjs +8 -2
  65. package/tests/acceptance/v1/tx/tx.test.mjs +23 -1
  66. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +34 -6
  67. package/tests/fixtures/networkV1.fixtures.js +2 -28
  68. package/tests/helpers/transactionPayloads.mjs +2 -2
  69. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +239 -79
  70. package/tests/unit/messages/network/NetworkMessageDirector.test.js +223 -77
  71. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
  72. package/tests/unit/network/ProtocolSession.test.js +127 -0
  73. package/tests/unit/network/networkModule.test.js +4 -1
  74. package/tests/unit/network/services/ConnectionManager.test.js +450 -0
  75. package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
  76. package/tests/unit/network/services/PendingRequestService.test.js +431 -0
  77. package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
  78. package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
  79. package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
  80. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
  81. package/tests/unit/network/services/services.test.js +17 -0
  82. package/tests/unit/network/utils/v1TestUtils.js +153 -0
  83. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
  84. package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
  85. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
  86. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
  87. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
  88. package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
  89. package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
  90. package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
  91. package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
  92. package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
  93. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
  94. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
  95. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
  96. package/tests/unit/network/v1/v1.handlers.test.js +15 -0
  97. package/tests/unit/network/v1/v1.test.js +19 -0
  98. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
  99. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
  100. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
  101. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
  102. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
  103. package/tests/unit/unit.test.js +2 -2
  104. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
  105. package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
  106. package/tests/unit/utils/utils.test.js +1 -0
  107. package/.github/workflows/acceptance-tests.yml +0 -38
  108. package/.github/workflows/lint-pr-title.yml +0 -26
  109. package/.github/workflows/publish.yml +0 -33
  110. package/.github/workflows/unit-tests.yml +0 -34
  111. package/proto/network.proto +0 -74
  112. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
  113. package/src/utils/protobuf/network.cjs +0 -840
  114. package/tests/unit/network/ConnectionManager.test.js +0 -191
@@ -1,11 +1,20 @@
1
- import { sleep } from '../../../utils/helpers.js';
1
+ import { generateUUID, publicKeyToAddress, 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';
3
11
  /**
4
12
  * MessageOrchestrator coordinates message submission, retry, and validator management.
5
13
  * It works with ConnectionManager and ledger state to ensure reliable message delivery.
6
14
  */
7
15
  class MessageOrchestrator {
8
16
  #config;
17
+ #wallet;
9
18
  /**
10
19
  * Attempts to send a message to validators with retries and state checks.
11
20
  * @param {ConnectionManager} connectionManager - The connection manager instance
@@ -16,46 +25,161 @@ class MessageOrchestrator {
16
25
  this.connectionManager = connectionManager;
17
26
  this.state = state;
18
27
  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;
19
69
  }
20
70
 
21
71
  /**
22
72
  * Sends a message to a single randomly selected connected validator.
23
73
  * @param {object} message - The message object to be sent
74
+ * @param retries - The current retry count for this message
24
75
  * @returns {Promise<boolean>} - true if successful, false otherwise
25
76
  */
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;
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);
35
106
  }
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
+
36
167
  }
37
- return false;
168
+ return success;
38
169
  }
39
170
 
40
- async #attemptSendMessage(validator, message) {
41
- let attempts = 0;
171
+ // TODO: Delete this function after legacy protocol is deprecated
172
+ async #attemptSendMessageForLegacy(validatorPublicKey, message) {
42
173
  const deductedTxType = operationToPayload(message.type);
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;
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);
53
180
  }
54
- attempts++;
181
+ return true;
55
182
  }
56
-
57
- // If all retries fail, remove validator from pool
58
- this.connectionManager.remove(validator);
59
183
  return false;
60
184
  }
61
185
 
@@ -0,0 +1,172 @@
1
+ import {NetworkOperationType, ResultCode} from '../../../utils/constants.js';
2
+ import {isHexString} from '../../../utils/helpers.js';
3
+ import {V1ProtocolError, V1TimeoutError, V1UnexpectedError} from "../protocols/v1/V1ProtocolError.js";
4
+ import {Config} from '../../../config/config.js';
5
+ import b4a from 'b4a';
6
+
7
+ const PEER_PUBLIC_KEY_HEX_LENGTH = 64;
8
+
9
+ class PendingRequestService {
10
+ #pendingRequests;
11
+ #requestMessageTypes = [NetworkOperationType.LIVENESS_REQUEST, NetworkOperationType.BROADCAST_TRANSACTION_REQUEST];
12
+ #config;
13
+
14
+ constructor(config) {
15
+ this.#pendingRequests = new Map(); // Map<id, pendingRequestEntry>
16
+ this.#config = config;
17
+ }
18
+
19
+ has(id) {
20
+ return this.#pendingRequests.has(id);
21
+ }
22
+
23
+ isProbePending(peerPubKeyHex) {
24
+ for (const [, entry] of this.#pendingRequests) {
25
+ if (entry.requestedTo === peerPubKeyHex && entry.requestType === NetworkOperationType.LIVENESS_REQUEST) {
26
+ return true;
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+
32
+ #validateRegisterInput(peerPubKeyHex, message) {
33
+ if (!isHexString(peerPubKeyHex) || peerPubKeyHex.length !== PEER_PUBLIC_KEY_HEX_LENGTH) {
34
+ throw new Error('Invalid peer public key. Expected 32-byte hex string.');
35
+ }
36
+
37
+ if (!message || typeof message !== 'object') {
38
+ throw new Error('Pending request message must be an object.');
39
+ }
40
+
41
+ if (typeof message.id !== 'string' || message.id.length === 0) {
42
+ throw new Error('Pending request ID must be a non-empty string.');
43
+ }
44
+
45
+ if (!this.#requestMessageTypes.includes(message.type)) {
46
+ throw new Error('Unsupported pending request type.');
47
+ }
48
+ }
49
+
50
+ /*
51
+ @returns {Promise}
52
+ */
53
+ registerPendingRequest(peerPubKeyHex, message) {
54
+ this.#validateRegisterInput(peerPubKeyHex, message);
55
+ const id = message.id;
56
+ if (this.#pendingRequests.size >= this.#config.maxPendingRequestsInPendingRequestsService) {
57
+ throw new Error('Maximum number of pending requests reached.');
58
+ }
59
+
60
+ if (this.#pendingRequests.has(id)) {
61
+ throw new Error(`Pending request with ID ${id} from peer ${peerPubKeyHex} already exists.`);
62
+ }
63
+
64
+ const entry = {
65
+ id: id,
66
+ requestType: message.type,
67
+ requestTxData: this.#extractRequestTxData(message),
68
+ requestedTo: peerPubKeyHex,
69
+ timeoutMs: this.#config.pendingRequestTimeout,
70
+ timeoutId: null,
71
+ resolve: null,
72
+ reject: null,
73
+ }
74
+
75
+ const promise = new Promise((resolve, reject) => {
76
+ entry.resolve = resolve;
77
+ entry.reject = reject;
78
+ });
79
+
80
+ entry.timeoutId = setTimeout(() => {
81
+ this.rejectPendingRequest(
82
+ id,
83
+ new V1TimeoutError(
84
+ `Pending request with ID ${id} from peer ${peerPubKeyHex} timed out after ${entry.timeoutMs} ms.`,
85
+ true
86
+ ));
87
+
88
+ }, entry.timeoutMs);
89
+
90
+ this.#pendingRequests.set(id, entry);
91
+ return promise;
92
+ }
93
+
94
+ #extractRequestTxData(message) {
95
+ if (message.type !== NetworkOperationType.BROADCAST_TRANSACTION_REQUEST) return null;
96
+ const txData = message.broadcast_transaction_request?.data;
97
+ return b4a.isBuffer(txData) ? txData : null;
98
+ }
99
+
100
+ getAndDeletePendingRequest(id) {
101
+ const entry = this.#pendingRequests.get(id);
102
+ if (!entry) return null;
103
+
104
+ clearTimeout(entry.timeoutId);
105
+ this.#pendingRequests.delete(id);
106
+ return entry;
107
+ }
108
+
109
+ getPendingRequest(id) {
110
+ const entry = this.#pendingRequests.get(id);
111
+ if (!entry) return null;
112
+ return entry;
113
+ }
114
+
115
+ // for now, we are resolving only resultCode, but we can extend it in the future if needed...
116
+ resolvePendingRequest(id, resultCode = ResultCode.OK) {
117
+ const entry = this.getAndDeletePendingRequest(id);
118
+ if (!entry) return false;
119
+ entry.resolve(resultCode);
120
+ return true;
121
+ }
122
+
123
+ rejectPendingRequest(id, error) {
124
+ const entry = this.getAndDeletePendingRequest(id);
125
+ if (!entry) return false;
126
+ const err = error instanceof V1ProtocolError
127
+ ? error
128
+ : new V1UnexpectedError(error?.message ?? 'Unexpected error');
129
+ entry.reject(err);
130
+ return true;
131
+ }
132
+
133
+ rejectPendingRequestsForPeer(peerPubKeyHex, error) {
134
+ const idsToReject = [];
135
+ for (const [id, entry] of this.#pendingRequests) {
136
+ if (entry.requestedTo === peerPubKeyHex) idsToReject.push(id);
137
+ }
138
+
139
+ for (const id of idsToReject) {
140
+ this.rejectPendingRequest(id, error);
141
+ }
142
+
143
+ return idsToReject.length;
144
+ }
145
+
146
+ stopPendingRequestTimeout(id) {
147
+ const entry = this.#pendingRequests.get(id);
148
+ if (!entry) return false;
149
+
150
+ clearTimeout(entry.timeoutId);
151
+ entry.timeoutId = null;
152
+ return true;
153
+ }
154
+
155
+ close() {
156
+ for (const [id, entry] of this.#pendingRequests) {
157
+ clearTimeout(entry.timeoutId);
158
+ try {
159
+ entry.reject(
160
+ new V1UnexpectedError(
161
+ `Pending request ${id} cancelled (shutdown).`,
162
+ false)
163
+ );
164
+ } catch (error) {
165
+ console.error(`PendingRequestService.close: failed to reject pending request ${id}:`, error);
166
+ }
167
+ }
168
+ this.#pendingRequests.clear();
169
+ }
170
+ }
171
+
172
+ export default PendingRequestService;
@@ -0,0 +1,149 @@
1
+ import {isHexString} from '../../../utils/helpers.js';
2
+ import {TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE} from '../../../utils/constants.js';
3
+
4
+ const TX_HASH_HEX_STRING_LENGTH = 64;
5
+
6
+ class TransactionCommitService {
7
+ #pendingCommits;
8
+ #config;
9
+
10
+ constructor(config) {
11
+ this.#pendingCommits = new Map(); // Map<txHash, pendingCommitEntry>
12
+ this.#config = config;
13
+ }
14
+
15
+ #assertTxHash(txHash) {
16
+ if (!isHexString(txHash) || txHash.length !== TX_HASH_HEX_STRING_LENGTH) {
17
+ throw new PendingCommitInvalidTxHashError(txHash);
18
+ }
19
+ }
20
+
21
+ has(txHash) {
22
+ this.#assertTxHash(txHash);
23
+ return this.#pendingCommits.has(txHash);
24
+ }
25
+
26
+ /*
27
+ @returns {Promise}
28
+ */
29
+ registerPendingCommit(txHash) {
30
+ this.#assertTxHash(txHash);
31
+
32
+ if (this.#pendingCommits.size >= TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE) {
33
+ throw new PendingCommitBufferFullError(TRANSACTION_COMMIT_SERVICE_BUFFER_SIZE);
34
+ }
35
+
36
+ if (this.#pendingCommits.has(txHash)) {
37
+ throw new PendingCommitAlreadyExistsError(txHash);
38
+ }
39
+
40
+ const timeoutMs = this.#config.txCommitTimeout;
41
+
42
+ const entry = {
43
+ txHash,
44
+ timeoutMs,
45
+ timeoutId: null,
46
+ resolve: null,
47
+ reject: null,
48
+ };
49
+
50
+ const promise = new Promise((resolve, reject) => {
51
+ entry.resolve = resolve;
52
+ entry.reject = reject;
53
+ });
54
+
55
+ entry.timeoutId = setTimeout(() => {
56
+ this.rejectPendingCommit(
57
+ txHash,
58
+ new PendingCommitTimeoutError(txHash, timeoutMs)
59
+ );
60
+ }, timeoutMs);
61
+
62
+ this.#pendingCommits.set(txHash, entry);
63
+ return promise;
64
+ }
65
+
66
+ getAndDeletePendingCommit(txHash) {
67
+ this.#assertTxHash(txHash);
68
+ const entry = this.#pendingCommits.get(txHash);
69
+ if (!entry) return null;
70
+
71
+ clearTimeout(entry.timeoutId);
72
+ this.#pendingCommits.delete(txHash);
73
+ return entry;
74
+ }
75
+
76
+ resolvePendingCommit(txHash, receipt = null) {
77
+ this.#assertTxHash(txHash);
78
+ const entry = this.getAndDeletePendingCommit(txHash);
79
+ if (!entry) return false;
80
+ entry.resolve(receipt);
81
+ return true;
82
+ }
83
+
84
+ rejectPendingCommit(txHash, error) {
85
+ this.#assertTxHash(txHash);
86
+ const entry = this.getAndDeletePendingCommit(txHash);
87
+ if (!entry) return false;
88
+
89
+ entry.reject(
90
+ error instanceof Error
91
+ ? error
92
+ : new PendingCommitUnexpectedError('Unexpected commit error')
93
+ );
94
+
95
+ return true;
96
+ }
97
+
98
+ close() {
99
+ for (const [txHash, entry] of this.#pendingCommits) {
100
+ clearTimeout(entry.timeoutId);
101
+ try {
102
+ entry.reject(
103
+ new PendingCommitCancelledError(txHash)
104
+ );
105
+ } catch (error) {
106
+ console.error(`TransactionCommitService.close: failed to reject pending commit ${txHash}:`, error);
107
+ }
108
+ }
109
+ this.#pendingCommits.clear();
110
+ }
111
+ }
112
+
113
+ export default TransactionCommitService;
114
+
115
+ export class PendingCommitInvalidTxHashError extends Error {
116
+ constructor(txHash) {
117
+ super(`Invalid txHash format: ${txHash}`);
118
+ }
119
+ }
120
+
121
+ export class PendingCommitBufferFullError extends Error {
122
+ constructor(limit) {
123
+ super(`Maximum number of pending commits reached (limit=${limit}).`);
124
+ }
125
+ }
126
+
127
+ export class PendingCommitAlreadyExistsError extends Error {
128
+ constructor(txHash) {
129
+ super(`Pending commit for txHash ${txHash} already exists.`);
130
+ }
131
+ }
132
+
133
+ export class PendingCommitTimeoutError extends Error {
134
+ constructor(txHash, timeoutMs) {
135
+ super(`Pending commit for txHash ${txHash} timed out after ${timeoutMs} ms.`);
136
+ }
137
+ }
138
+
139
+ export class PendingCommitCancelledError extends Error {
140
+ constructor(txHash) {
141
+ super(`Pending commit ${txHash} cancelled (shutdown).`);
142
+ }
143
+ }
144
+
145
+ export class PendingCommitUnexpectedError extends Error {
146
+ constructor(message = 'Unexpected commit error') {
147
+ super(message);
148
+ }
149
+ }