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.
- package/.github/workflows/acceptance-tests.yml +7 -11
- package/.github/workflows/lint-pr-title.yml +26 -0
- package/.github/workflows/unit-tests.yml +2 -8
- package/CODE_OF_CONDUCT.md +128 -0
- package/README.md +33 -18
- package/docker-compose.yml +1 -0
- package/docs/trac_network_http_api.openapi.yaml +889 -0
- package/msb.mjs +4 -21
- package/package.json +8 -9
- package/rpc/handlers.js +163 -90
- package/rpc/routes/v1.js +3 -1
- package/rpc/rpc_server.js +3 -3
- package/rpc/rpc_services.js +25 -29
- package/rpc/utils/helpers.js +82 -51
- package/src/config/args.js +46 -0
- package/src/config/config.js +78 -5
- package/src/config/env.js +69 -4
- package/src/core/network/Network.js +6 -10
- package/src/core/network/protocols/NetworkMessages.js +1 -1
- package/src/core/network/protocols/legacy/NetworkMessageRouter.js +1 -1
- package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
- package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +2 -2
- package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +1 -1
- package/src/core/network/protocols/shared/handlers/TransferOperationHandler.js +1 -1
- package/src/core/network/protocols/shared/handlers/base/BaseOperationHandler.js +5 -6
- package/src/core/network/services/ConnectionManager.js +1 -1
- package/src/core/network/services/MessageOrchestrator.js +1 -1
- package/src/core/network/services/TransactionPoolService.js +4 -4
- package/src/core/network/services/TransactionRateLimiterService.js +8 -11
- package/src/core/network/services/ValidatorObserverService.js +5 -6
- package/src/core/state/State.js +2 -3
- package/src/index.js +3 -1
- package/src/messages/network/v1/NetworkMessageBuilder.js +2 -2
- package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
- package/src/utils/check.js +1 -1
- package/src/utils/constants.js +0 -17
- package/src/utils/fileUtils.js +13 -0
- package/src/utils/type.js +26 -0
- package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
- package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
- package/tests/acceptance/v1/health/health.test.mjs +33 -0
- package/tests/acceptance/v1/rpc.test.mjs +3 -2
- package/tests/acceptance/v1/tx/tx.test.mjs +27 -16
- package/tests/acceptance/v1/tx-details/tx-details.test.mjs +26 -12
- package/tests/fixtures/check.fixtures.js +33 -32
- package/tests/fixtures/networkV1.fixtures.js +3 -2
- package/tests/fixtures/protobuf.fixtures.js +33 -32
- package/tests/helpers/StateNetworkFactory.js +2 -2
- package/tests/helpers/address.js +6 -0
- package/tests/helpers/autobaseTestHelpers.js +2 -1
- package/tests/helpers/config.js +2 -1
- package/tests/helpers/setupApplyTests.js +6 -10
- package/tests/unit/messages/network/NetworkMessageBuilder.test.js +3 -3
- package/tests/unit/messages/network/NetworkMessageDirector.test.js +3 -5
- package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
- package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
- package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
- package/tests/unit/utils/type/type.test.js +25 -0
- 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 {
|
|
3
|
+
import { isRpcEnabled, resolveConfig } from './src/config/args.js';
|
|
4
4
|
|
|
5
|
-
const
|
|
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 (
|
|
9
|
+
if (isRpcEnabled()) {
|
|
23
10
|
console.log('Starting RPC server...');
|
|
24
|
-
|
|
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.
|
|
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
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"env-
|
|
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.
|
|
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 {
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
+
throw new ValidationError("Payload is missing.");
|
|
66
111
|
}
|
|
67
112
|
|
|
68
113
|
if (!isBase64(payload)) {
|
|
69
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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;
|
|
266
|
+
let headersSent = false;
|
|
154
267
|
|
|
155
268
|
req.on('data', chunk => {
|
|
156
|
-
if (headersSent) return;
|
|
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();
|
|
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;
|
|
170
|
-
|
|
281
|
+
if (headersSent) return;
|
|
171
282
|
|
|
172
283
|
try {
|
|
173
|
-
if (body
|
|
174
|
-
|
|
284
|
+
if (!body) {
|
|
285
|
+
throw new ValidationError("Missing payload.");
|
|
175
286
|
}
|
|
176
287
|
|
|
177
288
|
const sanitizedPayload = sanitizeBulkPayloadsRequestBody(body);
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
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
|
}
|
package/rpc/rpc_services.js
CHANGED
|
@@ -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
|
|
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
|
|
56
|
-
const partialTransactionValidator = new PartialTransaction(msbInstance.state, null
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
107
|
+
throw new ValidationError("Missing hash list.");
|
|
101
108
|
}
|
|
102
109
|
|
|
103
110
|
if (hashes.length > 1500) {
|
|
104
|
-
throw new
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
+
}
|