trac-msb 0.2.9 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.github/workflows/acceptance-tests.yml +7 -11
  2. package/.github/workflows/lint-pr-title.yml +26 -0
  3. package/.github/workflows/unit-tests.yml +2 -8
  4. package/CODE_OF_CONDUCT.md +128 -0
  5. package/README.md +33 -18
  6. package/docker-compose.yml +1 -0
  7. package/docs/trac_network_http_api.openapi.yaml +889 -0
  8. package/msb.mjs +4 -21
  9. package/package.json +8 -9
  10. package/rpc/handlers.js +163 -90
  11. package/rpc/routes/v1.js +3 -1
  12. package/rpc/rpc_server.js +3 -3
  13. package/rpc/rpc_services.js +25 -29
  14. package/rpc/utils/helpers.js +82 -51
  15. package/src/config/args.js +46 -0
  16. package/src/config/config.js +78 -5
  17. package/src/config/env.js +69 -4
  18. package/src/core/network/Network.js +6 -10
  19. package/src/core/network/protocols/NetworkMessages.js +1 -1
  20. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +1 -1
  21. package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
  22. package/src/core/network/protocols/shared/handlers/RoleOperationHandler.js +2 -2
  23. package/src/core/network/protocols/shared/handlers/SubnetworkOperationHandler.js +1 -1
  24. package/src/core/network/protocols/shared/handlers/TransferOperationHandler.js +1 -1
  25. package/src/core/network/protocols/shared/handlers/base/BaseOperationHandler.js +5 -6
  26. package/src/core/network/services/ConnectionManager.js +1 -1
  27. package/src/core/network/services/MessageOrchestrator.js +1 -1
  28. package/src/core/network/services/TransactionPoolService.js +4 -4
  29. package/src/core/network/services/TransactionRateLimiterService.js +8 -11
  30. package/src/core/network/services/ValidatorObserverService.js +5 -6
  31. package/src/core/state/State.js +2 -3
  32. package/src/index.js +3 -1
  33. package/src/messages/network/v1/NetworkMessageBuilder.js +2 -2
  34. package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
  35. package/src/utils/check.js +1 -1
  36. package/src/utils/constants.js +0 -17
  37. package/src/utils/fileUtils.js +13 -0
  38. package/src/utils/type.js +26 -0
  39. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  40. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  41. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  42. package/tests/acceptance/v1/rpc.test.mjs +3 -2
  43. package/tests/acceptance/v1/tx/tx.test.mjs +27 -16
  44. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +26 -12
  45. package/tests/fixtures/check.fixtures.js +33 -32
  46. package/tests/fixtures/networkV1.fixtures.js +3 -2
  47. package/tests/fixtures/protobuf.fixtures.js +33 -32
  48. package/tests/helpers/StateNetworkFactory.js +2 -2
  49. package/tests/helpers/address.js +6 -0
  50. package/tests/helpers/autobaseTestHelpers.js +2 -1
  51. package/tests/helpers/config.js +2 -1
  52. package/tests/helpers/setupApplyTests.js +6 -10
  53. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +3 -3
  54. package/tests/unit/messages/network/NetworkMessageDirector.test.js +3 -5
  55. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  56. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  57. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  58. package/tests/unit/utils/type/type.test.js +25 -0
  59. package/tests/unit/utils/utils.test.js +1 -0
@@ -1,26 +1,23 @@
1
1
  import b4a from 'b4a';
2
- import {
3
- CLEANUP_INTERVAL_MS,
4
- CONNECTION_TIMEOUT_MS,
5
- MAX_TRANSACTIONS_PER_SECOND
6
- } from '../../../utils/constants.js';
7
2
 
8
3
  class TransactionRateLimiterService {
9
4
  #lastCleanup;
10
5
  #connectionsStatistics;
11
6
  #swarm;
7
+ #config;
12
8
 
13
- constructor(swarm) {
9
+ constructor(swarm, config) {
14
10
  this.#lastCleanup = Date.now();
15
11
  this.#connectionsStatistics = new Map();
16
12
  this.#swarm = swarm
13
+ this.#config = config
17
14
  }
18
15
 
19
16
  /*
20
17
  Checks if the peer has exceeded the rate limit.
21
18
  A peer is considered to have exceeded the rate limit if:
22
19
  - The time since the last activity is greater than or equal to 1000 ms (1 second)
23
- - The number of transactions in the current session is greater than or equal to MAX_TRANSACTIONS_PER_SECOND
20
+ - The number of transactions in the current session is greater than or equal to rateLimitMaxTransactionsPerSecond
24
21
  If the rate limit is exceeded, the peer is disconnected.
25
22
  */
26
23
  #hasExceededRateLimit(peer) {
@@ -33,7 +30,7 @@ class TransactionRateLimiterService {
33
30
  this.#connectionsStatistics.set(peer, peerData);
34
31
  }
35
32
 
36
- return peerData.transactionCount >= MAX_TRANSACTIONS_PER_SECOND;
33
+ return peerData.transactionCount >= this.#config.rateLimitMaxTransactionsPerSecond;
37
34
  }
38
35
 
39
36
  /*
@@ -65,13 +62,13 @@ class TransactionRateLimiterService {
65
62
  }
66
63
 
67
64
  #shouldCleanupConnections(currentTime) {
68
- return currentTime - this.#lastCleanup >= CLEANUP_INTERVAL_MS;
65
+ return currentTime - this.#lastCleanup >= this.#config.rateLimitCleanupIntervalMs;
69
66
  }
70
67
 
71
68
  /**
72
69
  Cleans up old connections that have timed out.
73
70
  Condition for cleanup based on #shouldCleanupConnections:
74
- - If the last cleanup was more than CLEANUP_INTERVAL_MS ago
71
+ - If the last cleanup was more than rateLimitCleanupIntervalMs ago
75
72
  */
76
73
  #cleanUpOldConnections(currentTime) {
77
74
  if (!this.#shouldCleanupConnections(currentTime)) {
@@ -125,7 +122,7 @@ class TransactionRateLimiterService {
125
122
  */
126
123
  #isConnectionExpired(peer) {
127
124
  const peerData = this.#connectionsStatistics.get(peer);
128
- return peerData.lastActivityTime - peerData.sessionStartTime >= CONNECTION_TIMEOUT_MS;
125
+ return peerData.lastActivityTime - peerData.sessionStartTime >= this.#config.rateLimitConnectionTimeoutMs;
129
126
  }
130
127
  }
131
128
 
@@ -1,6 +1,5 @@
1
1
  import PeerWallet from "trac-wallet";
2
2
  import b4a from "b4a";
3
- import { MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION } from '../../../utils/constants.js';
4
3
  import { bufferToAddress } from '../../state/utils/address.js';
5
4
  import { sleep } from '../../../utils/helpers.js';
6
5
  import Scheduler from "../../../utils/Scheduler.js";
@@ -31,7 +30,7 @@ class ValidatorObserverService {
31
30
  * @param {Network} network
32
31
  * @param {State} state
33
32
  * @param {string} address
34
- * @param {object} config
33
+ * @param {Config} config
35
34
  **/
36
35
  constructor(network, state, address, config) {
37
36
  this.#config = config
@@ -132,7 +131,7 @@ class ValidatorObserverService {
132
131
  const validatorPubKeyHex = validatorPubKeyBuffer.toString('hex');
133
132
  const adminEntry = await this.state.getAdminEntry();
134
133
 
135
- if (validatorAddress !== adminEntry?.address || validatorListLength < MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION) {
134
+ if (validatorAddress !== adminEntry?.address || validatorListLength < this.#config.maxWritersForAdminIndexerConnection) {
136
135
  this.#network.tryConnect(validatorPubKeyHex, 'validator');
137
136
  }
138
137
  };
@@ -151,7 +150,7 @@ class ValidatorObserverService {
151
150
  return false;
152
151
  }
153
152
 
154
- if (validatorAddress === adminEntry?.address && validatorListLength >= MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION) {
153
+ if (validatorAddress === adminEntry?.address && validatorListLength >= this.#config.maxWritersForAdminIndexerConnection) {
155
154
  if (this.#network.validatorConnectionManager.exists(validatorPubKeyBuffer)) {
156
155
  this.#network.validatorConnectionManager.remove(validatorPubKeyBuffer)
157
156
  }
@@ -161,12 +160,12 @@ class ValidatorObserverService {
161
160
  // - Cannot connect if already connected to a validator
162
161
  // - Validator must exist and be a writer
163
162
  // - Cannot connect to indexers, except for admin-indexer
164
- // - Admin-indexer connection is allowed only when writers length has less than 10 writers
163
+ // - Admin-indexer connection is allowed only when writers length is below maxWritersForAdminIndexerConnection
165
164
  if (this.#network.validatorConnectionManager.connected(validatorPubKeyBuffer) ||
166
165
  this.#network.validatorConnectionManager.maxConnectionsReached() ||
167
166
  validatorEntry === null ||
168
167
  !validatorEntry.isWriter ||
169
- (validatorEntry.isIndexer && (validatorAddress !== adminEntry?.address || validatorListLength >= MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION))
168
+ (validatorEntry.isIndexer && (validatorAddress !== adminEntry?.address || validatorListLength >= this.#config.maxWritersForAdminIndexerConnection))
170
169
  ) {
171
170
  return false;
172
171
  }
@@ -12,7 +12,6 @@ import {
12
12
  HYPERBEE_VALUE_ENCODING,
13
13
  BATCH_SIZE,
14
14
  ADMIN_INITIAL_STAKED_BALANCE,
15
- MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION,
16
15
  TRAC_NAMESPACE,
17
16
  CustomEventType
18
17
  } from '../../utils/constants.js';
@@ -58,7 +57,7 @@ class State extends ReadyResource {
58
57
  /**
59
58
  * @param {Corestore} store
60
59
  * @param {PeerWallet} wallet
61
- * @param {object} config
60
+ * @param {Config} config
62
61
  **/
63
62
  constructor(store, wallet, config) {
64
63
  super();
@@ -178,7 +177,7 @@ class State extends ReadyResource {
178
177
  async isAdminAllowedToValidate() {
179
178
  const isAdmin = this.writingKey.toString('hex') === this.#config.bootstrap.toString('hex');
180
179
  const isIndexer = this.isIndexer();
181
- const lengthCondition = await this.getWriterLength() <= MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION;
180
+ const lengthCondition = await this.getWriterLength() <= this.#config.maxWritersForAdminIndexerConnection;
182
181
  return !!(isAdmin && isIndexer && lengthCondition);
183
182
  }
184
183
 
package/src/index.js CHANGED
@@ -55,7 +55,7 @@ export class MainSettlementBus extends ReadyResource {
55
55
  #config
56
56
 
57
57
  /**
58
- * @param {object} config
58
+ * @param {Config} config
59
59
  **/
60
60
  constructor(config) {
61
61
  super();
@@ -96,6 +96,8 @@ export class MainSettlementBus extends ReadyResource {
96
96
  }
97
97
 
98
98
  async _open() {
99
+ await fileUtils.ensureCoresStoreDir(this.#config);
100
+
99
101
  if (this.#config.enableWallet) {
100
102
  await this.#wallet.initKeyPair(
101
103
  this.#config.keyPairPath,
@@ -8,7 +8,7 @@ import {encodeCapabilities} from "../../../utils/buffer.js";
8
8
  /**
9
9
  * Builder for v1 internal network protocol messages.
10
10
  * @param {PeerWallet} wallet
11
- * @param {object} config
11
+ * @param {Config} config
12
12
  */
13
13
  class NetworkMessageBuilder {
14
14
  #wallet;
@@ -26,7 +26,7 @@ class NetworkMessageBuilder {
26
26
 
27
27
  /**
28
28
  * @param {PeerWallet} wallet
29
- * @param {object} config
29
+ * @param {Config} config
30
30
  */
31
31
  constructor(wallet, config) {
32
32
  this.#config = config;
@@ -23,7 +23,7 @@ import { isHexString } from '../../utils/helpers.js';
23
23
  /**
24
24
  * Builder for partial/complete ApplyState messages.
25
25
  * @param {PeerWallet} wallet
26
- * @param {object} config
26
+ * @param {Config} config
27
27
  */
28
28
  class ApplyStateMessageBuilder {
29
29
  #address;
@@ -26,7 +26,7 @@ class Check {
26
26
  #config
27
27
 
28
28
  /**
29
- * @param {object} config
29
+ * @param {Config} config
30
30
  **/
31
31
  constructor(config) {
32
32
  this.#config = config
@@ -82,13 +82,6 @@ export const TRAC_NAMESPACE = 'TracNetwork';
82
82
  export const WHITELIST_SLEEP_INTERVAL = 1_000;
83
83
  export const BALANCE_MIGRATION_SLEEP_INTERVAL = 500;
84
84
 
85
- // Connectivity constants
86
- export const MAX_PEERS = 64;
87
- export const MAX_PARALLEL = 64;
88
- export const MAX_SERVER_CONNECTIONS = Infinity;
89
- export const MAX_CLIENT_CONNECTIONS = Infinity;
90
- export const MAX_WRITERS_FOR_ADMIN_INDEXER_CONNECTION = 10;
91
-
92
85
  // State
93
86
  export const ACK_INTERVAL = 1_000;
94
87
  export const AUTOBASE_VALUE_ENCODING = 'binary';
@@ -115,16 +108,6 @@ export const BOOTSTRAP_HEXSTRING_LENGTH = 64;
115
108
 
116
109
  // Pool constants
117
110
  export const BATCH_SIZE = 10;
118
- export const PROCESS_INTERVAL_MS = 50;
119
-
120
- // Rate limiting constants
121
- export const CLEANUP_INTERVAL_MS = 120_000;
122
- export const CONNECTION_TIMEOUT_MS = 60_000;
123
- export const MAX_TRANSACTIONS_PER_SECOND = 50;
124
-
125
- // Operation handler constants
126
- export const MAX_PARTIAL_TX_PAYLOAD_BYTE_SIZE = 3072;
127
- export const TRANSACTION_POOL_SIZE = 1000;
128
111
 
129
112
  // Network message constants
130
113
  export const NETWORK_MESSAGE_TYPES = Object.freeze({
@@ -123,6 +123,18 @@ export async function getAllMigrationFiles(migrationDirectory = BALANCE_MIGRATED
123
123
  }
124
124
  }
125
125
 
126
+ export async function ensureCoresStoreDir(config) {
127
+ try {
128
+ // const storesDirectoryStats = await fs.promises.stat(config.storesDirectory);
129
+ // if (!storesDirectoryStats.isDirectory()) {
130
+ // throw new Error(`Stores directory path is not a directory: ${config.storesDirectory}`);
131
+ // }
132
+ await fs.promises.mkdir(config.storesFullPath, { recursive: true });
133
+ } catch (err) {
134
+ throw new Error(`Failed to ensure corestore directory: ${err.message}`);
135
+ }
136
+ }
137
+
126
138
  export async function validateBalanceMigrationData(addresses) {
127
139
  const migrationFiles = await getAllMigrationFiles(BALANCE_MIGRATED_DIR);
128
140
  const addressSet = new Set(addresses.map(a => a.address));
@@ -197,6 +209,7 @@ export default {
197
209
  readAddressesFromWhitelistFile,
198
210
  readBalanceMigrationFile,
199
211
  getAllMigrationFiles,
212
+ ensureCoresStoreDir,
200
213
  validateBalanceMigrationData,
201
214
  validateWhitelistMigrationData,
202
215
  getNextMigrationNumber,
@@ -0,0 +1,26 @@
1
+ import _ from "lodash"
2
+
3
+ /**
4
+ * Checks if `value` is considered defined akin to RoR `#defined?`.
5
+ *
6
+ * @static
7
+ * @param {*} value The value to check.
8
+ * @returns {boolean} Returns `false` if `value` is nullish, else `true`.
9
+ * @example
10
+ *
11
+ * isDefined(undefined);
12
+ * // => false
13
+ *
14
+ * isDefined(null);
15
+ * // => false
16
+ *
17
+ * isDefined(void 0);
18
+ * // => false
19
+ *
20
+ * isDefined(NaN);
21
+ * // => false
22
+ */
23
+ export function isDefined(value) {
24
+ return !_.isNil(value) && !_.isNaN(value)
25
+ }
26
+
@@ -44,8 +44,7 @@ export const registerBalanceTests = (context) => {
44
44
  expect(BigInt(res.body.balance)).toBe(0n)
45
45
  })
46
46
 
47
- //TODO: This test should return 400, but for backward compatibility reasons it currently returns 200 with zero balance. Please fix this.
48
- it.skip("returns zero balance for an invalid address format", async () => {
47
+ it("returns 400 for an invalid address format", async () => {
49
48
  const invalidAddress = "not-a-valid-address"
50
49
  const res = await request(context.server).get(`/v1/balance/${invalidAddress}`)
51
50
  expect(res.statusCode).toBe(400)
@@ -1,6 +1,5 @@
1
1
  import request from "supertest"
2
2
  import b4a from "b4a"
3
- import { $TNK } from "../../../../src/core/state/utils/balance.js"
4
3
  import { buildRpcSelfTransferPayload, waitForConnection } from "../../../helpers/transactionPayloads.mjs"
5
4
 
6
5
  const toBase64 = (value) => b4a.toString(b4a.from(JSON.stringify(value)), "base64")
@@ -52,8 +51,7 @@ export const registerBroadcastTransactionTests = (context) => {
52
51
  expect(res.body).toEqual({ error: "Payload must be a valid base64 string." })
53
52
  })
54
53
 
55
- // TODO: enable once handler returns 400 for client-side decode errors
56
- it.skip("returns 400 when decoded payload is not valid JSON", async () => {
54
+ it("returns 400 when decoded payload is not valid JSON", async () => {
57
55
  const invalidJsonBase64 = b4a.toString(b4a.from("{{invalid"), "base64")
58
56
 
59
57
  await waitForConnection(context.rpcMsb)
@@ -66,8 +64,7 @@ export const registerBroadcastTransactionTests = (context) => {
66
64
  expect(res.body).toEqual({ error: "Decoded payload is not valid JSON." })
67
65
  })
68
66
 
69
- // TODO: enable once handler returns 400 for client-side validation errors
70
- it.skip("returns 400 for invalid transaction structure", async () => {
67
+ it("returns 400 for invalid transaction structure", async () => {
71
68
  const invalidStructure = {
72
69
  type: 1,
73
70
  address: context.wallet.address,
@@ -83,38 +80,37 @@ export const registerBroadcastTransactionTests = (context) => {
83
80
  expect(res.body).toEqual({ error: "Invalid payload structure." })
84
81
  })
85
82
 
86
- // TODO: AFTER REFACTORIZATION IMPROVE THESE IMPLEMENTATIONS ENDPOINT TO COVER THESE TESTS.
87
- it.skip("returns 413 when payload exceeds size limit", async () => {
88
- const largeString = "a".repeat(3_000_000)
89
- const payload = toBase64({ type: 1, address: context.wallet.address, txo: { large: largeString } })
83
+ it("returns 413 when payload exceeds size limit", async () => {
84
+ const largeString = "a".repeat(2_100_000);
85
+ const payload = toBase64({ type: 1, address: context.wallet.address, txo: { large: largeString } });
90
86
 
91
- await waitForConnection(context.rpcMsb)
87
+ await waitForConnection(context.rpcMsb);
92
88
  const res = await request(context.server)
93
89
  .post("/v1/broadcast-transaction")
94
90
  .set("Accept", "application/json")
95
- .send(JSON.stringify({ payload }))
91
+ .send(JSON.stringify({ payload }));
96
92
 
97
- expect(res.statusCode).toBe(413)
98
- })
93
+ expect(res.statusCode).toBe(413);
94
+ });
99
95
 
100
- it.skip("returns 429 on repeated broadcast failures", async () => {
101
- // TODO: Would require forcing msb to throw 'Failed to broadcast transaction after multiple attempts.'
102
- const txData = await tracCrypto.transaction.preBuild(
103
- context.wallet.address,
104
- context.wallet.address,
105
- b4a.toString($TNK(1n), 'hex'),
106
- b4a.toString(await context.rpcMsb.state.getIndexerSequenceState(), 'hex')
107
- )
108
96
 
109
- const payload = tracCrypto.transaction.build(txData, b4a.from(context.wallet.secretKey, 'hex'))
110
- await waitForConnection(context.rpcMsb)
111
- const res = await request(context.server)
112
- .post("/v1/broadcast-transaction")
113
- .set("Accept", "application/json")
114
- .send(JSON.stringify({ payload }))
97
+ it("returns 429 on repeated broadcast failures", async () => {
98
+ const { payload } = await buildRpcSelfTransferPayload(context, context.rpcMsb.state, 1n);
99
+ const originalMethod = context.rpcMsb.broadcastPartialTransaction;
100
+ context.rpcMsb.broadcastPartialTransaction = async () => false;
115
101
 
116
- expect(res.statusCode).toBe(429)
117
- expect(res.body).toEqual({ error: "Failed to broadcast transaction after multiple attempts." })
118
- })
102
+ try {
103
+ await waitForConnection(context.rpcMsb);
104
+ const res = await request(context.server)
105
+ .post("/v1/broadcast-transaction")
106
+ .set("Accept", "application/json")
107
+ .send(JSON.stringify({ payload }));
108
+
109
+ expect(res.statusCode).toBe(429);
110
+ expect(res.body).toEqual({ error: "Failed to broadcast transaction after multiple attempts." });
111
+ } finally {
112
+ context.rpcMsb.broadcastPartialTransaction = originalMethod;
113
+ }
114
+ });
119
115
  })
120
116
  }
@@ -0,0 +1,33 @@
1
+ import request from "supertest"
2
+
3
+ export const registerHealthTests = (context) => {
4
+ describe("GET /v1/health", () => {
5
+ it("should return 200 and ok:true when healthy", async () => {
6
+ const res = await request(context.server).get("/v1/health")
7
+ expect(res.statusCode).toBe(200)
8
+ expect(res.body).toEqual({ ok: true })
9
+ })
10
+
11
+ it("should return 503 when the state is unavailable", async () => {
12
+ const originalState = context.rpcMsb.state;
13
+ Object.defineProperty(context.rpcMsb, 'state', {
14
+ get: () => null,
15
+ configurable: true
16
+ });
17
+
18
+ try {
19
+ const res = await request(context.server).get("/v1/health")
20
+
21
+ expect(res.statusCode).toBe(503)
22
+ expect(res.body).toEqual({
23
+ error: "Could not connect to RPC server"
24
+ })
25
+ } finally {
26
+ Object.defineProperty(context.rpcMsb, 'state', {
27
+ get: () => originalState,
28
+ configurable: true
29
+ });
30
+ }
31
+ })
32
+ })
33
+ }
@@ -13,6 +13,7 @@ import { registerTxDetailsTests } from "./tx-details/tx-details.test.mjs"
13
13
  import { registerTxTests } from "./tx/tx.test.mjs"
14
14
  import { registerTxvTests } from "./txv/txv.test.mjs"
15
15
  import { registerUnconfirmedLengthTests } from "./unconfirmed-length/unconfirmed-length.test.mjs"
16
+ import { registerHealthTests } from "./health/health.test.mjs"
16
17
 
17
18
  let toClose
18
19
  let tmpDirectory
@@ -37,8 +38,7 @@ const setupNetwork = async () => {
37
38
  enableInteractiveMode: false,
38
39
  disableRateLimit: true,
39
40
  enableTxApplyLogs: false,
40
- storesDirectory: `${tmpDirectory}/stores/`,
41
- storeName: '/admin'
41
+ storesDirectory: `${tmpDirectory}/admin/`,
42
42
  }
43
43
 
44
44
  const admin = await setupMsbAdmin(testKeyPair1, tmpDirectory, rpcOpts)
@@ -91,4 +91,5 @@ describe("API acceptance tests", () => {
91
91
  registerTxPayloadsBulkTests(testContext)
92
92
  registerTxDetailsTests(testContext)
93
93
  registerAccountTests(testContext)
94
+ registerHealthTests(testContext)
94
95
  })
@@ -29,70 +29,81 @@ export const registerTxTests = (context) => {
29
29
  expect(res.body).toEqual({ txDetails: null })
30
30
  })
31
31
 
32
- // TODO: adjust implementation to cover tests below
33
- it.skip("returns 400 for invalid hash format (too short)", async () => {
32
+ it("returns 400 for invalid hash format (too short)", async () => {
34
33
  const invalidHash = '0'.repeat(63)
35
34
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
36
35
  expect(res.statusCode).toBe(400)
37
36
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
38
37
  })
39
38
 
40
- it.skip("returns 400 for invalid hash format (non-hex)", async () => {
39
+ it("returns 400 for invalid hash format (non-hex)", async () => {
41
40
  const invalidHash = 'Z'.repeat(64)
42
41
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
43
42
  expect(res.statusCode).toBe(400)
44
43
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
45
44
  })
46
45
 
47
- it.skip("returns 400 when no hash provided", async () => {
46
+ it("returns 400 when no hash provided", async () => {
48
47
  const res = await request(context.server).get('/v1/tx')
49
48
  expect(res.statusCode).toBe(400)
50
49
  expect(res.body).toEqual({ error: "Transaction hash is required" })
51
50
  })
52
51
 
53
- it.skip("returns 400 for hash with invalid characters", async () => {
52
+ it("returns 400 for hash with invalid characters", async () => {
54
53
  const invalidHash = '0b4d1c1dac48$af13212f6166017399457476a0b644850875b7f4b79df6ff89c'
55
54
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
56
55
  expect(res.statusCode).toBe(400)
57
56
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
58
57
  })
59
58
 
60
- it.skip("returns 400 for hash with special characters", async () => {
59
+ it("returns 400 for hash with special characters", async () => {
61
60
  const invalidHash = '!@#$%^&*'.repeat(8)
62
61
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
63
62
  expect(res.statusCode).toBe(400)
64
63
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
65
64
  })
66
65
 
67
- it.skip("returns 400 for hash with spaces", async () => {
66
+ it("returns 400 for hash with spaces", async () => {
68
67
  const invalidHash = '0b4d1c1dac48af13212f616601d7399457476a0b644850875b7 4b79df6ff89c'
69
68
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
70
69
  expect(res.statusCode).toBe(400)
71
70
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
72
71
  })
73
72
 
74
- it.skip("returns 400 for hash with 0x prefix", async () => {
73
+ it("returns 400 for hash with 0x prefix", async () => {
75
74
  const hash = "0x" + "0".repeat(62)
76
75
  const res = await request(context.server).get(`/v1/tx/${hash}`)
77
76
  expect(res.statusCode).toBe(400)
78
77
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
79
78
  })
80
79
 
81
- it.skip("returns 400 for odd-length hex", async () => {
80
+ it("returns 400 for odd-length hex", async () => {
82
81
  const hash = "a".repeat(63)
83
82
  const res = await request(context.server).get(`/v1/tx/${hash}`)
84
83
  expect(res.statusCode).toBe(400)
85
84
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
86
85
  })
87
86
 
88
- it.skip("accepts uppercase hex", async () => {
89
- const hash = "A".repeat(64)
90
- const res = await request(context.server).get(`/v1/tx/${hash}`)
91
- expect(res.statusCode).toBe(200)
92
- })
93
87
 
94
- it.skip("returns 400 for trailing space hash", async () => {
95
- const hash = `${"a".repeat(64)} `
88
+ it("accepts uppercase hex", async () => {
89
+ const { payload, txHashHex } = await buildRpcSelfTransferPayload(context, context.rpcMsb.state, 1n);
90
+
91
+ // Send the transaction
92
+ await request(context.server)
93
+ .post("/v1/broadcast-transaction")
94
+ .send(JSON.stringify({ payload }));
95
+
96
+ // Waits for the node indexer to process
97
+ await new Promise(resolve => setTimeout(resolve, 500));
98
+
99
+ const uppercaseHash = txHashHex.toUpperCase();
100
+ const res = await request(context.server).get(`/v1/tx/${uppercaseHash}`);
101
+
102
+ expect(res.statusCode).toBe(200);
103
+ });
104
+
105
+ it("returns 400 for trailing space hash", async () => {
106
+ const hash = "a".repeat(64) + "%20" // Forcing space
96
107
  const res = await request(context.server).get(`/v1/tx/${hash}`)
97
108
  expect(res.statusCode).toBe(400)
98
109
  })
@@ -182,18 +182,32 @@ export const registerTxDetailsTests = (context) => {
182
182
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
183
183
  })
184
184
 
185
- // TODOadjust implementation to cover tests below
186
- it.skip("accepts uppercase hex", async () => {
187
- const hash = "A".repeat(64)
188
- const res = await request(context.server).get(`/v1/tx/details/${hash}?confirmed=false`)
189
- expect([200]).toContain(res.statusCode)
190
- expect(res.statusCode).toBe(200)
191
- })
185
+ it("accepts uppercase hex", async () => {
186
+ const { payload, txHashHex } = await buildRpcSelfTransferPayload(
187
+ context,
188
+ context.rpcMsb.state,
189
+ 1n
190
+ );
192
191
 
193
- it.skip("returns 400 for trailing space hash", async () => {
194
- const hash = `${"a".repeat(64)} `
195
- const res = await request(context.server).get(`/v1/tx/details/${hash}`)
196
- expect(res.statusCode).toBe(400)
197
- })
192
+ await waitForConnection(context.rpcMsb);
193
+ await request(context.server)
194
+ .post("/v1/broadcast-transaction")
195
+ .send(JSON.stringify({ payload }));
196
+
197
+ const upperHash = txHashHex.toUpperCase();
198
+ const res = await request(context.server)
199
+ .get(`/v1/tx/details/${upperHash}?confirmed=false`);
200
+
201
+ expect(res.statusCode).toBe(200);
202
+ });
203
+
204
+ it("returns 400 for trailing space hash", async () => {
205
+ const hash = "a".repeat(64) + " ";
206
+ const res = await request(context.server)
207
+ .get(`/v1/tx/details/${encodeURIComponent(hash)}`);
208
+
209
+ expect(res.statusCode).toBe(400);
210
+ expect(res.body).toEqual({ error: "Invalid transaction hash format" });
211
+ });
198
212
  })
199
213
  }