trac-msb 0.2.0 → 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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "trac-msb",
3
3
  "main": "msb.mjs",
4
- "version": "0.2.0",
4
+ "version": "0.2.1",
5
5
  "pear": {
6
6
  "name": "trac-msb",
7
7
  "type": "terminal"
@@ -32,9 +32,23 @@ export const createServer = (msbInstance) => {
32
32
 
33
33
  // Find the matching route
34
34
  let foundRoute = false;
35
- for (const route of routes) {
36
- // Simple path matching
37
- if (req.method === route.method && req.url.startsWith(route.path)) {
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
+
38
52
  foundRoute = true;
39
53
  try {
40
54
  // This try/catch covers synchronous errors and errors from awaited promises
@@ -43,8 +57,6 @@ export const createServer = (msbInstance) => {
43
57
  } catch (error) {
44
58
  // Catch errors thrown directly from the handler (or its awaited parts)
45
59
  console.error(`Error on ${route.path}:`, error);
46
-
47
- // FIX: Pass an object payload
48
60
  respond(500, { error: 'An error occurred processing the request.' });
49
61
  }
50
62
  break;
package/rpc/handlers.mjs CHANGED
@@ -1,5 +1,6 @@
1
- import {decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure} from "./utils/helpers.mjs"
1
+ import { decodeBase64Payload, isBase64, sanitizeBulkPayloadsRequestBody, sanitizeTransferPayload, validatePayloadStructure } from "./utils/helpers.mjs"
2
2
  import { MAX_SIGNED_LENGTH } from "./constants.mjs";
3
+ import { isHexString } from "../src/utils/helpers";
3
4
 
4
5
  export async function handleBalance({ req, respond, msbInstance }) {
5
6
  const [path, queryString] = req.url.split("?");
@@ -198,4 +199,47 @@ export async function handleFetchBulkTxPayloads({ msbInstance, respond, req }) {
198
199
  console.error('Stream error in handleFetchBulkTxPayloads:', err);
199
200
  respond(500, { error: 'Request stream failed during body transfer.' });
200
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
+ }
201
245
  }
package/rpc/routes/v1.mjs CHANGED
@@ -7,7 +7,8 @@ import {
7
7
  handleTxHashes,
8
8
  handleUnconfirmedLength,
9
9
  handleTransactionDetails,
10
- handleFetchBulkTxPayloads
10
+ handleFetchBulkTxPayloads,
11
+ handleTransactionExtendedDetails
11
12
  } from '../handlers.mjs';
12
13
 
13
14
  export const v1Routes = [
@@ -20,4 +21,5 @@ export const v1Routes = [
20
21
  { method: 'GET', path: '/unconfirmed-length', handler: handleUnconfirmedLength },
21
22
  { method: 'GET', path: '/tx', handler: handleTransactionDetails },
22
23
  { method: 'POST', path: '/tx-payloads-bulk', handler: handleFetchBulkTxPayloads },
24
+ { method: 'GET', path: '/tx/details', handler: handleTransactionExtendedDetails },
23
25
  ];
@@ -5,6 +5,6 @@ export function startRpcServer(msbInstance, host, port) {
5
5
  const server = createServer(msbInstance)
6
6
 
7
7
  return server.listen(port, host, () => {
8
- console.log(`Running RPC with https at https://${host}:${port}`);
8
+ console.log(`Running RPC with http at http://${host}:${port}`);
9
9
  });
10
10
  }
@@ -17,7 +17,7 @@ import {
17
17
  TRAC_NAMESPACE,
18
18
  CustomEventType
19
19
  } from '../../utils/constants.js';
20
- import { isHexString, sleep } from '../../utils/helpers.js';
20
+ import { isHexString, sleep, isTransactionRecordPut } from '../../utils/helpers.js';
21
21
  import PeerWallet from 'trac-wallet';
22
22
  import Check from '../../utils/check.js';
23
23
  import { safeDecodeApplyOperation } from '../../utils/protobuf/operationHelpers.js';
@@ -265,6 +265,24 @@ class State extends ReadyResource {
265
265
  return b4a.equals(initialization, safeWriteUInt32BE(0, 0))
266
266
  }
267
267
  }
268
+ async getTransactionConfirmedLength(hash) {
269
+ if (!isHexString(hash) || hash.length !== 64) {
270
+ throw new Error("Invalid hash format");
271
+ }
272
+
273
+ const confirmedLength = this.getSignedLength();
274
+ const historyStream = this.#base.view.createHistoryStream({
275
+ gte: 0,
276
+ lte: confirmedLength
277
+ });
278
+
279
+ for await (const entry of historyStream) {
280
+ if (isTransactionRecordPut(entry) && entry.key === hash) {
281
+ return entry.seq;
282
+ }
283
+ }
284
+ return null;
285
+ }
268
286
 
269
287
  async confirmedTransactionsBetween(startSignedLength, endSignedLength) {
270
288
  if (!Number.isInteger(startSignedLength) || !Number.isInteger(endSignedLength)) {
package/src/index.js CHANGED
@@ -8,7 +8,7 @@ import readline from "readline";
8
8
  import tty from "tty";
9
9
 
10
10
  import { sleep, getFormattedIndexersWithAddresses, isHexString, convertAdminCoreOperationPayloadToHex } from "./utils/helpers.js";
11
- import { verifyDag, printHelp, printWalletInfo, get_tx_info, printBalance } from "./utils/cli.js";
11
+ import { verifyDag, printHelp, printWalletInfo, get_confirmed_tx_info, printBalance, get_unconfirmed_tx_info } from "./utils/cli.js";
12
12
  import CompleteStateMessageOperations from "./messages/completeStateMessages/CompleteStateMessageOperations.js";
13
13
  import { safeDecodeApplyOperation } from "./utils/protobuf/operationHelpers.js";
14
14
  import { bufferToAddress, isAddressValid } from "./core/state/utils/address.js";
@@ -821,11 +821,11 @@ export class MainSettlementBus extends ReadyResource {
821
821
  }
822
822
 
823
823
  const { addressBalancePair, totalBalance, totalAddresses, addresses } = await fileUtils.readBalanceMigrationFile();
824
-
824
+
825
825
  for (let i = 0; i < addresses.length; i++) {
826
826
  await migrationUtils.validateAddressFromIncomingFile(this.#state, addresses[i].address, adminEntry);
827
827
  }
828
-
828
+
829
829
  await fileUtils.validateBalanceMigrationData(addresses);
830
830
  const migrationNumber = await fileUtils.getNextMigrationNumber();
831
831
  await fileUtils.createMigrationEntryFile(addressBalancePair, migrationNumber);
@@ -1070,7 +1070,7 @@ export class MainSettlementBus extends ReadyResource {
1070
1070
  } else if (input.startsWith("/get_tx_info")) {
1071
1071
  const splitted = input.split(" ");
1072
1072
  const txHash = splitted[1];
1073
- const txInfo = await get_tx_info(this.#state, txHash);
1073
+ const txInfo = await get_confirmed_tx_info(this.#state, txHash);
1074
1074
  if (txInfo) {
1075
1075
  console.log(`Payload for transaction hash ${txHash}:`);
1076
1076
  console.log(txInfo.decoded);
@@ -1218,7 +1218,7 @@ export class MainSettlementBus extends ReadyResource {
1218
1218
  throw new Error("Length of input tx hashes exceeded.");
1219
1219
  }
1220
1220
 
1221
- const promises = hashes.map(hash => get_tx_info(this.#state, hash));
1221
+ const promises = hashes.map(hash => get_confirmed_tx_info(this.#state, hash));
1222
1222
  const results = await Promise.all(promises);
1223
1223
 
1224
1224
  // Iterate and categorize
@@ -1251,9 +1251,8 @@ export class MainSettlementBus extends ReadyResource {
1251
1251
  } else if (input.startsWith("/get_tx_details")) {
1252
1252
  const splitted = input.split(' ')
1253
1253
  const hash = splitted[1];
1254
-
1255
1254
  try {
1256
- const rawPayload = await get_tx_info(this.#state, hash);
1255
+ const rawPayload = await get_confirmed_tx_info(this.#state, hash);
1257
1256
  if (!rawPayload) {
1258
1257
  console.log(`No payload found for tx hash: ${hash}`)
1259
1258
  return null
@@ -1264,6 +1263,52 @@ export class MainSettlementBus extends ReadyResource {
1264
1263
  throw new Error("Invalid params to perform the request.", error.message);
1265
1264
  }
1266
1265
  }
1266
+ else if (input.startsWith("/get_extended_tx_details")) {
1267
+ const splitted = input.split(' ');
1268
+ const hash = splitted[1];
1269
+ const confirmed = splitted[2] === 'true';
1270
+
1271
+ if (confirmed) {
1272
+ const rawPayload = await get_confirmed_tx_info(this.#state, hash);
1273
+ if (!rawPayload) {
1274
+ throw new Error(`No payload found for tx hash: ${hash}`);
1275
+ }
1276
+ const confirmedLength = await this.#state.getTransactionConfirmedLength(hash);
1277
+ const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, true);
1278
+ if (confirmedLength === null) {
1279
+ throw new Error(`No confirmed length found for tx hash: ${hash} in confirmed mode`);
1280
+ }
1281
+ const fee = this.#state.getFee();
1282
+ return {
1283
+ txDetails: normalizedPayload,
1284
+ confirmed_length: confirmedLength,
1285
+ fee: bufferToBigInt(fee).toString()
1286
+ }
1287
+ }
1288
+ else {
1289
+ const rawPayload = await get_unconfirmed_tx_info(this.#state, hash);
1290
+ if (!rawPayload) {
1291
+ throw new Error(`No payload found for tx hash: ${hash}`);
1292
+ }
1293
+ const normalizedPayload = normalizeDecodedPayloadForJson(rawPayload.decoded, true);
1294
+ const length = await this.#state.getTransactionConfirmedLength(hash)
1295
+ if (length === null) {
1296
+ return {
1297
+ txDetails: normalizedPayload,
1298
+ confirmed_length: 0,
1299
+ fee: '0'
1300
+ }
1301
+ }
1302
+
1303
+ const fee = this.#state.getFee();
1304
+ return {
1305
+ txDetails: normalizedPayload,
1306
+ confirmed_length: length,
1307
+ fee: bufferToBigInt(fee).toString()
1308
+ }
1309
+
1310
+ }
1311
+ }
1267
1312
  }
1268
1313
  if (rl) rl.prompt();
1269
1314
  }
package/src/utils/cli.js CHANGED
@@ -103,7 +103,7 @@ export const printBalance = async (address, state, wallet_enabled) => {
103
103
  }
104
104
  }
105
105
 
106
- export const get_tx_info = async (state_instance, txHash) => {
106
+ export const get_confirmed_tx_info = async (state_instance, txHash) => {
107
107
  const payload = await state_instance.getSigned(txHash);
108
108
  if (!payload) {
109
109
  return null
@@ -119,3 +119,20 @@ export const get_tx_info = async (state_instance, txHash) => {
119
119
  decoded
120
120
  }
121
121
  }
122
+
123
+ export const get_unconfirmed_tx_info = async (state_instance, txHash) => {
124
+ const payload = await state_instance.get(txHash);
125
+ if (!payload) {
126
+ return null
127
+ }
128
+
129
+ const decoded = safeDecodeApplyOperation(payload);
130
+ if (!decoded) {
131
+ throw new Error(`Failed to decode payload for transaction hash: ${txHash}`);
132
+ }
133
+
134
+ return {
135
+ payload,
136
+ decoded
137
+ }
138
+ }
@@ -97,3 +97,10 @@ export function convertAdminCoreOperationPayloadToHex(payload) {
97
97
  },
98
98
  };
99
99
  }
100
+
101
+ export function isTransactionRecordPut(entry) {
102
+ const isPut = entry.type === "put";
103
+ const isHex = isHexString(entry.key);
104
+ const is64 = entry.key.length === 64;
105
+ return isPut && isHex && is64;
106
+ }
@@ -28,7 +28,7 @@ const setupNetwork = async () => {
28
28
  store_name: '/admin'
29
29
  }
30
30
 
31
- const peer =await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts)
31
+ const peer = await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts)
32
32
  const writer = await setupMsbWriter(peer, 'writer', testKeyPair2, tmpDirectory, peer.options);
33
33
  return { writer, peer }
34
34
  }
@@ -76,7 +76,7 @@ describe("API acceptance tests", () => {
76
76
  it("< 1000", async () => {
77
77
  const res = await request(server).get("/v1/tx-hashes/1/1001")
78
78
  expect(res.statusCode).toBe(200)
79
- expect(res.body).toEqual({
79
+ expect(res.body).toEqual({
80
80
  hashes: expect.arrayContaining([
81
81
  expect.objectContaining({
82
82
  hash: expect.any(String),
@@ -137,8 +137,8 @@ describe("API acceptance tests", () => {
137
137
  const res = await request(server)
138
138
  .post("/v1/tx-payloads-bulk")
139
139
  .set("Accept", "application/json")
140
- .send(JSON.stringify( payload ))
141
-
140
+ .send(JSON.stringify(payload))
141
+
142
142
  expect(res.statusCode).toBe(200)
143
143
  expect(res.body).toMatchObject({
144
144
  results: expect.arrayOf(
@@ -146,7 +146,177 @@ describe("API acceptance tests", () => {
146
146
  hash: expect.any(String),
147
147
  })
148
148
  ),
149
- missing:[]
149
+ missing: []
150
150
  })
151
151
  })
152
+
153
+ describe('GET /v1/tx/details', () => {
154
+
155
+ it("positive case - should return 200 for valid already broadcasted hash confirmed and unconfirmed", async () => {
156
+ const txData = await tracCrypto.transaction.preBuild(
157
+ wallet.address,
158
+ wallet.address,
159
+ b4a.toString($TNK(1n), 'hex'),
160
+ b4a.toString(await msb.state.getIndexerSequenceState(), 'hex')
161
+ );
162
+
163
+ const payload = tracCrypto.transaction.build(txData, b4a.from(wallet.secretKey, 'hex'));
164
+ const broadcastRes = await request(server)
165
+ .post("/v1/broadcast-transaction")
166
+ .set("Accept", "application/json")
167
+ .send(JSON.stringify({ payload }));
168
+ expect(broadcastRes.statusCode).toBe(200);
169
+
170
+ const resConfirmed = await request(server)
171
+ .get(`/v1/tx/details/${txData.hash.toString('hex')}?confirmed=true`);
172
+ expect(resConfirmed.statusCode).toBe(200);
173
+
174
+ expect(resConfirmed.body).toMatchObject({
175
+ txDetails: expect.any(Object),
176
+ confirmed_length: expect.any(Number),
177
+ fee: expect.any(String)
178
+ })
179
+
180
+ const resUnconfirmed = await request(server)
181
+ .get(`/v1/tx/details/${txData.hash.toString('hex')}?confirmed=false`);
182
+ expect(resUnconfirmed.statusCode).toBe(200);
183
+
184
+ expect(resUnconfirmed.body).toMatchObject({
185
+ txDetails: expect.any(Object),
186
+ confirmed_length: expect.any(Number),
187
+ fee: expect.any(String)
188
+ })
189
+ });
190
+
191
+ it("should handle null confirmed_length for unconfirmed transaction", async () => {
192
+ const txData = await tracCrypto.transaction.preBuild(
193
+ wallet.address,
194
+ wallet.address,
195
+ b4a.toString($TNK(1n), 'hex'),
196
+ b4a.toString(await msb.state.getIndexerSequenceState(), 'hex')
197
+ );
198
+
199
+ const payload = tracCrypto.transaction.build(txData, b4a.from(wallet.secretKey, 'hex'));
200
+
201
+ const originalGetConfirmedLength = msb.state.getTransactionConfirmedLength;
202
+ msb.state.getTransactionConfirmedLength = async () => null;
203
+
204
+ try {
205
+ const broadcastRes = await request(server)
206
+ .post("/v1/broadcast-transaction")
207
+ .set("Accept", "application/json")
208
+ .send(JSON.stringify({ payload }));
209
+ expect(broadcastRes.statusCode).toBe(200);
210
+
211
+ const res = await request(server)
212
+ .get(`/v1/tx/details/${txData.hash.toString('hex')}?confirmed=false`);
213
+ expect(res.statusCode).toBe(200);
214
+
215
+ expect(res.body).toMatchObject({
216
+ txDetails: expect.any(Object),
217
+ confirmed_length: 0,
218
+ fee: '0'
219
+ });
220
+ } finally {
221
+ msb.state.getTransactionConfirmedLength = originalGetConfirmedLength;
222
+ }
223
+ });
224
+
225
+ it("should return 404 for non-existent transaction hash", async () => {
226
+ const nonExistentHash = "0b4d1c1dac48af13212f616601d7399457476a0b644850875b7f4b79df6ff89c";
227
+ const res = await request(server)
228
+ .get(`/v1/tx/details/${nonExistentHash}`);
229
+
230
+ expect(res.statusCode).toBe(404);
231
+ expect(res.body).toEqual({
232
+ error: `No payload found for tx hash: ${nonExistentHash}`
233
+ });
234
+ });
235
+
236
+ it("should return 400 for invalid hash format (too short)", async () => {
237
+ const invalidHash = '0'.repeat(63);
238
+ const res = await request(server)
239
+ .get(`/v1/tx/details/${invalidHash}`);
240
+
241
+ expect(res.statusCode).toBe(400);
242
+ expect(res.body).toEqual({
243
+ error: "Invalid transaction hash format"
244
+ });
245
+ });
246
+
247
+ it("should return 400 for invalid hash format (non-hex)", async () => {
248
+ const invalidHash = 'Z'.repeat(64);
249
+ const res = await request(server)
250
+ .get(`/v1/tx/details/${invalidHash}`);
251
+
252
+ expect(res.statusCode).toBe(400);
253
+ expect(res.body).toEqual({
254
+ error: "Invalid transaction hash format"
255
+ });
256
+ });
257
+
258
+ it("should return 400 for invalid confirmed parameter", async () => {
259
+ const hash = "0b4d1c1dac48af13212f616601d7399457476a0b644850875b7f4b79df6ff89c";
260
+
261
+ const res = await request(server)
262
+ .get(`/v1/tx/details/${hash}?confirmed=invalid`);
263
+
264
+ expect(res.statusCode).toBe(400);
265
+ expect(res.body).toEqual({
266
+ error: 'Parameter "confirmed" must be exactly "true" or "false"'
267
+ });
268
+ });
269
+
270
+ it("should return 400 for invalid confirmed parameter case (UPPERCASE)", async () => {
271
+ const hash = "0b4d1c1dac48af13212f616601d7399457476a0b644850875b7f4b79df6ff89c";
272
+ const res = await request(server).get(`/v1/tx/details/${hash}?confirmed=TRUE`);
273
+ expect(res.statusCode).toBe(400);
274
+ expect(res.body).toEqual({
275
+ error: 'Parameter "confirmed" must be exactly "true" or "false"'
276
+ });
277
+ });
278
+
279
+ it("should return 400 when no hash provided", async () => {
280
+ const res = await request(server)
281
+ .get('/v1/tx/details');
282
+
283
+ expect(res.statusCode).toBe(400);
284
+ expect(res.body).toEqual({
285
+ error: "Transaction hash is required"
286
+ });
287
+ });
288
+
289
+ it("should return 400 for hash with invalid characters", async () => {
290
+ const invalidHash = '0b4d1c1dac48$af13212f6166017399457476a0b644850875b7f4b79df6ff89c';
291
+ const res = await request(server)
292
+ .get(`/v1/tx/details/${invalidHash}`);
293
+
294
+ expect(res.statusCode).toBe(400);
295
+ expect(res.body).toEqual({
296
+ error: "Invalid transaction hash format"
297
+ });
298
+ });
299
+
300
+ it("should return 400 for hash with special characters", async () => {
301
+ const invalidHash = '!@#$%^&*'.repeat(8);
302
+ const res = await request(server)
303
+ .get(`/v1/tx/details/${invalidHash}`);
304
+
305
+ expect(res.statusCode).toBe(400);
306
+ expect(res.body).toEqual({
307
+ error: "Invalid transaction hash format"
308
+ });
309
+ });
310
+
311
+ it("should return 400 for hash with spaces", async () => {
312
+ const invalidHash = '0b4d1c1dac48af13212f616601d7399457476a0b644850875b7 4b79df6ff89c';
313
+ const res = await request(server)
314
+ .get(`/v1/tx/details/${invalidHash}`);
315
+
316
+ expect(res.statusCode).toBe(400);
317
+ expect(res.body).toEqual({
318
+ error: "Invalid transaction hash format"
319
+ });
320
+ });
321
+ })
152
322
  })
package/test/all.test.js CHANGED
@@ -6,15 +6,16 @@ async function runTests() {
6
6
  test.pause();
7
7
 
8
8
  await import('./state/stateTests.test.js');
9
- await import('./check/check.test.js');
10
- await import('./protobuf/protobuf.test.js');
11
- await import('./functions/functions.test.js');
12
- await import('./fileUtils/readAddressesFromWhitelistFile.test.js');
13
- await import('./fileUtils/readBalanceMigrationFile.test.js');
14
- await import('./migrationUtils/validateAddressFromIncomingFile.test.js');
9
+ // await import('./state/apply.addAdmin.basic.test.js');
10
+ // await import('./check/check.test.js');
11
+ // await import('./protobuf/protobuf.test.js');
12
+ // await import('./functions/functions.test.js');
13
+ // await import('./fileUtils/readAddressesFromWhitelistFile.test.js');
14
+ // await import('./fileUtils/readBalanceMigrationFile.test.js');
15
+ // await import('./migrationUtils/validateAddressFromIncomingFile.test.js');
15
16
  // await import('./messageOperations/stateMessageOperations.test.js');
16
- await import('./buffer/buffer.test.js')
17
- await import('./network/connectionManagerTests.test.js')
17
+ // await import('./buffer/buffer.test.js')
18
+ // await import('./network/connectionManagerTests.test.js')
18
19
  // await import('./apply/apply.test.js'); // This test has been disabled because Github CI fails due to lack of resources. This test can still be run locally but sometimes it hangs when destroying resources.
19
20
  test.resume();
20
21
  }
@@ -0,0 +1,111 @@
1
+ import { test, hook } from '../utils/wrapper.js'
2
+ import Corestore from 'corestore'
3
+ import path from 'path'
4
+ import os from 'os'
5
+ import { promises as fsp } from 'fs'
6
+ import b4a from 'b4a'
7
+
8
+ import PeerWallet from 'trac-wallet'
9
+ import State from '../../src/core/state/State.js'
10
+ import CompleteStateMessageOperations from '../../src/messages/completeStateMessages/CompleteStateMessageOperations.js'
11
+ import { ADMIN_INITIAL_BALANCE } from '../../src/utils/constants.js'
12
+
13
+ // Prosty, szybki test apply(add_admin) inspirowany stylem Autobase/test/basic.js
14
+ // Minimalny setup: Corestore + State + Wallet, bez sieci i dodatkowych warstw
15
+
16
+ let tmpDir
17
+ let store
18
+ let wallet
19
+ let state
20
+
21
+ const STATE_OPTIONS = {
22
+ enable_tx_apply_logs: false,
23
+ enable_error_apply_logs: false
24
+ }
25
+
26
+ async function createTempStore() {
27
+ const base = path.join(os.tmpdir(), `msb-state-test-${Date.now()}-${Math.random().toString(16).slice(2)}`)
28
+ await fsp.mkdir(base, { recursive: true })
29
+ return { base, db: path.join(base, 'db') }
30
+ }
31
+
32
+ async function createWalletFromFixture({ mnemonic }) {
33
+ const w = new PeerWallet({ mnemonic })
34
+ await w.ready
35
+ return w
36
+ }
37
+
38
+ hook('setup state for add_admin', async () => {
39
+ const paths = await createTempStore()
40
+ tmpDir = paths.base
41
+
42
+ const { testKeyPair1 } = await import('../fixtures/apply.fixtures.js')
43
+ wallet = await createWalletFromFixture(testKeyPair1)
44
+
45
+ // wyciągnij writing key bootstrapu (== klucz lokalnego writera)
46
+ const bootstrapKey = await deriveBootstrapWriterKey(paths.db, wallet)
47
+
48
+ // właściwy store + stan testowy
49
+ store = new Corestore(paths.db)
50
+ await store.ready()
51
+
52
+ state = new State(store, bootstrapKey, wallet, STATE_OPTIONS)
53
+ await state.ready()
54
+
55
+ // pierwszy pusty append zapewnia, że widok/indexery są zainicjalizowane
56
+ await state.append(null)
57
+ await fastForwardIfAvailable(state)
58
+ await state.base.view.update()
59
+ })
60
+
61
+ test('State.apply(add_admin) – podstawowy scenariusz', async t => {
62
+ // preconditions
63
+ const beforeAdmin = await state.getAdminEntry()
64
+ t.is(beforeAdmin, null, 'admin entry nie istnieje przed operacją')
65
+
66
+ // assemble + append
67
+ const validity = await state.getIndexerSequenceState()
68
+ const msg = await CompleteStateMessageOperations.assembleAddAdminMessage(
69
+ wallet,
70
+ state.writingKey,
71
+ validity
72
+ )
73
+
74
+ await state.append(msg)
75
+ // wymuś natychmiastowe przetworzenie apply i aktualizację widoku
76
+ await fastForwardIfAvailable(state)
77
+ await state.base.view.update()
78
+
79
+ // assertions
80
+ const adminEntry = await state.getAdminEntry()
81
+ t.ok(adminEntry, 'admin entry powinien zostać dodany')
82
+ t.ok(b4a.equals(adminEntry.wk, state.writingKey), 'wk admina == writingKey')
83
+
84
+ const node = await state.getNodeEntry(adminEntry.address)
85
+ t.ok(node?.isWriter, 'admin powinien być writerem')
86
+ t.ok(node?.isIndexer, 'admin powinien być indexerem')
87
+ t.ok(b4a.equals(node.balance, ADMIN_INITIAL_BALANCE), 'admin powinien mieć saldo początkowe')
88
+ })
89
+
90
+ hook('teardown state for add_admin', async () => {
91
+ try { if (state) await state.close() } catch {}
92
+ try { if (store) await store.close() } catch {}
93
+ try { if (tmpDir) await fsp.rm(tmpDir, { recursive: true, force: true }) } catch {}
94
+ })
95
+
96
+ async function deriveBootstrapWriterKey(dbPath, walletInstance) {
97
+ const bootstrapStore = new Corestore(dbPath)
98
+ await bootstrapStore.ready()
99
+ const bootstrapState = new State(bootstrapStore, null, walletInstance, STATE_OPTIONS)
100
+ await bootstrapState.ready()
101
+ const wk = bootstrapState.writingKey
102
+ await bootstrapState.close()
103
+ await bootstrapStore.close()
104
+ return wk
105
+ }
106
+
107
+ async function fastForwardIfAvailable(testState) {
108
+ if (typeof testState.base.forceFastForward === 'function') {
109
+ await testState.base.forceFastForward()
110
+ }
111
+ }