genlayer 0.22.1 → 0.24.0
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/CHANGELOG.md +12 -0
- package/dist/index.js +68 -3
- package/docker-compose.yml +6 -1
- package/package.json +1 -1
- package/src/commands/general/init.ts +5 -4
- package/src/commands/transactions/index.ts +22 -1
- package/src/commands/transactions/receipt.ts +64 -0
- package/src/lib/interfaces/ISimulatorService.ts +1 -0
- package/src/lib/services/simulator.ts +12 -0
- package/tests/actions/init.test.ts +7 -2
- package/tests/actions/receipt.test.ts +173 -0
- package/tests/commands/receipt.test.ts +108 -0
- package/tests/services/simulator.test.ts +40 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0 (2025-07-16)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* Add Docker Volume Cleanup to Init Flow ([#240](https://github.com/yeagerai/genlayer-cli/issues/240)) ([e61b82c](https://github.com/yeagerai/genlayer-cli/commit/e61b82c3d31aa71860eccad510141cd3f932e4aa))
|
|
8
|
+
|
|
9
|
+
## 0.23.0 (2025-07-15)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* fetch transaction receipt command ([#234](https://github.com/yeagerai/genlayer-cli/issues/234)) ([5cf868c](https://github.com/yeagerai/genlayer-cli/commit/5cf868c28ce036e41b7b40ed6cdde222cdc5dcf0))
|
|
14
|
+
|
|
3
15
|
## 0.22.1 (2025-07-14)
|
|
4
16
|
|
|
5
17
|
## 0.22.0 (2025-07-14)
|
package/dist/index.js
CHANGED
|
@@ -17856,7 +17856,7 @@ var require_semver2 = __commonJS({
|
|
|
17856
17856
|
import { program } from "commander";
|
|
17857
17857
|
|
|
17858
17858
|
// package.json
|
|
17859
|
-
var version = "0.
|
|
17859
|
+
var version = "0.24.0";
|
|
17860
17860
|
var package_default = {
|
|
17861
17861
|
name: "genlayer",
|
|
17862
17862
|
version,
|
|
@@ -41939,6 +41939,16 @@ Run npm install -g genlayer to update
|
|
|
41939
41939
|
await image.remove({ force: true });
|
|
41940
41940
|
}
|
|
41941
41941
|
}
|
|
41942
|
+
async resetDockerVolumes() {
|
|
41943
|
+
const volumes = await this.docker.listVolumes();
|
|
41944
|
+
const genlayerVolumes = volumes.Volumes?.filter(
|
|
41945
|
+
(volume) => volume.Name.startsWith("genlayer_")
|
|
41946
|
+
) || [];
|
|
41947
|
+
for (const volumeInfo of genlayerVolumes) {
|
|
41948
|
+
const volume = this.docker.getVolume(volumeInfo.Name);
|
|
41949
|
+
await volume.remove({ force: true });
|
|
41950
|
+
}
|
|
41951
|
+
}
|
|
41942
41952
|
async cleanDatabase() {
|
|
41943
41953
|
try {
|
|
41944
41954
|
await rpcClient.request({ method: "sim_clearDbTables", params: [["current_state", "transactions"]] });
|
|
@@ -42038,12 +42048,13 @@ var InitAction = class extends BaseAction {
|
|
|
42038
42048
|
return;
|
|
42039
42049
|
}
|
|
42040
42050
|
this.stopSpinner();
|
|
42041
|
-
const confirmMessage = isRunning ? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
42051
|
+
const confirmMessage = isRunning ? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?` : `This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
|
|
42042
42052
|
await this.confirmPrompt(confirmMessage);
|
|
42043
42053
|
this.logInfo(`Initializing GenLayer CLI with ${options.numValidators} validators`);
|
|
42044
|
-
this.startSpinner("Resetting Docker containers and
|
|
42054
|
+
this.startSpinner("Resetting Docker containers, images, and volumes...");
|
|
42045
42055
|
await this.simulatorService.resetDockerContainers();
|
|
42046
42056
|
await this.simulatorService.resetDockerImages();
|
|
42057
|
+
await this.simulatorService.resetDockerVolumes();
|
|
42047
42058
|
this.stopSpinner();
|
|
42048
42059
|
const llmQuestions = [
|
|
42049
42060
|
{
|
|
@@ -42909,6 +42920,51 @@ function initializeNetworkCommands(program2) {
|
|
|
42909
42920
|
return program2;
|
|
42910
42921
|
}
|
|
42911
42922
|
|
|
42923
|
+
// src/commands/transactions/receipt.ts
|
|
42924
|
+
var ReceiptAction = class extends BaseAction {
|
|
42925
|
+
constructor() {
|
|
42926
|
+
super();
|
|
42927
|
+
}
|
|
42928
|
+
validateTransactionStatus(status) {
|
|
42929
|
+
const upperStatus = status.toUpperCase();
|
|
42930
|
+
if (!(upperStatus in TransactionStatus)) {
|
|
42931
|
+
const validStatuses = Object.values(TransactionStatus);
|
|
42932
|
+
this.failSpinner(
|
|
42933
|
+
"Invalid transaction status",
|
|
42934
|
+
`Invalid status: ${status}. Valid values are: ${validStatuses.join(", ")}`
|
|
42935
|
+
);
|
|
42936
|
+
return;
|
|
42937
|
+
}
|
|
42938
|
+
return TransactionStatus[upperStatus];
|
|
42939
|
+
}
|
|
42940
|
+
async receipt({
|
|
42941
|
+
txId,
|
|
42942
|
+
status = TransactionStatus.FINALIZED,
|
|
42943
|
+
retries,
|
|
42944
|
+
interval,
|
|
42945
|
+
rpc
|
|
42946
|
+
}) {
|
|
42947
|
+
const client = await this.getClient(rpc);
|
|
42948
|
+
await client.initializeConsensusSmartContract();
|
|
42949
|
+
this.startSpinner(`Waiting for transaction receipt ${txId} (status: ${status})...`);
|
|
42950
|
+
try {
|
|
42951
|
+
let validatedStatus = this.validateTransactionStatus(status);
|
|
42952
|
+
if (!validatedStatus) {
|
|
42953
|
+
return;
|
|
42954
|
+
}
|
|
42955
|
+
const result = await client.waitForTransactionReceipt({
|
|
42956
|
+
hash: txId,
|
|
42957
|
+
status: validatedStatus,
|
|
42958
|
+
retries,
|
|
42959
|
+
interval
|
|
42960
|
+
});
|
|
42961
|
+
this.succeedSpinner("Transaction receipt retrieved successfully", result);
|
|
42962
|
+
} catch (error) {
|
|
42963
|
+
this.failSpinner("Error retrieving transaction receipt", error);
|
|
42964
|
+
}
|
|
42965
|
+
}
|
|
42966
|
+
};
|
|
42967
|
+
|
|
42912
42968
|
// src/commands/transactions/appeal.ts
|
|
42913
42969
|
var AppealAction = class extends BaseAction {
|
|
42914
42970
|
constructor() {
|
|
@@ -42938,7 +42994,16 @@ var AppealAction = class extends BaseAction {
|
|
|
42938
42994
|
};
|
|
42939
42995
|
|
|
42940
42996
|
// src/commands/transactions/index.ts
|
|
42997
|
+
function parseIntOption(value, fallback2) {
|
|
42998
|
+
const parsed = parseInt(value, 10);
|
|
42999
|
+
return isNaN(parsed) ? fallback2 : parsed;
|
|
43000
|
+
}
|
|
42941
43001
|
function initializeTransactionsCommands(program2) {
|
|
43002
|
+
const validStatuses = Object.values(TransactionStatus).join(", ");
|
|
43003
|
+
program2.command("receipt <txId>").description("Get transaction receipt by hash").option("--status <status>", `Transaction status to wait for (${validStatuses})`, TransactionStatus.FINALIZED).option("--retries <retries>", "Number of retries", (value) => parseIntOption(value, 100), 100).option("--interval <interval>", "Interval between retries in milliseconds", (value) => parseIntOption(value, 5e3), 5e3).option("--rpc <rpcUrl>", "RPC URL for the network").action(async (txId, options) => {
|
|
43004
|
+
const receiptAction = new ReceiptAction();
|
|
43005
|
+
await receiptAction.receipt({ txId, ...options });
|
|
43006
|
+
});
|
|
42942
43007
|
program2.command("appeal <txId>").description("Appeal a transaction by its hash").option("--rpc <rpcUrl>", "RPC URL for the network").action(async (txId, options) => {
|
|
42943
43008
|
const appealAction = new AppealAction();
|
|
42944
43009
|
await appealAction.appeal({ txId, ...options });
|
package/docker-compose.yml
CHANGED
|
@@ -143,7 +143,12 @@ services:
|
|
|
143
143
|
volumes:
|
|
144
144
|
- hardhat_artifacts:/app/artifacts
|
|
145
145
|
- hardhat_deployments:/app/deployments
|
|
146
|
+
- hardhat_cache:/app/cache
|
|
147
|
+
- hardhat_snapshots:/app/snapshots
|
|
146
148
|
|
|
147
149
|
volumes:
|
|
148
150
|
hardhat_artifacts:
|
|
149
|
-
hardhat_deployments:
|
|
151
|
+
hardhat_deployments:
|
|
152
|
+
hardhat_cache:
|
|
153
|
+
hardhat_snapshots:
|
|
154
|
+
|
package/package.json
CHANGED
|
@@ -76,17 +76,18 @@ export class InitAction extends BaseAction {
|
|
|
76
76
|
this.stopSpinner();
|
|
77
77
|
|
|
78
78
|
const confirmMessage = isRunning
|
|
79
|
-
? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
80
|
-
: `This command is going to reset GenLayer docker images and
|
|
79
|
+
? `GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`
|
|
80
|
+
: `This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
|
|
81
81
|
|
|
82
82
|
await this.confirmPrompt(confirmMessage);
|
|
83
83
|
|
|
84
84
|
this.logInfo(`Initializing GenLayer CLI with ${options.numValidators} validators`);
|
|
85
85
|
|
|
86
|
-
// Reset Docker containers and
|
|
87
|
-
this.startSpinner("Resetting Docker containers and
|
|
86
|
+
// Reset Docker containers, images, and volumes
|
|
87
|
+
this.startSpinner("Resetting Docker containers, images, and volumes...");
|
|
88
88
|
await this.simulatorService.resetDockerContainers();
|
|
89
89
|
await this.simulatorService.resetDockerImages();
|
|
90
|
+
await this.simulatorService.resetDockerVolumes();
|
|
90
91
|
this.stopSpinner();
|
|
91
92
|
|
|
92
93
|
const llmQuestions: DistinctQuestion[] = [
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import {Command} from "commander";
|
|
2
|
-
import {TransactionHash} from "genlayer-js/types";
|
|
2
|
+
import {TransactionStatus, TransactionHash} from "genlayer-js/types";
|
|
3
|
+
import {ReceiptAction, ReceiptOptions} from "./receipt";
|
|
3
4
|
import {AppealAction, AppealOptions} from "./appeal";
|
|
4
5
|
|
|
6
|
+
function parseIntOption(value: string, fallback: number): number {
|
|
7
|
+
const parsed = parseInt(value, 10);
|
|
8
|
+
return isNaN(parsed) ? fallback : parsed;
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
export function initializeTransactionsCommands(program: Command) {
|
|
12
|
+
const validStatuses = Object.values(TransactionStatus).join(", ");
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.command("receipt <txId>")
|
|
16
|
+
.description("Get transaction receipt by hash")
|
|
17
|
+
.option("--status <status>", `Transaction status to wait for (${validStatuses})`, TransactionStatus.FINALIZED)
|
|
18
|
+
.option("--retries <retries>", "Number of retries", (value) => parseIntOption(value, 100), 100)
|
|
19
|
+
.option("--interval <interval>", "Interval between retries in milliseconds", (value) => parseIntOption(value, 5000), 5000)
|
|
20
|
+
.option("--rpc <rpcUrl>", "RPC URL for the network")
|
|
21
|
+
.action(async (txId: TransactionHash, options: ReceiptOptions) => {
|
|
22
|
+
const receiptAction = new ReceiptAction();
|
|
23
|
+
|
|
24
|
+
await receiptAction.receipt({txId, ...options});
|
|
25
|
+
})
|
|
26
|
+
|
|
6
27
|
program
|
|
7
28
|
.command("appeal <txId>")
|
|
8
29
|
.description("Appeal a transaction by its hash")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {BaseAction} from "../../lib/actions/BaseAction";
|
|
2
|
+
import {TransactionHash, TransactionStatus} from "genlayer-js/types";
|
|
3
|
+
|
|
4
|
+
export interface ReceiptParams {
|
|
5
|
+
txId: TransactionHash;
|
|
6
|
+
status?: string | TransactionStatus;
|
|
7
|
+
retries?: number;
|
|
8
|
+
interval?: number;
|
|
9
|
+
rpc?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ReceiptOptions extends Omit<ReceiptParams, 'txId'> {}
|
|
13
|
+
|
|
14
|
+
export class ReceiptAction extends BaseAction {
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private validateTransactionStatus(status: string): TransactionStatus | undefined {
|
|
20
|
+
const upperStatus = status.toUpperCase() as keyof typeof TransactionStatus;
|
|
21
|
+
|
|
22
|
+
if (!(upperStatus in TransactionStatus)) {
|
|
23
|
+
const validStatuses = Object.values(TransactionStatus);
|
|
24
|
+
this.failSpinner(
|
|
25
|
+
"Invalid transaction status",
|
|
26
|
+
`Invalid status: ${status}. Valid values are: ${validStatuses.join(", ")}`
|
|
27
|
+
);
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return TransactionStatus[upperStatus];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async receipt({
|
|
35
|
+
txId,
|
|
36
|
+
status = TransactionStatus.FINALIZED,
|
|
37
|
+
retries,
|
|
38
|
+
interval,
|
|
39
|
+
rpc,
|
|
40
|
+
}: ReceiptParams): Promise<void> {
|
|
41
|
+
const client = await this.getClient(rpc);
|
|
42
|
+
await client.initializeConsensusSmartContract();
|
|
43
|
+
this.startSpinner(`Waiting for transaction receipt ${txId} (status: ${status})...`);
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
let validatedStatus = this.validateTransactionStatus(status);
|
|
47
|
+
|
|
48
|
+
if (!validatedStatus) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await client.waitForTransactionReceipt({
|
|
53
|
+
hash: txId,
|
|
54
|
+
status: validatedStatus,
|
|
55
|
+
retries,
|
|
56
|
+
interval,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
this.succeedSpinner("Transaction receipt retrieved successfully", result);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
this.failSpinner("Error retrieving transaction receipt", error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -15,6 +15,7 @@ export interface ISimulatorService {
|
|
|
15
15
|
stopDockerContainers(): Promise<void>;
|
|
16
16
|
resetDockerContainers(): Promise<void>;
|
|
17
17
|
resetDockerImages(): Promise<void>;
|
|
18
|
+
resetDockerVolumes(): Promise<void>;
|
|
18
19
|
checkCliVersion(): Promise<void>;
|
|
19
20
|
cleanDatabase(): Promise<boolean>;
|
|
20
21
|
addConfigToEnvFile(newConfig: Record<string, string>): void;
|
|
@@ -278,6 +278,18 @@ export class SimulatorService implements ISimulatorService {
|
|
|
278
278
|
}
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
public async resetDockerVolumes(): Promise<void> {
|
|
282
|
+
const volumes = await this.docker.listVolumes();
|
|
283
|
+
const genlayerVolumes = volumes.Volumes?.filter(volume =>
|
|
284
|
+
volume.Name.startsWith('genlayer_')
|
|
285
|
+
) || [];
|
|
286
|
+
|
|
287
|
+
for (const volumeInfo of genlayerVolumes) {
|
|
288
|
+
const volume = this.docker.getVolume(volumeInfo.Name);
|
|
289
|
+
await volume.remove({force: true});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
281
293
|
public async cleanDatabase(): Promise<boolean> {
|
|
282
294
|
|
|
283
295
|
try {
|
|
@@ -13,6 +13,7 @@ describe("InitAction", () => {
|
|
|
13
13
|
let checkVersionRequirementsSpy: ReturnType<typeof vi.spyOn>;
|
|
14
14
|
let resetDockerContainersSpy: ReturnType<typeof vi.spyOn>;
|
|
15
15
|
let resetDockerImagesSpy: ReturnType<typeof vi.spyOn>;
|
|
16
|
+
let resetDockerVolumesSpy: ReturnType<typeof vi.spyOn>;
|
|
16
17
|
let addConfigToEnvFileSpy: ReturnType<typeof vi.spyOn>;
|
|
17
18
|
let runSimulatorSpy: ReturnType<typeof vi.spyOn>;
|
|
18
19
|
let waitForSimulatorSpy: ReturnType<typeof vi.spyOn>;
|
|
@@ -57,6 +58,9 @@ describe("InitAction", () => {
|
|
|
57
58
|
resetDockerImagesSpy = vi
|
|
58
59
|
.spyOn(SimulatorService.prototype, "resetDockerImages")
|
|
59
60
|
.mockResolvedValue(undefined);
|
|
61
|
+
resetDockerVolumesSpy = vi
|
|
62
|
+
.spyOn(SimulatorService.prototype, "resetDockerVolumes")
|
|
63
|
+
.mockResolvedValue(undefined);
|
|
60
64
|
addConfigToEnvFileSpy = vi.spyOn(SimulatorService.prototype, "addConfigToEnvFile").mockResolvedValue();
|
|
61
65
|
runSimulatorSpy = vi
|
|
62
66
|
.spyOn(SimulatorService.prototype, "runSimulator")
|
|
@@ -98,7 +102,7 @@ describe("InitAction", () => {
|
|
|
98
102
|
await initAction.execute(defaultOptions);
|
|
99
103
|
|
|
100
104
|
expect(confirmPromptSpy).toHaveBeenCalledWith(
|
|
101
|
-
"GenLayer Localnet is already running and this command is going to reset GenLayer docker images and
|
|
105
|
+
"GenLayer Localnet is already running and this command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
|
|
102
106
|
);
|
|
103
107
|
});
|
|
104
108
|
|
|
@@ -114,7 +118,7 @@ describe("InitAction", () => {
|
|
|
114
118
|
await initAction.execute(defaultOptions);
|
|
115
119
|
|
|
116
120
|
expect(confirmPromptSpy).toHaveBeenCalledWith(
|
|
117
|
-
"This command is going to reset GenLayer docker images and
|
|
121
|
+
"This command is going to reset GenLayer docker images, containers and volumes, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
|
|
118
122
|
);
|
|
119
123
|
});
|
|
120
124
|
|
|
@@ -130,6 +134,7 @@ describe("InitAction", () => {
|
|
|
130
134
|
expect(checkVersionRequirementsSpy).toHaveBeenCalled();
|
|
131
135
|
expect(resetDockerContainersSpy).toHaveBeenCalled();
|
|
132
136
|
expect(resetDockerImagesSpy).toHaveBeenCalled();
|
|
137
|
+
expect(resetDockerVolumesSpy).toHaveBeenCalled();
|
|
133
138
|
expect(addConfigToEnvFileSpy).toHaveBeenCalledWith({
|
|
134
139
|
OPENAIKEY: "API_KEY_OPENAI",
|
|
135
140
|
HEURISTAIAPIKEY: "API_KEY_HEURIST",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {describe, test, vi, beforeEach, afterEach, expect} from "vitest";
|
|
2
|
+
import {createClient, createAccount} from "genlayer-js";
|
|
3
|
+
import type {TransactionHash} from "genlayer-js/types";
|
|
4
|
+
import {TransactionStatus} from "genlayer-js/types";
|
|
5
|
+
import {ReceiptAction, type ReceiptParams} from "../../src/commands/transactions/receipt";
|
|
6
|
+
|
|
7
|
+
vi.mock("genlayer-js");
|
|
8
|
+
|
|
9
|
+
describe("ReceiptAction", () => {
|
|
10
|
+
let receiptAction: ReceiptAction;
|
|
11
|
+
const mockClient = {
|
|
12
|
+
waitForTransactionReceipt: vi.fn(),
|
|
13
|
+
initializeConsensusSmartContract: vi.fn(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const mockPrivateKey = "mocked_private_key";
|
|
17
|
+
const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234" as TransactionHash;
|
|
18
|
+
const defaultRetries = 100;
|
|
19
|
+
const defaultInterval = 5000;
|
|
20
|
+
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.clearAllMocks();
|
|
23
|
+
vi.mocked(createClient).mockReturnValue(mockClient as any);
|
|
24
|
+
vi.mocked(createAccount).mockReturnValue({privateKey: mockPrivateKey} as any);
|
|
25
|
+
receiptAction = new ReceiptAction();
|
|
26
|
+
vi.spyOn(receiptAction as any, "getPrivateKey").mockResolvedValue(mockPrivateKey);
|
|
27
|
+
|
|
28
|
+
vi.spyOn(receiptAction as any, "startSpinner").mockImplementation(() => {});
|
|
29
|
+
vi.spyOn(receiptAction as any, "succeedSpinner").mockImplementation(() => {});
|
|
30
|
+
vi.spyOn(receiptAction as any, "failSpinner").mockImplementation(() => {});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("retrieves transaction receipt successfully with default options", async () => {
|
|
38
|
+
const mockReceipt = {status: "FINALIZED", data: {hash: mockTxId}};
|
|
39
|
+
|
|
40
|
+
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
|
|
41
|
+
|
|
42
|
+
await receiptAction.receipt({
|
|
43
|
+
txId: mockTxId,
|
|
44
|
+
retries: defaultRetries,
|
|
45
|
+
interval: defaultInterval,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(mockClient.waitForTransactionReceipt).toHaveBeenCalledWith({
|
|
49
|
+
hash: mockTxId,
|
|
50
|
+
status: TransactionStatus.FINALIZED,
|
|
51
|
+
retries: defaultRetries,
|
|
52
|
+
interval: defaultInterval,
|
|
53
|
+
});
|
|
54
|
+
expect(receiptAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
55
|
+
"Transaction receipt retrieved successfully",
|
|
56
|
+
mockReceipt,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("retrieves transaction receipt with custom options", async () => {
|
|
61
|
+
const mockReceipt = {status: "ACCEPTED", data: {hash: mockTxId}};
|
|
62
|
+
|
|
63
|
+
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
|
|
64
|
+
|
|
65
|
+
await receiptAction.receipt({
|
|
66
|
+
txId: mockTxId,
|
|
67
|
+
status: "ACCEPTED",
|
|
68
|
+
retries: 50,
|
|
69
|
+
interval: 3000,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
expect(mockClient.waitForTransactionReceipt).toHaveBeenCalledWith({
|
|
73
|
+
hash: mockTxId,
|
|
74
|
+
status: TransactionStatus.ACCEPTED,
|
|
75
|
+
retries: 50,
|
|
76
|
+
interval: 3000,
|
|
77
|
+
});
|
|
78
|
+
expect(receiptAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
79
|
+
"Transaction receipt retrieved successfully",
|
|
80
|
+
mockReceipt,
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("handles waitForTransactionReceipt errors", async () => {
|
|
85
|
+
vi.mocked(mockClient.waitForTransactionReceipt).mockRejectedValue(new Error("Mocked receipt error"));
|
|
86
|
+
|
|
87
|
+
await receiptAction.receipt({
|
|
88
|
+
txId: mockTxId,
|
|
89
|
+
retries: defaultRetries,
|
|
90
|
+
interval: defaultInterval,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(receiptAction["failSpinner"]).toHaveBeenCalledWith(
|
|
94
|
+
"Error retrieving transaction receipt",
|
|
95
|
+
expect.any(Error),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("uses custom RPC URL for receipt operations", async () => {
|
|
100
|
+
const rpcUrl = "https://custom-rpc-url.com";
|
|
101
|
+
const mockReceipt = {status: "FINALIZED", data: {hash: mockTxId}};
|
|
102
|
+
|
|
103
|
+
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
|
|
104
|
+
|
|
105
|
+
await receiptAction.receipt({
|
|
106
|
+
txId: mockTxId,
|
|
107
|
+
retries: defaultRetries,
|
|
108
|
+
interval: defaultInterval,
|
|
109
|
+
rpc: rpcUrl,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(createClient).toHaveBeenCalledWith(
|
|
113
|
+
expect.objectContaining({
|
|
114
|
+
endpoint: rpcUrl,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
expect(mockClient.waitForTransactionReceipt).toHaveBeenCalledWith({
|
|
118
|
+
hash: mockTxId,
|
|
119
|
+
status: TransactionStatus.FINALIZED,
|
|
120
|
+
retries: defaultRetries,
|
|
121
|
+
interval: defaultInterval,
|
|
122
|
+
});
|
|
123
|
+
expect(receiptAction["succeedSpinner"]).toHaveBeenCalledWith(
|
|
124
|
+
"Transaction receipt retrieved successfully",
|
|
125
|
+
mockReceipt,
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("validates transaction status and shows error for invalid status", async () => {
|
|
130
|
+
await receiptAction.receipt({
|
|
131
|
+
txId: mockTxId,
|
|
132
|
+
status: "INVALID_STATUS",
|
|
133
|
+
retries: defaultRetries,
|
|
134
|
+
interval: defaultInterval,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
expect(receiptAction["failSpinner"]).toHaveBeenCalledWith(
|
|
138
|
+
"Invalid transaction status",
|
|
139
|
+
expect.stringContaining("Invalid status: INVALID_STATUS")
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(mockClient.waitForTransactionReceipt).not.toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("accepts valid transaction statuses", async () => {
|
|
146
|
+
const mockReceipt = {status: "PENDING", data: {hash: mockTxId}};
|
|
147
|
+
vi.mocked(mockClient.waitForTransactionReceipt).mockResolvedValue(mockReceipt);
|
|
148
|
+
|
|
149
|
+
const testStatuses = [
|
|
150
|
+
{input: "accepted", expected: TransactionStatus.ACCEPTED},
|
|
151
|
+
{input: "FINALIZED", expected: TransactionStatus.FINALIZED},
|
|
152
|
+
{input: "pending", expected: TransactionStatus.PENDING},
|
|
153
|
+
{input: "COMMITTING", expected: TransactionStatus.COMMITTING},
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
for (const {input, expected} of testStatuses) {
|
|
157
|
+
await receiptAction.receipt({
|
|
158
|
+
txId: mockTxId,
|
|
159
|
+
status: input,
|
|
160
|
+
retries: defaultRetries,
|
|
161
|
+
interval: defaultInterval,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(mockClient.waitForTransactionReceipt).toHaveBeenCalledWith({
|
|
165
|
+
hash: mockTxId,
|
|
166
|
+
status: expected,
|
|
167
|
+
retries: defaultRetries,
|
|
168
|
+
interval: defaultInterval,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {Command} from "commander";
|
|
2
|
+
import {ReceiptAction} from "../../src/commands/transactions/receipt";
|
|
3
|
+
import {vi, describe, beforeEach, afterEach, test, expect} from "vitest";
|
|
4
|
+
import {initializeTransactionsCommands} from "../../src/commands/transactions";
|
|
5
|
+
|
|
6
|
+
vi.mock("../../src/commands/transactions/receipt");
|
|
7
|
+
|
|
8
|
+
describe("receipt command", () => {
|
|
9
|
+
let program: Command;
|
|
10
|
+
const mockTxId = "0x1234567890123456789012345678901234567890123456789012345678901234";
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
program = new Command();
|
|
14
|
+
initializeTransactionsCommands(program);
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("ReceiptAction.receipt is called with default options", async () => {
|
|
23
|
+
program.parse(["node", "test", "receipt", mockTxId]);
|
|
24
|
+
expect(ReceiptAction).toHaveBeenCalledTimes(1);
|
|
25
|
+
expect(ReceiptAction.prototype.receipt).toHaveBeenCalledWith({
|
|
26
|
+
txId: mockTxId,
|
|
27
|
+
status: "FINALIZED",
|
|
28
|
+
retries: 100,
|
|
29
|
+
interval: 5000,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("ReceiptAction.receipt is called with custom options", async () => {
|
|
34
|
+
program.parse([
|
|
35
|
+
"node",
|
|
36
|
+
"test",
|
|
37
|
+
"receipt",
|
|
38
|
+
mockTxId,
|
|
39
|
+
"--status",
|
|
40
|
+
"ACCEPTED",
|
|
41
|
+
"--retries",
|
|
42
|
+
"50",
|
|
43
|
+
"--interval",
|
|
44
|
+
"3000",
|
|
45
|
+
"--rpc",
|
|
46
|
+
"https://custom-rpc-url-for-receipt.com",
|
|
47
|
+
]);
|
|
48
|
+
expect(ReceiptAction).toHaveBeenCalledTimes(1);
|
|
49
|
+
expect(ReceiptAction.prototype.receipt).toHaveBeenCalledWith({
|
|
50
|
+
txId: mockTxId,
|
|
51
|
+
status: "ACCEPTED",
|
|
52
|
+
retries: 50,
|
|
53
|
+
interval: 3000,
|
|
54
|
+
rpc: "https://custom-rpc-url-for-receipt.com",
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("ReceiptAction is instantiated when the receipt command is executed", async () => {
|
|
59
|
+
program.parse(["node", "test", "receipt", mockTxId]);
|
|
60
|
+
expect(ReceiptAction).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("throws error for unrecognized options", async () => {
|
|
64
|
+
const receiptCommand = program.commands.find(cmd => cmd.name() === "receipt");
|
|
65
|
+
receiptCommand?.exitOverride();
|
|
66
|
+
expect(() =>
|
|
67
|
+
program.parse(["node", "test", "receipt", mockTxId, "--invalid-option"]),
|
|
68
|
+
).toThrowError("error: unknown option '--invalid-option'");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("parses numeric options correctly", async () => {
|
|
72
|
+
program.parse([
|
|
73
|
+
"node",
|
|
74
|
+
"test",
|
|
75
|
+
"receipt",
|
|
76
|
+
mockTxId,
|
|
77
|
+
"--retries",
|
|
78
|
+
"25",
|
|
79
|
+
"--interval",
|
|
80
|
+
"1000",
|
|
81
|
+
]);
|
|
82
|
+
expect(ReceiptAction.prototype.receipt).toHaveBeenCalledWith({
|
|
83
|
+
txId: mockTxId,
|
|
84
|
+
status: "FINALIZED",
|
|
85
|
+
retries: 25,
|
|
86
|
+
interval: 1000,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("uses fallback value for invalid numeric options", async () => {
|
|
91
|
+
program.parse([
|
|
92
|
+
"node",
|
|
93
|
+
"test",
|
|
94
|
+
"receipt",
|
|
95
|
+
mockTxId,
|
|
96
|
+
"--retries",
|
|
97
|
+
"invalid",
|
|
98
|
+
"--interval",
|
|
99
|
+
"notanumber",
|
|
100
|
+
]);
|
|
101
|
+
expect(ReceiptAction.prototype.receipt).toHaveBeenCalledWith({
|
|
102
|
+
txId: mockTxId,
|
|
103
|
+
status: "FINALIZED",
|
|
104
|
+
retries: 100,
|
|
105
|
+
interval: 5000,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -499,6 +499,46 @@ describe("SimulatorService - Docker Tests", () => {
|
|
|
499
499
|
expect(mockRemove).toHaveBeenCalledWith({force: true});
|
|
500
500
|
});
|
|
501
501
|
|
|
502
|
+
test("should remove Docker volumes with genlayer_ prefix", async () => {
|
|
503
|
+
const mockVolumes = {
|
|
504
|
+
Volumes: [
|
|
505
|
+
{ Name: "genlayer_volume1" },
|
|
506
|
+
{ Name: "genlayer_postgres" },
|
|
507
|
+
{ Name: "genlayer_data" },
|
|
508
|
+
{ Name: "unrelated_volume" },
|
|
509
|
+
{ Name: "another_volume" },
|
|
510
|
+
{ Name: "hardhat_artifacts" },
|
|
511
|
+
],
|
|
512
|
+
Warnings: [],
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
const mockListVolumes = vi.mocked(Docker.prototype.listVolumes);
|
|
516
|
+
const mockGetVolume = vi.mocked(Docker.prototype.getVolume);
|
|
517
|
+
|
|
518
|
+
mockListVolumes.mockResolvedValue(mockVolumes as any);
|
|
519
|
+
|
|
520
|
+
const mockRemove = vi.fn().mockResolvedValue(undefined);
|
|
521
|
+
mockGetVolume.mockImplementation(
|
|
522
|
+
() =>
|
|
523
|
+
({
|
|
524
|
+
remove: mockRemove,
|
|
525
|
+
}) as unknown as Docker.Volume,
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const result = await simulatorService.resetDockerVolumes();
|
|
529
|
+
|
|
530
|
+
expect(result).toBe(undefined);
|
|
531
|
+
expect(mockListVolumes).toHaveBeenCalled();
|
|
532
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_volume1");
|
|
533
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_postgres");
|
|
534
|
+
expect(mockGetVolume).toHaveBeenCalledWith("genlayer_data");
|
|
535
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("unrelated_volume");
|
|
536
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("another_volume");
|
|
537
|
+
expect(mockGetVolume).not.toHaveBeenCalledWith("hardhat_artifacts");
|
|
538
|
+
expect(mockRemove).toHaveBeenCalledTimes(3);
|
|
539
|
+
expect(mockRemove).toHaveBeenCalledWith({force: true});
|
|
540
|
+
});
|
|
541
|
+
|
|
502
542
|
test("should execute command when docker is installed but is not available", async () => {
|
|
503
543
|
vi.mocked(checkCommand).mockResolvedValueOnce(undefined);
|
|
504
544
|
|