trac-msb 0.2.8 → 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 (151) 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 +5 -22
  9. package/package.json +14 -10
  10. package/proto/network.proto +74 -0
  11. package/rpc/create_server.js +2 -2
  12. package/rpc/handlers.js +165 -92
  13. package/rpc/routes/v1.js +3 -1
  14. package/rpc/rpc_server.js +4 -4
  15. package/rpc/rpc_services.js +62 -25
  16. package/rpc/utils/helpers.js +83 -52
  17. package/src/config/args.js +46 -0
  18. package/src/config/config.js +78 -5
  19. package/src/config/env.js +70 -3
  20. package/src/core/network/Network.js +34 -70
  21. package/src/core/network/identity/NetworkWalletFactory.js +2 -2
  22. package/src/core/network/protocols/LegacyProtocol.js +67 -0
  23. package/src/core/network/protocols/NetworkMessages.js +48 -0
  24. package/src/core/network/protocols/ProtocolInterface.js +31 -0
  25. package/src/core/network/protocols/ProtocolSession.js +59 -0
  26. package/src/core/network/protocols/V1Protocol.js +64 -0
  27. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +84 -0
  28. package/src/core/network/protocols/legacy/handlers/GetRequestHandler.js +53 -0
  29. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +37 -0
  30. package/src/core/network/{messaging → protocols/legacy}/validators/base/BaseResponse.js +2 -3
  31. package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +88 -0
  32. package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +93 -0
  33. package/src/core/network/{messaging → protocols/shared}/handlers/TransferOperationHandler.js +17 -16
  34. package/src/core/network/{messaging → protocols/shared}/handlers/base/BaseOperationHandler.js +10 -15
  35. package/src/core/network/{messaging → protocols/shared}/validators/PartialBootstrapDeployment.js +2 -2
  36. package/src/core/network/{messaging → protocols/shared}/validators/PartialRoleAccess.js +5 -5
  37. package/src/core/network/{messaging → protocols/shared}/validators/PartialTransaction.js +4 -4
  38. package/src/core/network/{messaging → protocols/shared}/validators/PartialTransfer.js +4 -4
  39. package/src/core/network/{messaging → protocols/shared}/validators/base/PartialOperation.js +14 -12
  40. package/src/core/network/protocols/v1/NetworkMessageRouter.js +15 -0
  41. package/src/core/network/services/ConnectionManager.js +5 -5
  42. package/src/core/network/services/MessageOrchestrator.js +2 -2
  43. package/src/core/network/services/TransactionPoolService.js +5 -6
  44. package/src/core/network/services/TransactionRateLimiterService.js +12 -13
  45. package/src/core/network/services/ValidatorObserverService.js +5 -6
  46. package/src/core/state/State.js +3 -5
  47. package/src/index.js +156 -181
  48. package/src/messages/network/v1/NetworkMessageBuilder.js +325 -0
  49. package/src/messages/network/v1/NetworkMessageDirector.js +137 -0
  50. package/src/messages/network/v1/networkMessageFactory.js +12 -0
  51. package/src/messages/state/ApplyStateMessageBuilder.js +661 -0
  52. package/src/messages/state/ApplyStateMessageDirector.js +516 -0
  53. package/src/messages/state/applyStateMessageFactory.js +12 -0
  54. package/src/utils/buffer.js +53 -1
  55. package/src/utils/check.js +1 -1
  56. package/src/utils/cli.js +0 -8
  57. package/src/utils/constants.js +33 -30
  58. package/src/utils/fileUtils.js +13 -0
  59. package/src/utils/normalizers.js +84 -2
  60. package/src/utils/protobuf/network.cjs +840 -0
  61. package/src/utils/protobuf/operationHelpers.js +10 -0
  62. package/src/utils/type.js +26 -0
  63. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  64. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  65. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  66. package/tests/acceptance/v1/rpc.test.mjs +4 -3
  67. package/tests/acceptance/v1/tx/tx.test.mjs +27 -16
  68. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +26 -12
  69. package/tests/fixtures/check.fixtures.js +33 -32
  70. package/tests/fixtures/networkV1.fixtures.js +85 -0
  71. package/tests/fixtures/protobuf.fixtures.js +109 -25
  72. package/tests/helpers/StateNetworkFactory.js +2 -2
  73. package/tests/helpers/address.js +6 -0
  74. package/tests/helpers/autobaseTestHelpers.js +2 -1
  75. package/tests/helpers/config.js +2 -1
  76. package/tests/helpers/setupApplyTests.js +59 -56
  77. package/tests/unit/messages/messages.test.js +12 -0
  78. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +276 -0
  79. package/tests/unit/messages/network/NetworkMessageDirector.test.js +201 -0
  80. package/tests/unit/messages/state/applyStateMessageBuilder.complete.test.js +521 -0
  81. package/tests/unit/messages/state/applyStateMessageBuilder.partial.test.js +233 -0
  82. package/tests/unit/network/ConnectionManager.test.js +6 -5
  83. package/tests/unit/network/networkModule.test.js +3 -2
  84. package/tests/unit/state/apply/addAdmin/addAdminHappyPathScenario.js +10 -6
  85. package/tests/unit/state/apply/addAdmin/addAdminScenarioHelpers.js +9 -6
  86. package/tests/unit/state/apply/addAdmin/state.apply.addAdmin.test.js +10 -7
  87. package/tests/unit/state/apply/addIndexer/addIndexerScenarioHelpers.js +18 -21
  88. package/tests/unit/state/apply/addWriter/addWriterScenarioHelpers.js +53 -38
  89. package/tests/unit/state/apply/adminRecovery/adminRecoveryScenarioHelpers.js +46 -35
  90. package/tests/unit/state/apply/appendWhitelist/appendWhitelistScenarioHelpers.js +13 -16
  91. package/tests/unit/state/apply/balanceInitialization/balanceInitializationScenarioHelpers.js +17 -11
  92. package/tests/unit/state/apply/banValidator/banValidatorScenarioHelpers.js +11 -12
  93. package/tests/unit/state/apply/bootstrapDeployment/bootstrapDeploymentScenarioHelpers.js +9 -7
  94. package/tests/unit/state/apply/common/commonScenarioHelper.js +15 -14
  95. package/tests/unit/state/apply/common/payload-structure/initializationDisabledScenario.js +9 -4
  96. package/tests/unit/state/apply/disableInitialization/disableInitializationScenarioHelpers.js +17 -11
  97. package/tests/unit/state/apply/removeWriter/removeWriterScenarioHelpers.js +19 -14
  98. package/tests/unit/state/apply/transfer/transferDoubleSpendAcrossValidatorsScenario.js +37 -29
  99. package/tests/unit/state/apply/transfer/transferScenarioHelpers.js +9 -7
  100. package/tests/unit/state/apply/txOperation/txOperationScenarioHelpers.js +11 -9
  101. package/tests/unit/unit.test.js +1 -1
  102. package/tests/unit/utils/buffer/buffer.test.js +62 -1
  103. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  104. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  105. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  106. package/tests/unit/utils/normalizers/normalizers.test.js +469 -0
  107. package/tests/unit/utils/protobuf/operationHelpers.test.js +120 -2
  108. package/tests/unit/utils/type/type.test.js +25 -0
  109. package/tests/unit/utils/utils.test.js +1 -0
  110. package/docs/networking-dualstack-plan.md +0 -75
  111. package/docs/networking-layer-redesign.md +0 -155
  112. package/src/core/network/messaging/NetworkMessages.js +0 -64
  113. package/src/core/network/messaging/handlers/GetRequestHandler.js +0 -113
  114. package/src/core/network/messaging/handlers/ResponseHandler.js +0 -107
  115. package/src/core/network/messaging/handlers/RoleOperationHandler.js +0 -114
  116. package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +0 -149
  117. package/src/core/network/messaging/routes/NetworkMessageRouter.js +0 -98
  118. package/src/core/network/messaging/validators/AdminResponse.js +0 -58
  119. package/src/core/network/messaging/validators/CustomNodeResponse.js +0 -46
  120. package/src/messages/base/StateBuilder.js +0 -25
  121. package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +0 -425
  122. package/src/messages/completeStateMessages/CompleteStateMessageDirector.js +0 -252
  123. package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +0 -296
  124. package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +0 -272
  125. package/src/messages/partialStateMessages/PartialStateMessageDirector.js +0 -137
  126. package/src/messages/partialStateMessages/PartialStateMessageOperations.js +0 -138
  127. package/tests/integration/apply/addAdmin/addAdminBasic.test.js +0 -69
  128. package/tests/integration/apply/addAdmin/addAdminRecovery.test.js +0 -126
  129. package/tests/integration/apply/addIndexer.test.js +0 -239
  130. package/tests/integration/apply/addWhitelist.test.js +0 -53
  131. package/tests/integration/apply/addWriter.test.js +0 -245
  132. package/tests/integration/apply/apply.test.js +0 -19
  133. package/tests/integration/apply/banValidator.test.js +0 -116
  134. package/tests/integration/apply/postTx/invalidSubValues.test.js +0 -103
  135. package/tests/integration/apply/postTx/postTx.test.js +0 -196
  136. package/tests/integration/apply/removeIndexer.test.js +0 -132
  137. package/tests/integration/apply/removeWriter.test.js +0 -168
  138. package/tests/integration/apply/transfer.test.js +0 -83
  139. package/tests/integration/integration.test.js +0 -9
  140. package/tests/unit/messageOperations/assembleAddIndexerMessage.test.js +0 -21
  141. package/tests/unit/messageOperations/assembleAddWriterMessage.test.js +0 -17
  142. package/tests/unit/messageOperations/assembleAdminMessage.test.js +0 -68
  143. package/tests/unit/messageOperations/assembleBanWriterMessage.test.js +0 -17
  144. package/tests/unit/messageOperations/assemblePostTransaction.test.js +0 -424
  145. package/tests/unit/messageOperations/assembleRemoveIndexerMessage.test.js +0 -19
  146. package/tests/unit/messageOperations/assembleRemoveWriterMessage.test.js +0 -17
  147. package/tests/unit/messageOperations/assembleWhitelistMessages.test.js +0 -59
  148. package/tests/unit/messageOperations/commonsStateMessageOperationsTest.js +0 -278
  149. package/tests/unit/messageOperations/stateMessageOperations.test.js +0 -19
  150. /package/src/core/network/{messaging → protocols/legacy}/validators/ValidatorResponse.js +0 -0
  151. /package/src/utils/{operations.js → applyOperations.js} +0 -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
-
19
- const msb = new MainSettlementBus(createConfig(ENV.MAINNET, options));
5
+ const config = resolveConfig()
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, 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.8",
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,13 +42,15 @@
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": "0.0.43-msb-r2.9",
53
- "tty": "npm:bare-node-tty"
51
+ "trac-wallet": "1.0.2",
52
+ "tty": "npm:bare-node-tty",
53
+ "uuid": "^13.0.0"
54
54
  },
55
55
  "devDependencies": {
56
56
  "bare-os": "^3.6.1",
@@ -66,5 +66,9 @@
66
66
  "publishConfig": {
67
67
  "registry": "https://registry.npmjs.org",
68
68
  "access": "public"
69
+ },
70
+ "repository": {
71
+ "type": "git",
72
+ "url": "https://github.com/Trac-Systems/main_settlement_bus"
69
73
  }
70
74
  }
@@ -0,0 +1,74 @@
1
+ syntax = "proto3";
2
+
3
+ package network.v1;
4
+
5
+ enum MessageType {
6
+ MESSAGE_TYPE_UNSPECIFIED = 0;
7
+ MESSAGE_TYPE_VALIDATOR_CONNECTION_REQUEST = 1;
8
+ MESSAGE_TYPE_VALIDATOR_CONNECTION_RESPONSE = 2;
9
+ MESSAGE_TYPE_LIVENESS_REQUEST = 3;
10
+ MESSAGE_TYPE_LIVENESS_RESPONSE = 4;
11
+ MESSAGE_TYPE_BROADCAST_TRANSACTION_REQUEST = 5;
12
+ MESSAGE_TYPE_BROADCAST_TRANSACTION_RESPONSE = 6;
13
+ }
14
+
15
+ enum ResultCode {
16
+ RESULT_CODE_UNSPECIFIED = 0;
17
+ RESULT_CODE_OK = 1;
18
+ RESULT_CODE_INVALID_PAYLOAD = 2;
19
+ RESULT_CODE_UNSUPPORTED_VERSION = 3;
20
+ RESULT_CODE_RATE_LIMITED = 4;
21
+ RESULT_CODE_TIMEOUT = 5;
22
+ RESULT_CODE_SIGNATURE_INVALID = 6;
23
+ }
24
+
25
+ message ValidatorConnectionRequest {
26
+ string issuer_address = 1;
27
+ bytes nonce = 2;
28
+ bytes signature = 3;
29
+ }
30
+
31
+ message ValidatorConnectionResponse {
32
+ string issuer_address = 1;
33
+ bytes nonce = 2;
34
+ bytes signature = 3;
35
+ ResultCode result = 4;
36
+ }
37
+
38
+ message LivenessRequest {
39
+ bytes nonce = 1;
40
+ bytes signature = 2;
41
+ }
42
+
43
+ message LivenessResponse {
44
+ bytes nonce = 1;
45
+ bytes signature = 2;
46
+ ResultCode result = 3;
47
+ }
48
+
49
+ message BroadcastTransactionRequest {
50
+ bytes data = 1; // binary encoded payload
51
+ bytes nonce = 2;
52
+ bytes signature = 3;
53
+ }
54
+
55
+ message BroadcastTransactionResponse {
56
+ bytes nonce = 1;
57
+ bytes signature = 2;
58
+ ResultCode result = 3;
59
+ }
60
+
61
+ message MessageHeader {
62
+ MessageType type = 1;
63
+ string id = 2;
64
+ uint64 timestamp = 3;
65
+ oneof field {
66
+ ValidatorConnectionRequest validator_connection_request = 4;
67
+ ValidatorConnectionResponse validator_connection_response = 5;
68
+ LivenessRequest liveness_request = 6;
69
+ LivenessResponse liveness_response = 7;
70
+ BroadcastTransactionRequest broadcast_transaction_request = 8;
71
+ BroadcastTransactionResponse broadcast_transaction_response = 9;
72
+ }
73
+ repeated string capabilities = 10;
74
+ }
@@ -3,7 +3,7 @@ import http from 'http'
3
3
  import { applyCors } from './cors.js';
4
4
  import { routes } from './routes/index.js';
5
5
 
6
- export const createServer = (msbInstance) => {
6
+ export const createServer = (msbInstance, config) => {
7
7
  const server = http.createServer({}, async (req, res) => {
8
8
 
9
9
  // --- 1. Define safe 'respond' utility (Payload MUST be an object) ---
@@ -53,7 +53,7 @@ export const createServer = (msbInstance) => {
53
53
  try {
54
54
  // This try/catch covers synchronous errors and errors from awaited promises
55
55
  // within the route.handler function.
56
- await route.handler({ req, res, respond, msbInstance });
56
+ await route.handler({ req, res, respond, msbInstance, config});
57
57
  } catch (error) {
58
58
  // Catch errors thrown directly from the handler (or its awaited parts)
59
59
  console.error(`Error on ${route.path}:`, error);
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
 
@@ -52,46 +75,77 @@ export async function handleConfirmedLength({ msbInstance, respond }) {
52
75
  respond(200, { confirmed_length });
53
76
  }
54
77
 
55
- export async function handleBroadcastTransaction({ msbInstance, respond, req }) {
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);
75
- const result = await broadcastTransaction(msbInstance, sanitizedPayload);
120
+
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, host, port) {
5
- const server = createServer(msbInstance)
4
+ export function startRpcServer(msbInstance, config) {
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
  }