genlayer 0.0.17 → 0.0.18

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 CHANGED
@@ -1,8 +1,11 @@
1
1
  {
2
2
  "name": "genlayer",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "description": "GenLayer Command Line Tool",
5
5
  "main": "src/index.ts",
6
+ "bin": {
7
+ "genlayer": "./dist/index.js"
8
+ },
6
9
  "scripts": {
7
10
  "test": "jest",
8
11
  "dev": "cross-env NODE_ENV=development node esbuild.config.js",
@@ -32,6 +35,7 @@
32
35
  "@types/jest": "^29.5.12",
33
36
  "@types/node": "^20.12.7",
34
37
  "@types/sinon": "^17.0.3",
38
+ "@types/uuid": "^9.0.8",
35
39
  "@typescript-eslint/eslint-plugin": "^7.7.0",
36
40
  "@typescript-eslint/parser": "^7.7.0",
37
41
  "cross-env": "^7.0.3",
@@ -50,6 +54,7 @@
50
54
  "dependencies": {
51
55
  "commander": "^12.0.0",
52
56
  "inquirer": "^9.2.19",
53
- "node-fetch": "^3.3.2"
57
+ "node-fetch": "^3.3.2",
58
+ "uuid": "^9.0.1"
54
59
  }
55
60
  }
@@ -1,24 +1,105 @@
1
1
  import inquirer from "inquirer";
2
2
 
3
+ import {
4
+ initializeDatabase,
5
+ checkRequirements,
6
+ downloadSimulator,
7
+ runSimulator,
8
+ waitForSimulatorToBeReady,
9
+ updateSimulator,
10
+ } from "@/lib/services/simulator";
3
11
  export interface InitActionOptions {
4
12
  numValidators: number;
5
13
  }
6
14
 
15
+ function getRequirementsErrorMessage({git, docker}: Record<string, boolean>): string {
16
+ if (!git && !docker) {
17
+ return "Git and Docker are not installed. Please install them and try again.\n";
18
+ }
19
+ if (!git) {
20
+ return "Git is not installed. Please install Git and try again.\n";
21
+ }
22
+ if (!docker) {
23
+ return "Docker is not installed. Please install Docker and try again.\n";
24
+ }
25
+ return "";
26
+ }
27
+
7
28
  export async function initAction(options: InitActionOptions) {
8
- console.log("Initializing GenLayer CLI...");
9
- console.log("Number of validators:", options.numValidators);
29
+ console.log(`Initializing GenLayer CLI with ${options.numValidators} validators`);
10
30
 
31
+ // Check if git and docker are installed
32
+ try {
33
+ const {git, docker} = await checkRequirements();
34
+ const errorMessage = getRequirementsErrorMessage({git, docker});
35
+ if (errorMessage) {
36
+ console.error(errorMessage);
37
+ return;
38
+ }
39
+ } catch (error) {
40
+ console.error(error);
41
+ return;
42
+ }
43
+
44
+ // Ask for confirmation on downloading the GenLayer Simulator from GitHub
11
45
  const answers = await inquirer.prompt([
12
46
  {
13
47
  type: "confirm",
14
- name: "confirmDelete",
48
+ name: "confirmDownload",
15
49
  message: `This action is going to download the GenLayer Simulator from GitHub. Do you want to continue?`,
16
- default: false,
50
+ default: true,
17
51
  },
18
52
  ]);
19
53
 
20
- if (!answers.confirmDelete) {
54
+ if (!answers.confirmDownload) {
21
55
  console.log("Aborted!");
22
56
  return;
23
57
  }
58
+
59
+ // Download the GenLayer Simulator from GitHub
60
+ console.log(`Downloading GenLayer Simulator from GitHub...`);
61
+ try {
62
+ const {wasInstalled} = await downloadSimulator();
63
+ if (wasInstalled) {
64
+ await updateSimulator();
65
+ }
66
+ } catch (error) {
67
+ console.error(error);
68
+ return;
69
+ }
70
+
71
+ // Run the GenLayer Simulator
72
+ console.log("Running the GenLayer Simulator...");
73
+ try {
74
+ await runSimulator();
75
+ } catch (error) {
76
+ console.error(error);
77
+ return;
78
+ }
79
+
80
+ try {
81
+ const initialized = await waitForSimulatorToBeReady();
82
+ if (!initialized) {
83
+ console.error("Unable to initialize the GenLayer simulator. Please try again.");
84
+ return;
85
+ }
86
+ console.log("Simulator is running!");
87
+ } catch (error) {
88
+ console.error(error);
89
+ return;
90
+ }
91
+
92
+ // Initialize the database
93
+ console.log("Initializing the database...");
94
+ try {
95
+ const {createResponse, tablesResponse} = await initializeDatabase();
96
+ if (!createResponse || !tablesResponse) {
97
+ console.error("Unable to initialize the database. Please try again.");
98
+ return;
99
+ }
100
+ } catch (error) {
101
+ console.error(error);
102
+ return;
103
+ }
104
+ console.log("GenLayer simulator initialized successfully!");
24
105
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  import {program} from "commander";
2
3
 
3
4
  import {CLI_DESCRIPTION} from "@/lib/config/text";
@@ -1,19 +1,42 @@
1
1
  import fetch from "node-fetch";
2
+ import {v4 as uuidv4} from "uuid";
2
3
 
3
4
  import {DEFAULT_JSON_RPC_URL} from "../config/simulator";
4
5
 
5
- export async function requestJsonRpc(method: string, body: object) {
6
- try {
7
- const response = await fetch(DEFAULT_JSON_RPC_URL, {
8
- method,
9
- body: JSON.stringify(body),
10
- });
6
+ export interface JsonRPCParams {
7
+ method: string;
8
+ params: any[];
9
+ }
10
+
11
+ export class JsonRpcClient {
12
+ serverUrl: string;
13
+
14
+ constructor(serverUrl: string) {
15
+ this.serverUrl = serverUrl;
16
+ }
17
+
18
+ async request({method, params}: JsonRPCParams): Promise<any | null> {
19
+ try {
20
+ const response = await fetch(this.serverUrl, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ },
25
+ body: JSON.stringify({
26
+ jsonrpc: "2.0",
27
+ id: uuidv4(),
28
+ method,
29
+ params,
30
+ }),
31
+ });
11
32
 
12
- if (response.ok) {
13
- return response.json();
33
+ if (response.ok) {
34
+ return response.json();
35
+ }
36
+ } catch (error: any) {
37
+ throw new Error(`Fetch Error: ${error.message}`);
14
38
  }
15
- } catch (error: any) {
16
- throw new Error(`Fetch Error: ${error.message}`);
39
+ return null;
17
40
  }
18
- return null;
19
41
  }
42
+ export const rpcClient = new JsonRpcClient(DEFAULT_JSON_RPC_URL);
@@ -0,0 +1,57 @@
1
+ import util from "node:util";
2
+ import {PromiseWithChild, exec} from "child_process";
3
+ import os from "os";
4
+
5
+ import {RunningPlatform, AVAILABLE_PLATFORMS} from "@/lib/config/simulator";
6
+ import {MissingRequirementError} from "../errors/missingRequirement";
7
+
8
+ const asyncExec = util.promisify(exec);
9
+
10
+ export async function checkCommand(command: string, toolName: string): Promise<void> {
11
+ const {stderr} = await asyncExec(command);
12
+ if (stderr) {
13
+ throw new MissingRequirementError(toolName);
14
+ }
15
+ console.log(`${toolName} is installed.`);
16
+ }
17
+
18
+ type ExecuteCommandResult = {
19
+ stdout: string;
20
+ stderr: string;
21
+ };
22
+
23
+ export function executeCommand(command: string, toolName: string): Promise<ExecuteCommandResult> {
24
+ try {
25
+ return asyncExec(command);
26
+ } catch (error: any) {
27
+ throw new Error(`Error executing ${toolName}: ${error.message}`);
28
+ }
29
+ }
30
+
31
+ type ExecuteCommandInNewTerminalInput = {
32
+ [key in RunningPlatform]: string;
33
+ };
34
+
35
+ export function executeCommandInNewTerminal(
36
+ cmdsByPlatform: ExecuteCommandInNewTerminalInput,
37
+ ): PromiseWithChild<{stdout: string; stderr: string}> {
38
+ const runningPlatform = getPlatform();
39
+ const command = cmdsByPlatform[runningPlatform];
40
+ try {
41
+ return asyncExec(command);
42
+ } catch (error: any) {
43
+ throw new Error(`Error executing command ${command}`);
44
+ }
45
+ }
46
+
47
+ export function getHomeDirectory(): string {
48
+ return os.homedir();
49
+ }
50
+
51
+ function getPlatform(): RunningPlatform {
52
+ const currentPlatform = process.platform as RunningPlatform;
53
+ if (!AVAILABLE_PLATFORMS.includes(currentPlatform)) {
54
+ throw new Error(`Unsupported platform: ${currentPlatform}`);
55
+ }
56
+ return currentPlatform;
57
+ }
@@ -1 +1,10 @@
1
- export const DEFAULT_JSON_RPC_URL = "http://localhost:8545";
1
+ export const DEFAULT_JSON_RPC_URL = "http://localhost:4000/api";
2
+ export const DEFAULT_REPO_GH_URL = "git@github.com:yeagerai/genlayer-simulator.git";
3
+ export const DEFAULT_RUN_SIMULATOR_COMMAND = (simulatorLocation: string) => ({
4
+ darwin: `osascript -e 'tell application "Terminal" to do script "cd ${simulatorLocation} && cp .env.example .env && docker compose build && docker compose up"'`,
5
+ win32: `start cmd.exe /k "cd ${simulatorLocation} && xcopy .env.example .env && docker compose build && docker compose up"`,
6
+ linux: `x-terminal-emulator -e 'bash -c "cd ${simulatorLocation} && cp .env.example .env && docker compose build && docker compose up"'`,
7
+ });
8
+ export const AVAILABLE_PLATFORMS = ["darwin", "win32", "linux"] as const;
9
+ export type RunningPlatform = (typeof AVAILABLE_PLATFORMS)[number];
10
+ export const STARTING_TIMEOUT_WAIT_CYLCE = 2000;
@@ -0,0 +1,9 @@
1
+ export class MissingRequirementError extends Error {
2
+ requirement: string;
3
+
4
+ constructor(requirement: string) {
5
+ super(`${requirement} is not installed. Please install ${requirement}.`);
6
+ this.name = "MissingRequirement";
7
+ this.requirement = requirement;
8
+ }
9
+ }
@@ -0,0 +1,9 @@
1
+ export class MissingRequirementError extends Error {
2
+ requirement: string;
3
+
4
+ constructor(requirement: string) {
5
+ super(`${requirement} is not installed. Please install ${requirement}.`);
6
+ this.name = "MissingRequirement";
7
+ this.requirement = requirement;
8
+ }
9
+ }
@@ -0,0 +1,118 @@
1
+ import {rpcClient} from "@/lib/clients/jsonRpcClient";
2
+ import {
3
+ DEFAULT_REPO_GH_URL,
4
+ DEFAULT_RUN_SIMULATOR_COMMAND,
5
+ STARTING_TIMEOUT_WAIT_CYLCE,
6
+ } from "@/lib/config/simulator";
7
+ import {
8
+ checkCommand,
9
+ getHomeDirectory,
10
+ executeCommand,
11
+ executeCommandInNewTerminal,
12
+ } from "@/lib/clients/system";
13
+ import {MissingRequirementError} from "../errors/missingRequirement";
14
+
15
+ // Private helper functions
16
+ function getSimulatorLocation(): string {
17
+ return `${getHomeDirectory()}/genlayer-simulator`;
18
+ }
19
+
20
+ function sleep(millliseconds: number): Promise<void> {
21
+ return new Promise(resolve => setTimeout(resolve, millliseconds));
22
+ }
23
+
24
+ // Public functions
25
+ export async function checkRequirements(): Promise<Record<string, boolean>> {
26
+ const requirementsInstalled = {
27
+ git: false,
28
+ docker: false,
29
+ };
30
+
31
+ try {
32
+ await checkCommand("git --version", "git");
33
+ requirementsInstalled.git = true;
34
+ } catch (error) {
35
+ if (!(error instanceof MissingRequirementError)) {
36
+ throw error;
37
+ }
38
+ }
39
+ try {
40
+ await checkCommand("docker --version", "docker");
41
+ requirementsInstalled.docker = true;
42
+ } catch (error) {
43
+ if (!(error instanceof MissingRequirementError)) {
44
+ throw error;
45
+ }
46
+ }
47
+
48
+ return requirementsInstalled;
49
+ }
50
+
51
+ type DownloadSimulatorResultType = {
52
+ wasInstalled: boolean;
53
+ };
54
+
55
+ export async function downloadSimulator(): Promise<DownloadSimulatorResultType> {
56
+ const simulatorLocation = getSimulatorLocation();
57
+
58
+ try {
59
+ await executeCommand(`git clone ${DEFAULT_REPO_GH_URL} ${simulatorLocation}`, "git");
60
+ } catch (error: any) {
61
+ if (error.toString().includes("already exists and is not an empty directory")) {
62
+ return {wasInstalled: true};
63
+ }
64
+ throw error;
65
+ }
66
+ return {wasInstalled: false};
67
+ }
68
+
69
+ export async function updateSimulator(): Promise<DownloadSimulatorResultType> {
70
+ const simulatorLocation = getSimulatorLocation();
71
+
72
+ try {
73
+ await executeCommand(`cd ${simulatorLocation} && git pull`, "git");
74
+ } catch (error: any) {
75
+ if (error.toString().includes("already exists and is not an empty directory")) {
76
+ return {wasInstalled: true};
77
+ }
78
+ throw error;
79
+ }
80
+ return {wasInstalled: false};
81
+ }
82
+
83
+ export function runSimulator(): Promise<{stdout: string; stderr: string}> {
84
+ const simulatorLocation = getSimulatorLocation();
85
+ const commandsByPlatform = DEFAULT_RUN_SIMULATOR_COMMAND(simulatorLocation);
86
+ return executeCommandInNewTerminal(commandsByPlatform);
87
+ }
88
+
89
+ export async function waitForSimulatorToBeReady(retries: number = 10): Promise<boolean> {
90
+ try {
91
+ const response = await rpcClient.request({method: "ping", params: []});
92
+ if (response && response.result.status === "OK") {
93
+ return true;
94
+ }
95
+ if (retries > 0) {
96
+ await sleep(STARTING_TIMEOUT_WAIT_CYLCE);
97
+ return waitForSimulatorToBeReady(retries - 1);
98
+ }
99
+ } catch (error: any) {
100
+ if (error.message.includes("ECONNREFUSED") && retries > 0) {
101
+ await sleep(STARTING_TIMEOUT_WAIT_CYLCE * 2);
102
+ return waitForSimulatorToBeReady(retries - 1);
103
+ }
104
+ }
105
+
106
+ return false;
107
+ }
108
+
109
+ type InitializeDatabaseResultType = {
110
+ createResponse: any;
111
+ tablesResponse: any;
112
+ };
113
+
114
+ export async function initializeDatabase(): Promise<InitializeDatabaseResultType> {
115
+ const createResponse = await rpcClient.request({method: "create_db", params: []});
116
+ const tablesResponse = await rpcClient.request({method: "create_tables", params: []});
117
+ return {createResponse, tablesResponse};
118
+ }
@@ -11,6 +11,7 @@ const action = jest.fn();
11
11
  describe("init command", () => {
12
12
  initializeGeneralCommands(program);
13
13
  const initCommand = getCommand("init");
14
+ initCommand?.action(action);
14
15
 
15
16
  beforeEach(() => {
16
17
  jest.clearAllMocks();
@@ -20,11 +21,11 @@ describe("init command", () => {
20
21
  jest.restoreAllMocks();
21
22
  });
22
23
 
23
- test("doesn't have required arguments nor options", async () => {
24
+ test("doesn't have required arguments nor options", () => {
24
25
  expect(() => program.parse(["node", "test", "init"])).not.toThrow();
25
26
  });
26
27
 
27
- test("option -n, --numValidators is accepted", async () => {
28
+ test("option -n, --numValidators is accepted", () => {
28
29
  expect(() => program.parse(["node", "test", "init", "-n", "10"])).not.toThrow();
29
30
  expect(() => program.parse(["node", "test", "init", "--numValidators", "10"])).not.toThrow();
30
31
  });
@@ -45,10 +46,8 @@ describe("init command", () => {
45
46
  );
46
47
  });
47
48
 
48
- test("action is called", async () => {
49
- // Given
50
- initCommand?.action(action);
51
- // When
49
+ test("action is called", () => {
50
+ // Given When
52
51
  program.parse(["node", "test", "init"]);
53
52
  // Then
54
53
  expect(action).toHaveBeenCalledTimes(1);