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
|
@@ -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
|
|
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 >=
|
|
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 >=
|
|
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
|
|
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 >=
|
|
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 {
|
|
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 <
|
|
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 >=
|
|
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
|
|
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 >=
|
|
168
|
+
(validatorEntry.isIndexer && (validatorAddress !== adminEntry?.address || validatorListLength >= this.#config.maxWritersForAdminIndexerConnection))
|
|
170
169
|
) {
|
|
171
170
|
return false;
|
|
172
171
|
}
|
package/src/core/state/State.js
CHANGED
|
@@ -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 {
|
|
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() <=
|
|
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 {
|
|
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 {
|
|
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 {
|
|
29
|
+
* @param {Config} config
|
|
30
30
|
*/
|
|
31
31
|
constructor(wallet, config) {
|
|
32
32
|
this.#config = config;
|
package/src/utils/check.js
CHANGED
package/src/utils/constants.js
CHANGED
|
@@ -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({
|
package/src/utils/fileUtils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
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
|
-
|
|
110
|
-
await
|
|
111
|
-
const
|
|
112
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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}/
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
95
|
-
const
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
}
|