diff --git a/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeClientImpl.java b/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeClientImpl.java index 204160b4..2a733429 100644 --- a/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeClientImpl.java +++ b/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeClientImpl.java @@ -25,14 +25,27 @@ public class MirrorNodeClientImpl extends AbstractMirrorNodeClient { + private static final String TRANSACTIONS_PATH = "/api/v1/transactions"; + private static final String TOKENS_PATH = "/api/v1/tokens"; + private static final String TOPICS_PATH = "/api/v1/topics"; + + private static final String MSG_REST_CLIENT_MUST_NOT_BE_NULL = "restClient must not be null"; + private static final String MSG_JSON_CONVERTER_MUST_NOT_BE_NULL = + "jsonConverter must not be null"; + private static final String MSG_ACCOUNT_ID_MUST_NOT_BE_NULL = "accountId must not be null"; + private static final String MSG_TYPE_MUST_NOT_BE_NULL = "type must not be null"; + private static final String MSG_RESULT_MUST_NOT_BE_NULL = "result must not be null"; + private static final String MSG_TOKEN_ID_MUST_NOT_BE_NULL = "tokenId must not be null"; + private static final String MSG_TOPIC_ID_MUST_NOT_BE_NULL = "topicId must not be null"; + private final MirrorNodeRestClientImpl restClient; private final MirrorNodeJsonConverter jsonConverter; public MirrorNodeClientImpl( MirrorNodeRestClientImpl restClient, MirrorNodeJsonConverter jsonConverter) { - this.restClient = Objects.requireNonNull(restClient, "restClient must not be null"); - this.jsonConverter = Objects.requireNonNull(jsonConverter, "jsonConverter must not be null"); + this.restClient = Objects.requireNonNull(restClient, MSG_REST_CLIENT_MUST_NOT_BE_NULL); + this.jsonConverter = Objects.requireNonNull(jsonConverter, MSG_JSON_CONVERTER_MUST_NOT_BE_NULL); } @Override @@ -64,8 +77,8 @@ public MirrorNodeClientImpl( @Override public @NonNull Page queryTransactionsByAccount(@NonNull AccountId accountId) throws HieroException { - Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId; + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toTransactionInfos(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -74,9 +87,10 @@ public MirrorNodeClientImpl( @Override public @NonNull Page queryTransactionsByAccountAndType( @NonNull AccountId accountId, @NonNull TransactionType type) throws HieroException { - Objects.requireNonNull(accountId, "accountId must not be null"); - Objects.requireNonNull(type, "type must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId + "&transactiontype=" + type; + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + Objects.requireNonNull(type, MSG_TYPE_MUST_NOT_BE_NULL); + final String path = + TRANSACTIONS_PATH + "?account.id=" + accountId + "&transactiontype=" + type.getType(); final Function> dataExtractionFunction = node -> jsonConverter.toTransactionInfos(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -85,9 +99,9 @@ public MirrorNodeClientImpl( @Override public @NonNull Page queryTransactionsByAccountAndResult( @NonNull AccountId accountId, @NonNull Result result) throws HieroException { - Objects.requireNonNull(accountId, "accountId must not be null"); - Objects.requireNonNull(result, "result must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId + "&result=" + result; + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + Objects.requireNonNull(result, MSG_RESULT_MUST_NOT_BE_NULL); + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId + "&result=" + result.name(); final Function> dataExtractionFunction = node -> jsonConverter.toTransactionInfos(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -96,9 +110,9 @@ public MirrorNodeClientImpl( @Override public @NonNull Page queryTransactionsByAccountAndModification( @NonNull AccountId accountId, @NonNull BalanceModification type) throws HieroException { - Objects.requireNonNull(accountId, "accountId must not be null"); - Objects.requireNonNull(type, "type must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId + "&type=" + type; + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + Objects.requireNonNull(type, MSG_TYPE_MUST_NOT_BE_NULL); + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId + "&type=" + type.name(); final Function> dataExtractionFunction = node -> jsonConverter.toTransactionInfos(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -106,8 +120,8 @@ public MirrorNodeClientImpl( @Override public Page queryTokensForAccount(@NonNull AccountId accountId) throws HieroException { - Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId; + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + final String path = TOKENS_PATH + "?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toTokens(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -115,8 +129,8 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page queryTokenBalances(@NonNull TokenId tokenId) throws HieroException { - Objects.requireNonNull(tokenId, "tokenId must not be null"); - final String path = "/api/v1/tokens/" + tokenId + "/balances"; + Objects.requireNonNull(tokenId, MSG_TOKEN_ID_MUST_NOT_BE_NULL); + final String path = TOKENS_PATH + "/" + tokenId + "/balances"; final Function> dataExtractionFunction = node -> jsonConverter.toBalances(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -125,9 +139,9 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page queryTokenBalancesForAccount( @NonNull TokenId tokenId, @NonNull AccountId accountId) throws HieroException { - Objects.requireNonNull(tokenId, "tokenId must not be null"); - Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/tokens/" + tokenId + "/balances?account.id=" + accountId; + Objects.requireNonNull(tokenId, MSG_TOKEN_ID_MUST_NOT_BE_NULL); + Objects.requireNonNull(accountId, MSG_ACCOUNT_ID_MUST_NOT_BE_NULL); + final String path = TOKENS_PATH + "/" + tokenId + "/balances?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toBalances(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -135,8 +149,8 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page queryTopicMessages(TopicId topicId) throws HieroException { - Objects.requireNonNull(topicId, "topicId must not be null"); - final String path = "/api/v1/topics/" + topicId + "/messages"; + Objects.requireNonNull(topicId, MSG_TOPIC_ID_MUST_NOT_BE_NULL); + final String path = TOPICS_PATH + "/" + topicId + "/messages"; final Function> dataExtractionFunction = node -> jsonConverter.toTopicMessages(node); return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path); @@ -144,11 +158,11 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page findNftTypesByOwner(AccountId ownerId) { - throw new RuntimeException("Not implemented"); + throw new UnsupportedOperationException("Not implemented"); } @Override public @NonNull Page findAllNftTypes() { - throw new RuntimeException("Not implemented"); + throw new UnsupportedOperationException("Not implemented"); } } diff --git a/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java b/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java index 35c1968e..ecffc57b 100644 --- a/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java +++ b/hiero-enterprise-microprofile/src/main/java/org/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java @@ -207,19 +207,19 @@ public class MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter getNullableString(JsonObject jsonObject, String key) { + if (!jsonObject.containsKey(key) || jsonObject.isNull(key)) { + return Optional.empty(); + } + return Optional.of(jsonObject.getString(key)); + } + @Override public List toNfts(@NonNull JsonObject jsonObject) { if (!jsonObject.containsKey("transactions")) { @@ -355,9 +362,6 @@ public List toNfts(@NonNull JsonObject jsonObject) { @NonNull private Stream jsonArrayToStream(@NonNull final JsonArray jsonObject) { - if (jsonObject.isEmpty()) { - throw new IllegalStateException("not an array"); - } return StreamSupport.stream( Spliterators.spliteratorUnknownSize(jsonObject.iterator(), Spliterator.ORDERED), false); } diff --git a/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/MirrorNodeClientImplPathTest.java b/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/MirrorNodeClientImplPathTest.java new file mode 100644 index 00000000..a22adc41 --- /dev/null +++ b/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/MirrorNodeClientImplPathTest.java @@ -0,0 +1,66 @@ +package org.hiero.microprofile.test; + +import com.hedera.hashgraph.sdk.AccountId; +import org.hiero.base.data.BalanceModification; +import org.hiero.base.data.Result; +import org.hiero.base.protocol.data.TransactionType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MirrorNodeClientImplPathTest { + + private static final AccountId TEST_ACCOUNT = AccountId.fromString("0.0.12345"); + private static final String TRANSACTIONS_PATH = "/api/v1/transactions"; + private static final String TOKENS_PATH = "/api/v1/tokens"; + + @Test + void queryByAccountUsesTransactionsPath() { + final String path = TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT; + + Assertions.assertEquals(TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT, path); + Assertions.assertTrue(path.startsWith(TRANSACTIONS_PATH)); + Assertions.assertFalse(path.startsWith(TOKENS_PATH)); + } + + @Test + void queryByAccountAndTypeUsesProtocolString() { + final TransactionType type = TransactionType.ACCOUNT_CREATE; + final String path = + TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&transactiontype=" + type.getType(); + + Assertions.assertEquals( + TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&transactiontype=CRYPTOCREATEACCOUNT", + path); + Assertions.assertFalse(path.contains("ACCOUNT_CREATE")); + } + + @Test + void queryByAccountAndResultUsesTransactionsPath() { + final Result result = Result.SUCCESS; + final String path = + TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&result=" + result.name(); + + Assertions.assertEquals( + TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&result=SUCCESS", path); + } + + @Test + void queryByAccountAndModificationUsesTransactionsPath() { + final BalanceModification type = BalanceModification.DEBIT; + final String path = TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&type=" + type.name(); + + Assertions.assertEquals( + TRANSACTIONS_PATH + "?account.id=" + TEST_ACCOUNT + "&type=DEBIT", path); + } + + @Test + void transactionTypeGetTypeReturnsProtocolString() { + Assertions.assertEquals("CRYPTOCREATEACCOUNT", TransactionType.ACCOUNT_CREATE.getType()); + Assertions.assertEquals("ACCOUNT_CREATE", TransactionType.ACCOUNT_CREATE.name()); + Assertions.assertNotEquals( + TransactionType.ACCOUNT_CREATE.name(), TransactionType.ACCOUNT_CREATE.getType()); + Assertions.assertEquals("CRYPTOTRANSFER", TransactionType.CRYPTO_TRANSFER.getType()); + Assertions.assertEquals("CONTRACTCALL", TransactionType.CONTRACT_CALL.getType()); + Assertions.assertEquals("TOKENCREATION", TransactionType.TOKEN_CREATE.getType()); + } +} diff --git a/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TransactionRepositoryTest.java b/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TransactionRepositoryTest.java new file mode 100644 index 00000000..cbb7b038 --- /dev/null +++ b/hiero-enterprise-microprofile/src/test/java/org/hiero/microprofile/test/TransactionRepositoryTest.java @@ -0,0 +1,108 @@ +package org.hiero.microprofile.test; + +import com.hedera.hashgraph.sdk.AccountId; +import io.helidon.microprofile.tests.junit5.AddBean; +import io.helidon.microprofile.tests.junit5.Configuration; +import io.helidon.microprofile.tests.junit5.HelidonTest; +import jakarta.inject.Inject; +import java.util.List; +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.spi.ConfigProviderResolver; +import org.hiero.base.AccountClient; +import org.hiero.base.data.Account; +import org.hiero.base.data.BalanceModification; +import org.hiero.base.data.Page; +import org.hiero.base.data.Result; +import org.hiero.base.data.TransactionInfo; +import org.hiero.base.data.Transfer; +import org.hiero.base.mirrornode.TransactionRepository; +import org.hiero.base.protocol.data.TransactionType; +import org.hiero.microprofile.ClientProvider; +import org.hiero.test.HieroTestUtils; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@HelidonTest +@AddBean(ClientProvider.class) +@Configuration(useExisting = true) +public class TransactionRepositoryTest { + + @BeforeAll + static void setup() { + final Config build = + ConfigProviderResolver.instance().getBuilder().withSources(new TestConfigSource()).build(); + ConfigProviderResolver.instance() + .registerConfig(build, Thread.currentThread().getContextClassLoader()); + } + + @Inject private TransactionRepository transactionRepository; + + @Inject private AccountClient accountClient; + + @Inject private HieroTestUtils hieroTestUtils; + + @Test + void testFindTransactionByAccountId() throws Exception { + final Account account = accountClient.createAccount(1); + hieroTestUtils.waitForMirrorNodeRecords(account.accountId()); + final Page page = transactionRepository.findByAccount(account.accountId()); + + final List data = page.getData(); + Assertions.assertFalse(data.isEmpty()); + } + + @Test + void findByAccountReturnsEmptyWhenNoTransactions() throws Exception { + final AccountId accountId = AccountId.fromString("0.0.0"); + hieroTestUtils.waitForMirrorNodeRecords(); + final Page page = transactionRepository.findByAccount(accountId); + + final List data = page.getData(); + Assertions.assertTrue(data.isEmpty()); + } + + @Test + void testFindTransactionByAccountIdAndType() throws Exception { + final Account account = accountClient.createAccount(1); + hieroTestUtils.waitForMirrorNodeRecords(); + final Page page = + transactionRepository.findByAccountAndType( + account.accountId(), TransactionType.ACCOUNT_CREATE); + + final List data = page.getData(); + Assertions.assertFalse(data.isEmpty()); + data.forEach(tx -> Assertions.assertEquals(TransactionType.ACCOUNT_CREATE, tx.name())); + } + + @Test + void testFindTransactionByAccountIdAndResult() throws Exception { + final Account account = accountClient.createAccount(1); + hieroTestUtils.waitForMirrorNodeRecords(); + final Page page = + transactionRepository.findByAccountAndResult(account.accountId(), Result.SUCCESS); + + final List data = page.getData(); + Assertions.assertFalse(data.isEmpty()); + data.forEach(tx -> Assertions.assertEquals(Result.SUCCESS.name(), tx.result())); + } + + @Test + void testFindTransactionByAccountIdAndBalanceModification() throws Exception { + final Account account = accountClient.createAccount(1); + hieroTestUtils.waitForMirrorNodeRecords(); + final Page page = + transactionRepository.findByAccountAndModification( + account.accountId(), BalanceModification.CREDIT); + + final List data = page.getData(); + Assertions.assertFalse(data.isEmpty()); + data.forEach( + tx -> + Assertions.assertTrue( + tx.transfers().stream() + .anyMatch( + (Transfer t) -> + t.account().equals(account.accountId()) && t.amount() > 0L))); + } +} diff --git a/hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/MirrorNodeClientImpl.java b/hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/MirrorNodeClientImpl.java index ec41cf1c..e8d98d66 100644 --- a/hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/MirrorNodeClientImpl.java +++ b/hiero-enterprise-spring/src/main/java/org/hiero/spring/implementation/MirrorNodeClientImpl.java @@ -27,6 +27,11 @@ public class MirrorNodeClientImpl extends AbstractMirrorNodeClient { + private static final String ACCOUNTS_PATH = "/api/v1/accounts"; + private static final String TRANSACTIONS_PATH = "/api/v1/transactions"; + private static final String TOKENS_PATH = "/api/v1/tokens"; + private static final String TOPICS_PATH = "/api/v1/topics"; + private final ObjectMapper objectMapper; private final RestClient restClient; @@ -61,7 +66,7 @@ protected final MirrorNodeJsonConverter getJsonConverter() { @Override public Page queryNftsByAccount(@NonNull final AccountId accountId) throws HieroException { Objects.requireNonNull(accountId, "newAccountId must not be null"); - final String path = "/api/v1/accounts/" + accountId + "/nfts"; + final String path = ACCOUNTS_PATH + "/" + accountId + "/nfts"; final Function> dataExtractionFunction = node -> jsonConverter.toNfts(node); return new RestBasedPage<>( objectMapper, restClient.mutate().clone(), path, dataExtractionFunction); @@ -72,7 +77,7 @@ public Page queryNftsByAccountAndTokenId( @NonNull final AccountId accountId, @NonNull final TokenId tokenId) { Objects.requireNonNull(accountId, "accountId must not be null"); Objects.requireNonNull(tokenId, "tokenId must not be null"); - final String path = "/api/v1/tokens/" + tokenId + "/nfts/?account.id=" + accountId; + final String path = TOKENS_PATH + "/" + tokenId + "/nfts/?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toNfts(node); return new RestBasedPage<>( objectMapper, restClient.mutate().clone(), path, dataExtractionFunction); @@ -80,7 +85,7 @@ public Page queryNftsByAccountAndTokenId( @Override public Page queryNftsByTokenId(@NonNull TokenId tokenId) { - final String path = "/api/v1/tokens/" + tokenId + "/nfts"; + final String path = TOKENS_PATH + "/" + tokenId + "/nfts"; final Function> dataExtractionFunction = node -> jsonConverter.toNfts(node); return new RestBasedPage<>( objectMapper, restClient.mutate().clone(), path, dataExtractionFunction); @@ -90,7 +95,7 @@ public Page queryNftsByTokenId(@NonNull TokenId tokenId) { public Page queryTransactionsByAccount(@NonNull final AccountId accountId) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/transactions?account.id=" + accountId; + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId; final Function> dataExtractionFunction = n -> jsonConverter.toTransactionInfos(n); return new RestBasedPage<>( @@ -102,7 +107,7 @@ public Page queryTransactionsByAccount(@NonNull final AccountId @NonNull AccountId accountId, @NonNull TransactionType type) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); final String path = - "/api/v1/transactions?account.id=" + accountId + "&transactiontype=" + type.getType(); + TRANSACTIONS_PATH + "?account.id=" + accountId + "&transactiontype=" + type.getType(); final Function> dataExtractionFunction = n -> jsonConverter.toTransactionInfos(n); return new RestBasedPage<>( @@ -113,7 +118,7 @@ public Page queryTransactionsByAccount(@NonNull final AccountId public @NonNull Page queryTransactionsByAccountAndResult( @NonNull AccountId accountId, @NonNull Result result) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/transactions?account.id=" + accountId + "&result=" + result.name(); + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId + "&result=" + result.name(); final Function> dataExtractionFunction = n -> jsonConverter.toTransactionInfos(n); return new RestBasedPage<>( @@ -124,7 +129,7 @@ public Page queryTransactionsByAccount(@NonNull final AccountId public @NonNull Page queryTransactionsByAccountAndModification( @NonNull AccountId accountId, @NonNull BalanceModification type) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/transactions?account.id=" + accountId + "&type=" + type.name(); + final String path = TRANSACTIONS_PATH + "?account.id=" + accountId + "&type=" + type.name(); final Function> dataExtractionFunction = n -> jsonConverter.toTransactionInfos(n); return new RestBasedPage<>( @@ -134,7 +139,7 @@ public Page queryTransactionsByAccount(@NonNull final AccountId @Override public Page queryTokensForAccount(@NonNull AccountId accountId) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/tokens?account.id=" + accountId; + final String path = TOKENS_PATH + "?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toTokens(node); return new RestBasedPage<>( @@ -144,7 +149,7 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page queryTokenBalances(TokenId tokenId) throws HieroException { Objects.requireNonNull(tokenId, "tokenId must not be null"); - final String path = "/api/v1/tokens/" + tokenId + "/balances"; + final String path = TOKENS_PATH + "/" + tokenId + "/balances"; final Function> dataExtractionFunction = node -> jsonConverter.toBalances(node); return new RestBasedPage<>( @@ -156,7 +161,7 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @NonNull TokenId tokenId, @NonNull AccountId accountId) throws HieroException { Objects.requireNonNull(tokenId, "tokenId must not be null"); Objects.requireNonNull(accountId, "accountId must not be null"); - final String path = "/api/v1/tokens/" + tokenId + "/balances?account.id=" + accountId; + final String path = TOKENS_PATH + "/" + tokenId + "/balances?account.id=" + accountId; final Function> dataExtractionFunction = node -> jsonConverter.toBalances(node); return new RestBasedPage<>( @@ -166,7 +171,7 @@ public Page queryTokensForAccount(@NonNull AccountId accountId) throws Hi @Override public @NonNull Page queryTopicMessages(TopicId topicId) { Objects.requireNonNull(topicId, "topicId must not be null"); - final String path = "/api/v1/topics/" + topicId + "/messages"; + final String path = TOPICS_PATH + "/" + topicId + "/messages"; final Function> dataExtractionFunction = node -> jsonConverter.toTopicMessages(node); return new RestBasedPage<>( diff --git a/hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TransactionRepositoryTest.java b/hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TransactionRepositoryTest.java index 741e1e35..fff98ec9 100644 --- a/hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TransactionRepositoryTest.java +++ b/hiero-enterprise-spring/src/test/java/org/hiero/spring/test/TransactionRepositoryTest.java @@ -28,7 +28,7 @@ public class TransactionRepositoryTest { @Test void testFindTransactionByAccountId() throws HieroException { final Account account = accountClient.createAccount(1); - hieroTestUtils.waitForMirrorNodeRecords(); + hieroTestUtils.waitForMirrorNodeRecords(account.accountId()); final Page page = transactionRepository.findByAccount(account.accountId()); Assertions.assertNotNull(page); diff --git a/hiero-enterprise-test/src/main/java/org/hiero/test/HieroTestUtils.java b/hiero-enterprise-test/src/main/java/org/hiero/test/HieroTestUtils.java index e3209dd2..2156b718 100644 --- a/hiero-enterprise-test/src/main/java/org/hiero/test/HieroTestUtils.java +++ b/hiero-enterprise-test/src/main/java/org/hiero/test/HieroTestUtils.java @@ -1,9 +1,11 @@ package org.hiero.test; +import com.hedera.hashgraph.sdk.AccountId; import com.hedera.hashgraph.sdk.Status; import com.hedera.hashgraph.sdk.TransactionId; import java.io.Serializable; import java.time.LocalDateTime; +import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import org.hiero.base.HieroException; import org.hiero.base.mirrornode.MirrorNodeClient; @@ -82,4 +84,37 @@ public void waitForMirrorNodeRecords() { log.debug("No transaction to wait for"); } } + + /** + * Waits for the last submitted transaction to be available at the mirror node, and additionally + * waits for the given account to be queryable via the {@code ?account.id=} index. The mirror node + * updates its by-transaction-id and by-account indexes independently, so after a new account is + * created the account index can lag a few hundred milliseconds behind the transaction-id index. + * Tests that assert on results obtained through the account index should use this overload. + */ + public void waitForMirrorNodeRecords(final AccountId accountId) { + Objects.requireNonNull(accountId, "accountId must not be null"); + waitForMirrorNodeRecords(); + log.debug("Waiting for account '{}' available at mirror node", accountId); + final LocalDateTime start = LocalDateTime.now(); + while (true) { + try { + if (!mirrorNodeClient.queryTransactionsByAccount(accountId).getData().isEmpty()) { + log.debug("Account '{}' is available at mirror node", accountId); + return; + } + } catch (HieroException e) { + throw new RuntimeException("Error in mirror node query!", e); + } + if (LocalDateTime.now().isAfter(start.plusSeconds(30))) { + throw new RuntimeException("Timeout waiting for account '" + accountId + "'"); + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Interrupted while waiting for account", e); + } + } + } }