Skip to content

feat: Added subscribeTopic for TopicClient#147

Open
manishdait wants to merge 13 commits into
hiero-ledger:mainfrom
manishdait:feat/subscribe-topic-impl
Open

feat: Added subscribeTopic for TopicClient#147
manishdait wants to merge 13 commits into
hiero-ledger:mainfrom
manishdait:feat/subscribe-topic-impl

Conversation

@manishdait

@manishdait manishdait commented May 7, 2026

Copy link
Copy Markdown
Contributor

Description:

This PR introduces subscribeTopic() method to the TopicClient interface, which help to subscribe and listen to the messages received by the specific Hedera Topic.

Key Changes:

  • Added a separate handling for mirror node grpc and rest endpoint by adding getMirrornodeRestUrl() and getMirrornodeGrpcs() to NetworkSettings to store the HCS gRPC endpoint to set the mirrorNodeAddress in Client because TopicMessageQuery uses the gRPC protocol.

Method Added:

  • subscribeTopic(topicId, consumer)
  • subscribeTopic(topicId, consumer, limit)
  • subscribeTopic(topicId, consumer, startTime, endTime)
  • subscribeTopic(topicId, consumer, limit, startTime, endTime)

Related issue(s):

Fixes #68

Notes for reviewer:
The current implemetation will break the backward compatiblity with as the env names has been changed.

Checklist

  • Documented (Code comments, README, etc.)
  • Tested (unit, integration, etc.)

@manishdait manishdait force-pushed the feat/subscribe-topic-impl branch from d48e9b7 to f387a73 Compare May 14, 2026 15:36
@manishdait manishdait marked this pull request as ready for review May 15, 2026 12:40
@aceppaluni aceppaluni added the status: ready-for-review PR is ready to be reviewed by team member label May 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a subscribeTopic() API to TopicClient (backed by Hedera TopicMessageQuery.subscribe) and updates configuration to distinguish mirror-node gRPC addresses (for HCS subscriptions) from the mirror-node REST base URL (for Mirror Node REST queries) across base, Spring, and MicroProfile modules.

Changes:

  • Introduces TopicClient.subscribeTopic(...) overloads plus protocol-layer request/response types to support topic message subscriptions.
  • Splits mirror-node configuration into gRPC addresses vs REST URL (getMirrorNodeGrpcAddresses() + getMirrorNodeRestUrl()), updating config implementations and auto-configuration wiring.
  • Adds/extends unit and integration tests for topic subscription behavior in base, Spring, and MicroProfile modules.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java Updates test network settings to provide mirror-node gRPC address + REST URL separately.
hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java Comment formatting adjustment in tests (no functional change).
hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java Adds Spring integration tests covering subscribeTopic() overloads.
hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroNetworkProperties.java Reworks Spring properties to model mirror node as nested REST/grpc config.
hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroConfigImpl.java Maps Spring properties/network settings to new mirror-node REST + gRPC fields.
hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroAutoConfiguration.java Switches mirror-node client creation to use REST URL from HieroConfig.
hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java Adds MicroProfile integration tests for subscribeTopic().
hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/HieroConfigImplTest.java Updates tests for new mirror-node config shape.
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java Updates MicroProfile config impl to use mirror-node REST URL + gRPC addresses.
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java Introduces nested mirror-node config parsing for MicroProfile.
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java Updates mirror-node client producer to read REST URL from HieroConfig.
hiero-enterprise-base/src/test/java/org/hiero/base/test/TopicClientImplTest.java Adds unit tests validating subscription request construction and validation.
hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java Updates base test settings for separated mirror-node REST/grpc config.
hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java Uses mirror-node gRPC addresses when configuring SDK Client.setMirrorNetwork(...).
hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java Adds subscribeTopic() overloads and refines several javadocs.
hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageResult.java Returns SubscriptionHandle from topic message query execution.
hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java Adds factory overloads to include limit and optional time range.
hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java Implements subscribeTopic() and validates limit/time range parameters.
hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java Executes Hedera TopicMessageQuery.subscribe() and returns the handle.
hiero-enterprise-base/src/main/java/org/hiero/base/config/NetworkSettings.java Renames mirror-node address method to gRPC + adds REST URL accessor.
hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/NetworkSettingsBasedHieroConfig.java Adapts config wrapper to new mirror-node REST/gRPC accessors.
hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java Splits mirror-node env config into gRPC address + REST URL env vars.
hiero-enterprise-base/src/main/java/org/hiero/base/config/HieroConfig.java Updates client creation to use mirror-node gRPC addresses and exposes REST URL.
hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaTestnetSettings.java Provides Hedera testnet mirror-node gRPC address + REST URL separately.
hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaMainnetSettings.java Provides Hedera mainnet mirror-node gRPC address + REST URL separately.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Objects.requireNonNull(handler, "handler must not be null");

if (limit < -1) {
throw new IllegalArgumentException("limit must be -1 (infinite) or greater than 0");
*/
default SubscriptionHandle subscribeTopic(
@NonNull String topicId, @NonNull Consumer<TopicMessage> handler, long limit)
throws HieroException {
Comment on lines +352 to +354
* @param topicId the topicId of topic
* @param handler the handler to call when a message is receive
* @return SubscriptionHandle for the Topic
.findFirst()
hieroConfig
.getMirrorNodeRestUrl()
.orElseThrow(() -> new IllegalStateException("No mirror node addresses configured"));
Comment on lines +42 to +43
mirrorNodeGrpcAddress = getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").orElse(null);
mirrorNodeRestUrl = getEnv("HEDERA_MIRROR_NODE_REST_URL").orElse(null);
Comment on lines +62 to +66
@ConfigProperty(name = "hiero.network.mirrornode.rest-url")
Optional<String> restUrl = Optional.empty();

@ConfigProperty(name = "hiero.network.mirrornode.grpc-addresses")
public Optional<String> grpcAddresses = Optional.empty();
Comment on lines +218 to +222
topicClient.subscribeTopic(
topicId,
(message) -> {
messages.add(new String(message.contents));
});
Comment on lines +224 to +228
topicClient.submitMessage(topicId, msg);
hieroTestUtils.waitForMirrorNodeRecords();
Thread.sleep(5000); // Make sure to wait after message get recorded in mirrornode

Assertions.assertNotNull(handler);
Comment on lines +46 to +51
final SubscriptionHandle handler =
topicClient.subscribeTopic(
topicId,
(message) -> {
messages.add(new String(message.contents));
});
Comment on lines +53 to +57
topicClient.submitMessage(topicId, msg);
hieroTestUtils.waitForMirrorNodeRecords();
Thread.sleep(5000); // Make sure to wait after message get recorded in mirrornode

Assertions.assertNotNull(handler);

topicClient.submitMessage(topicId, msg);
hieroTestUtils.waitForMirrorNodeRecords();
Thread.sleep(5000); // Make sure to wait after message get recorded in mirrornode

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the

Thread.sleep(5000);

could/should this be updated to either a polling with timeout or latch/countdown mechanism to avoid flakiness?

Something like

await().atMost(10, SECONDS)
       .until(() -> messages.size() == 1);

Assertions.assertNotNull(handler);
Assertions.assertEquals(1, messages.size());
Assertions.assertEquals(msg, messages.getFirst());
handler.unsubscribe();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these be verified?

@aceppaluni aceppaluni added the enhancement New feature or request label May 28, 2026
@Ndacyayisenga-droid

Copy link
Copy Markdown
Member

@manishdait could you resolve the conflicts!

@manishdait manishdait force-pushed the feat/subscribe-topic-impl branch from f387a73 to 933afda Compare May 29, 2026 10:08
@manishdait manishdait requested review from a team as code owners May 29, 2026 10:08
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown

Review Change Stack

Note

.coderabbit.yml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key: "path_instructions"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

This PR adds subscribeTopic() to TopicClient and updates mirror-node configuration to expose separate gRPC and REST endpoints across base, MicroProfile, Spring, and test code.

Changes

Topic Subscription and Mirror Node Configuration

Layer / File(s) Summary
Topic Subscription Interface Contract
hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java
TopicClient adds subscription overloads and corrects @throws Javadoc text for update, delete, and submit message methods.
Topic Subscription Implementation and Validation
hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java
TopicClientImpl implements subscription overloads, validates limit and time ordering, builds TopicMessageRequest, and returns the subscription handle.
Protocol Layer Subscription Support
hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java, hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageResult.java, hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java
TopicMessageRequest gains subscription factories, TopicMessageResult now carries SubscriptionHandle, and ProtocolLayerClientImpl preserves the returned handle.
Base Module Subscription Tests
hiero-enterprise-base/src/test/java/org/hiero/base/test/TopicClientImplTest.java
TopicClientImplTest adds unit coverage for subscription overloads, captured request fields, and invalid limit and time-window validation.
Mirror Node Configuration API Contracts
hiero-enterprise-base/src/main/java/org/hiero/base/config/NetworkSettings.java, hiero-enterprise-base/src/main/java/org/hiero/base/config/HieroConfig.java
NetworkSettings and HieroConfig replace the single mirror-node address accessor with separate gRPC and REST accessors, and HieroConfig#createClient() uses the gRPC set.
Hedera Preset Network Configurations
hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaMainnetSettings.java, hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaTestnetSettings.java
HederaMainnetSettings and HederaTestnetSettings expose mirror-node gRPC addresses and REST URLs through the split API.
Base Module Configuration Implementations
hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java, hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/NetworkSettingsBasedHieroConfig.java, hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java, hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java
EnvBasedHieroConfig, NetworkSettingsBasedHieroConfig, HieroTestContext, and SoloActionNetworkSettings switch to gRPC and REST mirror-node accessors.
MicroProfile Configuration Property Binding
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java, hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java
HieroNetworkConfiguration now binds a nested mirror-node object, and MicroProfile HieroConfigImpl stores separate gRPC and REST values while deriving them from either NetworkSettings or configuration.
Spring Configuration Property Binding
hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroNetworkProperties.java, hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroConfigImpl.java
HieroNetworkProperties binds nested mirror-node properties, and Spring HieroConfigImpl exposes separate gRPC and REST mirror-node values.
Client Providers Updated for Mirror Node Configuration
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java, hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroAutoConfiguration.java
ClientProvider reads the mirror-node REST URL directly from configuration, and HieroAutoConfiguration validates the REST endpoint before building the mirror-node client.
MicroProfile Integration Subscription Tests
hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java
TopicClientTest adds MicroProfile integration coverage for subscription callbacks, limits, time windows, and validation failures.
Spring Integration Subscription Tests and Cleanup
hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java, hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java
TopicClientTest adds Spring integration coverage for the same subscription flows, and TopicRepositoryTest removes a debug print.
Test Utilities Updated for Mirror Node API
hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java
SoloActionNetworkSettings switches to the split mirror-node accessors.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The code implements #68 by adding TopicClient subscribeTopic APIs, implementations, and tests.
Out of Scope Changes check ✅ Passed The mirror-node config updates support the new subscription flow and are not clearly unrelated.
Title check ✅ Passed The title accurately names the main change: adding subscribeTopic to TopicClient.
Description check ✅ Passed The description is on-topic and summarizes the subscribeTopic and mirror-node config changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d27faa52-fe3f-4ef4-8a69-21b8ade3604e

📥 Commits

Reviewing files that changed from the base of the PR and between 91f336c and 933afda.

📒 Files selected for processing (25)
  • hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/HieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/NetworkSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaMainnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaTestnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/NetworkSettingsBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageResult.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/TopicClientImplTest.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/HieroConfigImplTest.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroAutoConfiguration.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroConfigImpl.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroNetworkProperties.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java
  • hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java

Comment on lines +42 to +43
mirrorNodeGrpcAddress = getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").orElse(null);
mirrorNodeRestUrl = getEnv("HEDERA_MIRROR_NODE_REST_URL").orElse(null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalize blank mirror-node env vars to “unset”.

If either env var is present but empty, this stores "" as a real endpoint. That later becomes Set.of("") / Optional.of("") and propagates an invalid mirror-node configuration instead of falling back to absent values.

Proposed fix
-    mirrorNodeGrpcAddress = getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").orElse(null);
-    mirrorNodeRestUrl = getEnv("HEDERA_MIRROR_NODE_REST_URL").orElse(null);
+    mirrorNodeGrpcAddress =
+        getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").filter(value -> !value.isBlank()).orElse(null);
+    mirrorNodeRestUrl =
+        getEnv("HEDERA_MIRROR_NODE_REST_URL").filter(value -> !value.isBlank()).orElse(null);

Comment on lines +270 to +272
if (limit < -1) {
throw new IllegalArgumentException("limit must be -1 (infinite) or greater than 0");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align the limit check with the documented boundary.

Line 270 accepts limit == 0, but Line 271 says the valid values are -1 or numbers greater than 0. The new tests also codify that message, so callers currently get contradictory behavior depending on whether they trust the predicate or the error text. Either reject 0 here or update the message/tests/Javadoc to >= 0.

Comment thread hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java
Comment on lines +71 to 75
if (!networkSettings.getMirrorNodeGrpcAddresses().isEmpty()) {
try {
client.setMirrorNetwork(networkSettings.getMirrorNodeAddresses().stream().toList());
client.setMirrorNetwork(networkSettings.getMirrorNodeGrpcAddresses().stream().toList());
} catch (InterruptedException e) {
throw new RuntimeException("Error in configuring Mirror Node", e);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C2 'InterruptedException|currentThread\(\)\.interrupt' \
  hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 326


Restore the interrupt flag before rethrowing (InterruptedException catch in HieroTestContext).

The catch (InterruptedException e) block rethrows RuntimeException without restoring the thread’s interrupted status.

Proposed fix
       try {
         client.setMirrorNetwork(networkSettings.getMirrorNodeGrpcAddresses().stream().toList());
       } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
         throw new RuntimeException("Error in configuring Mirror Node", e);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!networkSettings.getMirrorNodeGrpcAddresses().isEmpty()) {
try {
client.setMirrorNetwork(networkSettings.getMirrorNodeAddresses().stream().toList());
client.setMirrorNetwork(networkSettings.getMirrorNodeGrpcAddresses().stream().toList());
} catch (InterruptedException e) {
throw new RuntimeException("Error in configuring Mirror Node", e);
if (!networkSettings.getMirrorNodeGrpcAddresses().isEmpty()) {
try {
client.setMirrorNetwork(networkSettings.getMirrorNodeGrpcAddresses().stream().toList());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Error in configuring Mirror Node", e);
}
}

Comment on lines +767 to +788
@Test
void shouldThrowExceptionOnSubscribeTopicWithLimitLessThanNegOne() {
final String msg = "limit must be -1 (infinite) or greater than 0";
// given
final TopicId topicId = TopicId.fromString("1.2.3");
final Consumer<TopicMessage> subscription = (message) -> {};
final Instant startTime = Instant.now().plusSeconds(120);
final Instant endTime = startTime.plusSeconds(120);
final int limit = -2;

final IllegalArgumentException e1 =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> topicClient.subscribeTopic(topicId, subscription, limit));
final IllegalArgumentException e2 =
Assertions.assertThrows(
IllegalArgumentException.class,
() -> topicClient.subscribeTopic(topicId, subscription, startTime, endTime, limit));

Assertions.assertEquals(msg, e1.getMessage());
Assertions.assertEquals(msg, e2.getMessage());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add a regression test for limit == 0.

The new suite only covers -2, so it doesn't lock down the exact boundary where the implementation and the published error message currently diverge. A dedicated 0 case would prevent this contract from drifting again.

Comment on lines +213 to +230
final List<String> messages = new ArrayList<>();
final TopicId topicId = topicClient.createTopic();
hieroTestUtils.waitForMirrorNodeRecords();

final SubscriptionHandle handler =
topicClient.subscribeTopic(
topicId,
(message) -> {
messages.add(new String(message.contents));
});

topicClient.submitMessage(topicId, msg);
hieroTestUtils.waitForMirrorNodeRecords();
Thread.sleep(5000); // Make sure to wait after message get recorded in mirrornode

Assertions.assertNotNull(handler);
Assertions.assertEquals(1, messages.size());
Assertions.assertEquals(msg, messages.getFirst());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Synchronize the async callback before asserting.

These tests write messages from the subscription callback thread and read it from the test thread, but ArrayList plus Thread.sleep(5000) provides neither thread safety nor deterministic timing. That makes the assertions flaky and can mask missed deliveries. Use a thread-safe collection and a bounded await/latch instead.

Proposed pattern
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;

-    final List<String> messages = new ArrayList<>();
+    final List<String> messages = new CopyOnWriteArrayList<>();
+    final CountDownLatch delivered = new CountDownLatch(1);
     final TopicId topicId = topicClient.createTopic();
     hieroTestUtils.waitForMirrorNodeRecords();

     final SubscriptionHandle handler =
         topicClient.subscribeTopic(
             topicId,
             (message) -> {
               messages.add(new String(message.contents));
+              delivered.countDown();
             });

-    topicClient.submitMessage(topicId, msg);
-    hieroTestUtils.waitForMirrorNodeRecords();
-    Thread.sleep(5000); // Make sure to wait after message get recorded in mirrornode
-
-    Assertions.assertNotNull(handler);
-    Assertions.assertEquals(1, messages.size());
-    Assertions.assertEquals(msg, messages.getFirst());
-    handler.unsubscribe();
+    try {
+      topicClient.submitMessage(topicId, msg);
+      hieroTestUtils.waitForMirrorNodeRecords();
+
+      Assertions.assertTrue(delivered.await(10, TimeUnit.SECONDS));
+      Assertions.assertEquals(1, messages.size());
+      Assertions.assertEquals(msg, messages.get(0));
+    } finally {
+      handler.unsubscribe();
+    }

Also applies to: 239-260

@manishdait manishdait force-pushed the feat/subscribe-topic-impl branch from 933afda to dc63b48 Compare June 15, 2026 09:46

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 207adb6b-fcd5-4b2f-8de4-46e0d6a3737d

📥 Commits

Reviewing files that changed from the base of the PR and between 933afda and dc63b48.

📒 Files selected for processing (25)
  • hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/HieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/NetworkSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaMainnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaTestnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/NetworkSettingsBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageResult.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/TopicClientImplTest.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/HieroConfigImplTest.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroAutoConfiguration.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroConfigImpl.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroNetworkProperties.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java
  • hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java

Comment on lines +51 to +57
public static TopicMessageRequest of(
@NonNull TopicId topicId,
@NonNull Consumer<TopicMessage> subscription,
@NonNull Instant startTime,
@NonNull Instant endTime,
long limit) {
return new TopicMessageRequest(topicId, subscription, startTime, endTime, limit, null, null);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix nullability contract on the 5-argument factory.

Line 54 and Line 55 are marked @NonNull, but callers pass nullable bounds for open-ended subscriptions. This breaks the cross-file contract.

Suggested fix
   public static TopicMessageRequest of(
       `@NonNull` TopicId topicId,
       `@NonNull` Consumer<TopicMessage> subscription,
-      `@NonNull` Instant startTime,
-      `@NonNull` Instant endTime,
+      `@Nullable` Instant startTime,
+      `@Nullable` Instant endTime,
       long limit) {
     return new TopicMessageRequest(topicId, subscription, startTime, endTime, limit, null, null);
   }

Comment on lines 146 to 150
final String target =
hieroConfig.getMirrorNodeAddresses().stream()
.findFirst()
hieroConfig
.getMirrorNodeRestUrl()
.orElseThrow(() -> new IllegalStateException("No mirror node addresses configured"));
final MirrorNodeRestClientImpl restClient = new MirrorNodeRestClientImpl(target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject blank REST URLs before creating the mirror-node client.

This only rejects missing values, not blank ones. A blank configured REST URL can pass and produce an invalid client target with late runtime failures.

Proposed fix
-    final String target =
-        hieroConfig
-            .getMirrorNodeRestUrl()
-            .orElseThrow(() -> new IllegalStateException("No mirror node addresses configured"));
+    final String target =
+        hieroConfig
+            .getMirrorNodeRestUrl()
+            .map(String::trim)
+            .filter(url -> !url.isBlank())
+            .orElseThrow(() -> new IllegalStateException("No mirror node REST URL configured"));

Comment on lines +61 to +80
public static class MirrorNode {
@ConfigProperty(name = "hiero.network.mirrornode.rest-url")
Optional<String> restUrl = Optional.empty();

@ConfigProperty(name = "hiero.network.mirrornode.grpc-addresses")
public Optional<String> grpcAddresses = Optional.empty();

public Optional<String> getRestUrl() {
return restUrl;
}

public Set<String> getGrpcAddresses() {
// eg: mirrornode.mirrornode:443,testnet.mirrornode:5600
return grpcAddresses
.map(n -> n.split(","))
.map(n -> Stream.of(n))
.orElse(Stream.empty())
.collect(Collectors.toUnmodifiableSet());
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there are integration tests that verify the mirrornode config is actually read
rg -n "mirrornode" --type=java -g '**/*Test*.java' -C3

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 20992


🌐 Web query:

MicroProfile Config @ConfigProperty nested object injection

💡 Result:

MicroProfile Config does not support direct injection of nested objects using the @ConfigProperty annotation [1][2]. The @ConfigProperty annotation is designed to inject scalar values, arrays, or collections (like List or Set) into individual injection points, provided a suitable Converter exists for the target type [1][2]. To group related configuration properties into a structured object or "nested" configuration, you should use the @ConfigProperties annotation instead of @ConfigProperty [3][4]. When you annotate a POJO with @ConfigProperties(prefix = "my.prefix"), MicroProfile Config automatically maps fields within that class to configuration keys starting with that prefix [4][5]. If a field in your POJO is itself a POJO, some implementations (such as Quarkus) provide extended support for nesting these objects using the same @ConfigProperties mechanism [6]. Key differences for configuration injection: 1. @ConfigProperty: Used for injecting individual, flat properties [1][2]. It supports types with registered Converters, Optional, Provider, and collections [1][2]. 2. @ConfigProperties: Used to aggregate multiple related properties into a single CDI bean or POJO [3][4]. This is the standard way to handle structured configuration [3][4]. If you are working in an environment like Quarkus, you can define nested structures by nesting classes and using @ConfigProperties, as demonstrated in common implementation guides [6]. For pure MicroProfile implementations, you may need to ensure your configuration structure is flattened or use @ConfigProperties to bind a group of properties to a bean [3][4].

Citations:


🏁 Script executed:

cat -n hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java | head -100

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 2976


🏁 Script executed:

# Search for property files containing mirrornode config
find . -type f \( -name "*.properties" -o -name "*.yaml" -o -name "*.yml" \) | xargs grep -l "mirrornode" 2>/dev/null | head -10

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 60


🏁 Script executed:

# Search for actual usage of getMirrornode() or getRestUrl/getGrpcAddresses
rg "getMirrornode|getRestUrl|getGrpcAddresses" --type=java -A2 -B2

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 6869


🏁 Script executed:

cat -n hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java | grep -A10 -B5 "getMirrornode"

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 965


🏁 Script executed:

# Check for any microprofile-config.properties or application.properties in the microprofile module
find hiero-enterprise-microprofile -type f \( -name "*.properties" -o -name "*.yaml" \) | xargs cat 2>/dev/null

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 120


🏁 Script executed:

# Check the HieroConfigImplTest to understand how the microprofile config is tested
cat -n hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/HieroConfigImplTest.java | head -150

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 3787


🏁 Script executed:

# Search for any test configuration or property files in test resources
find hiero-enterprise-microprofile/src/test -type f \( -name "*.properties" -o -name "*.yaml" -o -name "*.xml" \) | xargs ls -la

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 315


🏁 Script executed:

# Check what MicroProfile Config implementation is being used
rg "microprofile-config" hiero-enterprise-microprofile/pom.xml -A2 -B2

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 255


🏁 Script executed:

# Look for how the HieroNetworkConfiguration is actually injected/used in the microprofile module
rg "HieroNetworkConfiguration" hiero-enterprise-microprofile/src/main --type=java -B3 -A3

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 3811


🏁 Script executed:

# Check the ClientProvider to see the full injection setup
cat -n hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 8820


🏁 Script executed:

# Look for any actual configuration examples or test setup that sets mirrornode properties
rg "hiero.network.mirrornode" --type=java --type=properties --type=xml -B2 -A2

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 113


🏁 Script executed:

# Search for property definitions with the full property names
rg "rest-url|grpc-addresses" --type-list | head -20

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 468


🏁 Script executed:

# Try a broader search for property files and configuration
find . -path ./target -prune -o -type f \( -name "*.properties" -o -name "*.yaml" -o -name "*.yml" \) -print | xargs grep -l "hiero.network" 2>/dev/null

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 375


🏁 Script executed:

# Check the sample and test property files for mirrornode configuration
for file in ./hiero-enterprise-microprofile-sample/src/main/resources/application.properties ./hiero-enterprise-spring-sample/src/main/resources/application.properties ./hiero-enterprise-spring/src/test/resources/application-solo.properties ./hiero-enterprise-spring/src/test/resources/application.properties; do
  echo "=== $file ==="
  cat "$file"
  echo ""
done

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 1461


🏁 Script executed:

# Check if there's a test that actually sets mirrornode properties and expects them to be read
rg "hiero.network.mirrornode" -i --type=java --type=xml --type=yaml

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 408


🏁 Script executed:

# Check the actual Quarkus/MicroProfile Config behavior in Quarkus documentation comments or test setup
find . -name "*Test*.java" -o -name "*Producer*.java" | xargs grep -l "ConfigProperties" | head -5 | xargs cat | head -200

Repository: hiero-ledger/hiero-enterprise-java

Length of output: 60


@ConfigProperty annotations on MirrorNode fields will not be injected.

The MirrorNode class is instantiated manually via new MirrorNode() on line 27. While the parent HieroNetworkConfiguration is a CDI bean with @ConfigProperties, MicroProfile Config does not automatically apply configuration binding to manually instantiated nested objects. The restUrl and grpcAddresses fields will remain their default Optional.empty() values.

To enable configuration injection for nested properties, consider one of these approaches:

  1. Add @ConfigProperties(prefix = "hiero.network.mirrornode") to the MirrorNode class and make it a CDI bean
  2. Inject MirrorNode as a CDI dependency instead of instantiating it manually
  3. Read properties manually via ConfigProvider.getConfig().getOptionalValue(...)

Comment on lines +65 to +69
mirrorNodeGrpcAddresses =
networkConfiguration.getMirrornode().getGrpcAddresses() == null
? Set.of()
: Set.copyOf(networkConfiguration.getMirrornode().getGrpcAddresses());
mirrorNodeRestUrl = networkConfiguration.getMirrornode().getRestUrl();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Redundant null check, but safe.

getGrpcAddresses() returns an empty Set when grpcAddresses is empty (see line 77 in HieroNetworkConfiguration.MirrorNode), so the == null check on line 66 will never be true. However, this is defensive coding and not harmful.

Note: This code path depends on the MirrorNode configuration injection working correctly (see related comment on HieroNetworkConfiguration).

Comment on lines 120 to +123
@Override
public Set<String> getMirrorNodeAddresses() {
return mirrorNodeAddresses;
public Set<String> getMirrorNodeGrpcAddresses() {
return mirrorNodeGrpcAddresses;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing @NonNull annotation on getMirrorNodeGrpcAddresses().

The HieroConfig interface declares @NonNull Set<String> getMirrorNodeGrpcAddresses(). This implementation should include the annotation for consistency and to satisfy static analysis tools.

Suggested fix
   `@Override`
-  public Set<String> getMirrorNodeGrpcAddresses() {
+  public `@NonNull` Set<String> getMirrorNodeGrpcAddresses() {
     return mirrorNodeGrpcAddresses;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Override
public Set<String> getMirrorNodeAddresses() {
return mirrorNodeAddresses;
public Set<String> getMirrorNodeGrpcAddresses() {
return mirrorNodeGrpcAddresses;
}
`@Override`
public `@NonNull` Set<String> getMirrorNodeGrpcAddresses() {
return mirrorNodeGrpcAddresses;
}

manishdait added 13 commits July 8, 2026 13:13
Signed-off-by: Manish Dait <daitmanish88@gmail.com>

# Conflicts:
#	hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java
#	hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>

# Conflicts:
#	hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
@manishdait manishdait force-pushed the feat/subscribe-topic-impl branch from dc63b48 to e36529f Compare July 8, 2026 07:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (8)
hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java (1)

271-294: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

limit == 0 boundary contradicts the documented/thrown error message.

limit < -1 lets limit == 0 pass validation, but the error message (and TopicClient Javadoc/tests) says only -1 (infinite) or values > 0 are valid. Downstream, ProtocolLayerClientImpl.executeTopicMessageQuery calls query.setLimit(request.limit()) whenever limit() >= 0, so a 0 limit is forwarded straight to the Hedera SDK's TopicMessageQuery.setLimit, whose behavior for 0 isn't verified here — it may silently deliver zero messages, contradicting caller expectations.

🔧 Proposed fix
-    if (limit < -1) {
+    if (limit < -1 || limit == 0) {
       throw new IllegalArgumentException("limit must be -1 (infinite) or greater than 0");
     }
What does Hedera Java SDK TopicMessageQuery.setLimit(0) do — unlimited messages or zero messages?
hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java (1)

39-91: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Flaky async assertions: unsynchronized ArrayList + fixed Thread.sleep.

messages is written from the subscription callback thread and read from the test thread with no synchronization, and correctness is gated on a fixed Thread.sleep(5000) rather than a deterministic wait. This is the same pattern already flagged (by a human reviewer here, and by an automated review on the equivalent Spring test) as flaky/non-deterministic.

🔧 Suggested pattern
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
...
-    final List<String> messages = new ArrayList<>();
+    final List<String> messages = new CopyOnWriteArrayList<>();
+    final CountDownLatch delivered = new CountDownLatch(1);
     ...
     final SubscriptionHandle handler =
         topicClient.subscribeTopic(
             topicId,
             (message) -> {
               messages.add(new String(message.contents));
+              delivered.countDown();
             });

-    topicClient.submitMessage(topicId, msg);
-    hieroTestUtils.waitForMirrorNodeRecords();
-    Thread.sleep(5000);
-
-    Assertions.assertNotNull(handler);
-    Assertions.assertEquals(1, messages.size());
-    Assertions.assertEquals(msg, messages.getFirst());
-    handler.unsubscribe();
+    try {
+      topicClient.submitMessage(topicId, msg);
+      hieroTestUtils.waitForMirrorNodeRecords();
+      Assertions.assertTrue(delivered.await(10, TimeUnit.SECONDS));
+      Assertions.assertEquals(1, messages.size());
+      Assertions.assertEquals(msg, messages.get(0));
+    } finally {
+      handler.unsubscribe();
+    }
hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java (1)

209-262: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unresolved: async callback race + flaky fixed-delay wait.

This repeats a previously raised concern: messages (ArrayList) is mutated from the subscription callback thread and read from the test thread without synchronization, and the test relies on a fixed Thread.sleep(5000) instead of a deterministic wait/latch. This remains unaddressed in testSubscribeTopic (Lines 213-232) and testSubscribeTopicWithLimit (Lines 239-262).

🔧 Suggested pattern
-import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
...
-    final List<String> messages = new ArrayList<>();
+    final List<String> messages = new CopyOnWriteArrayList<>();
+    final CountDownLatch delivered = new CountDownLatch(1);
     ...
             (message) -> {
               messages.add(new String(message.contents));
+              delivered.countDown();
             });

-    topicClient.submitMessage(topicId, msg);
-    hieroTestUtils.waitForMirrorNodeRecords();
-    Thread.sleep(5000);
-
-    Assertions.assertNotNull(handler);
-    Assertions.assertEquals(1, messages.size());
-    Assertions.assertEquals(msg, messages.getFirst());
-    handler.unsubscribe();
+    try {
+      topicClient.submitMessage(topicId, msg);
+      hieroTestUtils.waitForMirrorNodeRecords();
+      Assertions.assertTrue(delivered.await(10, TimeUnit.SECONDS));
+      Assertions.assertEquals(1, messages.size());
+      Assertions.assertEquals(msg, messages.get(0));
+    } finally {
+      handler.unsubscribe();
+    }
hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java (1)

50-58: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Nullability contract broken again on the 5-arg factory.

startTime/endTime are annotated @NonNull here, but TopicClientImpl.subscribeTopic(topicId, handler, limit) delegates to subscribeTopic(topicId, handler, null, null, limit), which calls exactly this overload with null for both. This is the same issue flagged on a previous commit (then at lines 51-57); it has resurfaced at the new line numbers.

🐛 Proposed fix
   `@NonNull`
   public static TopicMessageRequest of(
       `@NonNull` TopicId topicId,
       `@NonNull` Consumer<TopicMessage> subscription,
-      `@NonNull` Instant startTime,
-      `@NonNull` Instant endTime,
+      `@Nullable` Instant startTime,
+      `@Nullable` Instant endTime,
       long limit) {
     return new TopicMessageRequest(topicId, subscription, startTime, endTime, limit, null, null);
   }
hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java (1)

42-43: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Still unaddressed: blank env vars propagate invalid endpoints.

getEnv() returns Optional.ofNullable(System.getenv(key)). When an env var is set but blank (""), System.getenv returns "" (not null), so .orElse(null) stores "". This becomes Set.of("") in getMirrorNodeGrpcAddresses() and Optional.of("") in getMirrorNodeRestUrl(), propagating an invalid mirror-node configuration instead of falling back to absent values.

This was previously flagged and remains unaddressed.

Proposed fix
-    mirrorNodeGrpcAddress = getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").orElse(null);
-    mirrorNodeRestUrl = getEnv("HEDERA_MIRROR_NODE_REST_URL").orElse(null);
+    mirrorNodeGrpcAddress =
+        getEnv("HEDERA_MIRROR_NODE_GRPC_ADDRESS").filter(v -> !v.isBlank()).orElse(null);
+    mirrorNodeRestUrl =
+        getEnv("HEDERA_MIRROR_NODE_REST_URL").filter(v -> !v.isBlank()).orElse(null);
hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java (1)

74-75: 🩺 Stability & Availability | 🟡 Minor | 💤 Low value

Still unaddressed: interrupt flag not restored before rethrowing.

The catch (InterruptedException e) block rethrows RuntimeException without calling Thread.currentThread().interrupt(), losing the thread's interrupted status. This was previously flagged and remains unaddressed.

Proposed fix
       try {
         client.setMirrorNetwork(networkSettings.getMirrorNodeGrpcAddresses().stream().toList());
       } catch (InterruptedException e) {
+        Thread.currentThread().interrupt();
         throw new RuntimeException("Error in configuring Mirror Node", e);
       }
hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java (1)

24-28: 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Still unaddressed (critical): @ConfigProperty on manually instantiated MirrorNode will not be injected.

MirrorNode is created via new MirrorNode() on line 27. MicroProfile Config only processes @ConfigProperty on CDI-managed beans, not on manually instantiated objects. The restUrl and grpcAddresses fields will remain at their default Optional.empty() values.

Downstream impact: HieroConfigImpl (lines 65-69) receives an empty Set and Optional.empty() for custom network configurations, silently disabling mirror-node gRPC and REST connectivity.

This was previously flagged as critical and remains unaddressed.

Proposed fix — inject MirrorNode as a CDI bean
+ `@Dependent`
  public static class MirrorNode {
-   `@ConfigProperty`(name = "hiero.network.mirrornode.rest-url")
+   `@ConfigProperty`(name = "mirrornode.rest-url")
    Optional<String> restUrl = Optional.empty();

-   `@ConfigProperty`(name = "hiero.network.mirrornode.grpc-addresses")
+   `@ConfigProperty`(name = "mirrornode.grpc-addresses")
    public Optional<String> grpcAddresses = Optional.empty();

And in HieroNetworkConfiguration:

- public MirrorNode mirrorNode = new MirrorNode();
+ `@Inject` MirrorNode mirrorNode;

Also applies to: 60-80

hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java (1)

65-69: 📐 Maintainability & Code Quality | 🟠 Major | 💤 Low value

Still unaddressed: redundant null check on getGrpcAddresses().

getGrpcAddresses() returns Collectors.toUnmodifiableSet() which is never null — it returns an empty Set when grpcAddresses is Optional.empty(). The == null check on line 66 will never be true. Set.copyOf on line 68 also creates a redundant copy of an already-unmodifiable set.

This was previously flagged and remains unaddressed.

Proposed fix
       networkName = networkConfiguration.getName().orElse(null);
       mirrorNodeGrpcAddresses =
-          networkConfiguration.getMirrornode().getGrpcAddresses() == null
-              ? Set.of()
-              : Set.copyOf(networkConfiguration.getMirrornode().getGrpcAddresses());
+          Collections.unmodifiableSet(networkConfiguration.getMirrornode().getGrpcAddresses());
       mirrorNodeRestUrl = networkConfiguration.getMirrornode().getRestUrl();

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 196085ee-a2dc-4adc-acf9-9b4e71ddc58c

📥 Commits

Reviewing files that changed from the base of the PR and between dc63b48 and e36529f.

📒 Files selected for processing (25)
  • hiero-enterprise-base/src/main/java/org/hiero/base/TopicClient.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/HieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/NetworkSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaMainnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/hedera/HederaTestnetSettings.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/EnvBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/config/implementation/NetworkSettingsBasedHieroConfig.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/ProtocolLayerClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/implementation/TopicClientImpl.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageRequest.java
  • hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/TopicMessageResult.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/TopicClientImplTest.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/HieroTestContext.java
  • hiero-enterprise-base/src/test/java/org/hiero/base/test/config/SoloActionNetworkSettings.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/ClientProvider.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/HieroNetworkConfiguration.java
  • hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/HieroConfigImpl.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/HieroConfigImplTest.java
  • hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TopicClientTest.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroAutoConfiguration.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroConfigImpl.java
  • hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/HieroNetworkProperties.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicClientTest.java
  • hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TopicRepositoryTest.java
  • hiero-enterprise-test/src/main/java/org/hiero/test/SoloActionNetworkSettings.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request status: ready-for-review PR is ready to be reviewed by team member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add subscribeTopic() to TopicClient

4 participants