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 +1 -1
- package/rpc/create_server.mjs +17 -5
- package/rpc/handlers.mjs +45 -1
- package/rpc/routes/v1.mjs +3 -1
- package/rpc/rpc_server.mjs +1 -1
- package/src/core/state/State.js +19 -1
- package/src/index.js +52 -7
- package/src/utils/cli.js +18 -1
- package/src/utils/helpers.js +7 -0
- package/test/acceptance/v1/rpc.test.mjs +175 -5
- package/test/all.test.js +9 -8
- package/test/state/apply.addAdmin.basic.test.js +111 -0
package/package.json
CHANGED
package/rpc/create_server.mjs
CHANGED
|
@@ -32,9 +32,23 @@ export const createServer = (msbInstance) => {
|
|
|
32
32
|
|
|
33
33
|
// Find the matching route
|
|
34
34
|
let foundRoute = false;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
];
|
package/rpc/rpc_server.mjs
CHANGED
|
@@ -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
|
|
8
|
+
console.log(`Running RPC with http at http://${host}:${port}`);
|
|
9
9
|
});
|
|
10
10
|
}
|
package/src/core/state/State.js
CHANGED
|
@@ -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,
|
|
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
|
|
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 =>
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src/utils/helpers.js
CHANGED
|
@@ -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(
|
|
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('./
|
|
10
|
-
await import('./
|
|
11
|
-
await import('./
|
|
12
|
-
await import('./
|
|
13
|
-
await import('./fileUtils/
|
|
14
|
-
await import('./
|
|
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
|
+
}
|