trac-msb 0.2.9 → 0.2.11

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 (143) hide show
  1. package/CODE_OF_CONDUCT.md +128 -0
  2. package/README.md +33 -18
  3. package/docker-compose.yml +1 -0
  4. package/docs/trac_network_http_api.openapi.yaml +889 -0
  5. package/msb.mjs +4 -21
  6. package/package.json +16 -12
  7. package/proto/network/v1/enums/message_type.proto +16 -0
  8. package/proto/network/v1/enums/result_code.proto +84 -0
  9. package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
  10. package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
  11. package/proto/network/v1/messages/liveness_request.proto +8 -0
  12. package/proto/network/v1/messages/liveness_response.proto +11 -0
  13. package/proto/network/v1/network_message.proto +22 -0
  14. package/rpc/handlers.js +163 -90
  15. package/rpc/routes/v1.js +3 -1
  16. package/rpc/rpc_server.js +3 -3
  17. package/rpc/rpc_services.js +45 -31
  18. package/rpc/utils/helpers.js +82 -51
  19. package/scripts/generate-protobufs.js +37 -12
  20. package/src/config/args.js +46 -0
  21. package/src/config/config.js +99 -5
  22. package/src/config/env.js +86 -7
  23. package/src/core/network/Network.js +79 -46
  24. package/src/core/network/protocols/LegacyProtocol.js +21 -11
  25. package/src/core/network/protocols/NetworkMessages.js +38 -17
  26. package/src/core/network/protocols/ProtocolInterface.js +14 -2
  27. package/src/core/network/protocols/ProtocolSession.js +144 -17
  28. package/src/core/network/protocols/V1Protocol.js +37 -18
  29. package/src/core/network/protocols/connectionPolicies.js +88 -0
  30. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +26 -20
  31. package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +25 -15
  32. package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
  33. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
  34. package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +20 -13
  35. package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +29 -18
  36. package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +18 -12
  37. package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
  38. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
  39. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
  40. package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
  41. package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
  42. package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
  43. package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
  44. package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
  45. package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
  46. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
  47. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
  48. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
  49. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
  50. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
  51. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
  52. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
  53. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
  54. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
  55. package/src/core/network/services/ConnectionManager.js +147 -95
  56. package/src/core/network/services/MessageOrchestrator.js +152 -28
  57. package/src/core/network/services/PendingRequestService.js +172 -0
  58. package/src/core/network/services/TransactionCommitService.js +149 -0
  59. package/src/core/network/services/TransactionPoolService.js +133 -22
  60. package/src/core/network/services/TransactionRateLimiterService.js +57 -42
  61. package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
  62. package/src/core/network/services/ValidatorObserverService.js +23 -32
  63. package/src/core/state/State.js +72 -22
  64. package/src/index.js +8 -5
  65. package/src/messages/network/v1/NetworkMessageBuilder.js +61 -81
  66. package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
  67. package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
  68. package/src/utils/Scheduler.js +0 -8
  69. package/src/utils/check.js +1 -1
  70. package/src/utils/constants.js +68 -19
  71. package/src/utils/deepEqualApplyPayload.js +40 -0
  72. package/src/utils/fileUtils.js +13 -0
  73. package/src/utils/helpers.js +10 -1
  74. package/src/utils/logger.js +25 -0
  75. package/src/utils/normalizers.js +38 -0
  76. package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
  77. package/src/utils/protobuf/operationHelpers.js +24 -3
  78. package/src/utils/type.js +26 -0
  79. package/tests/acceptance/v1/account/account.test.mjs +8 -2
  80. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  81. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  82. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  83. package/tests/acceptance/v1/rpc.test.mjs +3 -2
  84. package/tests/acceptance/v1/tx/tx.test.mjs +50 -17
  85. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +60 -18
  86. package/tests/fixtures/check.fixtures.js +33 -32
  87. package/tests/fixtures/networkV1.fixtures.js +2 -27
  88. package/tests/fixtures/protobuf.fixtures.js +33 -32
  89. package/tests/helpers/StateNetworkFactory.js +2 -2
  90. package/tests/helpers/address.js +6 -0
  91. package/tests/helpers/autobaseTestHelpers.js +2 -1
  92. package/tests/helpers/config.js +2 -1
  93. package/tests/helpers/setupApplyTests.js +6 -10
  94. package/tests/helpers/transactionPayloads.mjs +2 -2
  95. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +241 -81
  96. package/tests/unit/messages/network/NetworkMessageDirector.test.js +225 -81
  97. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
  98. package/tests/unit/network/ProtocolSession.test.js +127 -0
  99. package/tests/unit/network/networkModule.test.js +4 -1
  100. package/tests/unit/network/services/ConnectionManager.test.js +450 -0
  101. package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
  102. package/tests/unit/network/services/PendingRequestService.test.js +431 -0
  103. package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
  104. package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
  105. package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
  106. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
  107. package/tests/unit/network/services/services.test.js +17 -0
  108. package/tests/unit/network/utils/v1TestUtils.js +153 -0
  109. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
  110. package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
  111. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
  112. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
  113. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
  114. package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
  115. package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
  116. package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
  117. package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
  118. package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
  119. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
  120. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
  121. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
  122. package/tests/unit/network/v1/v1.handlers.test.js +15 -0
  123. package/tests/unit/network/v1/v1.test.js +19 -0
  124. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
  125. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
  126. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
  127. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
  128. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
  129. package/tests/unit/unit.test.js +2 -2
  130. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
  131. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  132. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  133. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  134. package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
  135. package/tests/unit/utils/type/type.test.js +25 -0
  136. package/tests/unit/utils/utils.test.js +2 -0
  137. package/.github/workflows/acceptance-tests.yml +0 -42
  138. package/.github/workflows/publish.yml +0 -33
  139. package/.github/workflows/unit-tests.yml +0 -40
  140. package/proto/network.proto +0 -74
  141. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
  142. package/src/utils/protobuf/network.cjs +0 -840
  143. package/tests/unit/network/ConnectionManager.test.js +0 -191
@@ -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
+ }
@@ -1,37 +1,40 @@
1
1
  // PoolService.js
2
- import { BATCH_SIZE, PROCESS_INTERVAL_MS } from '../../../utils/constants.js';
2
+ import { BATCH_SIZE } from '../../../utils/constants.js';
3
3
  import Scheduler from '../../../utils/Scheduler.js';
4
+ import Denque from "denque";
5
+ import b4a from "b4a";
4
6
 
5
7
  class TransactionPoolService {
6
8
  #state;
7
9
  #address;
8
10
  #config;
9
- #tx_pool = [];
11
+ #txPool = new Denque();
12
+ #transactionCommitService
10
13
  #scheduler = null;
14
+ #queuedTxHashes
11
15
 
12
16
  /**
13
17
  * @param {State} state
14
18
  * @param {string} address
15
- * @param {object} config
19
+ * @param {TransactionCommitService} transactionCommitService
20
+ * @param {Config} config
16
21
  **/
17
- constructor(state, address, config) {
22
+ constructor(state, address, transactionCommitService, config) {
18
23
  this.#state = state;
19
24
  this.#address = address;
25
+ this.#transactionCommitService = transactionCommitService;
26
+ this.#queuedTxHashes = new Set(); // to improve lookup performance when checking for duplicate transactions
20
27
  this.#config = config;
21
28
  }
22
29
 
23
- get tx_pool() {
24
- return this.#tx_pool;
30
+ get txPool() {
31
+ return this.#txPool;
25
32
  }
26
33
 
27
34
  get state() {
28
35
  return this.#state;
29
36
  }
30
37
 
31
- get address() {
32
- return this.#address;
33
- }
34
-
35
38
  async start() {
36
39
  if (!this.#config.enableWallet) {
37
40
  console.info('TransactionPoolService can not start. Wallet is not enabled');
@@ -49,10 +52,10 @@ class TransactionPoolService {
49
52
  async #worker(next) {
50
53
  try {
51
54
  await this.#processTransactions();
52
- if (this.#tx_pool.length > 0) {
55
+ if (this.#txPool.size() > 0) {
53
56
  next(0);
54
57
  } else {
55
- next(PROCESS_INTERVAL_MS);
58
+ next(this.#config.processIntervalMs);
56
59
  }
57
60
  } catch (error) {
58
61
  throw new Error(`TransactionPoolService worker error: ${error.message}`);
@@ -60,40 +63,148 @@ class TransactionPoolService {
60
63
  }
61
64
 
62
65
  #createScheduler() {
63
- return new Scheduler((next) => this.#worker(next), PROCESS_INTERVAL_MS);
66
+ return new Scheduler((next) => this.#worker(next), this.#config.processIntervalMs);
64
67
  }
65
68
 
66
69
  async #processTransactions() {
67
70
  const canValidate = await this.#checkValidationPermissions();
68
- if (canValidate && this.#tx_pool.length > 0) {
69
- const batch = this.#prepareBatch();
70
- await this.#state.append(batch);
71
+ if (!canValidate || this.#txPool.size() === 0) return;
72
+
73
+ const batchItems = this.#prepareBatch();
74
+ const encodedBatch = batchItems.map(item => item.encodedTx);
75
+ const batchTxHashes = batchItems.map(item => item.txHash);
76
+ try {
77
+ const receipts = await this.#state.appendWithProofOfPublication(encodedBatch, batchTxHashes);
78
+
79
+ const receiptsByHash = new Map();
80
+ for (const receipt of receipts) {
81
+ if (receipt.txHash) receiptsByHash.set(receipt.txHash, receipt);
82
+ }
83
+
84
+ for (const item of batchItems) {
85
+ const receipt = receiptsByHash.get(item.txHash);
86
+
87
+ if (!receipt) {
88
+ this.#transactionCommitService.rejectPendingCommit(
89
+ item.txHash,
90
+ new TransactionPoolMissingCommitReceiptError(item.txHash)
91
+ );
92
+ continue;
93
+ }
94
+
95
+ if (!receipt.proof) {
96
+ this.#transactionCommitService.rejectPendingCommit(
97
+ item.txHash,
98
+ new TransactionPoolProofUnavailableError(
99
+ item.txHash,
100
+ receipt.blockNumber,
101
+ receipt.proofError,
102
+ receipt.timestamp
103
+ )
104
+ );
105
+ continue;
106
+ }
107
+
108
+ this.#transactionCommitService.resolvePendingCommit(item.txHash, receipt);
109
+ }
110
+ } catch (error) {
111
+ for (const item of batchItems) {
112
+ this.#transactionCommitService.rejectPendingCommit(item.txHash, error);
113
+ }
114
+ console.error(
115
+ `TransactionPoolService: failed to process batch (size=${batchItems.length}): ${error?.message ?? 'unknown error'}`
116
+ );
71
117
  }
72
118
  }
73
119
 
74
120
  async #checkValidationPermissions() {
75
121
  const isAdminAllowedToValidate = await this.state.isAdminAllowedToValidate();
76
- const isNodeAllowedToValidate = await this.state.allowedToValidate(this.address);
122
+ const isNodeAllowedToValidate = await this.state.allowedToValidate(this.#address);
77
123
  return isNodeAllowedToValidate || isAdminAllowedToValidate;
78
124
  }
79
125
 
80
126
  #prepareBatch() {
81
- const length = Math.min(this.tx_pool.length, BATCH_SIZE);
82
- const batch = this.tx_pool.slice(0, length);
83
- this.tx_pool.splice(0, length);
127
+ const batch = [];
128
+ const batchSize = Math.min(this.#txPool.size(), BATCH_SIZE);
129
+
130
+ for (let i = 0; i < batchSize; i++) {
131
+ const tx = this.#txPool.shift();
132
+ this.#queuedTxHashes.delete(tx.txHash);
133
+ batch.push(tx);
134
+ }
84
135
  return batch;
85
136
  }
86
137
 
87
- addTransaction(tx) {
88
- this.tx_pool.push(tx);
138
+ addTransaction(txHash, encodedTx) {
139
+ this.validateEnqueue();
140
+ if (!txHash || !encodedTx || typeof txHash !== 'string' || !b4a.isBuffer(encodedTx)) {
141
+ throw new TransactionPoolInvalidIncomingDataError()
142
+ }
143
+ if (this.hasTransaction(txHash)) {
144
+ throw new TransactionPoolAlreadyQueuedError(txHash);
145
+ }
146
+ this.#queuedTxHashes.add(txHash);
147
+ const txData = { txHash, encodedTx };
148
+ this.txPool.push(txData);
89
149
  }
90
150
 
91
151
  async stopPool(waitForCurrent = true) {
92
152
  if (!this.#scheduler) return;
93
153
  await this.#scheduler.stop(waitForCurrent);
94
154
  this.#scheduler = null;
155
+ this.#queuedTxHashes.clear();
156
+ this.#txPool.clear();
95
157
  console.info('TransactionPoolService: closing gracefully...');
96
158
  }
159
+
160
+ validateEnqueue() {
161
+ if (this.#txPool.size() >= this.#config.txPoolSize) {
162
+ throw new TransactionPoolFullError(this.#config.txPoolSize);
163
+ }
164
+ }
165
+
166
+ hasTransaction(txHash) {
167
+ return this.#queuedTxHashes.has(txHash);
168
+ }
169
+ }
170
+
171
+ export class TransactionPoolProofUnavailableError extends Error {
172
+ constructor(txHash, blockNumber, reason = 'unknown', timestamp = 0) {
173
+ const timestampValue = timestamp instanceof Date ? timestamp.getTime() : timestamp;
174
+ const safeTimestamp = Number.isSafeInteger(timestampValue) ? timestampValue : 0;
175
+ super(`Proof unavailable for txHash ${txHash} at block ${blockNumber} at ${safeTimestamp}. Reason: ${reason}`);
176
+ this.txHash = txHash;
177
+ this.blockNumber = blockNumber;
178
+ this.timestamp = safeTimestamp;
179
+ this.reason = reason;
180
+ }
181
+ }
182
+
183
+ export class TransactionPoolMissingCommitReceiptError extends Error {
184
+ constructor(txHash) {
185
+ super(`Missing commit receipt for txHash ${txHash}`);
186
+ this.txHash = txHash;
187
+ }
188
+ }
189
+
190
+ export class TransactionPoolInvalidIncomingDataError extends Error {
191
+ constructor(message = 'Invalid transaction pool incoming data') {
192
+ super(message);
193
+ }
194
+ }
195
+
196
+ export class TransactionPoolFullError extends Error {
197
+ constructor(maxSize) {
198
+ super(`Transaction pool is full. Maximum size of ${maxSize} reached.`);
199
+ this.maxSize = maxSize
200
+ }
201
+
202
+ }
203
+
204
+ export class TransactionPoolAlreadyQueuedError extends Error {
205
+ constructor(txHash) {
206
+ super(`Transaction with hash ${txHash} is already queued in the transaction pool.`);
207
+ }
97
208
  }
98
209
 
99
210
  export default TransactionPoolService;