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 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.22.1";
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 containers, 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 and containers, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
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 images...");
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 });
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -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 containers, 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 and containers, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?`;
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 images
87
- this.startSpinner("Resetting Docker containers and images...");
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 containers, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
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 containers, providers API Keys, and GenLayer database (accounts, transactions, validators and logs). Contract code (gpy files) will be kept. Do you want to continue?",
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