hiero-ledger / hiero-ledger/hiero-enterprise-java
Proposal: share ContractVerificationClient logic in base and use framework-specific transports
- Dominant language
- Java
- Stars
- 6
- Forks
- 21
- Avg merge
- 10h 27m
- Merged PRs (30d)
- 37
Description
### Summary
There is already an open issue for the immediate MicroProfile gap: [#88: microprofile: Implement ContractVerificationClient (Hashscan verify API)](https://github.com/hiero-ledger/hiero-enterprise-java/issues/88).
While looking at that issue, I noticed that simply copying the Spring implementation into MicroProfile would fix the immediate problem, but it would duplicate contract verification behavior across both framework modules. This proposal suggests using a shape similar to the existing mirror-node client: shared logic in `base`, framework-specific REST/JSON implementations in Spring and MicroProfile.
### Current situation
`ContractVerificationClient` is defined in `hiero-enterprise-base`, but implementation behavior currently lives in framework modules.
Today:
- Spring has a complete implementation: `hiero-enterprise-spring/.../ContractVerificationClientImplementation.java`
- MicroProfile has an incomplete implementation: `hiero-enterprise-microprofile/.../ContractVerificationClientImpl.java`
- In MicroProfile, `checkVerification(ContractId)` and `verify(...)` currently throw `UnsupportedOperationException`.
- The MicroProfile file-level check calls `checkVerification(ContractId)` first, so it cannot work end-to-end today.
- The Spring implementation currently owns both shared verification rules and framework-specific details such as HTTP calls and JSON parsing.
This creates a risk of behavior drift between Spring and MicroProfile.
### Existing repo pattern: mirror-node client
The repo already has a similar pattern in the mirror-node layer.
In `hiero-enterprise-base`:
- `MirrorNodeClient` is the public API.
- `AbstractMirrorNodeClient` implements the shared orchestration.
- `MirrorNodeRestClient` is the REST adapter contract.
- `MirrorNodeJsonConverter` is the JSON/domain conversion contract.
Then each framework module provides its own implementations:
Spring:
- `MirrorNodeClientImpl extends AbstractMirrorNodeClient`
- `MirrorNodeRestClientImpl implements MirrorNodeRestClient` using Spring `RestClient`
- `MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter` using Jackson
MicroProfile:
- `MirrorNodeClientImpl extends AbstractMirrorNodeClient`
- `MirrorNodeRestClientImpl implements MirrorNodeRestClient` using JAX-RS
- `MirrorNodeJsonConverterImpl implements MirrorNodeJsonConverter` using Jakarta JSON
That keeps shared behavior in `base`, while each framework keeps its native HTTP/JSON stack.
### Proposal
Apply the same idea to contract verification.
Suggested base types:
```java
public interface ContractVerificationRestClient {
@NonNull
ContractVerificationState checkVerification(
@NonNull String chainId, @NonNull String contractAddress)
throws HieroException;
@NonNull
ContractVerificationState verify(
@NonNull String chainId,
@NonNull String contractAddress,
@NonNull String contractName,
@NonNull Map files)
throws HieroException;
@NonNull
List fetchFiles(
@NonNull String chainId, @NonNull String contractAddress)
throws HieroException;
}
public record ContractVerificationFile(
@NonNull String name,
@NonNull String content) {}
```
Then add a shared implementation in `base`, for example:
```java
public final class DefaultContractVerificationClient implements ContractVerificationClient {
private final HieroConfig hieroConfig;
private final ContractVerificationRestClient restClient;
@Override
public ContractVerificationState checkVerification(@NonNull ContractId contractId)
throws HieroException {
return restClient.checkVerification(getChainId(), contractId.toSolidityAddress());
}
@Override
public boolean checkVerification(
@NonNull ContractId contractId,
@NonNull String fileName,
@NonNull String fileContent)
throws HieroException {
if (checkVerification(contractId) != ContractVerificationState.FULL) {
throw new IllegalStateException("Contract is not verified");
}
final List matches =
restClient.fetchFiles(getChainId(), contractId.toSolidityAddress()).stream()
.filter(file -> file.name().equals(fileName))
.toList();
if (matches.size() != 1) {
throw new RuntimeException("Expected exactly one result, got " + matches.size());
}
return Objects.equals(matches.get(0).content(), fileContent);
}
}
```
The framework modules would provide the concrete REST/JSON implementation:
- Spring: `ContractVerificationRestClientImpl` using Spring `RestClient` + Jackson
- MicroProfile: `ContractVerificationRestClientImpl` using JAX-RS `Client` + Jakarta JSON
### Why this direction
This would:
- Solve the MicroProfile gap described in #88.
- Avoid copying Spring’s full implementation into MicroProfile.
- Keep contract verification rules in one place.
- Keep `hiero-enterprise-base` free of concrete HTTP/JSON dependencies.
- Match the existing mirror-node layering style.
- Allow tests for shared verification behavior in `base` using a fake `ContractVerificationRestClient`.
### Scope / acceptance criteria
If maintainers agree with this direction:
- `ContractVerificationClient` behavior is implemented once in `hiero-enterprise-base`.
- Spring and MicroProfile delegate to the shared implementation.
- MicroProfile no longer throws `UnsupportedOperationException` for `checkVerification(ContractId)` or `verify(...)`.
- Existing Spring behavior for `FULL`, `NONE`, verification, and file-content checks is preserved.
- No Spring, Jackson, Jakarta JSON, or JAX-RS dependencies are added to `hiero-enterprise-base`.
- Base-level tests cover shared verification rules with a fake `ContractVerificationRestClient`.
### Open questions
- Should the shared implementation live in `org.hiero.base.implementation`, or somewhere closer to `org.hiero.base.verification`?
- Should the Hashscan base URL remain hardcoded in the framework-specific `ContractVerificationRestClientImpl`, or become configurable?
Contributor guide
Assessment
This issue has not been assessed yet.