Repository: hashgraph/hedera-local-node
Pull request #517 (2024-01-26 09:13:27)

docs: Add Ubuntu docs
**Description**:
Add documentation for setting up Ubuntu system for the Hedera Local Node.

**Related issue(s)**:

Fixes https://github.com/hashgraph/hedera-local-node/issues/266

**Notes for reviewer**:
<!-- Provide logs, performance numbers or screenshots of the new functionality -->

**Checklist**

- [ ] Documented (Code comments, README, etc.)
- [ ] Tested (unit, integration, etc.)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=517) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=517&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=517&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
No data about Duplication  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=517)


sonarcloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=517) **Quality Gate passed**  
Kudos, no new issues were introduced!

[0 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=517&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=517&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
No data about Duplication  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=517)



Patch:
diff --git a/README.md b/README.md
index 5f5464f..9737e45 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,9 @@ Available commands:
             --async to enable or disable asynchronous creation of accounts.
             --b or --blocklist to enable or disable account blocklisting. Depending on how many private keys are blocklisted, this will affect the generated on startup accounts.
             --enable-debug  Enable or disable debugging of the local node [boolean] [default: false]
+            --network-tag Select custom network node tag [string] [defaults: predefined selected configuration]
+            --mirror-tag Select custom mirror node tag [string] [defaults: predefined selected configuration]
+            --relay-tag Select custom hedera-json-rpc relay tag [string] [defaults: predefined selected configuration]
             --workdir       Path to the working directory for local node [string] [default: "[USER APP DATA]/hedera-local"]
     stop - Stops the local hedera network and delete all the existing data.
     restart - Restart the local hedera network.
diff --git a/compose-network/mirror-node/init.sh b/compose-network/mirror-node/init.sh
index 7034a7d..05f0df9 100755
--- a/compose-network/mirror-node/init.sh
+++ b/compose-network/mirror-node/init.sh
@@ -1,23 +1,36 @@
 #!/bin/bash
 set -e
 
-PGCONF="${PGCONF:-/var/lib/postgresql/data}"
-PGHBA="${PGCONF}/pg_hba.conf"
+export PGCONNECT_TIMEOUT="${PGCONNECT_TIMEOUT:-3}"
+export PGDATABASE="${POSTGRES_DB:-postgres}"
+export PGHOST="${PGHOST}"
+export PGUSER="${POSTGRES_USER:-postgres}"
+
+DB_SPECIFIC_EXTENSION_SQL=
+# PostgreSQL 16 requires role A to have Admin privilege on role B in order to grant role B to any other role, and there
+# are several versioned v1 migrations that grant :importerUsername to :ownerUsername.
+DB_SPECIFIC_IMPORTER_ROLE_ADMIN="admin :ownerUsername"
 DB_SPECIFIC_SQL="alter user :ownerUsername with createrole;"
 
 # v2 schema no longer creates the REST API user, while v1 schema still does
 if [[ "${SCHEMA_V2}" == "true" ]]; then
+  DB_SPECIFIC_EXTENSION_SQL="create extension if not exists btree_gist;
+                             create extension citus;
+                             grant all privileges on database :dbName to :ownerUsername;
+                             grant create on database :dbName to :ownerUsername;
+                             grant all on schema public to :ownerUsername;
+                             grant temporary on database :dbName to :ownerUsername;
+                             alter type timestamptz owner to :ownerUsername;
+                             "
+  DB_SPECIFIC_MIRROR_IMPORTER_ROLE_ADMIN=
   DB_SPECIFIC_SQL="create user :restUsername with login password :'restPassword' in role readonly;"
 fi
 
-cp "${PGHBA}" "${PGHBA}.bak"
-echo "local all all trust" > "${PGHBA}"
-pg_ctl reload
-
-psql -d "user=postgres connect_timeout=3" \
-  --set ON_ERROR_STOP=1 \
+psql --set ON_ERROR_STOP=1 \
   --set "dbName=${DB_NAME:-mirror_node}" \
   --set "dbSchema=${DB_SCHEMA:-public}" \
+  --set "graphqlPassword=${GRAPHQL_PASSWORD:-mirror_graphql_pass}" \
+  --set "graphqlUsername=${GRAPHQL_USERNAME:-mirror_graphql}" \
   --set "grpcPassword=${GRPC_PASSWORD:-mirror_grpc_pass}" \
   --set "grpcUsername=${GRPC_USERNAME:-mirror_grpc}" \
   --set "importerPassword=${IMPORTER_PASSWORD:-mirror_importer_pass}" \
@@ -26,15 +39,17 @@ psql -d "user=postgres connect_timeout=3" \
   --set "ownerUsername=${OWNER_USERNAME:-mirror_node}" \
   --set "restPassword=${REST_PASSWORD:-mirror_api_pass}" \
   --set "restUsername=${REST_USERNAME:-mirror_api}" \
+  --set "restJavaPassword=${REST_JAVA_PASSWORD:-mirror_rest_java_pass}" \
+  --set "restJavaUsername=${REST_JAVA_USERNAME:-mirror_rest_java}" \
   --set "rosettaPassword=${ROSETTA_PASSWORD:-mirror_rosetta_pass}" \
   --set "rosettaUsername=${ROSETTA_USERNAME:-mirror_rosetta}" \
   --set "web3Password=${WEB3_PASSWORD:-mirror_web3_pass}" \
-  --set "web3Username=${WEB3_USERNAME:-mirror_web3}" <<__SQL__
+  --set "web3Username=${WEB3_USERNAME:-mirror_web3}" \
+  --set "tempSchema=${DB_TEMPSCHEMA:-temporary}" <<__SQL__
 
 -- Create database & owner
 create user :ownerUsername with login password :'ownerPassword';
 create database :dbName with owner :ownerUsername;
-alter database :dbName set timescaledb.telemetry_level = off;
 
 -- Add extensions
 create extension if not exists pg_stat_statements;
@@ -42,12 +57,20 @@ create extension if not exists pg_stat_statements;
 -- Create roles
 create role readonly;
 create role readwrite in role readonly;
+create role temporary_admin in role readwrite;
 
 -- Create users
+create user :graphqlUsername with login password :'graphqlPassword' in role readonly;
 create user :grpcUsername with login password :'grpcPassword' in role readonly;
-create user :importerUsername with login password :'importerPassword' in role readwrite;
+create user :importerUsername with login password :'importerPassword' in role readwrite ${DB_SPECIFIC_IMPORTER_ROLE_ADMIN};
+create user :restJavaUsername with login password :'restJavaPassword' in role readonly;
 create user :rosettaUsername with login password :'rosettaPassword' in role readonly;
 create user :web3Username with login password :'web3Password' in role readonly;
+
+-- Grant temp schema admin privileges
+grant temporary_admin to :ownerUsername;
+grant temporary_admin to :importerUsername;
+
 ${DB_SPECIFIC_SQL}
 
 \connect :dbName
@@ -59,24 +82,30 @@ create schema if not exists :dbSchema authorization :ownerUsername;
 grant usage on schema :dbSchema to public;
 revoke create on schema :dbSchema from public;
 
+-- Create temp table schema
+create schema if not exists :tempSchema authorization temporary_admin;
+grant usage on schema :tempSchema to public;
+revoke create on schema :tempSchema from public;
+
 -- Grant readonly privileges
 grant connect on database :dbName to readonly;
-grant select on all tables in schema :dbSchema to readonly;
-grant select on all sequences in schema :dbSchema to readonly;
-grant usage on schema :dbSchema to readonly;
-alter default privileges in schema :dbSchema grant select on tables to readonly;
-alter default privileges in schema :dbSchema grant select on sequences to readonly;
+grant select on all tables in schema :dbSchema, :tempSchema to readonly;
+grant select on all sequences in schema :dbSchema, :tempSchema to readonly;
+grant usage on schema :dbSchema, :tempSchema to readonly;
+alter default privileges in schema :dbSchema, :tempSchema grant select on tables to readonly;
+alter default privileges in schema :dbSchema, :tempSchema grant select on sequences to readonly;
 
 -- Grant readwrite privileges
-grant insert, update on all tables in schema :dbSchema to readwrite;
+grant insert, update, delete on all tables in schema :dbSchema to readwrite;
 grant usage on all sequences in schema :dbSchema to readwrite;
-alter default privileges in schema :dbSchema grant insert, update on tables to readwrite;
+alter default privileges in schema :dbSchema grant insert, update, delete on tables to readwrite;
 alter default privileges in schema :dbSchema grant usage on sequences to readwrite;
 
+-- Partition privileges
+\connect :dbName postgres
+${DB_SPECIFIC_EXTENSION_SQL}
+
 -- Alter search path
 \connect postgres postgres
-alter database :dbName set search_path = :dbSchema, public;
-__SQL__
-
-mv "${PGHBA}.bak" "${PGHBA}"
-pg_ctl reload
+alter database :dbName set search_path = :dbSchema, public, :tempSchema;
+__SQL__
\ No newline at end of file
diff --git a/docs/ubuntu-setup.md b/docs/ubuntu-setup.md
new file mode 100644
index 0000000..ec0e5f3
--- /dev/null
+++ b/docs/ubuntu-setup.md
@@ -0,0 +1,82 @@
+
+# Ubuntu setup instructions 
+
+---
+**NOTE**
+
+Please first review the main [README](https://github.com/hashgraph/hedera-local-node/blob/main/README.md) file.
+
+---
+
+This guide shows example steps for setting up Ubuntu and running Hedera Local Node on it. We are assuming clean install of Ubuntu 22.04. 
+
+## Getting Started
+
+### Requirements
+
+* CPUs: 6
+* Memory: 8GB
+* Swap: 1 GB
+* Disk space for docker: 64 GB
+
+
+### Installing Docker Engine
+
+The following steps are following the official guide. Please review it first for any updates or changes: [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
+
+1. Set up Docker's `apt` repository.
+
+```
+# Add Docker's official GPG key:
+sudo apt-get update
+sudo apt-get install -y ca-certificates curl gnupg
+sudo install -m 0755 -d /etc/apt/keyrings
+curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
+sudo chmod a+r /etc/apt/keyrings/docker.gpg
+
+# Add the repository to Apt sources:
+echo \
+  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
+  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
+  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
+sudo apt-get update
+```
+2. Install the Docker packages
+```
+sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
+```
+3. Add your user to the docker group
+ ```
+ sudo usermod -aG docker $USER
+ sudo su - $USER
+ ```
+### Install Node.js
+1. Installing the latest availble LTS version at the time of the writing. Please check [NodeSource Node.js Binary Distributions](https://github.com/nodesource/distributions) for specific or the current one:
+```
+curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - &&\
+sudo apt-get install -y nodejs
+sudo chown -R $USER /usr/lib/node_modules
+```
+
+### Local Development Installation
+
+1. Installing the `hedera-local` module. For more information or options check [main README](https://github.com/hashgraph/hedera-local-node#installation)
+
+```
+git clone https://github.com/hashgraph/hedera-local-node.git
+cd hedera-local-node
+
+npm install &&  npm run build && sudo npm install -g
+
+hedera start -d
+```
+
+
+
+## Referenced Documents  
+
+* [Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)
+* [Linux post-installation steps for Docker Engine](https://docs.docker.com/engine/install/linux-postinstall/)
+* [Installing Node.js via package manager](https://nodejs.org/en/download/package-manager)
+* [NodeSource Node.js Binary Distributions](https://github.com/nodesource/distributions)
+* [README file of hedera-local-node repo](https://github.com/hashgraph/hedera-local-node/blob/main/README.md)
diff --git a/package-lock.json b/package-lock.json
index fe2fc51..fe8a44e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,15 +1,15 @@
 {
   "name": "@hashgraph/hedera-local",
-  "version": "2.17.2",
+  "version": "2.18.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@hashgraph/hedera-local",
-      "version": "2.17.2",
+      "version": "2.18.0",
       "license": "Apache-2.0",
       "dependencies": {
-        "@hashgraph/sdk": "^2.39.0",
+        "@hashgraph/sdk": "^2.40.0",
         "blessed": "^0.1.81",
         "blessed-terminal": "^0.1.22",
         "csv-parser": "^3.0.0",
@@ -1116,9 +1116,9 @@
       }
     },
     "node_modules/@hashgraph/sdk": {
-      "version": "2.39.0",
-      "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.39.0.tgz",
-      "integrity": "sha512-/VvJvO9z1yOEzQgvvg3dTzNfbaOVIsLjmo2GzwEgvycuVwuvWT2cmuNVQ/D5Fd53ELqTHusk999TkxYUYQJeqA==",
+      "version": "2.40.0",
+      "resolved": "https://registry.npmjs.org/@hashgraph/sdk/-/sdk-2.40.0.tgz",
+      "integrity": "sha512-wgkjCsOldSPbWZY3sKd7vGHqfcR5yQL/YNIbW7O1cihGN0G4nTzvYLjNLoNNIsA+XWMb0zkxWpQ0PAZaeQJgcw==",
       "dependencies": {
         "@ethersproject/abi": "^5.7.0",
         "@ethersproject/bignumber": "^5.7.0",
@@ -1127,7 +1127,7 @@
         "@grpc/grpc-js": "1.8.2",
         "@hashgraph/cryptography": "1.4.8-beta.5",
         "@hashgraph/proto": "2.14.0-beta.3",
-        "axios": "^1.6.0",
+        "axios": "^1.6.4",
         "bignumber.js": "^9.1.1",
         "bn.js": "^5.1.1",
         "crypto-js": "^4.2.0",
@@ -1136,6 +1136,7 @@
         "pino": "^8.14.1",
         "pino-pretty": "^10.0.0",
         "protobufjs": "^7.2.5",
+        "rfc4648": "^1.5.3",
         "utf8": "^3.0.0"
       },
       "engines": {
@@ -2456,11 +2457,11 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.6.0",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.0.tgz",
-      "integrity": "sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==",
+      "version": "1.6.7",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
+      "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
       "dependencies": {
-        "follow-redirects": "^1.15.0",
+        "follow-redirects": "^1.15.4",
         "form-data": "^4.0.0",
         "proxy-from-env": "^1.1.0"
       }
@@ -4408,9 +4409,9 @@
       "dev": true
     },
     "node_modules/follow-redirects": {
-      "version": "1.15.4",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz",
-      "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==",
+      "version": "1.15.5",
+      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz",
+      "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==",
       "funding": [
         {
           "type": "individual",
@@ -7638,6 +7639,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/rfc4648": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz",
+      "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ=="
+    },
     "node_modules/rimraf": {
       "version": "5.0.5",
       "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz",
diff --git a/package.json b/package.json
index 79212e9..e241793 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@hashgraph/hedera-local",
-  "version": "2.17.2",
+  "version": "2.18.0",
   "description": "Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).",
   "main": "index.ts",
   "scripts": {
@@ -57,7 +57,7 @@
     "typescript": "^5.3.2"
   },
   "dependencies": {
-    "@hashgraph/sdk": "^2.39.0",
+    "@hashgraph/sdk": "^2.40.0",
     "blessed": "^0.1.81",
     "blessed-terminal": "^0.1.22",
     "csv-parser": "^3.0.0",
diff --git a/src/configuration/local.json b/src/configuration/local.json
index c85feb1..83c7605 100644
--- a/src/configuration/local.json
+++ b/src/configuration/local.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.45.0"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.45.0"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.94.1"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.37.0"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.47.0-alpha.0"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.47.0-alpha.0"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.98.0-SNAPSHOT"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.39.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "23.9.0"}
   ],
   "envConfiguration": [
diff --git a/src/configuration/mainnet.json b/src/configuration/mainnet.json
index 7c20992..3d64a77 100644
--- a/src/configuration/mainnet.json
+++ b/src/configuration/mainnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.43.4"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.43.4"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.92.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.37.0"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.45.2"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.45.2"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.96.0"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.39.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "23.9.0"}
   ],
   "nodeConfiguration": {
diff --git a/src/configuration/previewnet.json b/src/configuration/previewnet.json
index 05be5c7..60d9c09 100644
--- a/src/configuration/previewnet.json
+++ b/src/configuration/previewnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.45.0-alpha.3"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.45.0-alpha.3"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.93.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.37.0"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.47.0-alpha.0"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.47.0-alpha.0"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.97.0"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.39.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "23.9.0"}
   ],
   "nodeConfiguration": {
diff --git a/src/constants.ts b/src/constants.ts
index dc6ed00..ae890e2 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -46,7 +46,9 @@ export const OPTIONAL_PORTS = [7546, 8080, 6379, 3000];
 export const EVM_ADDRESSES_BLOCKLIST_FILE_RELATIVE_PATH = '../../compose-network/network-node'
 export const RELATIVE_TMP_DIR_PATH = 'services/record-parser/temp';
 export const RELATIVE_RECORDS_DIR_PATH = 'network-logs/node/recordStreams/record0.0.3';
-export const APPLICATION_YML_RELATIVE_PATH = '../../compose-network/mirror-node/application.yml';
+export const RECORD_PARSER_SOURCE_REL_PATH = '../../src/services/record-parser';
+export const NETWORK_NODE_CONFIG_DIR_PATH = 'compose-network/network-node/data/config';
+export const APPLICATION_YML_RELATIVE_PATH = 'compose-network/mirror-node/application.yml';
 export const MIN_MEMORY_SINGLE_MODE = 4;
 export const MIN_MEMORY_MULTI_MODE = 14;
 export const RECOMMENDED_MEMORY_SINGLE_MODE = 8;
@@ -88,4 +90,23 @@ export const DOCKER_CLEANING_VALUMES_MESSAGE = 'Cleaning the volumes and temp fi
 
 // Recovery state
 export const RECOVERY_STATE_INIT_MESSAGE = 'Recovery State Initialized!';
-export const RECOVERY_STATE_STARTING_MESSAGE = "Starting Recovery State...";
\ No newline at end of file
+export const RECOVERY_STATE_STARTING_MESSAGE = "Starting Recovery State...";
+
+// Network Prep State
+export const NETWORK_PREP_STATE_INIT_MESSAGE = 'Network Preparation State Initialized!'
+export const NETWORK_PREP_STATE_STARTING_MESSAGE = 'Starting Network Preparation State...';
+export const NETWORK_PREP_STATE_IMPORT_FEES_START = 'Starting Fees import...';
+export const NETWORK_PREP_STATE_IMPORT_FEES_END = 'Imported fees successfully';
+export const NETWORK_PREP_STATE_WAITING_TOPIC_CREATION = 'Waiting for topic creation...';
+export const NETWORK_PREP_STATE_TOPIC_CREATED = 'Topic was created!';
+
+// Init state
+export const INIT_STATE_INIT_MESSAGE = 'Initialization State Initialized!';
+export const INIT_STATE_STARTING_MESSAGE = "Initialization State Starting...";
+export const INIT_STATE_START_DOCKER_CHECK = "Making sure that Docker is started and it's correct version...";
+export const INIT_STATE_CONFIGURING_ENV_VARIABLES_FINISH = 'Needed environment variables were set for this configuration.';
+export const INIT_STATE_RELAY_LIMITS_DISABLED = 'Hedera JSON-RPC Relay rate limits were disabled.';
+export const INIT_STATE_BOOTSTRAPPED_PROP_SET = 'Needed bootsrap properties were set for this configuration.';
+export const INIT_STATE_MIRROR_PROP_SET = 'Needed mirror node properties were set for this configuration.';
+export const INIT_STATE_NO_NODE_CONF_NEEDED = 'No additional node configuration needed.';
+export const INIT_STATE_NO_ENV_VAR_CONFIGURED = 'No new environment variables were configured.';
diff --git a/src/data/ConfigurationData.ts b/src/data/ConfigurationData.ts
index d6bc8a9..b3de2f6 100644
--- a/src/data/ConfigurationData.ts
+++ b/src/data/ConfigurationData.ts
@@ -34,18 +34,18 @@ export class ConfigurationData {
      * @param {NetworkType} network - The type of the network.
      * @returns {NetworkConfiguration} The configuration data for the selected network.
      */
-    public getSelectedConfigurationData(network: NetworkType): NetworkConfiguration {
+    public static getSelectedConfigurationData(network: NetworkType): NetworkConfiguration {
         switch (network) {
             case NetworkType.LOCAL:
-                return this.getNetworkConfiguration(local as any);
+                return ConfigurationData.getNetworkConfiguration(local as any);
             case NetworkType.MAINNET:
-                return this.getNetworkConfiguration(mainnet as any);
+                return ConfigurationData.getNetworkConfiguration(mainnet as any);
             case NetworkType.TESTNET:
-                return this.getNetworkConfiguration(testnet as any);
+                return ConfigurationData.getNetworkConfiguration(testnet as any);
             case NetworkType.PREVIEWNET:
-                return this.getNetworkConfiguration(previewnet as any);
+                return ConfigurationData.getNetworkConfiguration(previewnet as any);
             default:
-                return this.getNetworkConfiguration(local as any);
+                return ConfigurationData.getNetworkConfiguration(local as any);
         }
     }
 
@@ -54,7 +54,7 @@ export class ConfigurationData {
      * @param {NetworkConfiguration} jsonConfiguration - The JSON configuration.
      * @returns The network configuration.
      */
-    private getNetworkConfiguration(jsonConfiguration: NetworkConfiguration): NetworkConfiguration {
+    private static getNetworkConfiguration(jsonConfiguration: NetworkConfiguration): NetworkConfiguration {
         const relayConfiguration = jsonConfiguration?.envConfiguration ?? undefined;
         const nodeProperties = jsonConfiguration?.nodeConfiguration!.properties ?? undefined;
         const configuration: NetworkConfiguration = {
diff --git a/src/services/CLIService.ts b/src/services/CLIService.ts
index 83c3b60..bdb23af 100644
--- a/src/services/CLIService.ts
+++ b/src/services/CLIService.ts
@@ -87,6 +87,9 @@ export class CLIService implements IService{
         CLIService.userComposeDirOption(yargs);
         CLIService.blocklistingOption(yargs);
         CLIService.enableDebugOption(yargs);
+        CLIService.selectNetworkTag(yargs);
+        CLIService.selectMirrorTag(yargs);
+        CLIService.selectRelayTag(yargs);
     }
 
     /**
@@ -152,8 +155,10 @@ export class CLIService implements IService{
         const verbose = CLIService.resolveVerboseLevel(argv.verbose as string);
         const timestamp = argv.timestamp as string;
         const enableDebug = argv.enableDebug as boolean;
+        const networkTag = argv.networkTag as string;
+        const mirrorTag = argv.mirrorTag as string;
+        const relayTag = argv.relayTag as string;
         const workDir = FileSystemUtils.parseWorkDir(argv.workdir as string);
-
         const currentArgv: CLIOptions = {
             accounts,
             async,
@@ -172,6 +177,9 @@ export class CLIService implements IService{
             verbose,
             timestamp,
             enableDebug,
+            networkTag,
+            mirrorTag,
+            relayTag,
             workDir,
         };
 
@@ -522,7 +530,61 @@ export class CLIService implements IService{
             describe: 'Enable or disable debugging of the local node',
             demandOption: false,
             default: false
-          });
+        });
+    }
+
+    /**
+     * Adds the 'network-tag' option to the command line arguments.
+     * This option is a string that enables usage of custom network tag.
+     * It is not required and defaults to predefined configuration.
+     * 
+     * @param {yargs.Argv<{}>} yargs - The yargs instance to which the option is added.
+     * @private
+     * @static
+     */
+    private static selectNetworkTag(yargs: Argv<{}>): void {
+        yargs.option('network-tag', {
+            type: 'string',
+            describe: 'Select custom network node tag',
+            demandOption: false,
+            default: ''
+        });
+    }
+
+    /**
+     * Adds the 'mirror-tag' option to the command line arguments.
+     * This option is a string that enables usage of custom mirror-node tag.
+     * It is not required and defaults to predefined configuration.
+     * 
+     * @param {yargs.Argv<{}>} yargs - The yargs instance to which the option is added.
+     * @private
+     * @static
+     */
+    private static selectMirrorTag(yargs: Argv<{}>): void {
+        yargs.option('mirror-tag', {
+            type: 'string',
+            describe: 'Select custom mirror-node tag',
+            demandOption: false,
+            default: ''
+        });
+    }
+
+    /**
+     * Adds the 'mirror-tag' option to the command line arguments.
+     * This option is a string that enables usage of custom mirror-node tag.
+     * It is not required and defaults to predefined configuration.
+     * 
+     * @param {yargs.Argv<{}>} yargs - The yargs instance to which the option is added.
+     * @private
+     * @static
+     */
+    private static selectRelayTag(yargs: Argv<{}>): void {
+        yargs.option('relay-tag', {
+            type: 'string',
+            describe: 'Select custom hedera-json-rpc relay tag',
+            demandOption: false,
+            default: ''
+        });
     }
     
     /**
diff --git a/src/state/InitState.ts b/src/state/InitState.ts
index d163638..0283d86 100644
--- a/src/state/InitState.ts
+++ b/src/state/InitState.ts
@@ -34,7 +34,22 @@ import { ConfigurationData } from '../data/ConfigurationData';
 import { Configuration } from '../types/NetworkConfiguration';
 import originalNodeConfiguration from '../configuration/originalNodeConfiguration.json';
 import { DockerService } from '../services/DockerService';
-import { APPLICATION_YML_RELATIVE_PATH, NECESSARY_PORTS, OPTIONAL_PORTS } from '../constants';
+import {
+    APPLICATION_YML_RELATIVE_PATH,
+    INIT_STATE_BOOTSTRAPPED_PROP_SET,
+    INIT_STATE_CONFIGURING_ENV_VARIABLES_FINISH,
+    INIT_STATE_INIT_MESSAGE,
+    INIT_STATE_MIRROR_PROP_SET,
+    INIT_STATE_NO_ENV_VAR_CONFIGURED,
+    INIT_STATE_NO_NODE_CONF_NEEDED,
+    INIT_STATE_RELAY_LIMITS_DISABLED,
+    INIT_STATE_STARTING_MESSAGE,
+    INIT_STATE_START_DOCKER_CHECK,
+    NECESSARY_PORTS,
+    NETWORK_NODE_CONFIG_DIR_PATH,
+    OPTIONAL_PORTS,
+    RECORD_PARSER_SOURCE_REL_PATH
+} from '../constants';
 
 configDotenv({ path: path.resolve(__dirname, '../../.env') });
 
@@ -77,7 +92,7 @@ export class InitState implements IState{
         this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name);
         this.cliOptions = ServiceLocator.Current.get<CLIService>(CLIService.name).getCurrentArgv();
         this.dockerService = ServiceLocator.Current.get<DockerService>(DockerService.name);
-        this.logger.trace('Initialization State Initialized!', this.stateName);
+        this.logger.trace(INIT_STATE_INIT_MESSAGE, this.stateName);
     }
 
     /**
@@ -94,13 +109,13 @@ export class InitState implements IState{
      * @returns {Promise<void>} A promise that resolves when the state has started.
      */
     public async onStart(): Promise<void> {
-        this.logger.trace('Initialization State Starting...', this.stateName);
-        const configurationData = new ConfigurationData().getSelectedConfigurationData(this.cliOptions.network);
-        this.logger.info("Making sure that Docker is started and it's correct version...", this.stateName);
+        this.logger.trace(INIT_STATE_STARTING_MESSAGE, this.stateName);
+        const configurationData = ConfigurationData.getSelectedConfigurationData(this.cliOptions.network);
+
         // Check if docker is running and it's the correct version
+        this.logger.info(INIT_STATE_START_DOCKER_CHECK, this.stateName);
         const isCorrectDockerComposeVersion = await this.dockerService.isCorrectDockerComposeVersion();
         const isDockerStarted = await this.dockerService.checkDocker();
-
         const dockerHasEnoughResources = await this.dockerService.checkDockerResources(this.cliOptions.multiNode);
 
         if (!(isCorrectDockerComposeVersion && isDockerStarted && dockerHasEnoughResources)) {
@@ -140,13 +155,13 @@ export class InitState implements IState{
     private prepareWorkDirectory() {
         this.logger.info(`Local Node Working directory set to ${this.cliOptions.workDir}`, this.stateName);
         FileSystemUtils.createEphemeralDirectories(this.cliOptions.workDir);
-        const configDirSource = join(__dirname, '../../compose-network/network-node/data/config/');
-        const configPathMirrorNodeSource = join(__dirname, APPLICATION_YML_RELATIVE_PATH);
-        const recordParserSource = join(__dirname,'../../src/services/record-parser');
+        const configDirSource = join(__dirname, `../../${NETWORK_NODE_CONFIG_DIR_PATH}`);
+        const configPathMirrorNodeSource = join(__dirname, `../../${APPLICATION_YML_RELATIVE_PATH}`);
+        const recordParserSource = join(__dirname, RECORD_PARSER_SOURCE_REL_PATH);
 
         const configFiles = {
-            [configDirSource]: `${this.cliOptions.workDir}/compose-network/network-node/data/config`,
-            [configPathMirrorNodeSource]: `${this.cliOptions.workDir}/compose-network/mirror-node/application.yml`,
+            [configDirSource]: `${this.cliOptions.workDir}/${NETWORK_NODE_CONFIG_DIR_PATH}`,
+            [configPathMirrorNodeSource]: `${this.cliOptions.workDir}/${APPLICATION_YML_RELATIVE_PATH}`,
             [recordParserSource]: `${this.cliOptions.workDir}/services/record-parser`
         };
         FileSystemUtils.copyPaths(configFiles);
@@ -159,12 +174,13 @@ export class InitState implements IState{
      */
     private configureEnvVariables(imageTagConfiguration: Array<Configuration>, envConfiguration: Array<Configuration> | undefined): void {
         imageTagConfiguration.forEach(variable => {
-            process.env[variable.key] = variable.value;
-            this.logger.trace(`Environment variable ${variable.key} will be set to ${variable.value}.`, this.stateName);
+            const { tag, node } = this.extractImageTag(variable);
+            this.logger.trace(`Environment variable ${node} will be set to ${tag}.`, this.stateName);
+            process.env[node] = tag;
         });
 
         if (!envConfiguration) {
-            this.logger.trace('No new environment variables were configured.', this.stateName);
+            this.logger.trace(INIT_STATE_NO_ENV_VAR_CONFIGURED, this.stateName);
             return;
         }
 
@@ -178,9 +194,36 @@ export class InitState implements IState{
             process.env.RELAY_HBAR_RATE_LIMIT_TINYBAR = '0';
             process.env.RELAY_HBAR_RATE_LIMIT_DURATION = '0';
             process.env.RELAY_RATE_LIMIT_DISABLED = `${relayLimitsDisabled}`;
-            this.logger.info('Hedera JSON-RPC Relay rate limits were disabled.', this.stateName);
+            this.logger.info(INIT_STATE_RELAY_LIMITS_DISABLED, this.stateName);
+        }
+        this.logger.info(INIT_STATE_CONFIGURING_ENV_VARIABLES_FINISH, this.stateName);
+    }
+
+    /**
+     * Extracts the image tag from the configuration.
+     * 
+     * @private
+     * @param {Configuration} variable - The configuration.
+     * @returns {string} The extracted image tag.
+     */
+    private extractImageTag(variable: Configuration): {
+        tag: string,
+        node: string
+    } {
+        const node = variable.key;
+        let tag = variable.value;
+        if (this.cliOptions.networkTag && (node === "NETWORK_NODE_IMAGE_TAG" || node === "HAVEGED_IMAGE_TAG")) {
+            tag = this.cliOptions.networkTag;
+        } else if(this.cliOptions.mirrorTag && node === "MIRROR_IMAGE_TAG") {
+            tag = this.cliOptions.mirrorTag;
+        } else if(this.cliOptions.relayTag && node === "RELAY_IMAGE_TAG") {
+            tag = this.cliOptions.relayTag;
+        }
+
+        return { 
+            tag,
+            node
         }
-        this.logger.info('Needed environment variables were set for this configuration.', this.stateName);
     }
 
     /**
@@ -188,7 +231,7 @@ export class InitState implements IState{
      * @param {Array<Configuration> | undefined} nodeConfiguration - The node configuration.
      */
     private configureNodeProperties(nodeConfiguration: Array<Configuration> | undefined): void {
-        const propertiesFilePath = join(this.cliOptions.workDir, 'compose-network/network-node/data/config/bootstrap.properties');
+        const propertiesFilePath = join(this.cliOptions.workDir, NETWORK_NODE_CONFIG_DIR_PATH, 'bootstrap.properties');
 
         let newProperties = '';
         originalNodeConfiguration.bootsrapProperties.forEach(property => {
@@ -196,7 +239,7 @@ export class InitState implements IState{
         });
 
         if (!nodeConfiguration) {
-            this.logger.trace('No additional node configuration needed.', this.stateName);
+            this.logger.trace(INIT_STATE_NO_NODE_CONF_NEEDED, this.stateName);
             return;
         }
         nodeConfiguration!.forEach(property => {
@@ -206,7 +249,7 @@ export class InitState implements IState{
 
         writeFileSync(propertiesFilePath, newProperties, { flag: 'w' });
 
-        this.logger.info('Needed bootsrap properties were set for this configuration.', this.stateName);
+        this.logger.info(INIT_STATE_BOOTSTRAPPED_PROP_SET, this.stateName);
     }
 
     /**
@@ -219,7 +262,6 @@ export class InitState implements IState{
         this.logger.trace('Configuring required mirror node properties, depending on selected configuration...', this.stateName);
         const turboMode = !this.cliOptions.fullMode;
         const debugMode = this.cliOptions.enableDebug;
-
         const multiNode = this.cliOptions.multiNode;
 
         const propertiesFilePath = join(this.cliOptions.workDir, 'compose-network/mirror-node/application.yml');
@@ -240,6 +282,6 @@ export class InitState implements IState{
         }
 
         writeFileSync(propertiesFilePath, yaml.dump(application, { lineWidth: 256 }));
-        this.logger.info('Needed mirror node properties were set for this configuration.', this.stateName);
+        this.logger.info(INIT_STATE_MIRROR_PROP_SET, this.stateName);
     }
 }
diff --git a/src/state/NetworkPrepState.ts b/src/state/NetworkPrepState.ts
index 542c793..544c1ab 100644
--- a/src/state/NetworkPrepState.ts
+++ b/src/state/NetworkPrepState.ts
@@ -27,6 +27,14 @@ import { ServiceLocator } from '../services/ServiceLocator';
 import { EventType } from '../types/EventType';
 import { IState } from './IState';
 import { DockerService } from '../services/DockerService';
+import {
+    NETWORK_PREP_STATE_IMPORT_FEES_END,
+    NETWORK_PREP_STATE_IMPORT_FEES_START,
+    NETWORK_PREP_STATE_INIT_MESSAGE,
+    NETWORK_PREP_STATE_STARTING_MESSAGE,
+    NETWORK_PREP_STATE_TOPIC_CREATED,
+    NETWORK_PREP_STATE_WAITING_TOPIC_CREATION
+  } from '../constants';
 
 /**
  * Represents the network preparation state of the Hedera Local Node.
@@ -67,7 +75,7 @@ export class NetworkPrepState implements IState {
         this.logger = ServiceLocator.Current.get<LoggerService>(LoggerService.name);
         this.clientService = ServiceLocator.Current.get<ClientService>(ClientService.name);
         this.dockerService = ServiceLocator.Current.get<DockerService>(DockerService.name);
-        this.logger.trace('Network Preparation State Initialized!', this.stateName);
+        this.logger.trace(NETWORK_PREP_STATE_INIT_MESSAGE, this.stateName);
     }
 
     /**
@@ -83,7 +91,7 @@ export class NetworkPrepState implements IState {
      * @returns {Promise<void>} A promise that resolves when the network preparation is complete.
      */
     public async onStart(): Promise<void> {
-        this.logger.info('Starting Network Preparation State...', this.stateName);
+        this.logger.info(NETWORK_PREP_STATE_STARTING_MESSAGE, this.stateName);
         const client = this.clientService.getClient();
 
         await this.importFees(client);
@@ -98,7 +106,7 @@ export class NetworkPrepState implements IState {
      * @returns {Promise<void>} A promise that resolves when the import is complete.
      */
     private async importFees(client: Client): Promise<void> {
-        this.logger.trace('Starting Fees import...', this.stateName);
+        this.logger.trace(NETWORK_PREP_STATE_IMPORT_FEES_START, this.stateName);
 
         const feesFileId = 111;
         const exchangeRatesFileId = 112;
@@ -106,9 +114,7 @@ export class NetworkPrepState implements IState {
         const timestamp = Date.now();
         const nullOutput = this.dockerService.getNullOutput();
 
-        const queryFees = new FileContentsQuery().setFileId(
-          `0.0.${feesFileId}`
-        );
+        const queryFees = this.buildQueryFees(feesFileId);
         const fees = Buffer.from(await queryFees.execute(client)).toString('hex');
         await shell.exec(
           `docker exec mirror-node-db psql mirror_node -U mirror_node -c "INSERT INTO public.file_data(file_data, consensus_timestamp, entity_id, transaction_type) VALUES (decode('${fees}', 'hex'), ${
@@ -116,9 +122,7 @@ export class NetworkPrepState implements IState {
           }, ${feesFileId}, 17);" >> ${nullOutput}`
         );
     
-        const queryExchangeRates = new FileContentsQuery().setFileId(
-          `0.0.${exchangeRatesFileId}`
-        );
+        const queryExchangeRates = this.buildQueryFees(exchangeRatesFileId);
         const exchangeRates = Buffer.from(await queryExchangeRates.execute(client)).toString('hex');
         await shell.exec(
           `docker exec mirror-node-db psql mirror_node -U mirror_node -c "INSERT INTO public.file_data(file_data, consensus_timestamp, entity_id, transaction_type) VALUES (decode('${exchangeRates}', 'hex'), ${
@@ -126,7 +130,18 @@ export class NetworkPrepState implements IState {
           }, ${exchangeRatesFileId}, 17);" >> ${nullOutput}`
         );
 
-        this.logger.info('Imported fees successfully', this.stateName);
+        this.logger.info(NETWORK_PREP_STATE_IMPORT_FEES_END, this.stateName);
+    }
+
+    /**
+     * Builds a query for the fees file.
+     * @param {number} feesFileId - The fees file ID.
+     * @returns {FileContentsQuery} The query for the fees file.
+     */
+    private buildQueryFees(feesFileId: number): FileContentsQuery {
+        return new FileContentsQuery().setFileId(
+            `0.0.${feesFileId}`
+        )
     }
 
     /**
@@ -136,7 +151,7 @@ export class NetworkPrepState implements IState {
      *  @returns {Promise<void>}
      */
     private async waitForTopicCreation(): Promise<void> {
-        this.logger.trace('Waiting for topic creation...', this.stateName);
+        this.logger.trace(NETWORK_PREP_STATE_WAITING_TOPIC_CREATION, this.stateName);
         const LOG_SEARCH_TEXT = 'Created TOPIC entity';
 
         return new Promise((resolve, reject) => {
@@ -148,7 +163,7 @@ export class NetworkPrepState implements IState {
             if (data.indexOf(LOG_SEARCH_TEXT) !== -1) {
               command.kill('SIGINT');
               command.stdout!.destroy();
-              this.logger.info('Topic was created!', this.stateName);
+              this.logger.info(NETWORK_PREP_STATE_TOPIC_CREATED, this.stateName);
               resolve();
             }
           });
diff --git a/src/types/CLIOptions.ts b/src/types/CLIOptions.ts
index 867b292..663e910 100644
--- a/src/types/CLIOptions.ts
+++ b/src/types/CLIOptions.ts
@@ -61,5 +61,8 @@ export interface CLIOptions {
     verbose: number,
     timestamp: string,
     enableDebug: boolean,
+    networkTag: string,
+    mirrorTag: string,
+    relayTag: string,
     workDir: string,
 }


Test patch:
diff --git a/src/configuration/testnet.json b/src/configuration/testnet.json
index f2b708d..d5b7884 100644
--- a/src/configuration/testnet.json
+++ b/src/configuration/testnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.44.4"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.44.4"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.93.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.37.0"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.46.2"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.46.2"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.97.0"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.39.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "23.9.0"}
   ],
   "nodeConfiguration": {
diff --git a/test/unit/states/InitState.spec.ts b/test/unit/states/InitState.spec.ts
new file mode 100644
index 0000000..71bbce7
--- /dev/null
+++ b/test/unit/states/InitState.spec.ts
@@ -0,0 +1,347 @@
+/*-
+ *
+ * Hedera Local Node
+ *
+ * Copyright (C) 2024 Hedera Hashgraph, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { expect } from 'chai';
+import fs from 'fs';
+import yaml from 'js-yaml';
+import { SinonSandbox, SinonSpy, SinonStub, SinonStubbedInstance } from 'sinon';
+import { LoggerService } from '../../../src/services/LoggerService';
+import { InitState } from '../../../src/state/InitState';
+import { getTestBed } from '../testBed';
+import {
+    APPLICATION_YML_RELATIVE_PATH,
+    INIT_STATE_BOOTSTRAPPED_PROP_SET,
+    INIT_STATE_CONFIGURING_ENV_VARIABLES_FINISH,
+    INIT_STATE_INIT_MESSAGE,
+    INIT_STATE_MIRROR_PROP_SET,
+    INIT_STATE_NO_ENV_VAR_CONFIGURED,
+    INIT_STATE_NO_NODE_CONF_NEEDED,
+    INIT_STATE_RELAY_LIMITS_DISABLED,
+    INIT_STATE_STARTING_MESSAGE,
+    INIT_STATE_START_DOCKER_CHECK,
+    NECESSARY_PORTS,
+    NETWORK_NODE_CONFIG_DIR_PATH,
+    OPTIONAL_PORTS,
+    RECORD_PARSER_SOURCE_REL_PATH,
+} from '../../../src/constants';
+import { DockerService } from '../../../src/services/DockerService';
+import { EventType } from '../../../src/types/EventType';
+import { CLIService } from '../../../src/services/CLIService';
+import { ConfigurationData } from '../../../src/data/ConfigurationData';
+import { FileSystemUtils } from '../../../src/utils/FileSystemUtils';
+import { join } from 'path';
+
+describe('InitState tests', () => {
+    let initState: InitState,
+        testSandbox: SinonSandbox, 
+        loggerService: SinonStubbedInstance<LoggerService>,
+        serviceLocator: SinonStub,
+        dockerService: SinonStubbedInstance<DockerService>,
+        observerSpy: SinonSpy,
+        configurationData: SinonStub,
+        prepareWorkDirectoryStub: SinonStub,
+        configureEnvVariablesStub: SinonStub,
+        configureNodePropertiesStub: SinonStub,
+        configureMirrorNodePropertiesStub: SinonStub,
+        extractImageTagStub: SinonStub;
+
+    const TEST_CONFIGURATION = {
+        imageTagConfiguration: [
+            { key: "TEST_VAR_1", value: 'test'}
+        ],
+        envConfiguration: [
+            { key: "TEST_VAR_2", value: 'test'}
+        ],
+        nodeConfiguration: {
+            properties: [
+                { key: "TEST_VAR_3", value: 'test'}
+            ],
+        },
+    }
+
+    const stubPrivateFunctions = () => {
+        prepareWorkDirectoryStub = testSandbox.stub(initState as any, 'prepareWorkDirectory')
+        configureEnvVariablesStub = testSandbox.stub(initState as any, 'configureEnvVariables')
+        configureNodePropertiesStub = testSandbox.stub(initState as any, 'configureNodeProperties')
+        configureMirrorNodePropertiesStub = testSandbox.stub(initState as any, 'configureMirrorNodeProperties')
+        extractImageTagStub = testSandbox.stub(initState as any, 'extractImageTag').returns({
+            node: TEST_CONFIGURATION.imageTagConfiguration[0].key,
+            tag: TEST_CONFIGURATION.imageTagConfiguration[0].value,
+        })
+    }
+
+    before(() => {
+        const { 
+            sandbox,
+            loggerServiceStub,
+            serviceLocatorStub,
+            dockerServiceStub,
+        } = getTestBed({
+            workDir: 'testDir',
+            fullMode: true,
+            enableDebug: true,
+            multiNode: true,
+            networkTag: 'test-network-tag',
+            mirrorTag: 'test-mirror-tag',
+            relayTag: 'test-relay-tag',
+        });
+    
+        testSandbox = sandbox
+        dockerService = dockerServiceStub
+        loggerService = loggerServiceStub
+        serviceLocator = serviceLocatorStub
+
+        initState = new InitState();
+        observerSpy = testSandbox.spy();
+        const observer = {
+            update: observerSpy
+        }
+        initState.subscribe(observer);
+        configurationData = testSandbox.stub(ConfigurationData, 'getSelectedConfigurationData').callsFake(() => TEST_CONFIGURATION)
+        stubPrivateFunctions()
+    });
+
+    afterEach(() => {
+        testSandbox.resetHistory();
+        observerSpy.resetHistory();
+        dockerService.isCorrectDockerComposeVersion.reset()
+        dockerService.checkDocker.reset()
+        dockerService.checkDockerResources.reset()
+    });
+
+    it('should initialize the Init State', async () => {
+        expect(initState).to.be.instanceOf(InitState);
+        testSandbox.assert.calledWith(serviceLocator, LoggerService.name);
+        testSandbox.assert.calledWith(serviceLocator, DockerService.name);
+        testSandbox.assert.calledWith(serviceLocator, CLIService.name);
+        testSandbox.assert.calledOnceWithExactly(loggerService.trace, INIT_STATE_INIT_MESSAGE, InitState.name);
+    })
+
+    it('should have a subscribe method', async () => {
+        expect(initState.subscribe).to.be.a('function');
+    })
+
+    it('should execute onStart', async () => {
+        dockerService.isCorrectDockerComposeVersion.resolves(true)
+        dockerService.checkDocker.resolves(true)
+        dockerService.checkDockerResources.resolves(true)
+
+        await initState.onStart();
+
+        // loggin messages
+        testSandbox.assert.calledWithExactly(loggerService.trace, INIT_STATE_STARTING_MESSAGE, InitState.name);
+        testSandbox.assert.calledOnce(configurationData)
+        testSandbox.assert.calledWithExactly(loggerService.info, INIT_STATE_START_DOCKER_CHECK, InitState.name);
+        testSandbox.assert.calledOnce(dockerService.isCorrectDockerComposeVersion);
+        testSandbox.assert.calledOnce(dockerService.checkDocker);
+        testSandbox.assert.calledOnce(dockerService.checkDockerResources);
+        
+        testSandbox.assert.calledOnceWithExactly(dockerService.isPortInUse, NECESSARY_PORTS.concat(OPTIONAL_PORTS));
+        testSandbox.assert.calledWith(observerSpy, EventType.Finish);
+        testSandbox.assert.calledOnce(prepareWorkDirectoryStub);
+        testSandbox.assert.calledOnce(configureEnvVariablesStub);
+        testSandbox.assert.calledOnce(configureNodePropertiesStub);
+        testSandbox.assert.calledOnce(configureMirrorNodePropertiesStub);
+    })
+
+    it('should execute onStart and finish with UnresolvableError on docker checks (Compose Version, Docker Running, Resources)', async () => {
+        dockerService.isCorrectDockerComposeVersion.resolves(false);
+        await initState.onStart();
+
+        // loggin messages
+        testSandbox.assert.calledOnceWithExactly(loggerService.trace, INIT_STATE_STARTING_MESSAGE, InitState.name);
+        testSandbox.assert.calledOnce(configurationData)
+        testSandbox.assert.calledOnceWithExactly(loggerService.info, INIT_STATE_START_DOCKER_CHECK, InitState.name);
+        testSandbox.assert.calledOnce(dockerService.isCorrectDockerComposeVersion);
+        testSandbox.assert.calledOnce(dockerService.checkDocker);
+        testSandbox.assert.calledOnce(dockerService.checkDockerResources);
+
+        testSandbox.assert.calledWith(observerSpy, EventType.UnresolvableError);
+        
+        testSandbox.assert.notCalled(dockerService.isPortInUse);
+    })
+
+    describe('private functions', () => {
+        let createEphemeralDirectoriesStub: SinonStub,
+            copyPathsStub: SinonStub,
+            onStartStub: SinonStub,
+            fsWriteFileSync: SinonStub,
+            fsReadFileSync: SinonStub,
+            ymlLoad: SinonStub,
+            ymlDump: SinonStub;
+        const rootDirSource = join(__dirname, `../`);
+
+        const configDirSource = join(rootDirSource, `../../${NETWORK_NODE_CONFIG_DIR_PATH}`);
+        const configPathMirrorNodeSource = join(rootDirSource, `../../${APPLICATION_YML_RELATIVE_PATH}`);
+        const recordParserSource = join(rootDirSource, RECORD_PARSER_SOURCE_REL_PATH);
+        const configFiles = {
+            [configDirSource]: `testDir/${NETWORK_NODE_CONFIG_DIR_PATH}`,
+            [configPathMirrorNodeSource]: `testDir/${APPLICATION_YML_RELATIVE_PATH}`,
+            [recordParserSource]: `testDir/services/record-parser`
+        };
+
+        before(() => {
+            createEphemeralDirectoriesStub = testSandbox.stub(FileSystemUtils, "createEphemeralDirectories").callsFake(() => {})
+            copyPathsStub = testSandbox.stub(FileSystemUtils, "copyPaths").callsFake(() => {})
+            fsWriteFileSync = testSandbox.stub(fs, 'writeFileSync');
+            fsReadFileSync = testSandbox.stub(fs, 'readFileSync').returns('test');
+            ymlLoad = testSandbox.stub(yaml, 'load').returns({
+                hedera: {
+                    mirror: {
+                        importer: {
+                            dataPath: {
+                            },
+                            downloader: {
+                                local: {}
+                            }
+                        },
+                        monitor: {
+                            nodes: {}
+                        }
+                    }
+                }
+            });
+            ymlDump = testSandbox.stub(yaml, 'dump');
+        })
+
+        afterEach(() => {
+            onStartStub.restore()
+        })
+        
+        it('should execute "prepareWorkDirectory" as expected', async () => {
+            prepareWorkDirectoryStub.restore()
+
+            onStartStub = testSandbox.stub(initState, 'onStart')
+                .callsFake(() => (initState  as any).prepareWorkDirectory())
+            await initState.onStart();
+            testSandbox.assert.calledWithExactly(loggerService.info, "Local Node Working directory set to testDir", InitState.name);
+            testSandbox.assert.calledOnceWithExactly(createEphemeralDirectoriesStub, "testDir");
+            testSandbox.assert.calledOnceWithExactly(copyPathsStub, configFiles);
+            
+            prepareWorkDirectoryStub = testSandbox.stub(initState as any, 'prepareWorkDirectory')
+        })
+
+        it('should execute "configureEnvVariables" as expected', async () => {
+            configureEnvVariablesStub.restore()
+
+            onStartStub = testSandbox.stub(initState, 'onStart')
+                .callsFake(() => (initState  as any).configureEnvVariables(
+                    TEST_CONFIGURATION.imageTagConfiguration,
+                    TEST_CONFIGURATION.envConfiguration,
+                ))
+            await initState.onStart();
+
+            testSandbox.assert.match(process.env.TEST_VAR_1, 'test');
+            testSandbox.assert.match(process.env.TEST_VAR_2, 'test');
+            testSandbox.assert.called(loggerService.trace);
+            testSandbox.assert.called(extractImageTagStub);
+            testSandbox.assert.calledWithExactly(loggerService.info, INIT_STATE_CONFIGURING_ENV_VARIABLES_FINISH, InitState.name);
+            testSandbox.assert.calledWithExactly(loggerService.info, INIT_STATE_RELAY_LIMITS_DISABLED, InitState.name);
+
+            configureEnvVariablesStub = testSandbox.stub(initState as any, 'configureEnvVariables')
+        })
+
+        it('should execute "configureEnvVariables" as expected (NO ENV configured)', async () => {
+            configureEnvVariablesStub.restore()
+
+            onStartStub = testSandbox.stub(initState, 'onStart')
+                .callsFake(() => (initState  as any).configureEnvVariables(
+                    TEST_CONFIGURATION.imageTagConfiguration,
+                ))
+            await initState.onStart();
+
+            testSandbox.assert.calledWithExactly(loggerService.trace, INIT_STATE_NO_ENV_VAR_CONFIGURED, InitState.name);
+
+            configureEnvVariablesStub = testSandbox.stub(initState as any, 'configureEnvVariables')
+        })
+
+        it('should execute "extractImageTag" as expected and return "NETWORK_NODE_IMAGE_TAG"', async () => {
+            extractImageTagStub.restore()
+
+            const {tag, node} = (initState as any).extractImageTag({
+                key: "NETWORK_NODE_IMAGE_TAG",
+                value: 'test'
+            })
+            expect(tag).to.be.equal('test-network-tag');
+            expect(node).to.be.equal('NETWORK_NODE_IMAGE_TAG');
+        })
+
+        it('should execute "extractImageTag" as expected and return "MIRROR_IMAGE_TAG"', async () => {
+            extractImageTagStub.restore()
+
+            const {tag, node} = (initState as any).extractImageTag({
+                key: "MIRROR_IMAGE_TAG",
+                value: 'test'
+            })
+            expect(tag).to.be.equal('test-mirror-tag');
+            expect(node).to.be.equal('MIRROR_IMAGE_TAG');
+        })
+
+        it('should execute "extractImageTag" as expected and return "RELAY_IMAGE_TAG"', async () => {
+            extractImageTagStub.restore()
+
+            const {tag, node} = (initState as any).extractImageTag({
+                key: "RELAY_IMAGE_TAG",
+                value: 'test'
+            })
+            expect(tag).to.be.equal('test-relay-tag');
+            expect(node).to.be.equal('RELAY_IMAGE_TAG');
+        })
+
+        it('should execute "configureNodeProperties" as write to file', async () => {
+            configureNodePropertiesStub.restore()
+            onStartStub = testSandbox.stub(initState, 'onStart')
+                .callsFake(() => (initState  as any).configureNodeProperties(
+                    TEST_CONFIGURATION.nodeConfiguration.properties
+                ))
+            await initState.onStart();
+            testSandbox.assert.calledWithExactly(loggerService.info, INIT_STATE_BOOTSTRAPPED_PROP_SET, InitState.name);
+            testSandbox.assert.called(fsWriteFileSync);
+
+            configureNodePropertiesStub = testSandbox.stub(initState as any, 'configureNodeProperties')
+        })
+
+        it('should execute "configureNodeProperties" as write to file (No additional node configuration needed)', async () => {
+            configureNodePropertiesStub.restore()
+            onStartStub = testSandbox.stub(initState, 'onStart')
+                .callsFake(() => (initState  as any).configureNodeProperties())
+            await initState.onStart();
+            testSandbox.assert.calledWithExactly(loggerService.trace, INIT_STATE_NO_NODE_CONF_NEEDED, InitState.name);
+            testSandbox.assert.notCalled(fsWriteFileSync);
+
+            configureNodePropertiesStub = testSandbox.stub(initState as any, 'configureNodeProperties')
+        })
+
+        it('should execute "configureMirrorNodeProperties" as write to file', async () => {
+            configureMirrorNodePropertiesStub.restore()
+
+            onStartStub = testSandbox.stub(initState, 'onStart')
+            .callsFake(() => (initState  as any).configureMirrorNodeProperties())
+            await initState.onStart();
+            testSandbox.assert.calledOnceWithExactly(loggerService.info, INIT_STATE_MIRROR_PROP_SET, InitState.name);
+            testSandbox.assert.called(fsReadFileSync);
+            testSandbox.assert.called(fsWriteFileSync);
+            testSandbox.assert.called(ymlLoad);
+            testSandbox.assert.called(ymlDump);
+
+            configureMirrorNodePropertiesStub = testSandbox.stub(initState as any, 'configureMirrorNodeProperties')
+        })
+    })
+});
diff --git a/test/unit/states/networkPrepState.spec.ts b/test/unit/states/networkPrepState.spec.ts
new file mode 100644
index 0000000..13ee232
--- /dev/null
+++ b/test/unit/states/networkPrepState.spec.ts
@@ -0,0 +1,222 @@
+/*-
+ *
+ * Hedera Local Node
+ *
+ * Copyright (C) 2024 Hedera Hashgraph, LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+import { expect } from 'chai';
+import { SinonSandbox, SinonSpy, SinonStub, SinonStubbedInstance } from 'sinon';
+import { LoggerService } from '../../../src/services/LoggerService';
+import { NetworkPrepState } from '../../../src/state/NetworkPrepState';
+import { getTestBed } from '../testBed';
+import {
+    NETWORK_PREP_STATE_IMPORT_FEES_END,
+    NETWORK_PREP_STATE_INIT_MESSAGE,
+    NETWORK_PREP_STATE_STARTING_MESSAGE,
+    NETWORK_PREP_STATE_TOPIC_CREATED,
+    NETWORK_PREP_STATE_WAITING_TOPIC_CREATION,
+} from '../../../src/constants';
+import { DockerService } from '../../../src/services/DockerService';
+import { EventType } from '../../../src/types/EventType';
+import { ClientService } from '../../../src/services/ClientService';
+
+describe('NetworkPrepState tests', () => {
+    let networkPrepState: NetworkPrepState,
+        testSandbox: SinonSandbox, 
+        loggerService: SinonStubbedInstance<LoggerService>,
+        serviceLocator: SinonStub,
+        dockerService: SinonStubbedInstance<DockerService>,
+        observerSpy: SinonSpy,
+        shellTestBed: {[key: string]: SinonStub};
+
+    before(() => {
+        const { 
+            sandbox,
+            loggerServiceStub,
+            serviceLocatorStub,
+            dockerServiceStub,
+            shellStubs
+        } = getTestBed({
+            workDir: 'testDir',
+        });
+    
+        testSandbox = sandbox
+        dockerService = dockerServiceStub
+        loggerService = loggerServiceStub
+        serviceLocator = serviceLocatorStub
+        shellTestBed = shellStubs
+
+        networkPrepState = new NetworkPrepState();
+        observerSpy = testSandbox.spy();
+        const observer = {
+            update: observerSpy
+        }
+        networkPrepState.subscribe(observer);
+    });
+
+    afterEach(() => {
+        testSandbox.resetHistory();
+        observerSpy.resetHistory();
+        dockerService.tryDockerRecovery.reset()
+    });
+
+    it('should initialize the Network Prep State', async () => {
+        expect(networkPrepState).to.be.instanceOf(NetworkPrepState);
+        testSandbox.assert.calledWith(serviceLocator, LoggerService.name);
+        testSandbox.assert.calledWith(serviceLocator, ClientService.name);
+        testSandbox.assert.calledOnce(loggerService.trace);
+        testSandbox.assert.calledWith(loggerService.trace, NETWORK_PREP_STATE_INIT_MESSAGE, NetworkPrepState.name);
+    })
+
+    it('should have a subscribe method', async () => {
+        expect(networkPrepState.subscribe).to.be.a('function');
+    })
+
+    it('should have a onStart method', async () => {
+        expect(networkPrepState.onStart).to.be.a('function');
+    })
+    
+    describe('onStart', () => {
+        let importStub: SinonStub, topicStub: SinonStub;
+
+        before(async () => {
+            importStub = testSandbox.stub(networkPrepState as any, 'importFees').resolves();
+            topicStub = testSandbox.stub(networkPrepState as any, 'waitForTopicCreation').resolves();
+        })
+
+        after(() => {
+            importStub.restore();
+            topicStub.restore();
+        })
+
+        beforeEach(async () => {
+            await networkPrepState.onStart();
+        })
+
+        it('should execute and log into LoggerService', async () => {
+            testSandbox.assert.calledOnce(loggerService.info);
+            testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_STARTING_MESSAGE, NetworkPrepState.name);
+        })
+
+        it('should execute and fire an event into observer', async () => {
+            testSandbox.assert.calledWith(observerSpy, EventType.Finish);
+        })
+
+        it('should execute and call "importFees" function', async () => {
+            testSandbox.assert.calledOnce(importStub);
+        })
+
+        it('should execute and call "importFees" function', async () => {
+            testSandbox.assert.calledOnce(importStub);
+        })
+    })
+
+    describe('importFees', () => {
+        let topicStub: SinonStub,
+            buildQueryFeesStub: SinonStub;
+
+        before(async () => {
+            class MockFileContentsQuery {
+                setFileId() {
+                    return this
+                }
+                execute() {
+                    return Promise.resolve('test')
+                }
+            }
+            topicStub = testSandbox.stub(networkPrepState as any, 'waitForTopicCreation').resolves();
+            buildQueryFeesStub = testSandbox.stub(networkPrepState as any, 'buildQueryFees').returns(new MockFileContentsQuery);
+        })
+
+        after(() => {
+            topicStub.restore();
+        })
+
+        beforeEach(async () => {
+            await networkPrepState.onStart();
+        })
+
+        it('should execute and log into LoggerService', async () => {
+            testSandbox.assert.calledTwice(loggerService.info);
+            testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_STARTING_MESSAGE, NetworkPrepState.name);
+            testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_IMPORT_FEES_END, NetworkPrepState.name);
+        })
+
+        it('should execute and call shell.exec', async () => {
+            const { shellExecStub } = shellTestBed;
+            testSandbox.assert.calledTwice(shellExecStub);
+        })
+
+        it('should execute and call "buildQueryFees" function', async () => {
+            testSandbox.assert.calledTwice(buildQueryFeesStub);
+        })
+
+        it('should execute and call "getNullOutput" function', async () => {
+            testSandbox.assert.calledOnce(dockerService.getNullOutput);
+        })
+
+    })
+
+    describe('waitForTopicCreation', () => {
+        let importStub: SinonStub,
+            shellTestBedExec: SinonStub,
+            destroyFake: SinonSpy<any[], any>,
+            killFake: SinonSpy<any[], any>;
+
+        before(async () => {
+            importStub = testSandbox.stub(networkPrepState as any, 'importFees').resolves();
+            const { shellExecStub } = shellTestBed;
+            shellTestBedExec = shellExecStub;
+            destroyFake = testSandbox.fake();
+            killFake = testSandbox.fake();
+            shellTestBedExec.returns({
+                stdout: {
+                    on: testSandbox.fake.yields(
+                        "Created TOPIC entity: 0.0.111",
+                    ),
+                    destroy: destroyFake
+                },
+                kill: killFake
+            })
+        })
+
+        after(() => {
+            importStub.restore();
+        })
+
+        beforeEach(async () => {
+            await networkPrepState.onStart();
+        })
+
+        it('should execute and log into LoggerService', async () => {
+            testSandbox.assert.calledTwice(loggerService.info);
+            testSandbox.assert.calledOnce(loggerService.trace);
+            testSandbox.assert.calledWith(loggerService.trace, NETWORK_PREP_STATE_WAITING_TOPIC_CREATION, NetworkPrepState.name);
+            testSandbox.assert.calledWith(loggerService.info, NETWORK_PREP_STATE_TOPIC_CREATED, NetworkPrepState.name);
+        })
+
+        it('should execute and call shell.exec', async () => {
+            testSandbox.assert.calledOnceWithExactly(
+                shellTestBedExec,
+                'docker logs mirror-node-monitor -f',
+                { silent: true, async: true }
+            );
+            testSandbox.assert.calledOnce(destroyFake);
+            testSandbox.assert.calledOnce(killFake);
+        })
+    })
+});
diff --git a/test/unit/states/startState.spec.ts b/test/unit/states/startState.spec.ts
index b927600..19094ec 100644
--- a/test/unit/states/startState.spec.ts
+++ b/test/unit/states/startState.spec.ts
@@ -81,7 +81,7 @@ describe('StartState tests', () => {
     afterEach(() => {
         testSandbox.resetHistory();
         observerSpy.resetHistory();
-        dockerService.dockerComposeUp.reset()
+        dockerService.dockerComposeUp.reset();
     });
 
     it('should initialize the Start State', async () => {
diff --git a/test/unit/testBed.ts b/test/unit/testBed.ts
index 1b2e535..4537816 100644
--- a/test/unit/testBed.ts
+++ b/test/unit/testBed.ts
@@ -1,4 +1,3 @@
-
 /*-
  *
  * Hedera Local Node
@@ -25,6 +24,7 @@ import { CLIService } from "../../src/services/CLIService";
 import { ServiceLocator } from "../../src/services/ServiceLocator";
 import { DockerService } from "../../src/services/DockerService";
 import { ConnectionService } from "../../src/services/ConnectionService";
+import { ClientService } from "../../src/services/ClientService";
 
 export interface LocalNodeTestBed {
     sandbox: sinon.SinonSandbox;
@@ -32,6 +32,7 @@ export interface LocalNodeTestBed {
     cliServiceStub: sinon.SinonStubbedInstance<CLIService>;
     dockerServiceStub: sinon.SinonStubbedInstance<DockerService>;
     connectionServiceStub: sinon.SinonStubbedInstance<ConnectionService>;
+    clientServiceStub: sinon.SinonStubbedInstance<ClientService>;
     serviceLocatorStub: sinon.SinonStub;
     proccesStubs: {
         processCWDStub: sinon.SinonStub;
@@ -72,6 +73,7 @@ function generateLocalNodeStubs(sandbox:sinon.SinonSandbox, cliServiceArgs?: any
     const dockerServiceStub = sandbox.createStubInstance(DockerService);
     const connectionServiceStub = sandbox.createStubInstance(ConnectionService);
     const loggerServiceStub = sandbox.createStubInstance(LoggerService);
+    const clientServiceStub = sandbox.createStubInstance(ClientService);
     const cliServiceStub = sandbox.createStubInstance(CLIService, {
       getCurrentArgv: {
         ...cliServiceArgs,
@@ -83,12 +85,14 @@ function generateLocalNodeStubs(sandbox:sinon.SinonSandbox, cliServiceArgs?: any
     getStub.withArgs(CLIService.name).returns(cliServiceStub);
     getStub.withArgs(DockerService.name).returns(dockerServiceStub);
     getStub.withArgs(ConnectionService.name).returns(connectionServiceStub);
+    getStub.withArgs(ClientService.name).returns(clientServiceStub);
 
     sandbox.replaceGetter(ServiceLocator, 'Current', () => sandbox.createStubInstance(ServiceLocator, {
         get: getStub
     }));
 
     return {
+        clientServiceStub,
         connectionServiceStub,
         dockerServiceStub,
         loggerServiceStub,