Repository: hashgraph/hedera-local-node (ts)

Issue:
Add CLI flag for 'persistTransactionBytes'
### Problem

With the new changes from https://hips.hedera.com/hip/hip-801, we need to enable the mirror-node to persist the `transactionBytes` and `transactionRecordBytes` in order to be able to use the new `/opcodes` endpoint.

### Solution

- Add a new CLI flag `--persistTransactionBytes` which will set these two properties to `true` for the mirror-node:
  - `hedera.mirror.importer.parser.record.entity.persist.transactionBytes`
  - `hedera.mirror.importer.parser.record.entity.persist.transactionRecordBytes`

### Alternatives

_No response_

Pull request description:
feat: Add CLI option for persistTransactionBytes
**Description**:

This PR:
- adds a new CLI flag `--persistTransactionBytes` which will set these two properties to `true` for the mirror-node:
  - `hedera.mirror.importer.parser.record.entity.persist.transactionBytes`
  - `hedera.mirror.importer.parser.record.entity.persist.transactionRecordBytes`

**Related issue(s)**:

Fixes #619 

**Checklist**

- [ ] Documented (Code comments, README, etc.)
- [ ] Tested (unit, integration, etc.)


Patch:
diff --git a/.env b/.env
index dd92e96..c69cb10 100644
--- a/.env
+++ b/.env
@@ -4,8 +4,8 @@ NETWORK_NODE_IMAGE_PREFIX=gcr.io/hedera-registry/
 NETWORK_NODE_IMAGE_NAME=main-network-node
 
 #### Image Tags/Hashes ####
-NETWORK_NODE_IMAGE_TAG=0.49.6
-HAVEGED_IMAGE_TAG=0.49.6
+NETWORK_NODE_IMAGE_TAG=0.49.7
+HAVEGED_IMAGE_TAG=0.49.7
 
 #### Java Process Settings ####
 PLATFORM_JAVA_HEAP_MIN=256m
@@ -29,7 +29,7 @@ PYTHON_VERSION=python3.7
 
 #### MirrorNode Prefixes & Tags ####
 MIRROR_IMAGE_PREFIX=gcr.io/mirrornode/
-MIRROR_IMAGE_TAG=0.103.0
+MIRROR_IMAGE_TAG=0.104.0
 
 #### MirrorNode settings ####
 MIRROR_POSTGRES_IMAGE=postgres:14-alpine
@@ -45,7 +45,7 @@ MINIO_ROOT_PASSWORD=minioadmin
 
 #### JSON RPC Relay Prefixes & Tags ####
 RELAY_IMAGE_PREFIX=ghcr.io/hashgraph/
-RELAY_IMAGE_TAG=0.46.1
+RELAY_IMAGE_TAG=0.47.0
 
 #### JSON RPC Relay limits ####
 RELAY_MEM_LIMIT=768m
diff --git a/compose-network/mirror-node/application.yml b/compose-network/mirror-node/application.yml
index b6ee418..e396c71 100644
--- a/compose-network/mirror-node/application.yml
+++ b/compose-network/mirror-node/application.yml
@@ -8,6 +8,9 @@ hedera:
       parser:
         record:
           entity:
+            persist:
+              transactionBytes: false
+              transactionRecordBytes: false
             redis:
               enabled: false
           sidecar:
diff --git a/package-lock.json b/package-lock.json
index 49f9ea9..35f7a2d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@hashgraph/hedera-local",
-  "version": "2.25.0",
+  "version": "2.25.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
diff --git a/package.json b/package.json
index 1b42c26..5a58193 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@hashgraph/hedera-local",
-  "version": "2.25.0",
+  "version": "2.25.1",
   "description": "Developer tooling for running Local Hedera Network (Consensus + Mirror Nodes).",
   "main": "index.ts",
   "scripts": {
diff --git a/src/configuration/local.json b/src/configuration/local.json
index 24792e9..dfb981e 100644
--- a/src/configuration/local.json
+++ b/src/configuration/local.json
@@ -1,8 +1,8 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.6"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.6"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.103.0"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.104.0"},
     {"key": "RELAY_IMAGE_TAG", "value": "0.47.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "24.4.0"}
   ],
diff --git a/src/configuration/mainnet.json b/src/configuration/mainnet.json
index 033b56f..61b9fb4 100644
--- a/src/configuration/mainnet.json
+++ b/src/configuration/mainnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.48.1"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.48.1"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.102.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.46.1"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.104.0"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.47.0"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "24.4.0"}
   ],
   "nodeConfiguration": {
diff --git a/src/configuration/previewnet.json b/src/configuration/previewnet.json
index 3bf3588..ad1a2ff 100644
--- a/src/configuration/previewnet.json
+++ b/src/configuration/previewnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.5"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.5"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.103.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.46.1"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.50.0-alpha.1"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.50.0-alpha.1"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.105.0-rc2"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.48.0-rc2"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "24.4.0"}
   ],
   "nodeConfiguration": {
diff --git a/src/services/CLIService.ts b/src/services/CLIService.ts
index 5125032..0f457fa 100644
--- a/src/services/CLIService.ts
+++ b/src/services/CLIService.ts
@@ -161,6 +161,7 @@ export class CLIService implements IService{
         const relayTag = argv.relayTag as string;
         const workDir = FileSystemUtils.parseWorkDir(argv.workdir as string);
         const createInitialResources = argv.createInitialResources as boolean;
+        const persistTransactionBytes = argv.persistTransactionBytes as boolean;
         const currentArgv: CLIOptions = {
             accounts,
             async,
@@ -183,7 +184,8 @@ export class CLIService implements IService{
             mirrorTag,
             relayTag,
             workDir,
-            createInitialResources
+            createInitialResources,
+            persistTransactionBytes,
         };
 
         return currentArgv;
diff --git a/src/state/InitState.ts b/src/state/InitState.ts
index 0c4e887..570548c 100644
--- a/src/state/InitState.ts
+++ b/src/state/InitState.ts
@@ -43,8 +43,8 @@ import {
     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,
+    INIT_STATE_STARTING_MESSAGE,
     NECESSARY_PORTS,
     NETWORK_NODE_CONFIG_DIR_PATH,
     OPTIONAL_PORTS,
@@ -263,27 +263,30 @@ export class InitState implements IState{
     // TODO: finish off multi node
     private configureMirrorNodeProperties(): void {
         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 { enableDebug, fullMode, multiNode, persistTransactionBytes, workDir } = this.cliOptions;
 
-        const propertiesFilePath = join(this.cliOptions.workDir, 'compose-network/mirror-node/application.yml');
+        const propertiesFilePath = join(workDir, 'compose-network/mirror-node/application.yml');
         const application = yaml.load(readFileSync(propertiesFilePath).toString()) as any;
 
-        if (turboMode) {
+        if (!fullMode) {
             application.hedera.mirror.importer.dataPath = originalNodeConfiguration.turboNodeProperties.dataPath;
             application.hedera.mirror.importer.downloader.sources = originalNodeConfiguration.turboNodeProperties.sources;
         }
 
-        if (debugMode) {
-            application.hedera.mirror.importer.downloader.local = originalNodeConfiguration.local
+        if (enableDebug) {
+            application.hedera.mirror.importer.downloader.local = originalNodeConfiguration.local;
         }
 
         if (multiNode) {
-            application['hedera']['mirror']['monitor']['nodes'] = originalNodeConfiguration.multiNodeProperties;
+            application.hedera.mirror.monitor.nodes = originalNodeConfiguration.multiNodeProperties;
             process.env.RELAY_HEDERA_NETWORK = '{"network-node:50211":"0.0.3","network-node-1:50211":"0.0.4","network-node-2:50211":"0.0.5","network-node-3:50211":"0.0.6"}';
         }
 
+        if (persistTransactionBytes) {
+            application.hedera.mirror.importer.parser.record.entity.persist.transactionBytes = true;
+            application.hedera.mirror.importer.parser.record.entity.persist.transactionRecordBytes = true;
+        }
+
         writeFileSync(propertiesFilePath, yaml.dump(application, { lineWidth: 256 }));
         this.logger.info(INIT_STATE_MIRROR_PROP_SET, this.stateName);
     }
diff --git a/src/types/CLIOptions.ts b/src/types/CLIOptions.ts
index ee379c0..8f4857d 100644
--- a/src/types/CLIOptions.ts
+++ b/src/types/CLIOptions.ts
@@ -47,6 +47,7 @@ import { NetworkType } from './NetworkType';
  * @property {string} relayTag - The tag for the relay.
  * @property {string} workDir - The working directory.
  * @property {boolean} createInitialResources - Whether to create initial resources.
+ * @property {boolean} persistTransactionBytes - Whether to persist transaction bytes.
  */
 export interface CLIOptions {
     accounts: number,
@@ -71,4 +72,5 @@ export interface CLIOptions {
     relayTag: string,
     workDir: string,
     createInitialResources: boolean,
+    persistTransactionBytes: boolean,
 }


Test patch:
diff --git a/src/configuration/testnet.json b/src/configuration/testnet.json
index 3bf3588..cad1a84 100644
--- a/src/configuration/testnet.json
+++ b/src/configuration/testnet.json
@@ -1,9 +1,9 @@
 {
   "imageTagConfiguration": [
-    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.5"},
-    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.5"},
-    {"key": "MIRROR_IMAGE_TAG", "value": "0.103.0"},
-    {"key": "RELAY_IMAGE_TAG", "value": "0.46.1"},
+    {"key": "NETWORK_NODE_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "HAVEGED_IMAGE_TAG", "value": "0.49.7"},
+    {"key": "MIRROR_IMAGE_TAG", "value": "0.104.0"},
+    {"key": "RELAY_IMAGE_TAG", "value": "0.48.0-rc2"},
     {"key": "MIRROR_NODE_EXPLORER_IMAGE_TAG", "value": "24.4.0"}
   ],
   "nodeConfiguration": {
diff --git a/test/unit/states/InitState.spec.ts b/test/unit/states/InitState.spec.ts
index 481711e..89b87de 100644
--- a/test/unit/states/InitState.spec.ts
+++ b/test/unit/states/InitState.spec.ts
@@ -24,20 +24,20 @@ import yaml from 'js-yaml';
 import { join } from 'path';
 import { SinonSandbox, SinonSpy, SinonStub, SinonStubbedInstance } from 'sinon';
 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,
+  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_START_DOCKER_CHECK,
+  INIT_STATE_STARTING_MESSAGE,
+  NECESSARY_PORTS,
+  NETWORK_NODE_CONFIG_DIR_PATH,
+  OPTIONAL_PORTS,
+  RECORD_PARSER_SOURCE_REL_PATH
 } from '../../../src/constants';
 import { ConfigurationData } from '../../../src/data/ConfigurationData';
 import { CLIService } from '../../../src/services/CLIService';
@@ -50,9 +50,10 @@ import { getTestBed } from '../testBed';
 
 describe('InitState tests', () => {
     let initState: InitState,
-        testSandbox: SinonSandbox, 
+        testSandbox: SinonSandbox,
         loggerService: SinonStubbedInstance<LoggerService>,
         serviceLocator: SinonStub,
+        cliService: SinonStubbedInstance<CLIService>,
         dockerService: SinonStubbedInstance<DockerService>,
         observerSpy: SinonSpy,
         configurationData: SinonStub,
@@ -88,11 +89,12 @@ describe('InitState tests', () => {
     }
 
     before(() => {
-        const { 
+        const {
             sandbox,
             loggerServiceStub,
             serviceLocatorStub,
             dockerServiceStub,
+            cliServiceStub
         } = getTestBed({
             workDir: 'testDir',
             fullMode: true,
@@ -102,9 +104,10 @@ describe('InitState tests', () => {
             mirrorTag: 'test-mirror-tag',
             relayTag: 'test-relay-tag',
         });
-    
+
         testSandbox = sandbox
         dockerService = dockerServiceStub
+        cliService = cliServiceStub
         loggerService = loggerServiceStub
         serviceLocator = serviceLocatorStub
 
@@ -152,7 +155,7 @@ describe('InitState tests', () => {
         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);
@@ -176,7 +179,7 @@ describe('InitState tests', () => {
         testSandbox.assert.calledOnce(dockerService.checkDockerResources);
 
         testSandbox.assert.calledWith(observerSpy, EventType.UnresolvableError);
-        
+
         testSandbox.assert.notCalled(dockerService.isPortInUse);
         testSandbox.assert.notCalled(loggerService.initializeTerminalUI);
     })
@@ -213,6 +216,16 @@ describe('InitState tests', () => {
                             },
                             downloader: {
                                 local: {}
+                            },
+                            parser: {
+                                record: {
+                                    entity: {
+                                        persist: {
+                                            transactionBytes: false,
+                                            transactionRecordBytes: false
+                                        }
+                                    }
+                                }
                             }
                         },
                         monitor: {
@@ -227,7 +240,7 @@ describe('InitState tests', () => {
         afterEach(() => {
             onStartStub.restore()
         })
-        
+
         it('should execute "prepareWorkDirectory" as expected', async () => {
             prepareWorkDirectoryStub.restore()
 
@@ -237,7 +250,7 @@ describe('InitState tests', () => {
             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')
         })
 
@@ -346,5 +359,52 @@ describe('InitState tests', () => {
 
             configureMirrorNodePropertiesStub = testSandbox.stub(initState as any, 'configureMirrorNodeProperties')
         })
+
+        describe('when persistTransactionBytes is set to true', () => {
+            let cliOptionsStub: SinonStub;
+
+            const expectedMirrorProperties = {
+                hedera: {
+                    mirror: {
+                        importer : {
+                            parser: {
+                                record: {
+                                    entity: {
+                                        persist: {
+                                            transactionBytes: true,
+                                            transactionRecordBytes: true
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            };
+
+            before(() => {
+                cliOptionsStub = testSandbox.stub((initState  as any), 'cliOptions').value({
+                    ...cliService.getCurrentArgv(),
+                    persistTransactionBytes: true
+                });
+                configureMirrorNodePropertiesStub.restore();
+                onStartStub = testSandbox.stub(initState, 'onStart')
+                  .callsFake(() => (initState  as any).configureMirrorNodeProperties());
+            })
+
+            after(() => {
+                cliOptionsStub.restore();
+                configureMirrorNodePropertiesStub = testSandbox.stub(initState as any, 'configureMirrorNodeProperties');
+            })
+
+            it('should set the persist properties for transactionBytes and transactionRecordBytes to true', async () => {
+                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.calledWithMatch(ymlDump, expectedMirrorProperties);
+            })
+        })
     })
 });