diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/Contract.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/Contract.java new file mode 100644 index 00000000..e0f06cfc --- /dev/null +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/Contract.java @@ -0,0 +1,41 @@ +package com.openelements.hiero.base.data; + +import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; +import com.hedera.hashgraph.sdk.PublicKey; +import java.time.Instant; +import java.util.Objects; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * Represents a smart contract on the Hiero network. + */ +public record Contract( + @NonNull ContractId contractId, + @Nullable PublicKey adminKey, + @Nullable AccountId autoRenewAccount, + int autoRenewPeriod, + @NonNull Instant createdTimestamp, + boolean deleted, + @Nullable Instant expirationTimestamp, + @Nullable String fileId, + @Nullable String evmAddress, + @Nullable String memo, + @Nullable Integer maxAutomaticTokenAssociations, + @Nullable Long nonce, + @Nullable String obtainerId, + boolean permanentRemoval, + @Nullable String proxyAccountId, + @NonNull Instant fromTimestamp, + @NonNull Instant toTimestamp, + @Nullable String bytecode, + @Nullable String runtimeBytecode +) { + public Contract { + Objects.requireNonNull(contractId, "contractId must not be null"); + Objects.requireNonNull(createdTimestamp, "createdTimestamp must not be null"); + Objects.requireNonNull(fromTimestamp, "fromTimestamp must not be null"); + Objects.requireNonNull(toTimestamp, "toTimestamp must not be null"); + } +} diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/SinglePage.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/SinglePage.java new file mode 100644 index 00000000..b77a89a5 --- /dev/null +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/data/SinglePage.java @@ -0,0 +1,54 @@ +package com.openelements.hiero.base.data; + +import java.util.List; +import java.util.Objects; + +/** + * Basic {@link Page} implementation backed by an in-memory list. + * + * @param element type for the page + */ +public final class SinglePage implements Page { + + private final List data; + + public SinglePage(final List data) { + this.data = List.copyOf(Objects.requireNonNull(data, "data must not be null")); + } + + @Override + public int getPageIndex() { + return 0; + } + + @Override + public int getSize() { + return data.size(); + } + + @Override + public List getData() { + return data; + } + + @Override + public boolean hasNext() { + return false; + } + + @Override + public Page next() { + throw new IllegalStateException("No next page"); + } + + @Override + public Page first() { + return this; + } + + @Override + public boolean isFirst() { + return true; + } +} + diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/AbstractMirrorNodeClient.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/AbstractMirrorNodeClient.java index cc796daf..0669b4f9 100644 --- a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/AbstractMirrorNodeClient.java +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/AbstractMirrorNodeClient.java @@ -1,16 +1,19 @@ package com.openelements.hiero.base.implementation; import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.TokenId; import com.hedera.hashgraph.sdk.TopicId; import com.openelements.hiero.base.HieroException; +import com.openelements.hiero.base.data.Contract; +import com.openelements.hiero.base.data.Nft; +import com.openelements.hiero.base.data.NftMetadata; import com.openelements.hiero.base.data.AccountInfo; import com.openelements.hiero.base.data.ExchangeRates; import com.openelements.hiero.base.data.NetworkFee; import com.openelements.hiero.base.data.NetworkStake; import com.openelements.hiero.base.data.NetworkSupplies; -import com.openelements.hiero.base.data.Nft; -import com.openelements.hiero.base.data.NftMetadata; +import com.openelements.hiero.base.data.Page; import com.openelements.hiero.base.data.TokenInfo; import com.openelements.hiero.base.data.TransactionInfo; import com.openelements.hiero.base.data.Topic; @@ -105,4 +108,17 @@ public final Optional queryTopicMessageBySequenceNumber(TopicId to throw new UnsupportedOperationException("Not yet implemented"); } + @Override + public @NonNull Page queryContracts() throws HieroException { + final JSON json = getRestClient().queryContracts(); + return getJsonConverter().toContractPage(json); + } + + @Override + public @NonNull Optional queryContractById(@NonNull final ContractId contractId) throws HieroException { + Objects.requireNonNull(contractId, "contractId must not be null"); + final JSON json = getRestClient().queryContractById(contractId); + return getJsonConverter().toContract(json); + } + } diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/ContractRepositoryImpl.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/ContractRepositoryImpl.java new file mode 100644 index 00000000..ac2bc042 --- /dev/null +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/ContractRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.openelements.hiero.base.implementation; + +import com.hedera.hashgraph.sdk.ContractId; +import com.openelements.hiero.base.HieroException; +import com.openelements.hiero.base.data.Contract; +import com.openelements.hiero.base.data.Page; +import com.openelements.hiero.base.mirrornode.ContractRepository; +import com.openelements.hiero.base.mirrornode.MirrorNodeClient; +import java.util.Objects; +import java.util.Optional; +import org.jspecify.annotations.NonNull; + +/** + * Implementation of ContractRepository that uses MirrorNodeClient to query contract data. + */ +public class ContractRepositoryImpl implements ContractRepository { + + private final MirrorNodeClient mirrorNodeClient; + + /** + * Creates a new ContractRepositoryImpl with the given MirrorNodeClient. + * + * @param mirrorNodeClient the mirror node client to use for queries + */ + public ContractRepositoryImpl(@NonNull final MirrorNodeClient mirrorNodeClient) { + this.mirrorNodeClient = Objects.requireNonNull(mirrorNodeClient, "mirrorNodeClient must not be null"); + } + + @NonNull + @Override + public Page findAll() throws HieroException { + return mirrorNodeClient.queryContracts(); + } + + @NonNull + @Override + public Optional findById(@NonNull final ContractId contractId) throws HieroException { + return mirrorNodeClient.queryContractById(contractId); + } +} diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeJsonConverter.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeJsonConverter.java index 442d5671..50c91651 100644 --- a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeJsonConverter.java +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeJsonConverter.java @@ -12,13 +12,14 @@ import com.openelements.hiero.base.data.Balance; import com.openelements.hiero.base.data.Topic; import com.openelements.hiero.base.data.TopicMessage; +import com.openelements.hiero.base.data.Contract; +import com.openelements.hiero.base.data.Page; import java.util.List; import java.util.Optional; import org.jspecify.annotations.NonNull; public interface MirrorNodeJsonConverter { - @NonNull Optional toNft(@NonNull JSON json); @@ -59,4 +60,13 @@ public interface MirrorNodeJsonConverter { @NonNull List toTopicMessages(JSON json); + + @NonNull + Optional toContract(@NonNull JSON json); + + @NonNull + Page toContractPage(@NonNull JSON json); + + @NonNull + List toContracts(@NonNull JSON json); } diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeRestClient.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeRestClient.java index da2a0351..8c0d3077 100644 --- a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeRestClient.java +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/implementation/MirrorNodeRestClient.java @@ -1,6 +1,7 @@ package com.openelements.hiero.base.implementation; import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.TokenId; import com.hedera.hashgraph.sdk.TopicId; import com.openelements.hiero.base.HieroException; @@ -68,4 +69,15 @@ default JSON queryTopicMessageBySequenceNumber(TopicId topicId, long sequenceNum @NonNull JSON doGetCall(@NonNull String path) throws HieroException; + + @NonNull + default JSON queryContracts() throws HieroException { + return doGetCall("/api/v1/contracts"); + } + + @NonNull + default JSON queryContractById(@NonNull final ContractId contractId) throws HieroException { + Objects.requireNonNull(contractId, "contractId must not be null"); + return doGetCall("/api/v1/contracts/" + contractId); + } } diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/ContractRepository.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/ContractRepository.java new file mode 100644 index 00000000..0a99f731 --- /dev/null +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/ContractRepository.java @@ -0,0 +1,48 @@ +package com.openelements.hiero.base.mirrornode; + +import com.hedera.hashgraph.sdk.ContractId; +import com.openelements.hiero.base.HieroException; +import com.openelements.hiero.base.data.Contract; +import com.openelements.hiero.base.data.Page; +import java.util.Objects; +import java.util.Optional; +import org.jspecify.annotations.NonNull; + +/** + * Interface for interacting with smart contracts on a Hiero network. This interface provides methods for searching for contracts. + */ +public interface ContractRepository { + + /** + * Return all contracts. + * + * @return first page of contracts + * @throws HieroException if the search fails + */ + @NonNull + Page findAll() throws HieroException; + + /** + * Return a contract by its contract ID. + * + * @param contractId id of the contract + * @return {@link Optional} containing the found contract or null + * @throws HieroException if the search fails + */ + @NonNull + Optional findById(@NonNull ContractId contractId) throws HieroException; + + /** + * Return a contract by its contract ID. + * + * @param contractId id of the contract + * @return {@link Optional} containing the found contract or null + * @throws HieroException if the search fails + */ + @NonNull + default Optional findById(@NonNull String contractId) throws HieroException { + Objects.requireNonNull(contractId, "contractId must not be null"); + return findById(ContractId.fromString(contractId)); + } + +} diff --git a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/MirrorNodeClient.java b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/MirrorNodeClient.java index c2eac423..7c167ee9 100644 --- a/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/MirrorNodeClient.java +++ b/hiero-enterprise-base/src/main/java/com/openelements/hiero/base/mirrornode/MirrorNodeClient.java @@ -1,11 +1,13 @@ package com.openelements.hiero.base.mirrornode; import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.TokenId; import com.hedera.hashgraph.sdk.TopicId; import com.openelements.hiero.base.HieroException; import com.openelements.hiero.base.data.AccountInfo; import com.openelements.hiero.base.data.Balance; +import com.openelements.hiero.base.data.Contract; import com.openelements.hiero.base.data.ExchangeRates; import com.openelements.hiero.base.data.NetworkFee; import com.openelements.hiero.base.data.NetworkStake; @@ -143,7 +145,7 @@ default Optional queryNftsByTokenIdAndSerial(@NonNull String tokenId, long */ @NonNull default Optional queryNftsByAccountAndTokenIdAndSerial(@NonNull AccountId accountId, @NonNull TokenId tokenId, - long serialNumber) throws HieroException { + long serialNumber) throws HieroException { Objects.requireNonNull(accountId, "newAccountId must not be null"); return queryNftsByTokenIdAndSerial(tokenId, serialNumber) .filter(nft -> Objects.equals(nft.owner(), accountId)); @@ -160,7 +162,7 @@ default Optional queryNftsByAccountAndTokenIdAndSerial(@NonNull AccountId a */ @NonNull default Optional queryNftsByAccountAndTokenIdAndSerial(@NonNull String accountId, @NonNull String tokenId, - long serialNumber) throws HieroException { + long serialNumber) throws HieroException { Objects.requireNonNull(accountId, "accountId must not be null"); Objects.requireNonNull(tokenId, "tokenId must not be null"); return queryNftsByAccountAndTokenIdAndSerial(AccountId.fromString(accountId), TokenId.fromString(tokenId), @@ -459,4 +461,37 @@ default Optional queryTopicMessageBySequenceNumber(String topicId, @NonNull Page findAllNftTypes(); + + /** + * Queries all contracts. + * + * @return the contracts + * @throws HieroException if an error occurs + */ + @NonNull + Page queryContracts() throws HieroException; + + /** + * Queries a contract by its contract ID. + * + * @param contractId the contract ID + * @return the contract information + * @throws HieroException if an error occurs + */ + @NonNull + Optional queryContractById(@NonNull ContractId contractId) throws HieroException; + + /** + * Queries a contract by its contract ID. + * + * @param contractId the contract ID + * @return the contract information + * @throws HieroException if an error occurs + */ + @NonNull + default Optional queryContractById(@NonNull String contractId) throws HieroException { + Objects.requireNonNull(contractId, "contractId must not be null"); + return queryContractById(ContractId.fromString(contractId)); + } + } diff --git a/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/ClientProvider.java b/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/ClientProvider.java index 8ed8ea38..e6c99a6d 100644 --- a/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/ClientProvider.java +++ b/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/ClientProvider.java @@ -18,12 +18,14 @@ import com.openelements.hiero.base.implementation.SmartContractClientImpl; import com.openelements.hiero.base.implementation.TokenRepositoryImpl; import com.openelements.hiero.base.implementation.TransactionRepositoryImpl; +import com.openelements.hiero.base.implementation.ContractRepositoryImpl; import com.openelements.hiero.base.mirrornode.AccountRepository; import com.openelements.hiero.base.mirrornode.MirrorNodeClient; import com.openelements.hiero.base.mirrornode.NetworkRepository; import com.openelements.hiero.base.mirrornode.NftRepository; import com.openelements.hiero.base.mirrornode.TokenRepository; import com.openelements.hiero.base.mirrornode.TransactionRepository; +import com.openelements.hiero.base.mirrornode.ContractRepository; import com.openelements.hiero.base.protocol.ProtocolLayerClient; import com.openelements.hiero.base.verification.ContractVerificationClient; import com.openelements.hiero.microprofile.implementation.ContractVerificationClientImpl; @@ -159,4 +161,11 @@ TransactionRepository createTransactionRepository(@NonNull final MirrorNodeClien TokenRepository createTokenRepository(@NonNull final MirrorNodeClient mirrorNodeClient) { return new TokenRepositoryImpl(mirrorNodeClient); } + + @NonNull + @Produces + @ApplicationScoped + ContractRepository createContractRepository(@NonNull final MirrorNodeClient mirrorNodeClient) { + return new ContractRepositoryImpl(mirrorNodeClient); + } } diff --git a/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java b/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java index 86547503..64e69762 100644 --- a/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java +++ b/hiero-enterprise-microprofile/src/main/java/com/openelements/hiero/microprofile/implementation/MirrorNodeJsonConverterImpl.java @@ -1,6 +1,7 @@ package com.openelements.hiero.microprofile.implementation; import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.TokenId; import com.hedera.hashgraph.sdk.TokenSupplyType; import com.hedera.hashgraph.sdk.TokenType; @@ -8,12 +9,15 @@ import com.hedera.hashgraph.sdk.TransactionId; import com.hedera.hashgraph.sdk.PublicKey; import com.openelements.hiero.base.data.AccountInfo; +import com.openelements.hiero.base.data.Contract; import com.openelements.hiero.base.data.ExchangeRate; import com.openelements.hiero.base.data.ExchangeRates; import com.openelements.hiero.base.data.NetworkFee; import com.openelements.hiero.base.data.NetworkStake; import com.openelements.hiero.base.data.NetworkSupplies; import com.openelements.hiero.base.data.Nft; +import com.openelements.hiero.base.data.Page; +import com.openelements.hiero.base.data.SinglePage; import com.openelements.hiero.base.data.TransactionInfo; import com.openelements.hiero.base.data.Token; import com.openelements.hiero.base.data.TokenInfo; @@ -606,5 +610,103 @@ private Optional toBalance(JsonObject jsonObject) { throw new IllegalStateException("Can not parse JSON: " + jsonObject, e); } } -} + // Contract-related methods + + @Override + public @NonNull Optional toContract(@NonNull JsonObject jsonObject) { + Objects.requireNonNull(jsonObject, "jsonObject must not be null"); + if (jsonObject.isEmpty()) { + return Optional.empty(); + } + + try { + final ContractId contractId = ContractId.fromString(jsonObject.getString("contract_id")); + final PublicKey adminKey = jsonObject.get("admin_key") == null ? null + : PublicKey.fromString(jsonObject.get("admin_key").asJsonObject().getString("key")); + final AccountId autoRenewAccount = jsonObject.get("auto_renew_account") == null ? null + : AccountId.fromString(jsonObject.getString("auto_renew_account")); + final int autoRenewPeriod = jsonObject.get("auto_renew_period") == null ? 0 + : jsonObject.getJsonNumber("auto_renew_period").intValue(); + final Instant createdTimestamp = jsonObject.get("created_timestamp") == null + ? Instant.ofEpochSecond(0) + : Instant.ofEpochSecond(Long.parseLong(jsonObject.get("created_timestamp").toString().replaceAll("[^0-9].*$", ""))); + final boolean deleted = jsonObject.get("deleted") != null && jsonObject.getBoolean("deleted"); + final Instant expirationTimestamp = jsonObject.get("expiration_timestamp") == null ? null + : Instant.ofEpochSecond(Long.parseLong(jsonObject.getString("expiration_timestamp").split("\\.")[0])); + final String fileId = jsonObject.getString("file_id", null); + final String evmAddress = jsonObject.getString("evm_address", null); + final String memo = jsonObject.getString("memo", null); + final Integer maxAutomaticTokenAssociations = jsonObject.get("max_automatic_token_associations") == null ? null + : jsonObject.getJsonNumber("max_automatic_token_associations").intValue(); + final Long nonce = jsonObject.get("nonce") == null ? null + : jsonObject.getJsonNumber("nonce").longValue(); + final String obtainerId = jsonObject.getString("obtainer_id", null); + final boolean permanentRemoval = jsonObject.get("permanent_removal") != null && jsonObject.getBoolean("permanent_removal"); + final String proxyAccountId = jsonObject.getString("proxy_account_id", null); + final Instant fromTimestamp = Instant.ofEpochSecond(jsonObject.getJsonObject("timestamp").getJsonNumber("from").longValue()); + final Instant toTimestamp = Instant.ofEpochSecond(jsonObject.getJsonObject("timestamp").getJsonNumber("to").longValue()); + final String bytecode = jsonObject.getString("bytecode", null); + final String runtimeBytecode = jsonObject.getString("runtime_bytecode", null); + + return Optional.of(new Contract( + contractId, + adminKey, + autoRenewAccount, + autoRenewPeriod, + createdTimestamp, + deleted, + expirationTimestamp, + fileId, + evmAddress, + memo, + maxAutomaticTokenAssociations, + nonce, + obtainerId, + permanentRemoval, + proxyAccountId, + fromTimestamp, + toTimestamp, + bytecode, + runtimeBytecode + )); + } catch (final Exception e) { + throw new IllegalStateException("Can not parse JSON: " + jsonObject, e); + } + } + + @Override + public @NonNull Page toContractPage(@NonNull JsonObject jsonObject) { + Objects.requireNonNull(jsonObject, "jsonObject must not be null"); + if (jsonObject.isEmpty()) { + return new SinglePage<>(List.of()); + } + + try { + final List contracts = toContracts(jsonObject); + return new SinglePage<>(contracts); + } catch (final Exception e) { + throw new IllegalStateException("Can not parse JSON: " + jsonObject, e); + } + } + + @Override + public @NonNull List toContracts(@NonNull JsonObject jsonObject) { + Objects.requireNonNull(jsonObject, "jsonObject must not be null"); + if (!jsonObject.containsKey("contracts")) { + return List.of(); + } + final JsonArray contractsArray = jsonObject.getJsonArray("contracts"); + if (contractsArray == null) { + throw new IllegalArgumentException("No contracts array in JSON"); + } + final Spliterator spliterator = Spliterators.spliteratorUnknownSize(contractsArray.iterator(), + Spliterator.ORDERED); + return StreamSupport.stream(spliterator, false) + .map(n -> toContract(n.asJsonObject())) + .filter(optional -> optional.isPresent()) + .map(optional -> optional.get()) + .toList(); + } + +} diff --git a/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/HieroAutoConfiguration.java b/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/HieroAutoConfiguration.java index 35879b71..7dd04468 100644 --- a/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/HieroAutoConfiguration.java +++ b/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/HieroAutoConfiguration.java @@ -9,25 +9,27 @@ import com.openelements.hiero.base.TopicClient; import com.openelements.hiero.base.config.HieroConfig; import com.openelements.hiero.base.implementation.AccountClientImpl; -import com.openelements.hiero.base.implementation.AccountRepositoryImpl; import com.openelements.hiero.base.implementation.FileClientImpl; import com.openelements.hiero.base.implementation.FungibleTokenClientImpl; -import com.openelements.hiero.base.implementation.NetworkRepositoryImpl; import com.openelements.hiero.base.implementation.NftClientImpl; -import com.openelements.hiero.base.implementation.NftRepositoryImpl; import com.openelements.hiero.base.implementation.ProtocolLayerClientImpl; import com.openelements.hiero.base.implementation.SmartContractClientImpl; -import com.openelements.hiero.base.implementation.TokenRepositoryImpl; import com.openelements.hiero.base.implementation.TopicClientImpl; +import com.openelements.hiero.base.implementation.ContractRepositoryImpl; +import com.openelements.hiero.base.implementation.NftRepositoryImpl; +import com.openelements.hiero.base.implementation.NetworkRepositoryImpl; +import com.openelements.hiero.base.implementation.TokenRepositoryImpl; +import com.openelements.hiero.base.implementation.AccountRepositoryImpl; import com.openelements.hiero.base.implementation.TopicRepositoryImpl; import com.openelements.hiero.base.implementation.TransactionRepositoryImpl; import com.openelements.hiero.base.interceptors.ReceiveRecordInterceptor; -import com.openelements.hiero.base.mirrornode.AccountRepository; import com.openelements.hiero.base.mirrornode.MirrorNodeClient; +import com.openelements.hiero.base.mirrornode.TopicRepository; import com.openelements.hiero.base.mirrornode.NetworkRepository; +import com.openelements.hiero.base.mirrornode.AccountRepository; +import com.openelements.hiero.base.mirrornode.ContractRepository; import com.openelements.hiero.base.mirrornode.NftRepository; import com.openelements.hiero.base.mirrornode.TokenRepository; -import com.openelements.hiero.base.mirrornode.TopicRepository; import com.openelements.hiero.base.mirrornode.TransactionRepository; import com.openelements.hiero.base.protocol.ProtocolLayerClient; import com.openelements.hiero.base.verification.ContractVerificationClient; @@ -38,7 +40,6 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -145,6 +146,13 @@ NftRepository nftRepository(final MirrorNodeClient mirrorNodeClient) { return new NftRepositoryImpl(mirrorNodeClient); } + @Bean + @ConditionalOnProperty(prefix = "spring.hiero", name = "mirrorNodeSupported", + havingValue = "true", matchIfMissing = true) + ContractRepository contractRepository(final MirrorNodeClient mirrorNodeClient) { + return new ContractRepositoryImpl(mirrorNodeClient); + } + @Bean @ConditionalOnProperty(prefix = "spring.hiero", name = "mirrorNodeSupported", havingValue = "true", matchIfMissing = true) diff --git a/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/MirrorNodeJsonConverterImpl.java b/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/MirrorNodeJsonConverterImpl.java index 99903ed0..0db47391 100644 --- a/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/MirrorNodeJsonConverterImpl.java +++ b/hiero-enterprise-spring/src/main/java/com/openelements/hiero/spring/implementation/MirrorNodeJsonConverterImpl.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.hedera.hashgraph.sdk.AccountId; +import com.hedera.hashgraph.sdk.ContractId; import com.hedera.hashgraph.sdk.TokenId; import com.hedera.hashgraph.sdk.TokenSupplyType; import com.hedera.hashgraph.sdk.TokenType; @@ -9,12 +10,15 @@ import com.hedera.hashgraph.sdk.TransactionId; import com.hedera.hashgraph.sdk.PublicKey; import com.openelements.hiero.base.data.AccountInfo; +import com.openelements.hiero.base.data.Contract; import com.openelements.hiero.base.data.ExchangeRate; import com.openelements.hiero.base.data.ExchangeRates; import com.openelements.hiero.base.data.NetworkFee; import com.openelements.hiero.base.data.NetworkStake; import com.openelements.hiero.base.data.NetworkSupplies; import com.openelements.hiero.base.data.Nft; +import com.openelements.hiero.base.data.Page; +import com.openelements.hiero.base.data.SinglePage; import com.openelements.hiero.base.data.TransactionInfo; import com.openelements.hiero.base.data.Token; import com.openelements.hiero.base.data.TokenInfo; @@ -43,13 +47,9 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; import org.jspecify.annotations.NonNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter { - private static final Logger log = LoggerFactory.getLogger(MirrorNodeJsonConverterImpl.class); - @Override public Optional toNft(final JsonNode node) { Objects.requireNonNull(node, "jsonNode must not be null"); @@ -388,9 +388,9 @@ private CustomFee getCustomFee(JsonNode node) { .map(n ->{ final long amount = n.get("amount").asLong(); final AccountId accountId = n.get("collector_account_id").isNull()? - null : AccountId.fromString(n.get("collector_account_id").asText()); + null : AccountId.fromString(n.get("collector_account_id").asText()); final TokenId tokenId = n.get("denominating_token_id").isNull()? - null : TokenId.fromString(n.get("denominating_token_id").asText()); + null : TokenId.fromString(n.get("denominating_token_id").asText()); return new FixedFee(amount, accountId, tokenId); }) .toList(); @@ -623,4 +623,102 @@ private Stream jsonArrayToStream(@NonNull final JsonNode node) { return StreamSupport .stream(Spliterators.spliteratorUnknownSize(node.iterator(), Spliterator.ORDERED), false); } + + @Override + public @NonNull Optional toContract(@NonNull JsonNode node) { + Objects.requireNonNull(node, "jsonNode must not be null"); + if (node.isNull() || node.isEmpty()) { + return Optional.empty(); + } + + try { + final ContractId contractId = ContractId.fromString(node.get("contract_id").asText()); + final PublicKey adminKey = node.get("admin_key").isNull() ? null + : PublicKey.fromString(node.get("admin_key").get("key").asText()); + final AccountId autoRenewAccount = node.get("auto_renew_account").isNull() ? null + : AccountId.fromString(node.get("auto_renew_account").asText()); + final int autoRenewPeriod = node.get("auto_renew_period").isNull() ? 0 + : node.get("auto_renew_period").asInt(); + final Instant createdTimestamp = Instant.ofEpochSecond( + node.get("created_timestamp").isNumber() + ? node.get("created_timestamp").asLong() + : Long.parseLong(node.get("created_timestamp").asText().split("\\.")[0]) + ); + final boolean deleted = !node.get("deleted").isNull() && node.get("deleted").asBoolean(); + final Instant expirationTimestamp = node.get("expiration_timestamp").isNull() ? null + : Instant.ofEpochSecond(Long.parseLong(node.get("expiration_timestamp").asText().split("\\.")[0])); + final String fileId = node.get("file_id").isNull() ? null : node.get("file_id").asText(); + final String evmAddress = node.get("evm_address").isNull() ? null : node.get("evm_address").asText(); + final String memo = node.get("memo").isNull() ? null : node.get("memo").asText(); + final Integer maxAutomaticTokenAssociations = node.get("max_automatic_token_associations").isNull() ? null + : node.get("max_automatic_token_associations").asInt(); + final Long nonce = node.get("nonce").isNull() ? null : node.get("nonce").asLong(); + final String obtainerId = node.get("obtainer_id").isNull() ? null : node.get("obtainer_id").asText(); + final boolean permanentRemoval = !node.get("permanent_removal").isNull() && node.get("permanent_removal").asBoolean(); + final String proxyAccountId = node.get("proxy_account_id").isNull() ? null : node.get("proxy_account_id").asText(); + final Instant fromTimestamp = Instant.ofEpochSecond(node.get("timestamp").get("from").asLong()); + final Instant toTimestamp = Instant.ofEpochSecond(node.get("timestamp").get("to").asLong()); + final String bytecode = node.get("bytecode").isNull() ? null : node.get("bytecode").asText(); + final String runtimeBytecode = node.get("runtime_bytecode").isNull() ? null : node.get("runtime_bytecode").asText(); + + return Optional.of(new Contract( + contractId, + adminKey, + autoRenewAccount, + autoRenewPeriod, + createdTimestamp, + deleted, + expirationTimestamp, + fileId, + evmAddress, + memo, + maxAutomaticTokenAssociations, + nonce, + obtainerId, + permanentRemoval, + proxyAccountId, + fromTimestamp, + toTimestamp, + bytecode, + runtimeBytecode + )); + } catch (final Exception e) { + throw new JsonParseException(node, e); + } + } + + @Override + public @NonNull Page toContractPage(@NonNull JsonNode node) { + Objects.requireNonNull(node, "jsonNode must not be null"); + if (node.isNull() || node.isEmpty()) { + return new SinglePage<>(List.of()); + } + + try { + final List contracts = toContracts(node); + return new SinglePage<>(contracts); + } catch (final Exception e) { + throw new JsonParseException(node, e); + } + } + + @Override + public @NonNull List toContracts(@NonNull JsonNode node) { + Objects.requireNonNull(node, "jsonNode must not be null"); + if (!node.has("contracts")) { + return List.of(); + } + final JsonNode contractsNode = node.get("contracts"); + if (!contractsNode.isArray()) { + throw new IllegalArgumentException("Contracts node is not an array: " + contractsNode); + } + Spliterator spliterator = Spliterators.spliteratorUnknownSize(contractsNode.iterator(), + Spliterator.ORDERED); + return StreamSupport.stream(spliterator, false) + .map(n -> toContract(n)) + .filter(optional -> optional.isPresent()) + .map(optional -> optional.get()) + .toList(); + } + } diff --git a/hiero-enterprise-spring/src/test/java/com/openelements/hiero/spring/test/ContractRepositoryTest.java b/hiero-enterprise-spring/src/test/java/com/openelements/hiero/spring/test/ContractRepositoryTest.java new file mode 100644 index 00000000..f00ae68b --- /dev/null +++ b/hiero-enterprise-spring/src/test/java/com/openelements/hiero/spring/test/ContractRepositoryTest.java @@ -0,0 +1,47 @@ +package com.openelements.hiero.spring.test; + +import com.openelements.hiero.base.HieroException; +import com.openelements.hiero.base.data.Contract; +import com.openelements.hiero.base.data.Page; +import com.openelements.hiero.base.mirrornode.ContractRepository; +import java.util.Optional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = HieroTestConfig.class) +public class ContractRepositoryTest { + + @Autowired + private ContractRepository contractRepository; + + @Test + void testNullParam() { + Assertions.assertThrows(NullPointerException.class, () -> contractRepository.findById((String) null)); + } + + @Test + void testFindAll() throws HieroException { + // when + final Page contracts = contractRepository.findAll(); + + // then + Assertions.assertNotNull(contracts); + Assertions.assertNotNull(contracts.getData()); + } + + @Test + void testFindByIdWithNonExistentContract() throws HieroException { + // given + final String nonExistentContractId = "0.0.999999"; + + // when + final Optional result = contractRepository.findById(nonExistentContractId); + + // then + Assertions.assertNotNull(result); + Assertions.assertFalse(result.isPresent()); + } + +}