trac-msb 0.1.82 → 0.2.1

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 (142) hide show
  1. package/.github/workflows/CI.yml +42 -0
  2. package/README.md +12 -0
  3. package/migration/.gitkeep +0 -0
  4. package/msb.mjs +40 -7
  5. package/package.json +40 -15
  6. package/proto/applyOperations.proto +117 -0
  7. package/rpc/constants.mjs +1 -0
  8. package/rpc/cors.mjs +15 -0
  9. package/rpc/create_server.mjs +72 -0
  10. package/rpc/handlers.mjs +245 -0
  11. package/rpc/routes/index.mjs +27 -0
  12. package/rpc/routes/v1.mjs +25 -0
  13. package/rpc/rpc_server.mjs +10 -0
  14. package/rpc/utils/helpers.mjs +74 -0
  15. package/scripts/generate-protobufs.js +42 -0
  16. package/src/core/network/Network.js +244 -0
  17. package/src/core/network/messaging/NetworkMessages.js +63 -0
  18. package/src/core/network/messaging/handlers/GetRequestHandler.js +112 -0
  19. package/src/core/network/messaging/handlers/ResponseHandler.js +108 -0
  20. package/src/core/network/messaging/handlers/RoleOperationHandler.js +116 -0
  21. package/src/core/network/messaging/handlers/SubnetworkOperationHandler.js +143 -0
  22. package/src/core/network/messaging/handlers/TransferOperationHandler.js +52 -0
  23. package/src/core/network/messaging/handlers/base/BaseOperationHandler.js +72 -0
  24. package/src/core/network/messaging/routes/NetworkMessageRouter.js +94 -0
  25. package/src/core/network/messaging/validators/AdminResponse.js +58 -0
  26. package/src/core/network/messaging/validators/CustomNodeResponse.js +46 -0
  27. package/src/core/network/messaging/validators/PartialBootstrapDeployment.js +34 -0
  28. package/src/core/network/messaging/validators/PartialRoleAccess.js +137 -0
  29. package/src/core/network/messaging/validators/PartialTransaction.js +64 -0
  30. package/src/core/network/messaging/validators/PartialTransfer.js +76 -0
  31. package/src/core/network/messaging/validators/ValidatorResponse.js +67 -0
  32. package/src/core/network/messaging/validators/base/BaseResponse.js +87 -0
  33. package/src/core/network/messaging/validators/base/PartialOperation.js +214 -0
  34. package/src/core/network/services/ConnectionManager.js +121 -0
  35. package/src/core/network/services/TransactionPoolService.js +99 -0
  36. package/src/core/network/services/TransactionRateLimiterService.js +130 -0
  37. package/src/core/network/services/ValidatorObserverService.js +123 -0
  38. package/src/core/state/State.js +3846 -0
  39. package/src/core/state/utils/address.js +71 -0
  40. package/src/core/state/utils/adminEntry.js +67 -0
  41. package/src/core/state/utils/balance.js +302 -0
  42. package/src/core/state/utils/deploymentEntry.js +75 -0
  43. package/src/core/state/utils/indexerEntry.js +105 -0
  44. package/src/core/state/utils/lengthEntry.js +94 -0
  45. package/src/core/state/utils/nodeEntry.js +346 -0
  46. package/src/core/state/utils/roles.js +53 -0
  47. package/src/core/state/utils/transaction.js +118 -0
  48. package/src/index.js +1095 -827
  49. package/src/messages/base/StateBuilder.js +25 -0
  50. package/src/messages/completeStateMessages/CompleteStateMessageBuilder.js +421 -0
  51. package/src/messages/completeStateMessages/CompleteStateMessageDirector.js +252 -0
  52. package/src/messages/completeStateMessages/CompleteStateMessageOperations.js +299 -0
  53. package/src/messages/partialStateMessages/PartialStateMessageBuilder.js +272 -0
  54. package/src/messages/partialStateMessages/PartialStateMessageDirector.js +137 -0
  55. package/src/messages/partialStateMessages/PartialStateMessageOperations.js +131 -0
  56. package/src/utils/Scheduler.js +118 -0
  57. package/src/utils/amountSerialization.js +110 -0
  58. package/src/utils/buffer.js +62 -0
  59. package/src/utils/check.js +473 -114
  60. package/src/utils/cli.js +138 -0
  61. package/src/utils/constants.js +105 -41
  62. package/src/utils/crypto.js +11 -0
  63. package/src/utils/fileUtils.js +192 -15
  64. package/src/utils/helpers.js +106 -0
  65. package/src/utils/migrationUtils.js +35 -0
  66. package/src/utils/normalizers.js +118 -0
  67. package/src/utils/operations.js +95 -0
  68. package/src/utils/protobuf/applyOperations.cjs +1373 -0
  69. package/src/utils/protobuf/operationHelpers.js +50 -0
  70. package/test/acceptance/v1/rpc.test.mjs +322 -0
  71. package/test/all.test.js +13 -8
  72. package/test/apply/addAdmin/addAdminBasic.test.js +68 -0
  73. package/test/apply/addAdmin/addAdminRecovery.test.js +125 -0
  74. package/test/apply/addIndexer.test.js +238 -0
  75. package/test/apply/addWhitelist.test.js +53 -0
  76. package/test/apply/addWriter.test.js +244 -0
  77. package/test/apply/apply.test.js +19 -0
  78. package/test/apply/banValidator.test.js +110 -0
  79. package/test/apply/postTx/invalidSubValues.test.js +104 -0
  80. package/test/apply/postTx/postTx.test.js +223 -0
  81. package/test/apply/removeIndexer.test.js +128 -0
  82. package/test/apply/removeWriter.test.js +167 -0
  83. package/test/apply/transfer.test.js +81 -0
  84. package/test/buffer/buffer.test.js +190 -0
  85. package/test/check/adminControlOperation.test.js +156 -0
  86. package/test/check/balanceInitializationOperation.test.js +54 -0
  87. package/test/check/bootstrapDeploymentOperation.test.js +105 -0
  88. package/test/check/check.test.js +17 -0
  89. package/test/check/common.test.js +418 -0
  90. package/test/check/coreAdminOperation.test.js +95 -0
  91. package/test/check/roleAccessOperation.test.js +254 -0
  92. package/test/check/transactionOperation.test.js +107 -0
  93. package/test/check/transferOperation.test.js +102 -0
  94. package/test/fileUtils/readAddressesFromWhitelistFile.test.js +93 -0
  95. package/test/fileUtils/readBalanceMigrationFile.test.js +148 -0
  96. package/test/fixtures/apply.fixtures.js +77 -0
  97. package/test/fixtures/assembleMessage.fixtures.js +40 -0
  98. package/test/fixtures/check.fixtures.js +427 -0
  99. package/test/fixtures/protobuf.fixtures.js +303 -0
  100. package/test/functions/amountSerialization.test.js +237 -0
  101. package/test/functions/applyOperations.test.js +91 -0
  102. package/test/functions/createHash.test.js +15 -0
  103. package/test/functions/functions.test.js +14 -0
  104. package/test/functions/isHexString.test.js +12 -0
  105. package/test/functions/normalizeHex.test.js +82 -0
  106. package/test/messageOperations/assembleAddIndexerMessage.test.js +21 -0
  107. package/test/messageOperations/assembleAddWriterMessage.test.js +16 -0
  108. package/test/messageOperations/assembleAdminMessage.test.js +69 -0
  109. package/test/messageOperations/assembleBanWriterMessage.test.js +16 -0
  110. package/test/messageOperations/assemblePostTransaction.test.js +442 -0
  111. package/test/messageOperations/assembleRemoveIndexerMessage.test.js +19 -0
  112. package/test/messageOperations/assembleRemoveWriterMessage.test.js +17 -0
  113. package/test/messageOperations/assembleWhitelistMessages.test.js +58 -0
  114. package/test/messageOperations/commonsStateMessageOperationsTest.js +277 -0
  115. package/test/messageOperations/stateMessageOperations.test.js +19 -0
  116. package/test/migrationUtils/validateAddressFromIncomingFile.test.js +92 -0
  117. package/test/network/ConnectionManager.test.js +219 -0
  118. package/test/network/connectionManagerTests.test.js +9 -0
  119. package/test/protobuf/protobuf.test.js +186 -0
  120. package/test/state/State.test.js +80 -0
  121. package/test/state/apply.addAdmin.basic.test.js +111 -0
  122. package/test/state/stateTestUtils.js +24 -0
  123. package/test/state/stateTests.test.js +23 -0
  124. package/test/state/utils/address.test.js +63 -0
  125. package/test/state/utils/adminEntry.test.js +51 -0
  126. package/test/state/utils/balance.test.js +320 -0
  127. package/test/state/utils/indexerEntry.test.js +83 -0
  128. package/test/state/utils/lengthEntry.test.js +54 -0
  129. package/test/state/utils/nodeEntry.test.js +298 -0
  130. package/test/state/utils/roles.test.js +44 -0
  131. package/test/state/utils/transaction.test.js +97 -0
  132. package/test/utils/regexHelper.js +3 -0
  133. package/test/utils/setupApplyTests.js +577 -0
  134. package/test/utils/wrapper.js +28 -0
  135. package/whitelist/.gitkeep +0 -0
  136. package/Whitelist/pubkeys.csv +0 -1
  137. package/src/network.js +0 -300
  138. package/src/utils/functions.js +0 -70
  139. package/src/utils/msgUtils.js +0 -137
  140. package/test/check.test.js +0 -21
  141. package/test/fileUtils.test.js +0 -16
  142. package/test/functions.test.js +0 -22
@@ -0,0 +1,42 @@
1
+ name: MSB-CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - '*'
10
+ workflow_dispatch:
11
+
12
+ concurrency:
13
+ group: ${{ github.workflow }}-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ jobs:
17
+ tests:
18
+ runs-on: ${{ matrix.os }}
19
+
20
+ strategy:
21
+ matrix:
22
+ node-version: [lts/*]
23
+ os: [ubuntu-latest, macos-latest, windows-latest]
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - name: Use Node.js ${{ matrix.node-version }}
27
+ uses: actions/setup-node@v3
28
+ with:
29
+ node-version: ${{ matrix.node-version }}
30
+ cache: 'npm'
31
+ - name: Install dependencies
32
+ run: npm ci
33
+ - name: Run node tests
34
+ run: npm run test:node
35
+ - name: Install bare
36
+ run: npm i -g bare
37
+ - name: Run bare tests
38
+ run: npm run test:bare
39
+ # For now acceptance tests should be disable due to instability of the running MSB instance in the github actions environment
40
+ # Github machines are not powerful enough to run MSB, and we won't ever finish our tests.
41
+ # - name: Run rpc acceptance tests
42
+ # run: npm run test:acceptance
package/README.md CHANGED
@@ -20,9 +20,21 @@ While the MSB supports native node-js, it is encouraged to use Pear:
20
20
  cd main_settlement_bus
21
21
  npm install -g pear
22
22
  npm install
23
+ ```
24
+
25
+ You can run the node in two modes:
26
+ 1. Regular node (validator/indexer):
27
+ ```js
23
28
  pear run . store1
24
29
  ```
25
30
 
31
+ 2. Admin node (access to administrative commands):
32
+ ```js
33
+ pear run . admin
34
+ ```
35
+
36
+ The admin mode provides access to additional commands such as `/add_admin`, `/add_whitelist`, `/balance_migration`, `/disable_initialization`, `/add_indexer`, `/remove_indexer`, and `/ban_writer`. These commands are only visible and available when running in admin mode.
37
+
26
38
  **Deploy Bootstrap (admin):**
27
39
 
28
40
  - Choose option 1)
File without changes
package/msb.mjs CHANGED
@@ -1,14 +1,47 @@
1
1
  import {MainSettlementBus} from './src/index.js';
2
2
 
3
+ const isPear = typeof Pear !== 'undefined';
4
+ const args = isPear ? Pear.config.args : process.argv.slice(2);
5
+
3
6
  const opts = {
4
- stores_directory : 'stores3/',
7
+ stores_directory : 'stores/',
5
8
  store_name : typeof process !== "undefined" ? process.argv[2] : Pear.config.args[0],
6
- bootstrap: 'a4951e5f744e2a9ceeb875a7965762481dab0a7bb0531a71568e34bf7abd2c53',
7
- channel: '0002tracnetworkmainsettlementbus'
9
+ bootstrap: 'acbc3a4344d3a804101d40e53db1dda82b767646425af73599d4cd6577d69685',
10
+ channel: '0000trac0network0msb0mainnet0000',
11
+ enable_role_requester: false,
12
+ enable_wallet: true,
13
+ enable_validator_observer: true,
14
+ enable_interactive_mode: true,
15
+ disable_rate_limit: false,
16
+ enable_tx_apply_logs: false,
17
+ enable_error_apply_logs: false,
8
18
  };
9
19
 
10
- const msb = new MainSettlementBus(opts);
20
+ const rpc_opts = {
21
+ ...opts,
22
+ enable_tx_apply_logs: false,
23
+ enable_error_apply_logs: false,
24
+
25
+ }
26
+
27
+ const msb = new MainSettlementBus(args.includes('--rpc') ? rpc_opts : opts);
28
+
29
+ msb.ready().then(async () => {
30
+ const runRpc = args.includes('--rpc');
31
+
32
+ if (runRpc) {
33
+ console.log('Starting RPC server...');
34
+ const portIndex = args.indexOf('--port');
35
+ const port = (portIndex !== -1 && args[portIndex + 1]) ? parseInt(args[portIndex + 1], 10) : 5000;
36
+ const hostIndex = args.indexOf('--host');
37
+ const host = (hostIndex !== -1 && args[hostIndex + 1]) ? args[hostIndex + 1] : 'localhost';
38
+
39
+ const {startRpcServer} = await import('./rpc/rpc_server.mjs');
40
+ startRpcServer(msb, host, port);
41
+ } else {
42
+ console.log('RPC server will not be started.');
43
+ }
44
+
45
+ msb.interactiveMode();
46
+ });
11
47
 
12
- msb.ready()
13
- .then(() => { msb.interactiveMode(); })
14
- .catch(function () { });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.1.82",
4
+ "version": "0.2.1",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -9,32 +9,57 @@
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "dev": "pear run -d .",
12
- "test:node": "brittle test/all.test.js",
13
- "test:bare": "bare test/all.test.js"
12
+ "prod": "NODE_OPTIONS='--max-old-space-size=4096' pear run . ${npm_config_store}",
13
+ "dev-rpc": "pear run -d . ${npm_config_store} --rpc --port ${npm_config_port}",
14
+ "prod-rpc": "pear run . ${npm_config_store} --rpc --host ${npm_config_host} --port ${npm_config_port}",
15
+ "protobuf": "node scripts/generate-protobufs.js",
16
+ "test:acceptance": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testTimeout=200000 test/acceptance/",
17
+ "test:node": "brittle-node -t 1200000 test/all.test.js",
18
+ "test:bare": "brittle-bare -t 1200000 test/all.test.js",
19
+ "test:node:cov": "brittle-node -c -t 1200000 test/all.test.js",
20
+ "test:bare:cov": "brittle-bare -c -t 1200000 test/all.test.js"
14
21
  },
15
22
  "dependencies": {
16
- "autobase": "7.6.3",
17
- "hypercore": "11.8.3",
18
- "corestore": "7.4.4",
23
+ "@tracsystems/blake3": "0.0.13",
24
+ "autobase": "7.20.1",
19
25
  "b4a": "1.6.7",
26
+ "bare-crypto": "1.12.0",
27
+ "bare-fs": "4.5.0",
28
+ "bare-http1": "4.1.5",
20
29
  "bare-readline": "1.0.7",
21
30
  "bare-tty": "5.0.2",
22
- "compact-encoding": "^2.16.0",
31
+ "bech32": "2.0.0",
32
+ "compact-encoding": "2.18.0",
33
+ "corestore": "7.5.0",
34
+ "crypto": "npm:bare-node-crypto",
23
35
  "fastest-validator": "1.19.0",
24
- "hyperbee": "^2.24.2",
25
- "hypercore-crypto": "^3.4.0",
26
- "hyperdht": "^6.20.5",
27
- "hyperswarm": "^4.11.5",
28
- "protomux": "^3.10.1",
36
+ "http": "npm:bare-node-http",
37
+ "hyperbee": "2.26.5",
38
+ "hypercore": "11.18.3",
39
+ "hypercore-crypto": "3.6.1",
40
+ "hyperdht": "6.27.0",
41
+ "hyperswarm": "4.14.2",
42
+ "protocol-buffers-encodings": "1.2.0",
43
+ "protomux": "3.10.1",
29
44
  "protomux-wakeup": "2.4.0",
30
45
  "readline": "npm:bare-node-readline",
31
46
  "ready-resource": "1.1.2",
32
- "trac-wallet": "0.0.43",
47
+ "trac-wallet": "0.0.43-msb-r2.8",
33
48
  "tty": "npm:bare-node-tty"
34
49
  },
50
+ "devDependencies": {
51
+ "bare-os": "^3.6.1",
52
+ "bip39-mnemonic": "^2.4.0",
53
+ "brittle": "^3.17.1",
54
+ "esmock": "^2.7.2",
55
+ "jest": "^30.1.3",
56
+ "protocol-buffers": "^4.2.0",
57
+ "sinon": "^21.0.0",
58
+ "supertest": "^7.1.4",
59
+ "trac-crypto-api": "^0.1.1"
60
+ },
35
61
  "publishConfig": {
36
62
  "registry": "https://registry.npmjs.org",
37
- "access": "public",
38
- "brittle": "^3.16.2"
63
+ "access": "public"
39
64
  }
40
65
  }
@@ -0,0 +1,117 @@
1
+ syntax = "proto3";
2
+
3
+ package apply.operations;
4
+
5
+ //DO NOT CHANGE THE ORDER AFTER RELEASE - ONLY APPEND NEW ONES
6
+ enum OperationType {
7
+ UNKNOWN = 0;
8
+ ADD_ADMIN = 1; // complete
9
+ DISABLE_INITIALIZATION = 2; // complete
10
+ BALANCE_INITIALIZATION = 3; // complete
11
+ APPEND_WHITELIST = 4; // complete
12
+ ADD_WRITER = 5; // partial
13
+ REMOVE_WRITER = 6; // partial
14
+ ADMIN_RECOVERY = 7; // partial
15
+ ADD_INDEXER = 8; // complete
16
+ REMOVE_INDEXER = 9; // complete
17
+ BAN_VALIDATOR = 10; // complete
18
+ BOOTSTRAP_DEPLOYMENT = 11; // partial
19
+ TX = 12; // partial
20
+ TRANSFER = 13; // partial
21
+ }
22
+
23
+ // add admin operation, disable_initialization (complete by default)
24
+ message CoreAdminOperation {
25
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
26
+ bytes txv = 2; // Transaction expiration
27
+ bytes iw = 3; // incoming writer key (admin)
28
+ bytes in = 4; // Nonce of the invoker (admin)
29
+ bytes is = 5; // Signature of the invoker (admin)
30
+ }
31
+
32
+ // append_whitelist, add_indexer, remove_indexer, ban_writer (complete by default)
33
+ message AdminControlOperation {
34
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
35
+ bytes txv = 2; // Transaction expiration
36
+ bytes ia = 3; // selected address to specific operation.
37
+ bytes in = 4; // Nonce of the invoker (admin)
38
+ bytes is = 5; // Signature of the invoker (admin)
39
+ }
40
+
41
+ // balance_initialization (complete by default)
42
+ message BalanceInitializationOperation {
43
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
44
+ bytes txv = 2; // Transaction expiration
45
+ bytes ia = 3; // selected address to specific operation.
46
+ bytes am = 4; // Initial balance to be set for the address
47
+ bytes in = 5; // Nonce of the contract invoker (admin)
48
+ bytes is = 6; // Signature of the contract invoker (admin)
49
+ }
50
+ // token_transfer (partial - need to be signed by another writer to be complete)
51
+ message TransferOperation {
52
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
53
+ bytes txv = 2; // Transaction expiration
54
+ bytes to = 3; // Address of the recipient (any account)
55
+ bytes am = 4; // Amount of tokens to transfer
56
+ bytes in = 5; // Nonce of the invoker (any account)
57
+ bytes is = 6; // Signature of the invoker (any account)
58
+ bytes va = 7; // Validator's address (optional)
59
+ bytes vn = 8; // Validator's nonce (optional)
60
+ bytes vs = 9; // Validator's signature (optional)
61
+ }
62
+
63
+ // add_writer, remove_writer, admin_recovery (partial - need to be signed by another writer to be complete)
64
+ message RoleAccessOperation {
65
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
66
+ bytes txv = 2; // Transaction expiration
67
+ bytes iw = 3; // Writing key of the invoker (whitelisted node, admin to be recovered, Trac Network)
68
+ bytes in = 4; // Nonce of the invoker (whitelisted node, admin to be recovered, Trac Network)
69
+ bytes is = 5; // Signature of the invoker (whitelisted node, admin to be recovered)
70
+ bytes va = 6; // Validator's address (optional)
71
+ bytes vn = 7; // Validator's nonce (optional)
72
+ bytes vs = 8; // Validator's signature (optional)
73
+ }
74
+
75
+ // transaction - (partial - need to be signed by another writer to be complete)
76
+ message TxOperation {
77
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
78
+ bytes txv = 2; // Transaction expiration
79
+ bytes iw = 3; // Writing key of the requesting node (external subnetwork)
80
+ bytes ch = 4; // Content hash (hash of the transaction's data)
81
+ bytes bs = 5; // External bootstrap contract
82
+ bytes mbs = 6; // MSB bootstrap key
83
+ bytes in = 7; // Nonce of the requesting node
84
+ bytes is = 8; // Requester's signature
85
+ bytes va = 9; // Validator's address (optional)
86
+ bytes vn = 10; // Validator's nonce (optional)
87
+ bytes vs = 11; // Validator's signature (optional)
88
+ }
89
+
90
+ // bootstrap deployment - (partial - need to be signed by another writer to be complete)
91
+ message BootstrapDeploymentOperation {
92
+ bytes tx = 1; // Transaction hash (unique identifier for the transaction)
93
+ bytes txv = 2; // Transaction expiration
94
+ bytes bs = 3; // Bootstrap contract address
95
+ bytes ic = 4; // Channel on which the contract will operate
96
+ bytes in = 5; // Nonce of the contract invoker
97
+ bytes is = 6; // Signature of the contract invoker
98
+ bytes va = 7; // Validator's address (optional)
99
+ bytes vn = 8; // Validator's nonce (optional)
100
+ bytes vs = 9; // Validator's signature (optional)
101
+ }
102
+
103
+ // Main structure for the operations where messages are
104
+ message Operation {
105
+ OperationType type = 1;
106
+ bytes address = 2; // address of the invoker
107
+
108
+ oneof value {
109
+ CoreAdminOperation cao = 3;
110
+ AdminControlOperation aco = 4;
111
+ BalanceInitializationOperation bio = 5;
112
+ TransferOperation tro = 6;
113
+ RoleAccessOperation rao = 7;
114
+ BootstrapDeploymentOperation bdo = 8;
115
+ TxOperation txo = 9;
116
+ }
117
+ }
@@ -0,0 +1 @@
1
+ export const MAX_SIGNED_LENGTH = 1000
package/rpc/cors.mjs ADDED
@@ -0,0 +1,15 @@
1
+ export function applyCors(req, res) {
2
+ // Set CORS headers
3
+ res.setHeader('Access-Control-Allow-Origin', '*'); // Allow all origins, or specify a domain
4
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
5
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
6
+
7
+ // Handle preflight OPTIONS request
8
+ if (req.method === 'OPTIONS') {
9
+ res.writeHead(204);
10
+ res.end();
11
+ return true; // Return true to indicate the request was handled
12
+ }
13
+
14
+ return false; // Return false to indicate the request should be handled by the main router
15
+ }
@@ -0,0 +1,72 @@
1
+ // rpc_server.mjs
2
+ import http from 'http'
3
+ import { applyCors } from './cors.mjs';
4
+ import { routes } from './routes/index.mjs';
5
+
6
+ export const createServer = (msbInstance) => {
7
+ const server = http.createServer({}, async (req, res) => {
8
+
9
+ // --- 1. Define safe 'respond' utility (Payload MUST be an object) ---
10
+ const respond = (code, payload) => {
11
+ // FIX: Prevent attempts to write headers if a response has already started
12
+ if (res.headersSent) {
13
+ console.warn(`Attempted to send response (Code: ${code}) after headers were already sent. payload:`, payload);
14
+ return;
15
+ }
16
+
17
+ // Enforce JSON content type for all responses
18
+ res.writeHead(code, { 'Content-Type': 'application/json' });
19
+
20
+ // CRITICAL: Always JSON.stringify the input object
21
+ res.end(JSON.stringify(payload));
22
+ }
23
+
24
+ // --- 2. Catch low-level request stream errors ---
25
+ req.on('error', (err) => {
26
+ console.error('Request stream error:', err);
27
+ // Use the safe respond utility
28
+ respond(500, { error: 'A stream-level request error occurred.' });
29
+ });
30
+
31
+ if (applyCors(req, res)) return;
32
+
33
+ // Find the matching route
34
+ let foundRoute = false;
35
+
36
+ // Extract the path without query parameters
37
+ const requestPath = req.url.split('?')[0];
38
+
39
+ // Sort routes by path length (longest first) to ensure more specific routes match first
40
+ const sortedRoutes = [...routes].sort((a, b) => b.path.length - a.path.length);
41
+
42
+ for (const route of sortedRoutes) {
43
+ // Exact path matching for base route, allow parameters after base path
44
+ const routeBase = route.path.endsWith('/') ? route.path.slice(0, -1) : route.path;
45
+ const requestParts = requestPath.split('/');
46
+ const routeParts = routeBase.split('/');
47
+
48
+ if (req.method === route.method &&
49
+ requestParts.length >= routeParts.length &&
50
+ routeParts.every((part, i) => part === requestParts[i])) {
51
+
52
+ foundRoute = true;
53
+ try {
54
+ // This try/catch covers synchronous errors and errors from awaited promises
55
+ // within the route.handler function.
56
+ await route.handler({ req, res, respond, msbInstance });
57
+ } catch (error) {
58
+ // Catch errors thrown directly from the handler (or its awaited parts)
59
+ console.error(`Error on ${route.path}:`, error);
60
+ respond(500, { error: 'An error occurred processing the request.' });
61
+ }
62
+ break;
63
+ }
64
+ }
65
+
66
+ if (!foundRoute) {
67
+ respond(404, { error: 'Not Found' });
68
+ }
69
+ });
70
+
71
+ return server
72
+ }
@@ -0,0 +1,245 @@
1
+ import { decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure } from "./utils/helpers.mjs"
2
+ import { MAX_SIGNED_LENGTH } from "./constants.mjs";
3
+ import { isHexString } from "../src/utils/helpers";
4
+
5
+ export async function handleBalance({ req, respond, msbInstance }) {
6
+ const [path, queryString] = req.url.split("?");
7
+ const parts = path.split("/").filter(Boolean);
8
+ const address = parts[2];
9
+
10
+ let confirmed = true; // default
11
+ if (queryString) {
12
+ const params = new URLSearchParams(queryString);
13
+ if (params.has("confirmed")) {
14
+ confirmed = params.get("confirmed") === "true";
15
+ }
16
+ }
17
+
18
+ if (!address) {
19
+ respond(400, { error: 'Wallet address is required' });
20
+ return;
21
+ }
22
+
23
+ const commandString =`/get_balance ${address} ${confirmed}`;
24
+ const nodeInfo = await msbInstance.handleCommand(commandString);
25
+ const balance = nodeInfo?.balance || 0;
26
+ respond(200, { address, balance });
27
+ }
28
+
29
+ export async function handleTxv({ msbInstance, respond }) {
30
+ const commandString = '/get_txv';
31
+ const txvRaw = await msbInstance.handleCommand(commandString);
32
+ const txv = txvRaw.toString('hex');
33
+ respond(200, { txv });
34
+ }
35
+
36
+ export async function handleFee({ msbInstance, respond }) {
37
+ const commandString = '/get_fee';
38
+ const fee = await msbInstance.handleCommand(commandString);
39
+ respond(200, { fee });
40
+ }
41
+
42
+ export async function handleConfirmedLength({ msbInstance, respond }) {
43
+ const commandString = '/confirmed_length';
44
+ const confirmed_length = await msbInstance.handleCommand(commandString);
45
+ respond(200, { confirmed_length });
46
+ }
47
+
48
+ export async function handleBroadcastTransaction({ msbInstance, respond, req }) {
49
+ let body = '';
50
+ req.on('data', chunk => {
51
+ body += chunk.toString();
52
+ });
53
+
54
+ req.on('end', async () => {
55
+ try {
56
+ const { payload } = JSON.parse(body);
57
+ if (!payload) {
58
+ return respond(400, { error: 'Payload is missing.' });
59
+ }
60
+
61
+ if (!isBase64(payload)) {
62
+ return respond(400, { error: 'Payload must be a valid base64 string.' });
63
+ }
64
+
65
+ const decodedPayload = decodeBase64Payload(payload);
66
+ validatePayloadStructure(decodedPayload);
67
+ const sanitizedPayload = sanitizeTransferPayload(decodedPayload);
68
+ const result = await msbInstance.handleCommand('/broadcast_transaction', null, sanitizedPayload);
69
+ respond(200, { result });
70
+ } catch (error) {
71
+ console.error('Error in handleBroadcastTransaction:', error);
72
+ // Use 400 for client errors (like bad JSON), 500 for server/command errors
73
+ const code = error instanceof SyntaxError ? 400 : 500;
74
+ respond(code, { error: code === 400 ? 'Invalid JSON payload.' : 'An error occurred processing the transaction.' });
75
+ }
76
+ });
77
+
78
+ req.on('error', (err) => {
79
+ console.error('Stream error in handleBroadcastTransaction:', err);
80
+ respond(500, { error: 'Request stream failed during body transfer.' });
81
+ });
82
+ }
83
+
84
+ export async function handleTxHashes({ msbInstance, respond, req }) {
85
+ const startSignedLengthStr = req.url.split('/')[3];
86
+ const endSignedLengthStr = req.url.split('/')[4];
87
+
88
+ const startSignedLength = parseInt(startSignedLengthStr);
89
+ const endSignedLength = parseInt(endSignedLengthStr);
90
+
91
+ // 1. Check if the parsed values are valid numbers
92
+ if (isNaN(startSignedLength) || isNaN(endSignedLength)) {
93
+ return respond(400, { error: 'Params must be integer' });
94
+ }
95
+
96
+ // 2. Check for non-negative numbers
97
+ // The requirement is "non-negative," which includes 0.
98
+ if (startSignedLength < 0 || endSignedLength < 0) {
99
+ return respond(400, { error: 'Params must be non-negative' });
100
+ }
101
+
102
+ // 3. endSignedLength must be >= startSignedLength
103
+ if (endSignedLength < startSignedLength) {
104
+ return respond(400, { error: 'endSignedLength must be greater than or equal to startSignedLength.' });
105
+ }
106
+
107
+ if (endSignedLength - startSignedLength > MAX_SIGNED_LENGTH) {
108
+ return respond(400, { error: `The max range for signedLength must be ${MAX_SIGNED_LENGTH}.` });
109
+ }
110
+
111
+ // 4. Get current confirmed length
112
+ const currentConfirmedLength = await msbInstance.handleCommand('/confirmed_length');
113
+
114
+ // 5. Adjust the end index to not exceed the confirmed length.
115
+ const adjustedEndLength = Math.min(endSignedLength, currentConfirmedLength)
116
+
117
+ // 6. Fetch txs hashes for the adjusted range, assuming the command takes start and end index.
118
+ const commandString = `/get_txs_hashes ${startSignedLength} ${adjustedEndLength}`;
119
+ const { hashes } = await msbInstance.handleCommand(commandString);
120
+ respond(200, { hashes });
121
+ }
122
+
123
+ export async function handleUnconfirmedLength({ msbInstance, respond }) {
124
+ const commandString = '/unconfirmed_length';
125
+ const unconfirmed_length = await msbInstance.handleCommand(commandString);
126
+ respond(200, { unconfirmed_length });
127
+ }
128
+
129
+ export async function handleTransactionDetails({ msbInstance, respond, req }) {
130
+ const hash = req.url.split('/')[3];
131
+ const commandString = `/get_tx_details ${hash}`;
132
+ const txDetails = await msbInstance.handleCommand(commandString);
133
+ respond(txDetails === null ? 404 : 200 , { txDetails });
134
+ }
135
+
136
+ export async function handleFetchBulkTxPayloads({ msbInstance, respond, req }) {
137
+ let body = ''
138
+ let bytesRead = 0;
139
+ let limitBytes = 1_000_000;
140
+ let headersSent = false; // Add a flag to prevent double response
141
+
142
+ req.on('data', chunk => {
143
+ if (headersSent) return; // Stop processing if response has started/errored
144
+
145
+ bytesRead += chunk.length;
146
+ if (bytesRead > limitBytes) {
147
+ respond(413, { error: 'Request body too large.' });
148
+ headersSent = true;
149
+ req.destroy(); // Stop receiving data (GOOD PRACTICE)
150
+ return;
151
+ }
152
+ body += chunk.toString();
153
+ });
154
+
155
+ req.on('end', async () => {
156
+ if (headersSent) return; // Don't process if an error already occurred
157
+
158
+
159
+ try {
160
+ if (body === null || body === ''){
161
+ return respond(400, { error: 'Missing payload.' });
162
+ }
163
+
164
+ const sanitizedPayload = sanitizeBulkPayloadsRequestBody(body);
165
+
166
+ if (sanitizedPayload === null){
167
+ return respond(400, { error: 'Invalid payload.' });
168
+ }
169
+
170
+ const { hashes } = sanitizedPayload;
171
+
172
+ if (!Array.isArray(hashes) || hashes.length === 0) {
173
+ return respond(400, { error: 'Missing hash list.' });
174
+ }
175
+
176
+ if (hashes.length > 1500) {
177
+ return respond(413, { error: 'Too many hashes. Max 1500 allowed per request.' });
178
+ }
179
+
180
+ const uniqueHashes = [...new Set(hashes)];
181
+
182
+ const commandResult = await msbInstance.handleCommand( `/get_tx_payloads_bulk`, null, uniqueHashes)
183
+
184
+ const responseString = JSON.stringify(commandResult);
185
+ if (Buffer.byteLength(responseString, 'utf8') > 2_000_000) {
186
+ return respond(413, { error: 'Response too large. Reduce number of hashes.'});
187
+ }
188
+
189
+ return respond(200, commandResult);
190
+ } catch (error) {
191
+ console.error('Error in handleFetchBulkTxPayloads:', error);
192
+ // Use 400 for JSON errors, 500 otherwise
193
+ const code = error instanceof SyntaxError ? 400 : 500;
194
+ respond(code, { error: code === 400 ? 'Invalid request body format.' : 'An internal error occurred.' });
195
+ }
196
+ })
197
+
198
+ req.on('error', (err) => {
199
+ console.error('Stream error in handleFetchBulkTxPayloads:', err);
200
+ respond(500, { error: 'Request stream failed during body transfer.' });
201
+ });
202
+ }
203
+
204
+ export async function handleTransactionExtendedDetails({ msbInstance, respond, req }) {
205
+ const [path, queryString] = req.url.split("?");
206
+ const pathParts = path.split('/');
207
+ const hash = pathParts[4];
208
+
209
+ if (!hash) {
210
+ return respond(400, { error: "Transaction hash is required" });
211
+ }
212
+
213
+ if (isHexString(hash) === false || hash.length !== 64) {
214
+ return respond(400, { error: "Invalid transaction hash format" });
215
+ }
216
+ let confirmed = true; // default
217
+ if (queryString) {
218
+ const params = new URLSearchParams(queryString);
219
+ if (params.has("confirmed")) {
220
+ const confirmedParam = params.get("confirmed");
221
+ if (confirmedParam !== "true" && confirmedParam !== "false") {
222
+ return respond(400, { error: 'Parameter "confirmed" must be exactly "true" or "false"' });
223
+ }
224
+ confirmed = confirmedParam === "true";
225
+ }
226
+ }
227
+
228
+ try {
229
+ let txDetails;
230
+ const commandString = `/get_extended_tx_details ${hash} ${confirmed}`;
231
+ txDetails = await msbInstance.handleCommand(commandString);
232
+ if (txDetails === null) {
233
+ respond(404, { error: `No payload found for tx hash: ${hash}` });
234
+ } else {
235
+ respond(200, txDetails);
236
+ }
237
+ } catch (error) {
238
+ if (error.message?.includes('No payload found for tx hash')) {
239
+ respond(404, { error: error.message });
240
+ } else {
241
+ console.error('Error in handleTransactionDetails:', error);
242
+ respond(500, { error: 'An error occurred processing the request.' });
243
+ }
244
+ }
245
+ }