trac-msb 0.2.9 → 0.2.10

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 (59) hide show
  1. package/.github/workflows/acceptance-tests.yml +7 -11
  2. package/.github/workflows/lint-pr-title.yml +26 -0
  3. package/.github/workflows/unit-tests.yml +2 -8
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/README.md +33 -18
  6. package/docker-compose.yml +1 -0
  7. package/docs/trac_network_http_api.openapi.yaml +889 -0
  8. package/msb.mjs +4 -21
  9. package/package.json +8 -9
  10. package/rpc/handlers.js +163 -90
  11. package/rpc/routes/v1.js +3 -1
  12. package/rpc/rpc_server.js +3 -3
  13. package/rpc/rpc_services.js +25 -29
  14. package/rpc/utils/helpers.js +82 -51
  15. package/src/config/args.js +46 -0
  16. package/src/config/config.js +78 -5
  17. package/src/config/env.js +69 -4
  18. package/src/core/network/Network.js +6 -10
  19. package/src/core/network/protocols/NetworkMessages.js +1 -1
  20. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +1 -1
  21. package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
  22. package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +2 -2
  23. package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +1 -1
  24. package/src/core/network/protocols/shared/handlers/TransferOperationHandler.js +1 -1
  25. package/src/core/network/protocols/shared/handlers/base/BaseOperationHandler.js +5 -6
  26. package/src/core/network/services/ConnectionManager.js +1 -1
  27. package/src/core/network/services/MessageOrchestrator.js +1 -1
  28. package/src/core/network/services/TransactionPoolService.js +4 -4
  29. package/src/core/network/services/TransactionRateLimiterService.js +8 -11
  30. package/src/core/network/services/ValidatorObserverService.js +5 -6
  31. package/src/core/state/State.js +2 -3
  32. package/src/index.js +3 -1
  33. package/src/messages/network/v1/NetworkMessageBuilder.js +2 -2
  34. package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
  35. package/src/utils/check.js +1 -1
  36. package/src/utils/constants.js +0 -17
  37. package/src/utils/fileUtils.js +13 -0
  38. package/src/utils/type.js +26 -0
  39. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  40. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  41. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  42. package/tests/acceptance/v1/rpc.test.mjs +3 -2
  43. package/tests/acceptance/v1/tx/tx.test.mjs +27 -16
  44. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +26 -12
  45. package/tests/fixtures/check.fixtures.js +33 -32
  46. package/tests/fixtures/networkV1.fixtures.js +3 -2
  47. package/tests/fixtures/protobuf.fixtures.js +33 -32
  48. package/tests/helpers/StateNetworkFactory.js +2 -2
  49. package/tests/helpers/address.js +6 -0
  50. package/tests/helpers/autobaseTestHelpers.js +2 -1
  51. package/tests/helpers/config.js +2 -1
  52. package/tests/helpers/setupApplyTests.js +6 -10
  53. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +3 -3
  54. package/tests/unit/messages/network/NetworkMessageDirector.test.js +3 -5
  55. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  56. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  57. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  58. package/tests/unit/utils/type/type.test.js +25 -0
  59. package/tests/unit/utils/utils.test.js +1 -0
package/msb.mjs CHANGED
@@ -1,31 +1,14 @@
1
1
  import { MainSettlementBus } from './src/index.js';
2
2
  import { startRpcServer } from './rpc/rpc_server.js';
3
- import { createConfig, ENV } from './src/config/env.js';
3
+ import { isRpcEnabled, resolveConfig } from './src/config/args.js';
4
4
 
5
- const pearApp = typeof Pear !== 'undefined' ? (Pear.app ?? Pear.config) : undefined;
6
- const runtimeArgs = typeof process !== 'undefined' ? process.argv.slice(2) : [];
7
- const args = pearApp?.args ?? runtimeArgs;
8
- const runRpc = args.includes('--rpc');
9
- const storeName = pearApp?.args?.[0] ?? runtimeArgs[0]
10
-
11
- const rpc = {
12
- storeName: pearApp?.args?.[0] ?? runtimeArgs[0],
13
- enableWallet: false,
14
- enableInteractiveMode: false
15
- }
16
-
17
- const options = args.includes('--rpc') ? rpc : { storeName }
18
- const config = createConfig(ENV.MAINNET, options)
5
+ const config = resolveConfig()
19
6
  const msb = new MainSettlementBus(config);
20
7
 
21
8
  msb.ready().then(async () => {
22
- if (runRpc) {
9
+ if (isRpcEnabled()) {
23
10
  console.log('Starting RPC server...');
24
- const portIndex = args.indexOf('--port');
25
- const port = (portIndex !== -1 && args[portIndex + 1]) ? parseInt(args[portIndex + 1], 10) : 5000;
26
- const hostIndex = args.indexOf('--host');
27
- const host = (hostIndex !== -1 && args[hostIndex + 1]) ? args[hostIndex + 1] : 'localhost';
28
- startRpcServer(msb, config , host, port);
11
+ startRpcServer(msb, config);
29
12
  } else {
30
13
  console.log('RPC server will not be started.');
31
14
  msb.interactiveMode();
package/package.json CHANGED
@@ -1,20 +1,18 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.2.9",
4
+ "version": "0.2.10",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
8
8
  },
9
9
  "type": "module",
10
10
  "scripts": {
11
- "dev": "pear run -d .",
12
- "dev-rpc": "pear run -d . ${npm_config_store} --rpc --port ${npm_config_port}",
13
- "prod": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store}",
14
- "prod-rpc": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store} --rpc --host ${npm_config_host} --port ${npm_config_port}",
15
- "env-prod": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run . ${MSB_STORE:-node-store}",
16
- "env-prod-rpc": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run . ${MSB_STORE:-rpc-node-store} --rpc --host ${MSB_HOST:-127.0.0.1} --port ${MSB_PORT:-5000}",
17
- "env-prod-rpc-docker": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' node msb.mjs ${MSB_STORE:-rpc-node-store} --rpc --host 0.0.0.0 --port ${MSB_PORT:-5000}",
11
+ "start": "NODE_OPTIONS='--max-old-space-size=4096' pear run .",
12
+ "rpc": "NODE_OPTIONS='--max-old-space-size=4096' pear run . --rpc --host ${npm_config_host} --port ${npm_config_port}",
13
+ "env": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run . --stores-directory ${STORES_DIRECTORY:-stores} --network ${NETWORK:-mainnet}",
14
+ "env-rpc": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' pear run . --stores-directory ${STORES_DIRECTORY:-stores} --rpc --host ${MSB_HOST:-127.0.0.1} --port ${MSB_PORT:-5000} --network ${NETWORK:-mainnet}",
15
+ "env-rpc-docker": "if [ -f .env ]; then set -a; . ./.env; set +a; fi; NODE_OPTIONS='--max-old-space-size=4096' node msb.mjs --stores-directory ${STORES_DIRECTORY:-stores} --rpc --host 0.0.0.0 --port ${MSB_PORT:-5000} --network ${NETWORK:-mainnet}",
18
16
  "protobuf": "node scripts/generate-protobufs.js",
19
17
  "test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 tests/acceptance/v1/rpc.test.mjs --runInBand",
20
18
  "test:integration": "brittle-node -t 1200000 tests/integration/integration.test.js",
@@ -44,12 +42,13 @@
44
42
  "hypercore-crypto": "3.6.1",
45
43
  "hyperdht": "6.27.0",
46
44
  "hyperswarm": "4.14.2",
45
+ "lodash": "^4.17.23",
47
46
  "protocol-buffers-encodings": "1.2.0",
48
47
  "protomux": "3.10.1",
49
48
  "protomux-wakeup": "2.4.0",
50
49
  "readline": "npm:bare-node-readline",
51
50
  "ready-resource": "1.1.2",
52
- "trac-wallet": "1.0.1",
51
+ "trac-wallet": "1.0.2",
53
52
  "tty": "npm:bare-node-tty",
54
53
  "uuid": "^13.0.0"
55
54
  },
package/rpc/handlers.js CHANGED
@@ -1,7 +1,17 @@
1
- import { decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure } from "./utils/helpers.js"
1
+ import {
2
+ decodeBase64Payload,
3
+ isBase64,
4
+ isValidTxHash,
5
+ sanitizeBulkPayloadsRequestBody,
6
+ sanitizeTransferPayload,
7
+ validatePayloadStructure,
8
+ hasSpacesInUrl,
9
+ BroadcastError,
10
+ ValidationError,
11
+ NotFoundError
12
+ } from "./utils/helpers.js"
2
13
  import { MAX_SIGNED_LENGTH, ZERO_WK } from "./constants.js";
3
14
  import { buildRequestUrl } from "./utils/url.js";
4
- import { isHexString } from "../src/utils/helpers.js";
5
15
  import {
6
16
  getBalance,
7
17
  getTxv,
@@ -12,28 +22,41 @@ import {
12
22
  getTxHashes,
13
23
  getTxDetails,
14
24
  fetchBulkTxPayloads,
15
- getExtendedTxDetails
25
+ getExtendedTxDetails,
16
26
  } from "./rpc_services.js";
17
27
  import { bufferToBigInt, licenseBufferToBigInt } from "../src/utils/amountSerialization.js";
18
28
  import { isAddressValid } from "../src/core/state/utils/address.js";
19
29
  import { getConfirmedParameter } from "./utils/confirmedParameter.js";
20
30
 
31
+ export async function handleHealth({ msbInstance, respond }) {
32
+ try {
33
+ const isReady = msbInstance && msbInstance.state;
34
+ if (isReady) return respond(200, { ok: true });
35
+ throw new Error("RPC_OFFLINE");
36
+ } catch (error) {
37
+ respond(503, { error: "Could not connect to RPC server" });
38
+ }
39
+ }
40
+
21
41
  export async function handleBalance({ req, respond, msbInstance }) {
22
42
  const url = buildRequestUrl(req);
23
43
  const parts = url.pathname.split("/").filter(Boolean);
24
44
  const address = parts[2];
25
45
 
26
- const confirmedParam = getConfirmedParameter(url);
27
- const confirmed = confirmedParam === null ? false : confirmedParam; // invalid -> fallback to unconfirmed
28
-
29
- // TODO: VALIDATION?
30
46
  if (!address) {
31
47
  respond(400, { error: 'Wallet address is required' });
32
48
  return;
33
49
  }
34
50
 
35
- const nodeInfo = await getBalance(msbInstance, address, confirmed);
36
- const balance = nodeInfo?.balance || 0;
51
+ const hrp = msbInstance.config.addressPrefix;
52
+ if (!isAddressValid(address, hrp)) {
53
+ respond(400, { error: 'Invalid account address format' });
54
+ return;
55
+ }
56
+
57
+ const nodeInfo = await getBalance(msbInstance, address, getConfirmedParameter(url) ?? false);
58
+ const balance = nodeInfo?.balance || "0";
59
+
37
60
  respond(200, { address, balance });
38
61
  }
39
62
 
@@ -54,44 +77,75 @@ export async function handleConfirmedLength({ msbInstance, respond }) {
54
77
 
55
78
  export async function handleBroadcastTransaction({ msbInstance, config, respond, req }) {
56
79
  let body = '';
80
+ const MAX_BODY_SIZE = 2_000_000;
81
+ let limitExceeded = false;
82
+
57
83
  req.on('data', chunk => {
84
+ if (limitExceeded) return;
58
85
  body += chunk.toString();
86
+ if (body.length > MAX_BODY_SIZE) {
87
+ limitExceeded = true;
88
+ respond(413, { error: 'Payload too large.' });
89
+ req.resume();
90
+ }
59
91
  });
60
92
 
61
93
  req.on('end', async () => {
94
+ if (limitExceeded) return;
95
+
62
96
  try {
63
- const { payload } = JSON.parse(body);
97
+ if (!body) {
98
+ throw new ValidationError("Invalid JSON payload.");
99
+ }
100
+
101
+ let parsedBody;
102
+ try {
103
+ parsedBody = JSON.parse(body);
104
+ } catch (e) {
105
+ throw new ValidationError("Invalid JSON payload.");
106
+ }
107
+
108
+ const { payload } = parsedBody;
64
109
  if (!payload) {
65
- return respond(400, { error: 'Payload is missing.' });
110
+ throw new ValidationError("Payload is missing.");
66
111
  }
67
112
 
68
113
  if (!isBase64(payload)) {
69
- return respond(400, { error: 'Payload must be a valid base64 string.' });
114
+ throw new ValidationError("Payload must be a valid base64 string.");
70
115
  }
71
116
 
72
117
  const decodedPayload = decodeBase64Payload(payload);
73
118
  validatePayloadStructure(decodedPayload);
74
119
  const sanitizedPayload = sanitizeTransferPayload(decodedPayload);
120
+
75
121
  const result = await broadcastTransaction(msbInstance, config, sanitizedPayload);
76
122
  respond(200, { result });
77
- } catch (error) {
78
- let code = error instanceof SyntaxError ? 400 : 500;
79
- let errorMsg = code === 400 ? 'Invalid JSON payload.' : 'An error occurred processing the transaction.'
80
123
 
81
- if (error.message.includes("Failed to broadcast transaction after multiple attempts.")) {
124
+ } catch (error) {
125
+ let code = 500;
126
+ let errorMsg = 'An error occurred processing the transaction.';
127
+
128
+ if (error instanceof ValidationError || error instanceof SyntaxError) {
129
+ code = 400;
130
+ errorMsg = error.message;
131
+ }
132
+ else if (error instanceof BroadcastError) {
82
133
  code = 429;
83
- errorMsg = "Failed to broadcast transaction after multiple attempts."
134
+ errorMsg = error.message;
135
+ }
136
+
137
+ if (code === 500) {
138
+ console.error('Error in handleBroadcastTransaction:', error);
84
139
  }
85
140
 
86
- console.error('Error in handleBroadcastTransaction:', error);
87
- // Use 400 for client errors (like bad JSON), 500 for server/command errors
88
141
  respond(code, { error: errorMsg });
89
142
  }
90
143
  });
91
144
 
92
145
  req.on('error', (err) => {
93
- console.error('Stream error in handleBroadcastTransaction:', err);
94
- respond(500, { error: 'Request stream failed during body transfer.' });
146
+ if (!limitExceeded) {
147
+ respond(500, { error: 'Request stream failed during body transfer.' });
148
+ }
95
149
  });
96
150
  }
97
151
 
@@ -104,18 +158,14 @@ export async function handleTxHashes({ msbInstance, respond, req }) {
104
158
  const startSignedLength = parseInt(startSignedLengthStr);
105
159
  const endSignedLength = parseInt(endSignedLengthStr);
106
160
 
107
- // 1. Check if the parsed values are valid numbers
108
161
  if (isNaN(startSignedLength) || isNaN(endSignedLength)) {
109
162
  return respond(400, { error: 'Params must be integer' });
110
163
  }
111
164
 
112
- // 2. Check for non-negative numbers
113
- // The requirement is "non-negative," which includes 0.
114
165
  if (startSignedLength < 0 || endSignedLength < 0) {
115
166
  return respond(400, { error: 'Params must be non-negative' });
116
167
  }
117
168
 
118
- // 3. endSignedLength must be >= startSignedLength
119
169
  if (endSignedLength < startSignedLength) {
120
170
  return respond(400, { error: 'endSignedLength must be greater than or equal to startSignedLength.' });
121
171
  }
@@ -124,13 +174,9 @@ export async function handleTxHashes({ msbInstance, respond, req }) {
124
174
  return respond(400, { error: `The max range for signedLength must be ${MAX_SIGNED_LENGTH}.` });
125
175
  }
126
176
 
127
- // 4. Get current confirmed length
128
177
  const currentConfirmedLength = await getConfirmedLength(msbInstance);
129
-
130
- // 5. Adjust the end index to not exceed the confirmed length.
131
178
  const adjustedEndLength = Math.min(endSignedLength, currentConfirmedLength)
132
179
 
133
- // 6. Fetch txs hashes for the adjusted range, assuming the command takes start and end index.
134
180
  const { hashes } = await getTxHashes(msbInstance, startSignedLength, adjustedEndLength);
135
181
  respond(200, { hashes });
136
182
  }
@@ -141,57 +187,111 @@ export async function handleUnconfirmedLength({ msbInstance, respond }) {
141
187
  }
142
188
 
143
189
  export async function handleTransactionDetails({ msbInstance, respond, req }) {
144
- const hash = req.url.split('/')[3];
145
- const txDetails = await getTxDetails(msbInstance, hash);
146
- respond(txDetails === null ? 404 : 200 , { txDetails });
190
+ if (hasSpacesInUrl(req.url)) {
191
+ return respond(400, { error: "Invalid transaction hash format" });
192
+ }
193
+
194
+ const url = buildRequestUrl(req);
195
+ const parts = url.pathname.split('/').filter(Boolean);
196
+ const rawHash = parts[parts.length - 1];
197
+
198
+ if (!rawHash || rawHash === 'tx') {
199
+ return respond(400, { error: "Transaction hash is required" });
200
+ }
201
+
202
+ const normalizedHash = rawHash.toLowerCase();
203
+ if (!isValidTxHash(normalizedHash)) {
204
+ return respond(400, { error: "Invalid transaction hash format" });
205
+ }
206
+
207
+ try {
208
+ const txDetails = await getTxDetails(msbInstance, normalizedHash);
209
+ respond(200, { txDetails });
210
+ } catch (error) {
211
+ let code = 500;
212
+ let errorMsg = "Internal error";
213
+
214
+ if (error instanceof NotFoundError) {
215
+ code = 404;
216
+ errorMsg = error.message;
217
+ }
218
+
219
+ respond(code, { [code === 404 ? 'txDetails' : 'error']: code === 404 ? null : errorMsg });
220
+ }
221
+ }
222
+
223
+ export async function handleTransactionExtendedDetails({ msbInstance, respond, req }) {
224
+ if (hasSpacesInUrl(req.url)) {
225
+ return respond(400, { error: "Invalid transaction hash format" });
226
+ }
227
+
228
+ const url = buildRequestUrl(req);
229
+ const pathParts = url.pathname.split('/').filter(Boolean);
230
+ const hashRaw = pathParts[pathParts.length - 1];
231
+
232
+ if (!hashRaw || hashRaw === 'details' || hashRaw === 'tx') {
233
+ return respond(400, { error: "Transaction hash is required" });
234
+ }
235
+
236
+ const hash = hashRaw.toLowerCase();
237
+ if (!isValidTxHash(hash)) {
238
+ return respond(400, { error: "Invalid transaction hash format" });
239
+ }
240
+
241
+ const confirmed = getConfirmedParameter(url);
242
+ if (confirmed === null) {
243
+ return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
244
+ }
245
+
246
+ try {
247
+ const details = await getExtendedTxDetails(msbInstance, hash, confirmed);
248
+ respond(200, details);
249
+ } catch (error) {
250
+ let code = 500;
251
+ let errorMsg = 'An error occurred processing the request.';
252
+
253
+ if (error instanceof NotFoundError) {
254
+ code = 404;
255
+ errorMsg = error.message;
256
+ }
257
+
258
+ respond(code, { error: errorMsg });
259
+ }
147
260
  }
148
261
 
149
262
  export async function handleFetchBulkTxPayloads({ msbInstance, respond, req }) {
150
263
  let body = ''
151
264
  let bytesRead = 0;
152
265
  let limitBytes = 1_000_000;
153
- let headersSent = false; // Add a flag to prevent double response
266
+ let headersSent = false;
154
267
 
155
268
  req.on('data', chunk => {
156
- if (headersSent) return; // Stop processing if response has started/errored
157
-
269
+ if (headersSent) return;
158
270
  bytesRead += chunk.length;
159
271
  if (bytesRead > limitBytes) {
160
272
  respond(413, { error: 'Request body too large.' });
161
273
  headersSent = true;
162
- req.destroy(); // Stop receiving data (GOOD PRACTICE)
274
+ req.destroy();
163
275
  return;
164
276
  }
165
277
  body += chunk.toString();
166
278
  });
167
279
 
168
280
  req.on('end', async () => {
169
- if (headersSent) return; // Don't process if an error already occurred
170
-
281
+ if (headersSent) return;
171
282
 
172
283
  try {
173
- if (body === null || body === '') {
174
- return respond(400, { error: 'Missing payload.' });
284
+ if (!body) {
285
+ throw new ValidationError("Missing payload.");
175
286
  }
176
287
 
177
288
  const sanitizedPayload = sanitizeBulkPayloadsRequestBody(body);
178
-
179
- if (sanitizedPayload === null) {
180
- return respond(400, { error: 'Invalid payload.' });
289
+ if (!sanitizedPayload) {
290
+ throw new ValidationError("Invalid payload.");
181
291
  }
182
292
 
183
293
  const { hashes } = sanitizedPayload;
184
-
185
- if (!Array.isArray(hashes) || hashes.length === 0) {
186
- return respond(400, { error: 'Missing hash list.' });
187
- }
188
-
189
- if (hashes.length > 1500) {
190
- return respond(413, { error: 'Too many hashes. Max 1500 allowed per request.' });
191
- }
192
-
193
294
  const uniqueHashes = [...new Set(hashes)];
194
-
195
295
  const commandResult = await fetchBulkTxPayloads(msbInstance, uniqueHashes);
196
296
 
197
297
  const responseString = JSON.stringify(commandResult);
@@ -201,50 +301,23 @@ export async function handleFetchBulkTxPayloads({ msbInstance, respond, req }) {
201
301
 
202
302
  return respond(200, commandResult);
203
303
  } catch (error) {
204
- console.error('Error in handleFetchBulkTxPayloads:', error);
205
- // Use 400 for JSON errors, 500 otherwise
206
- const code = error instanceof SyntaxError ? 400 : 500;
207
- respond(code, { error: code === 400 ? 'Invalid request body format.' : 'An internal error occurred.' });
304
+ let code = 500;
305
+ let errorMsg = 'An internal error occurred.';
306
+
307
+ if (error instanceof ValidationError || error instanceof SyntaxError) {
308
+ code = 400;
309
+ errorMsg = error instanceof SyntaxError ? 'Invalid request body format.' : error.message;
310
+ }
311
+
312
+ respond(code, { error: errorMsg });
208
313
  }
209
314
  })
210
315
 
211
316
  req.on('error', (err) => {
212
- console.error('Stream error in handleFetchBulkTxPayloads:', err);
213
317
  respond(500, { error: 'Request stream failed during body transfer.' });
214
318
  });
215
319
  }
216
320
 
217
- export async function handleTransactionExtendedDetails({ msbInstance, respond, req }) {
218
- const url = buildRequestUrl(req);
219
- const pathParts = url.pathname.split('/').filter(Boolean);
220
- const hash = pathParts[3];
221
-
222
- if (!hash) {
223
- return respond(400, { error: "Transaction hash is required" });
224
- }
225
-
226
- if (isHexString(hash) === false || hash.length !== 64) {
227
- return respond(400, { error: "Invalid transaction hash format" });
228
- }
229
-
230
- const confirmed = getConfirmedParameter(url);
231
- if (confirmed === null) {
232
- return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
233
- }
234
-
235
- try {
236
- const details = await getExtendedTxDetails(msbInstance, hash, confirmed);
237
- respond(200, details);
238
- } catch (error) {
239
- if (error.message?.includes('No payload found for tx hash')) {
240
- respond(404, { error: error.message });
241
- } else {
242
- console.error('Error in handleTransactionDetails:', error);
243
- respond(500, { error: 'An error occurred processing the request.' });
244
- }
245
- }
246
- }
247
-
248
321
  export async function handleAccountDetails({ msbInstance, respond, req }) {
249
322
  const url = buildRequestUrl(req);
250
323
  const address = url.pathname.split('/').filter(Boolean)[2];
@@ -292,4 +365,4 @@ export async function handleAccountDetails({ msbInstance, respond, req }) {
292
365
  balance: bufferToBigInt(nodeEntry.balance).toString(),
293
366
  stakedBalance: bufferToBigInt(nodeEntry.stakedBalance).toString(),
294
367
  });
295
- }
368
+ }
package/rpc/routes/v1.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ handleHealth,
2
3
  handleBalance,
3
4
  handleTxv,
4
5
  handleFee,
@@ -9,10 +10,11 @@ import {
9
10
  handleTransactionDetails,
10
11
  handleFetchBulkTxPayloads,
11
12
  handleTransactionExtendedDetails,
12
- handleAccountDetails
13
+ handleAccountDetails,
13
14
  } from '../handlers.js';
14
15
 
15
16
  export const v1Routes = [
17
+ { method: 'GET', path: '/health', handler: handleHealth },
16
18
  { method: 'GET', path: '/balance', handler: handleBalance },
17
19
  { method: 'GET', path: '/txv', handler: handleTxv },
18
20
  { method: 'GET', path: '/fee', handler: handleFee },
package/rpc/rpc_server.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createServer } from "./create_server.js";
2
2
 
3
3
  // Called by msb.mjs file
4
- export function startRpcServer(msbInstance, config ,host, port) {
4
+ export function startRpcServer(msbInstance, config) {
5
5
  const server = createServer(msbInstance, config)
6
6
 
7
- return server.listen(port, host, () => {
8
- console.log(`Running RPC with http at http://${host}:${port}`);
7
+ return server.listen(config.port, config.host, () => {
8
+ console.log(`Running RPC with http at http://${config.host}:${config.port}`);
9
9
  });
10
10
  }
@@ -5,10 +5,11 @@ import {
5
5
  normalizeTransferOperation
6
6
  } from "../src/utils/normalizers.js";
7
7
  import { get_confirmed_tx_info, get_unconfirmed_tx_info } from "../src/utils/cli.js";
8
- import {OperationType} from "../src/utils/constants.js";
8
+ import { OperationType } from "../src/utils/constants.js";
9
9
  import b4a from "b4a";
10
10
  import PartialTransaction from "../src/core/network/protocols/shared/validators/PartialTransaction.js";
11
11
  import PartialTransfer from "../src/core/network/protocols/shared/validators/PartialTransfer.js";
12
+ import { ValidationError, BroadcastError, NotFoundError } from "./utils/helpers.js";
12
13
 
13
14
  export async function getBalance(msbInstance, address, confirmed) {
14
15
  const state = msbInstance.state;
@@ -46,14 +47,15 @@ export async function getUnconfirmedLength(msbInstance) {
46
47
 
47
48
  export async function broadcastTransaction(msbInstance, config, payload) {
48
49
  if (!payload) {
49
- throw new Error("Transaction payload is required for broadcasting.");
50
+ throw new ValidationError("Transaction payload is required for broadcasting.");
50
51
  }
52
+
51
53
  let normalizedPayload;
52
54
  let isValid = false;
53
55
  let hash;
54
56
 
55
- const partialTransferValidator = new PartialTransfer(msbInstance.state, null , config);
56
- const partialTransactionValidator = new PartialTransaction(msbInstance.state, null , config);
57
+ const partialTransferValidator = new PartialTransfer(msbInstance.state, null, config);
58
+ const partialTransactionValidator = new PartialTransaction(msbInstance.state, null, config);
57
59
 
58
60
  if (payload.type === OperationType.TRANSFER) {
59
61
  normalizedPayload = normalizeTransferOperation(payload, config);
@@ -66,19 +68,24 @@ export async function broadcastTransaction(msbInstance, config, payload) {
66
68
  }
67
69
 
68
70
  if (!isValid) {
69
- throw new Error("Invalid transaction payload.");
71
+ throw new ValidationError("Invalid transaction payload.");
70
72
  }
71
73
 
72
74
  const success = await msbInstance.broadcastPartialTransaction(payload);
73
75
 
74
76
  if (!success) {
75
- throw new Error("Failed to broadcast transaction after multiple attempts.");
77
+ throw new BroadcastError("Failed to broadcast transaction after multiple attempts.");
76
78
  }
77
79
 
78
80
  const signedLength = msbInstance.state.getSignedLength();
79
81
  const unsignedLength = msbInstance.state.getUnsignedLength();
80
82
 
81
- return { message: "Transaction broadcasted successfully.", signedLength, unsignedLength, tx: hash };
83
+ return {
84
+ message: "Transaction broadcasted successfully.",
85
+ signedLength,
86
+ unsignedLength,
87
+ tx: hash
88
+ };
82
89
  }
83
90
 
84
91
  export async function getTxHashes(msbInstance, start, end) {
@@ -89,7 +96,7 @@ export async function getTxHashes(msbInstance, start, end) {
89
96
  export async function getTxDetails(msbInstance, hash) {
90
97
  const rawPayload = await get_confirmed_tx_info(msbInstance.state, hash);
91
98
  if (!rawPayload) {
92
- return null;
99
+ throw new NotFoundError(`Transaction ${hash} not found.`);
93
100
  }
94
101
 
95
102
  return normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
@@ -97,11 +104,11 @@ export async function getTxDetails(msbInstance, hash) {
97
104
 
98
105
  export async function fetchBulkTxPayloads(msbInstance, hashes) {
99
106
  if (!Array.isArray(hashes) || hashes.length === 0) {
100
- throw new Error("Missing hash list.");
107
+ throw new ValidationError("Missing hash list.");
101
108
  }
102
109
 
103
110
  if (hashes.length > 1500) {
104
- throw new Error("Length of input tx hashes exceeded.");
111
+ throw new ValidationError("Length of input tx hashes exceeded.");
105
112
  }
106
113
 
107
114
  const res = { results: [], missing: [] };
@@ -111,7 +118,7 @@ export async function fetchBulkTxPayloads(msbInstance, hashes) {
111
118
 
112
119
  results.forEach((result, index) => {
113
120
  const hash = hashes[index];
114
- if (result === null || result === undefined) {
121
+ if (!result) {
115
122
  res.missing.push(hash);
116
123
  } else {
117
124
  const decodedResult = normalizeDecodedPayloadForJson(result.decoded, msbInstance.config);
@@ -124,32 +131,21 @@ export async function fetchBulkTxPayloads(msbInstance, hashes) {
124
131
 
125
132
  export async function getExtendedTxDetails(msbInstance, hash, confirmed) {
126
133
  const state = msbInstance.state;
134
+ let rawPayload;
127
135
 
128
136
  if (confirmed) {
129
- const rawPayload = await get_confirmed_tx_info(state, hash);
130
- if (!rawPayload) {
131
- throw new Error(`No payload found for tx hash: ${hash}`);
132
- }
133
- const confirmedLength = await state.getTransactionConfirmedLength(hash);
134
- if (confirmedLength === null) {
135
- throw new Error(`No confirmed length found for tx hash: ${hash} in confirmed mode`);
136
- }
137
- const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
138
- const feeBuffer = state.getFee();
139
- return {
140
- txDetails: normalizedPayload,
141
- confirmed_length: confirmedLength,
142
- fee: bufferToBigInt(feeBuffer).toString(),
143
- };
137
+ rawPayload = await get_confirmed_tx_info(state, hash);
138
+ } else {
139
+ rawPayload = await get_unconfirmed_tx_info(state, hash);
144
140
  }
145
141
 
146
- const rawPayload = await get_unconfirmed_tx_info(state, hash);
147
142
  if (!rawPayload) {
148
- throw new Error(`No payload found for tx hash: ${hash}`);
143
+ throw new NotFoundError(`No payload found for tx hash: ${hash}`);
149
144
  }
150
145
 
151
146
  const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, msbInstance.config);
152
147
  const length = await state.getTransactionConfirmedLength(hash);
148
+
153
149
  if (length === null) {
154
150
  return {
155
151
  txDetails: normalizedPayload,
@@ -164,4 +160,4 @@ export async function getExtendedTxDetails(msbInstance, hash, confirmed) {
164
160
  confirmed_length: length,
165
161
  fee: bufferToBigInt(feeBuffer).toString(),
166
162
  };
167
- }
163
+ }