Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,27 @@

public class MirrorNodeClientImpl extends AbstractMirrorNodeClient<JsonObject> {

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<JsonObject> jsonConverter;

public MirrorNodeClientImpl(
MirrorNodeRestClientImpl restClient, MirrorNodeJsonConverter<JsonObject> 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
Expand Down Expand Up @@ -64,8 +77,8 @@ public MirrorNodeClientImpl(
@Override
public @NonNull Page<TransactionInfo> 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<JsonObject, List<TransactionInfo>> dataExtractionFunction =
node -> jsonConverter.toTransactionInfos(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
Expand All @@ -74,9 +87,10 @@ public MirrorNodeClientImpl(
@Override
public @NonNull Page<TransactionInfo> 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<JsonObject, List<TransactionInfo>> dataExtractionFunction =
node -> jsonConverter.toTransactionInfos(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
Expand All @@ -85,9 +99,9 @@ public MirrorNodeClientImpl(
@Override
public @NonNull Page<TransactionInfo> 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<JsonObject, List<TransactionInfo>> dataExtractionFunction =
node -> jsonConverter.toTransactionInfos(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
Expand All @@ -96,27 +110,27 @@ public MirrorNodeClientImpl(
@Override
public @NonNull Page<TransactionInfo> 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<JsonObject, List<TransactionInfo>> dataExtractionFunction =
node -> jsonConverter.toTransactionInfos(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
}

@Override
public Page<Token> 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<JsonObject, List<Token>> dataExtractionFunction =
node -> jsonConverter.toTokens(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
}

@Override
public @NonNull Page<Balance> 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<JsonObject, List<Balance>> dataExtractionFunction =
node -> jsonConverter.toBalances(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
Expand All @@ -125,30 +139,30 @@ public Page<Token> queryTokensForAccount(@NonNull AccountId accountId) throws Hi
@Override
public @NonNull Page<Balance> 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<JsonObject, List<Balance>> dataExtractionFunction =
node -> jsonConverter.toBalances(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
}

@Override
public @NonNull Page<TopicMessage> 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<JsonObject, List<TopicMessage>> dataExtractionFunction =
node -> jsonConverter.toTopicMessages(node);
return new RestBasedPage<>(restClient.getTarget(), dataExtractionFunction, path);
}

@Override
public @NonNull Page<NftMetadata> findNftTypesByOwner(AccountId ownerId) {
throw new RuntimeException("Not implemented");
throw new UnsupportedOperationException("Not implemented");
}

@Override
public @NonNull Page<NftMetadata> findAllNftTypes() {
throw new RuntimeException("Not implemented");
throw new UnsupportedOperationException("Not implemented");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,19 +207,19 @@ public class MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter<Json

try {
final String transactionId = jsonObject.getString("transaction_id");
final byte[] bytes = jsonObject.getString("bytes").getBytes();
final long chargedTxFee = Long.parseLong(jsonObject.getString("charged_tx_fee"));
final byte[] bytes = getNullableString(jsonObject, "bytes").orElse("").getBytes();
final long chargedTxFee = jsonObject.getJsonNumber("charged_tx_fee").longValue();
final Instant consensusTimestamp =
Instant.ofEpochSecond(
(long) Double.parseDouble(jsonObject.getString("consensus_timestamp")));
final String entityId = jsonObject.getString("entity_id");
final String entityId = getNullableString(jsonObject, "entity_id").orElse(null);
final String maxFee = jsonObject.getString("max_fee");
final byte[] memo = jsonObject.getString("memo_base64").getBytes();
final TransactionType name = TransactionType.from(jsonObject.getString("name"));
final String _node = jsonObject.getString("node");
final String _node = getNullableString(jsonObject, "node").orElse(null);
final int nonce = jsonObject.getInt("nonce");
final Instant parentConsensusTimestamp =
jsonObject.get("parent_consensus_timestamp").asJsonObject() == null
jsonObject.isNull("parent_consensus_timestamp")
? null
: Instant.ofEpochSecond(
(long) Double.parseDouble(jsonObject.getString("parent_consensus_timestamp")));
Expand Down Expand Up @@ -297,7 +297,7 @@ public class MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter<Json
private Transfer toTransfer(JsonValue node) {
final JsonObject jsonObject = node.asJsonObject();
final AccountId account = AccountId.fromString(jsonObject.getString("account"));
final long amount = Long.parseLong(jsonObject.getString("amount"));
final long amount = jsonObject.getJsonNumber("amount").longValue();
final boolean isApproval = jsonObject.getBoolean("is_approval");

return new Transfer(account, amount, isApproval);
Expand All @@ -307,7 +307,7 @@ private TokenTransfer toTokenTransfer(JsonValue node) {
final JsonObject jsonObject = node.asJsonObject();
final TokenId tokenId = TokenId.fromString(jsonObject.getString("token_id"));
final AccountId account = AccountId.fromString(jsonObject.getString("account"));
final long amount = Long.parseLong(jsonObject.getString("amount"));
final long amount = jsonObject.getJsonNumber("amount").longValue();
final boolean isApproval = jsonObject.getBoolean("is_approval");

return new TokenTransfer(tokenId, account, amount, isApproval);
Expand All @@ -316,7 +316,7 @@ private TokenTransfer toTokenTransfer(JsonValue node) {
private StakingRewardTransfer toStakingRewardTransfer(JsonValue node) {
final JsonObject jsonObject = node.asJsonObject();
final AccountId account = AccountId.fromString(jsonObject.getString("account"));
long amount = Long.parseLong(jsonObject.getString("amount"));
long amount = jsonObject.getJsonNumber("amount").longValue();

return new StakingRewardTransfer(account, amount);
}
Expand All @@ -328,12 +328,19 @@ private NftTransfer toNftTransfer(JsonValue node) {
AccountId.fromString(jsonObject.getString("receiver_account_id"));
final AccountId senderAccountId =
AccountId.fromString(jsonObject.getString("sender_account_id"));
final long serialNumber = Long.parseLong(jsonObject.getString("serial_number"));
final long serialNumber = jsonObject.getJsonNumber("serial_number").longValue();
final TokenId tokenId = TokenId.fromString(jsonObject.getString("token_id"));

return new NftTransfer(isApproval, receiverAccountId, senderAccountId, serialNumber, tokenId);
}

private Optional<String> getNullableString(JsonObject jsonObject, String key) {
if (!jsonObject.containsKey(key) || jsonObject.isNull(key)) {
return Optional.empty();
}
return Optional.of(jsonObject.getString(key));
}

@Override
public List<Nft> toNfts(@NonNull JsonObject jsonObject) {
if (!jsonObject.containsKey("transactions")) {
Expand All @@ -355,9 +362,6 @@ public List<Nft> toNfts(@NonNull JsonObject jsonObject) {

@NonNull
private Stream<JsonValue> jsonArrayToStream(@NonNull final JsonArray jsonObject) {
if (jsonObject.isEmpty()) {
throw new IllegalStateException("not an array");
}
return StreamSupport.stream(
Spliterators.spliteratorUnknownSize(jsonObject.iterator(), Spliterator.ORDERED), false);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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<TransactionInfo> page = transactionRepository.findByAccount(account.accountId());

final List<TransactionInfo> data = page.getData();
Assertions.assertFalse(data.isEmpty());
}

@Test
void findByAccountReturnsEmptyWhenNoTransactions() throws Exception {
final AccountId accountId = AccountId.fromString("0.0.0");
hieroTestUtils.waitForMirrorNodeRecords();
final Page<TransactionInfo> page = transactionRepository.findByAccount(accountId);

final List<TransactionInfo> data = page.getData();
Assertions.assertTrue(data.isEmpty());
}

@Test
void testFindTransactionByAccountIdAndType() throws Exception {
final Account account = accountClient.createAccount(1);
hieroTestUtils.waitForMirrorNodeRecords();
final Page<TransactionInfo> page =
transactionRepository.findByAccountAndType(
account.accountId(), TransactionType.ACCOUNT_CREATE);

final List<TransactionInfo> 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<TransactionInfo> page =
transactionRepository.findByAccountAndResult(account.accountId(), Result.SUCCESS);

final List<TransactionInfo> 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<TransactionInfo> page =
transactionRepository.findByAccountAndModification(
account.accountId(), BalanceModification.CREDIT);

final List<TransactionInfo> 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)));
}
}
Loading
Loading