Repository: hashgraph/hedera-local-node
Pull request #518 (2024-01-26 09:22:09)

feat: Create unit test for NetworkPrepState #515
**Description**:
Create unit test for NetworkPrepState

**Related issue(s)**:
[#515](https://github.com/hashgraph/hedera-local-node/issues/515)


sonarcloud[bot]: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=518) **Quality Gate failed**  
Failed conditions

[3.4% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=518&metric=new_duplicated_lines_density&view=list) (required ≤ 3%)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=518)


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=518) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[2 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=518&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=518&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=518&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=518)


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=518) **Quality Gate passed**  
Kudos, no new issues were introduced!

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


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=518) **Quality Gate passed**  
The SonarCloud Quality Gate passed, but some issues were introduced.

[2 New issues](https://sonarcloud.io/project/issues?id=hashgraph_hedera-local-node&pullRequest=518&resolved=false&inNewCodePeriod=true)  
[0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hashgraph_hedera-local-node&pullRequest=518&resolved=false&inNewCodePeriod=true)  
No data about Coverage  
[0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hashgraph_hedera-local-node&pullRequest=518&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarCloud](https://sonarcloud.io/dashboard?id=hashgraph_hedera-local-node&pullRequest=518)


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=518) **Quality Gate passed**  
Kudos, no new issues were introduced!

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



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/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..c4ecfc1 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -88,4 +88,12 @@ 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!';
\ No newline at end of file
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..b70ab94 100644
--- a/src/state/InitState.ts
+++ b/src/state/InitState.ts
@@ -159,8 +159,18 @@ 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 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;
+            }
+
+            this.logger.trace(`Environment variable ${node} will be set to ${tag}.`, this.stateName);
+            process.env[node] = tag;
         });
 
         if (!envConfiguration) {
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/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,