trac-msb 0.2.9 → 0.2.11

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 (143) hide show
  1. package/CODE_OF_CONDUCT.md +128 -0
  2. package/README.md +33 -18
  3. package/docker-compose.yml +1 -0
  4. package/docs/trac_network_http_api.openapi.yaml +889 -0
  5. package/msb.mjs +4 -21
  6. package/package.json +16 -12
  7. package/proto/network/v1/enums/message_type.proto +16 -0
  8. package/proto/network/v1/enums/result_code.proto +84 -0
  9. package/proto/network/v1/messages/broadcast_transaction_request.proto +9 -0
  10. package/proto/network/v1/messages/broadcast_transaction_response.proto +13 -0
  11. package/proto/network/v1/messages/liveness_request.proto +8 -0
  12. package/proto/network/v1/messages/liveness_response.proto +11 -0
  13. package/proto/network/v1/network_message.proto +22 -0
  14. package/rpc/handlers.js +163 -90
  15. package/rpc/routes/v1.js +3 -1
  16. package/rpc/rpc_server.js +3 -3
  17. package/rpc/rpc_services.js +45 -31
  18. package/rpc/utils/helpers.js +82 -51
  19. package/scripts/generate-protobufs.js +37 -12
  20. package/src/config/args.js +46 -0
  21. package/src/config/config.js +99 -5
  22. package/src/config/env.js +86 -7
  23. package/src/core/network/Network.js +79 -46
  24. package/src/core/network/protocols/LegacyProtocol.js +21 -11
  25. package/src/core/network/protocols/NetworkMessages.js +38 -17
  26. package/src/core/network/protocols/ProtocolInterface.js +14 -2
  27. package/src/core/network/protocols/ProtocolSession.js +144 -17
  28. package/src/core/network/protocols/V1Protocol.js +37 -18
  29. package/src/core/network/protocols/connectionPolicies.js +88 -0
  30. package/src/core/network/protocols/legacy/NetworkMessageRouter.js +26 -20
  31. package/src/core/network/protocols/{shared/handlers/base/BaseOperationHandler.js → legacy/handlers/BaseStateOperationHandler.js} +25 -15
  32. package/src/core/network/protocols/legacy/handlers/{GetRequestHandler.js → LegacyGetRequestHandler.js} +6 -6
  33. package/src/core/network/protocols/legacy/handlers/LegacyResponseHandler.js +23 -0
  34. package/src/core/network/protocols/{shared/handlers/RoleOperationHandler.js → legacy/handlers/LegacyRoleOperationHandler.js} +20 -13
  35. package/src/core/network/protocols/{shared/handlers/SubnetworkOperationHandler.js → legacy/handlers/LegacySubnetworkOperationHandler.js} +29 -18
  36. package/src/core/network/protocols/{shared/handlers/TransferOperationHandler.js → legacy/handlers/LegacyTransferOperationHandler.js} +18 -12
  37. package/src/core/network/protocols/legacy/validators/base/BaseResponse.js +1 -1
  38. package/src/core/network/protocols/shared/errors/SharedValidatorRejectionError.js +27 -0
  39. package/src/core/network/protocols/shared/validators/{PartialBootstrapDeployment.js → PartialBootstrapDeploymentValidator.js} +9 -4
  40. package/src/core/network/protocols/shared/validators/{base/PartialOperation.js → PartialOperationValidator.js} +47 -25
  41. package/src/core/network/protocols/shared/validators/{PartialRoleAccess.js → PartialRoleAccessValidator.js} +51 -17
  42. package/src/core/network/protocols/shared/validators/{PartialTransaction.js → PartialTransactionValidator.js} +21 -7
  43. package/src/core/network/protocols/shared/validators/{PartialTransfer.js → PartialTransferValidator.js} +26 -9
  44. package/src/core/network/protocols/v1/NetworkMessageRouter.js +91 -7
  45. package/src/core/network/protocols/v1/V1ProtocolError.js +91 -0
  46. package/src/core/network/protocols/v1/handlers/V1BaseOperationHandler.js +65 -0
  47. package/src/core/network/protocols/v1/handlers/V1BroadcastTransactionOperationHandler.js +389 -0
  48. package/src/core/network/protocols/v1/handlers/V1LivenessOperationHandler.js +87 -0
  49. package/src/core/network/protocols/v1/validators/V1BaseOperation.js +211 -0
  50. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionRequest.js +26 -0
  51. package/src/core/network/protocols/v1/validators/V1BroadcastTransactionResponse.js +276 -0
  52. package/src/core/network/protocols/v1/validators/V1LivenessRequest.js +15 -0
  53. package/src/core/network/protocols/v1/validators/V1LivenessResponse.js +17 -0
  54. package/src/core/network/protocols/v1/validators/V1ValidationSchema.js +210 -0
  55. package/src/core/network/services/ConnectionManager.js +147 -95
  56. package/src/core/network/services/MessageOrchestrator.js +152 -28
  57. package/src/core/network/services/PendingRequestService.js +172 -0
  58. package/src/core/network/services/TransactionCommitService.js +149 -0
  59. package/src/core/network/services/TransactionPoolService.js +133 -22
  60. package/src/core/network/services/TransactionRateLimiterService.js +57 -42
  61. package/src/core/network/services/ValidatorHealthCheckService.js +127 -0
  62. package/src/core/network/services/ValidatorObserverService.js +23 -32
  63. package/src/core/state/State.js +72 -22
  64. package/src/index.js +8 -5
  65. package/src/messages/network/v1/NetworkMessageBuilder.js +61 -81
  66. package/src/messages/network/v1/NetworkMessageDirector.js +16 -50
  67. package/src/messages/state/ApplyStateMessageBuilder.js +1 -1
  68. package/src/utils/Scheduler.js +0 -8
  69. package/src/utils/check.js +1 -1
  70. package/src/utils/constants.js +68 -19
  71. package/src/utils/deepEqualApplyPayload.js +40 -0
  72. package/src/utils/fileUtils.js +13 -0
  73. package/src/utils/helpers.js +10 -1
  74. package/src/utils/logger.js +25 -0
  75. package/src/utils/normalizers.js +38 -0
  76. package/src/utils/protobuf/networkV1.generated.cjs +2460 -0
  77. package/src/utils/protobuf/operationHelpers.js +24 -3
  78. package/src/utils/type.js +26 -0
  79. package/tests/acceptance/v1/account/account.test.mjs +8 -2
  80. package/tests/acceptance/v1/balance/balance.test.mjs +1 -2
  81. package/tests/acceptance/v1/broadcast-transaction/broadcast-transaction.test.mjs +26 -30
  82. package/tests/acceptance/v1/health/health.test.mjs +33 -0
  83. package/tests/acceptance/v1/rpc.test.mjs +3 -2
  84. package/tests/acceptance/v1/tx/tx.test.mjs +50 -17
  85. package/tests/acceptance/v1/tx-details/tx-details.test.mjs +60 -18
  86. package/tests/fixtures/check.fixtures.js +33 -32
  87. package/tests/fixtures/networkV1.fixtures.js +2 -27
  88. package/tests/fixtures/protobuf.fixtures.js +33 -32
  89. package/tests/helpers/StateNetworkFactory.js +2 -2
  90. package/tests/helpers/address.js +6 -0
  91. package/tests/helpers/autobaseTestHelpers.js +2 -1
  92. package/tests/helpers/config.js +2 -1
  93. package/tests/helpers/setupApplyTests.js +6 -10
  94. package/tests/helpers/transactionPayloads.mjs +2 -2
  95. package/tests/unit/messages/network/NetworkMessageBuilder.test.js +241 -81
  96. package/tests/unit/messages/network/NetworkMessageDirector.test.js +225 -81
  97. package/tests/unit/network/LegacyNetworkMessageRouter.test.js +54 -0
  98. package/tests/unit/network/ProtocolSession.test.js +127 -0
  99. package/tests/unit/network/networkModule.test.js +4 -1
  100. package/tests/unit/network/services/ConnectionManager.test.js +450 -0
  101. package/tests/unit/network/services/MessageOrchestrator.test.js +445 -0
  102. package/tests/unit/network/services/PendingRequestService.test.js +431 -0
  103. package/tests/unit/network/services/TransactionCommitService.test.js +246 -0
  104. package/tests/unit/network/services/TransactionPoolService.test.js +489 -0
  105. package/tests/unit/network/services/TransactionRateLimiterService.test.js +139 -0
  106. package/tests/unit/network/services/ValidatorHealthCheckService.test.js +115 -0
  107. package/tests/unit/network/services/services.test.js +17 -0
  108. package/tests/unit/network/utils/v1TestUtils.js +153 -0
  109. package/tests/unit/network/v1/NetworkMessageRouterV1.test.js +151 -0
  110. package/tests/unit/network/v1/V1BaseOperation.test.js +356 -0
  111. package/tests/unit/network/v1/V1BroadcastTransactionOperationHandler.test.js +129 -0
  112. package/tests/unit/network/v1/V1BroadcastTransactionRequest.test.js +53 -0
  113. package/tests/unit/network/v1/V1BroadcastTransactionResponse.test.js +512 -0
  114. package/tests/unit/network/v1/V1LivenessRequest.test.js +32 -0
  115. package/tests/unit/network/v1/V1LivenessResponse.test.js +45 -0
  116. package/tests/unit/network/v1/V1ResultCode.test.js +84 -0
  117. package/tests/unit/network/v1/V1ValidationSchema.test.js +13 -0
  118. package/tests/unit/network/v1/connectionPolicies.test.js +49 -0
  119. package/tests/unit/network/v1/handlers/V1BaseOperationHandler.test.js +284 -0
  120. package/tests/unit/network/v1/handlers/V1BroadcastTransactionOperationHandler.test.js +794 -0
  121. package/tests/unit/network/v1/handlers/V1LivenessOperationHandler.test.js +193 -0
  122. package/tests/unit/network/v1/v1.handlers.test.js +15 -0
  123. package/tests/unit/network/v1/v1.test.js +19 -0
  124. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionRequest.test.js +119 -0
  125. package/tests/unit/network/v1/v1ValidationSchema/broadcastTransactionResponse.test.js +136 -0
  126. package/tests/unit/network/v1/v1ValidationSchema/common.test.js +308 -0
  127. package/tests/unit/network/v1/v1ValidationSchema/livenessRequest.test.js +90 -0
  128. package/tests/unit/network/v1/v1ValidationSchema/livenessResponse.test.js +133 -0
  129. package/tests/unit/unit.test.js +2 -2
  130. package/tests/unit/utils/deepEqualApplyPayload/deepEqualApplyPayload.test.js +102 -0
  131. package/tests/unit/utils/fileUtils/readAddressesFromWhitelistFile.test.js +4 -3
  132. package/tests/unit/utils/fileUtils/readBalanceMigrationFile.test.js +3 -2
  133. package/tests/unit/utils/migrationUtils/validateAddressFromIncomingFile.test.js +3 -2
  134. package/tests/unit/utils/protobuf/operationHelpers.test.js +2 -4
  135. package/tests/unit/utils/type/type.test.js +25 -0
  136. package/tests/unit/utils/utils.test.js +2 -0
  137. package/.github/workflows/acceptance-tests.yml +0 -42
  138. package/.github/workflows/publish.yml +0 -33
  139. package/.github/workflows/unit-tests.yml +0 -40
  140. package/proto/network.proto +0 -74
  141. package/src/core/network/protocols/legacy/handlers/ResponseHandler.js +0 -37
  142. package/src/utils/protobuf/network.cjs +0 -840
  143. package/tests/unit/network/ConnectionManager.test.js +0 -191
@@ -1,7 +1,17 @@
1
1
  import applyOperations from './applyOperations.cjs';
2
- import networkV1Operations from './network.cjs';
2
+ import networkV1Generated from './networkV1.generated.cjs';
3
3
  import b4a from 'b4a';
4
4
 
5
+ const networkV1Operations = networkV1Generated.network.v1;
6
+ const NETWORK_TO_OBJECT_OPTIONS = Object.freeze({
7
+ enums: Number,
8
+ longs: Number,
9
+ bytes: Buffer,
10
+ defaults: true,
11
+ arrays: true,
12
+ oneofs: false
13
+ });
14
+
5
15
  /**
6
16
  * Safely encodes an operation using `applyOperations.Operation.encode`.
7
17
  * If the encoding fails (e.g., due to an invalid payload), returns an empty Buffer.
@@ -36,6 +46,14 @@ export const safeDecodeApplyOperation = (payload) => {
36
46
  return null;
37
47
  }
38
48
 
49
+ export const unsafeDecodeApplyOperation= (payload) => {
50
+ return applyOperations.Operation.decode(payload);
51
+ }
52
+
53
+ export const unsafeEncodeApplyOperation = (payload) => {
54
+ return applyOperations.Operation.encode(payload);
55
+ }
56
+
39
57
  export const normalizeIncomingMessage = (message) => {
40
58
  if (!message) return null;
41
59
  if (b4a.isBuffer(message)) {
@@ -51,10 +69,13 @@ export const normalizeIncomingMessage = (message) => {
51
69
  };
52
70
 
53
71
  export const encodeV1networkOperation = (payload) => {
54
- return networkV1Operations.MessageHeader.encode(payload);
72
+ return b4a.from(networkV1Operations.MessageHeader.encode(payload).finish());
55
73
  }
56
74
 
57
75
 
58
76
  export const decodeV1networkOperation = (payload) => {
59
- return networkV1Operations.MessageHeader.decode(payload);
77
+ return networkV1Operations.MessageHeader.toObject(
78
+ networkV1Operations.MessageHeader.decode(payload),
79
+ NETWORK_TO_OBJECT_OPTIONS
80
+ );
60
81
  }
@@ -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
+
@@ -108,11 +108,17 @@ export const registerAccountTests = (context) => {
108
108
 
109
109
  it("returns 500 on internal error", async () => {
110
110
  const originalGetNodeEntry = context.rpcMsb.state.getNodeEntry
111
+ const failingAddress = randomAddress(context.rpcMsb.config.addressPrefix)
111
112
 
112
- context.rpcMsb.state.getNodeEntry = async () => { throw new Error("test") }
113
+ context.rpcMsb.state.getNodeEntry = async (address) => {
114
+ if (address === failingAddress) {
115
+ throw new Error("test")
116
+ }
117
+ return originalGetNodeEntry.call(context.rpcMsb.state, address)
118
+ }
113
119
 
114
120
  try {
115
- const res = await request(context.server).get(`/v1/account/${context.wallet.address}`)
121
+ const res = await request(context.server).get(`/v1/account/${failingAddress}`)
116
122
  expect(res.statusCode).toBe(500)
117
123
  expect(res.body).toEqual({ error: 'An error occurred processing the request.' })
118
124
  } finally {
@@ -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
  })
@@ -1,5 +1,24 @@
1
1
  import request from "supertest"
2
2
  import { buildRpcSelfTransferPayload, waitForConnection } from "../../../helpers/transactionPayloads.mjs"
3
+ import { sleep } from "../../../../src/utils/helpers.js"
4
+
5
+ const TX_ENDPOINT_TIMEOUT_MS = 4000
6
+ const TX_ENDPOINT_RETRY_INTERVAL_MS = 100
7
+
8
+ const waitForStatusCode = async (requestFactory, expectedStatusCode, timeoutMs = TX_ENDPOINT_TIMEOUT_MS) => {
9
+ const startedAt = Date.now()
10
+ let response = null
11
+
12
+ while ((Date.now() - startedAt) < timeoutMs) {
13
+ response = await requestFactory()
14
+ if (response.statusCode === expectedStatusCode) {
15
+ return response
16
+ }
17
+ await sleep(TX_ENDPOINT_RETRY_INTERVAL_MS)
18
+ }
19
+
20
+ return response
21
+ }
3
22
 
4
23
  export const registerTxTests = (context) => {
5
24
  describe("GET /v1/tx/:hash", () => {
@@ -17,7 +36,10 @@ export const registerTxTests = (context) => {
17
36
  .send(JSON.stringify({ payload }))
18
37
  expect(broadcastRes.statusCode).toBe(200)
19
38
 
20
- const res = await request(context.server).get(`/v1/tx/${txHashHex}`)
39
+ const res = await waitForStatusCode(
40
+ () => request(context.server).get(`/v1/tx/${txHashHex}`),
41
+ 200
42
+ )
21
43
  expect(res.statusCode).toBe(200)
22
44
  expect(res.body).toMatchObject({ txDetails: expect.any(Object) })
23
45
  })
@@ -29,70 +51,81 @@ export const registerTxTests = (context) => {
29
51
  expect(res.body).toEqual({ txDetails: null })
30
52
  })
31
53
 
32
- // TODO: adjust implementation to cover tests below
33
- it.skip("returns 400 for invalid hash format (too short)", async () => {
54
+ it("returns 400 for invalid hash format (too short)", async () => {
34
55
  const invalidHash = '0'.repeat(63)
35
56
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
36
57
  expect(res.statusCode).toBe(400)
37
58
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
38
59
  })
39
60
 
40
- it.skip("returns 400 for invalid hash format (non-hex)", async () => {
61
+ it("returns 400 for invalid hash format (non-hex)", async () => {
41
62
  const invalidHash = 'Z'.repeat(64)
42
63
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
43
64
  expect(res.statusCode).toBe(400)
44
65
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
45
66
  })
46
67
 
47
- it.skip("returns 400 when no hash provided", async () => {
68
+ it("returns 400 when no hash provided", async () => {
48
69
  const res = await request(context.server).get('/v1/tx')
49
70
  expect(res.statusCode).toBe(400)
50
71
  expect(res.body).toEqual({ error: "Transaction hash is required" })
51
72
  })
52
73
 
53
- it.skip("returns 400 for hash with invalid characters", async () => {
74
+ it("returns 400 for hash with invalid characters", async () => {
54
75
  const invalidHash = '0b4d1c1dac48$af13212f6166017399457476a0b644850875b7f4b79df6ff89c'
55
76
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
56
77
  expect(res.statusCode).toBe(400)
57
78
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
58
79
  })
59
80
 
60
- it.skip("returns 400 for hash with special characters", async () => {
81
+ it("returns 400 for hash with special characters", async () => {
61
82
  const invalidHash = '!@#$%^&*'.repeat(8)
62
83
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
63
84
  expect(res.statusCode).toBe(400)
64
85
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
65
86
  })
66
87
 
67
- it.skip("returns 400 for hash with spaces", async () => {
88
+ it("returns 400 for hash with spaces", async () => {
68
89
  const invalidHash = '0b4d1c1dac48af13212f616601d7399457476a0b644850875b7 4b79df6ff89c'
69
90
  const res = await request(context.server).get(`/v1/tx/${invalidHash}`)
70
91
  expect(res.statusCode).toBe(400)
71
92
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
72
93
  })
73
94
 
74
- it.skip("returns 400 for hash with 0x prefix", async () => {
95
+ it("returns 400 for hash with 0x prefix", async () => {
75
96
  const hash = "0x" + "0".repeat(62)
76
97
  const res = await request(context.server).get(`/v1/tx/${hash}`)
77
98
  expect(res.statusCode).toBe(400)
78
99
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
79
100
  })
80
101
 
81
- it.skip("returns 400 for odd-length hex", async () => {
102
+ it("returns 400 for odd-length hex", async () => {
82
103
  const hash = "a".repeat(63)
83
104
  const res = await request(context.server).get(`/v1/tx/${hash}`)
84
105
  expect(res.statusCode).toBe(400)
85
106
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
86
107
  })
87
108
 
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
109
 
94
- it.skip("returns 400 for trailing space hash", async () => {
95
- const hash = `${"a".repeat(64)} `
110
+ it("accepts uppercase hex", async () => {
111
+ const { payload, txHashHex } = await buildRpcSelfTransferPayload(context, context.rpcMsb.state, 1n);
112
+
113
+ // Send the transaction
114
+ await request(context.server)
115
+ .post("/v1/broadcast-transaction")
116
+ .send(JSON.stringify({ payload }));
117
+
118
+ // Waits for the node indexer to process
119
+ await new Promise(resolve => setTimeout(resolve, 500));
120
+
121
+ const uppercaseHash = txHashHex.toUpperCase();
122
+ const res = await request(context.server).get(`/v1/tx/${uppercaseHash}`);
123
+
124
+ expect(res.statusCode).toBe(200);
125
+ });
126
+
127
+ it("returns 400 for trailing space hash", async () => {
128
+ const hash = "a".repeat(64) + "%20" // Forcing space
96
129
  const res = await request(context.server).get(`/v1/tx/${hash}`)
97
130
  expect(res.statusCode).toBe(400)
98
131
  })
@@ -1,5 +1,24 @@
1
1
  import request from "supertest"
2
2
  import { buildRpcSelfTransferPayload, waitForConnection } from "../../../helpers/transactionPayloads.mjs"
3
+ import { sleep } from "../../../../src/utils/helpers.js"
4
+
5
+ const TX_DETAILS_TIMEOUT_MS = 4000
6
+ const TX_DETAILS_RETRY_INTERVAL_MS = 100
7
+
8
+ const waitForStatusCode = async (requestFactory, expectedStatusCode, timeoutMs = TX_DETAILS_TIMEOUT_MS) => {
9
+ const startedAt = Date.now()
10
+ let response = null
11
+
12
+ while ((Date.now() - startedAt) < timeoutMs) {
13
+ response = await requestFactory()
14
+ if (response.statusCode === expectedStatusCode) {
15
+ return response
16
+ }
17
+ await sleep(TX_DETAILS_RETRY_INTERVAL_MS)
18
+ }
19
+
20
+ return response
21
+ }
3
22
 
4
23
  export const registerTxDetailsTests = (context) => {
5
24
  describe("GET /v1/tx/details", () => {
@@ -17,8 +36,11 @@ export const registerTxDetailsTests = (context) => {
17
36
  .send(JSON.stringify({ payload }))
18
37
  expect(broadcastRes.statusCode).toBe(200)
19
38
 
20
- const resConfirmed = await request(context.server)
21
- .get(`/v1/tx/details/${txHashHex}?confirmed=true`)
39
+ const resConfirmed = await waitForStatusCode(
40
+ () => request(context.server)
41
+ .get(`/v1/tx/details/${txHashHex}?confirmed=true`),
42
+ 200
43
+ )
22
44
  expect(resConfirmed.statusCode).toBe(200)
23
45
 
24
46
  expect(resConfirmed.body).toMatchObject({
@@ -27,8 +49,11 @@ export const registerTxDetailsTests = (context) => {
27
49
  fee: expect.any(String)
28
50
  })
29
51
 
30
- const resUnconfirmed = await request(context.server)
31
- .get(`/v1/tx/details/${txHashHex}?confirmed=false`)
52
+ const resUnconfirmed = await waitForStatusCode(
53
+ () => request(context.server)
54
+ .get(`/v1/tx/details/${txHashHex}?confirmed=false`),
55
+ 200
56
+ )
32
57
  expect(resUnconfirmed.statusCode).toBe(200)
33
58
 
34
59
  expect(resUnconfirmed.body).toMatchObject({
@@ -57,8 +82,11 @@ export const registerTxDetailsTests = (context) => {
57
82
  .send(JSON.stringify({ payload }))
58
83
  expect(broadcastRes.statusCode).toBe(200)
59
84
 
60
- const res = await request(context.server)
61
- .get(`/v1/tx/details/${txHashHex}?confirmed=false`)
85
+ const res = await waitForStatusCode(
86
+ () => request(context.server)
87
+ .get(`/v1/tx/details/${txHashHex}?confirmed=false`),
88
+ 200
89
+ )
62
90
  expect(res.statusCode).toBe(200)
63
91
 
64
92
  expect(res.body).toMatchObject({
@@ -182,18 +210,32 @@ export const registerTxDetailsTests = (context) => {
182
210
  expect(res.body).toEqual({ error: "Invalid transaction hash format" })
183
211
  })
184
212
 
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
- })
213
+ it("accepts uppercase hex", async () => {
214
+ const { payload, txHashHex } = await buildRpcSelfTransferPayload(
215
+ context,
216
+ context.rpcMsb.state,
217
+ 1n
218
+ );
192
219
 
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
- })
220
+ await waitForConnection(context.rpcMsb);
221
+ await request(context.server)
222
+ .post("/v1/broadcast-transaction")
223
+ .send(JSON.stringify({ payload }));
224
+
225
+ const upperHash = txHashHex.toUpperCase();
226
+ const res = await request(context.server)
227
+ .get(`/v1/tx/details/${upperHash}?confirmed=false`);
228
+
229
+ expect(res.statusCode).toBe(200);
230
+ });
231
+
232
+ it("returns 400 for trailing space hash", async () => {
233
+ const hash = "a".repeat(64) + " ";
234
+ const res = await request(context.server)
235
+ .get(`/v1/tx/details/${encodeURIComponent(hash)}`);
236
+
237
+ expect(res.statusCode).toBe(400);
238
+ expect(res.body).toEqual({ error: "Invalid transaction hash format" });
239
+ });
198
240
  })
199
241
  }