diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a86d93f9..8bd96977 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -35,7 +35,7 @@ jobs: - name: Prepare Hiero Solo id: solo - uses: hiero-ledger/hiero-solo-action@692b186bd2e4c8d46b9deb1c067dc6ddcf0abcd7 # v0.18.0 + uses: hiero-ledger/hiero-solo-action@328bc84c3b00a990a151418144fd682a4eb76ea6 # v0.19.0 with: installMirrorNode: true hieroVersion: v0.66.0 @@ -43,7 +43,6 @@ jobs: mirrorNodePortRest: 5551 mirrorNodePortGrpc: 5600 mirrorNodePortWeb3Rest: 8545 - soloVersion: 0.46.1 - name: Wait for Mirror Node run: | diff --git a/hiero-enterprise-base/src/main/java/org/hiero/base/AccountClient.java b/hiero-enterprise-base/src/main/java/org/hiero/base/AccountClient.java index 794d8aeb..fcaed3c2 100644 --- a/hiero-enterprise-base/src/main/java/org/hiero/base/AccountClient.java +++ b/hiero-enterprise-base/src/main/java/org/hiero/base/AccountClient.java @@ -3,8 +3,10 @@ import com.hedera.hashgraph.sdk.AccountId; import com.hedera.hashgraph.sdk.Hbar; import com.hedera.hashgraph.sdk.PrivateKey; +import java.util.List; import java.util.Objects; import org.hiero.base.data.Account; +import org.hiero.base.data.HookDetails; import org.jspecify.annotations.NonNull; /** @@ -134,4 +136,30 @@ default Hbar getAccountBalance(@NonNull String accountId) throws HieroException * @throws HieroException if the balance could not be retrieved */ @NonNull Hbar getOperatorAccountBalance() throws HieroException; + + /** Adds a hook to an account. */ + default void addHook(@NonNull Account account, @NonNull HookDetails hookDetails) + throws HieroException { + Objects.requireNonNull(account, "account must not be null"); + Objects.requireNonNull(hookDetails, "hookDetails must not be null"); + updateHooks(account, List.of(hookDetails), List.of()); + } + + /** Deletes a hook from an account. */ + default void deleteHook(@NonNull Account account, long hookId) throws HieroException { + Objects.requireNonNull(account, "account must not be null"); + if (hookId < 0) { + throw new IllegalArgumentException("hookId must be non-negative"); + } + updateHooks(account, List.of(), List.of(hookId)); + } + + /** Updates account hooks by creating and/or deleting hooks. */ + default void updateHooks( + @NonNull Account account, + @NonNull List hooksToCreate, + @NonNull List hookIdsToDelete) + throws HieroException { + throw new UnsupportedOperationException("Account hook management is not implemented yet."); + } } diff --git a/hiero-enterprise-base/src/main/java/org/hiero/base/data/HookDetails.java b/hiero-enterprise-base/src/main/java/org/hiero/base/data/HookDetails.java new file mode 100644 index 00000000..17f14a13 --- /dev/null +++ b/hiero-enterprise-base/src/main/java/org/hiero/base/data/HookDetails.java @@ -0,0 +1,39 @@ +package org.hiero.base.data; + +import com.hedera.hashgraph.sdk.ContractId; +import com.hedera.hashgraph.sdk.EvmHookStorageUpdate; +import com.hedera.hashgraph.sdk.HookExtensionPoint; +import com.hedera.hashgraph.sdk.Key; +import java.util.List; +import java.util.Objects; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +/** + * High-level representation of a hook to attach to an account. + * + * @param extensionPoint the extension point where the hook should be attached + * @param hookId unique identifier of the hook on the owning entity + * @param evmHookContractId contract implementing the hook logic + * @param initialStorageUpdates initial EVM storage updates to apply on hook creation + * @param adminKey optional key used to authorize management operations for this hook + */ +public record HookDetails( + @NonNull HookExtensionPoint extensionPoint, + long hookId, + @NonNull ContractId evmHookContractId, + @NonNull List initialStorageUpdates, + @Nullable Key adminKey) { + + public HookDetails { + Objects.requireNonNull(extensionPoint, "extensionPoint must not be null"); + Objects.requireNonNull(evmHookContractId, "evmHookContractId must not be null"); + Objects.requireNonNull(initialStorageUpdates, "initialStorageUpdates must not be null"); + initialStorageUpdates.forEach( + update -> Objects.requireNonNull(update, "initialStorageUpdates must not contain null")); + initialStorageUpdates = List.copyOf(initialStorageUpdates); + if (hookId < 0) { + throw new IllegalArgumentException("hookId must be non-negative"); + } + } +} diff --git a/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/ProtocolLayerClient.java b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/ProtocolLayerClient.java index 74dd0132..865ec07f 100644 --- a/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/ProtocolLayerClient.java +++ b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/ProtocolLayerClient.java @@ -8,6 +8,8 @@ import org.hiero.base.protocol.data.AccountCreateResult; import org.hiero.base.protocol.data.AccountDeleteRequest; import org.hiero.base.protocol.data.AccountDeleteResult; +import org.hiero.base.protocol.data.AccountHookUpdateRequest; +import org.hiero.base.protocol.data.AccountHookUpdateResult; import org.hiero.base.protocol.data.AccountUpdateRequest; import org.hiero.base.protocol.data.AccountUpdateResult; import org.hiero.base.protocol.data.ContractCallRequest; @@ -177,6 +179,19 @@ public interface ProtocolLayerClient { @NonNull AccountDeleteResult executeAccountDeleteTransaction( @NonNull AccountDeleteRequest request) throws HieroException; + /** + * Executes an account hook update transaction. + * + * @param request the request containing hooks to create and hooks to delete on an account + * @return the result of the account hook update transaction + * @throws HieroException if the transaction could not be executed + */ + @NonNull + default AccountHookUpdateResult executeAccountHookUpdateTransaction( + @NonNull AccountHookUpdateRequest request) throws HieroException { + throw new UnsupportedOperationException("Account hook update transaction is not implemented."); + } + /** * Executes an account update transaction. * diff --git a/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateRequest.java b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateRequest.java new file mode 100644 index 00000000..98d74afa --- /dev/null +++ b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateRequest.java @@ -0,0 +1,77 @@ +package org.hiero.base.protocol.data; + +import com.hedera.hashgraph.sdk.Hbar; +import java.time.Duration; +import java.util.List; +import java.util.Objects; +import org.hiero.base.data.Account; +import org.hiero.base.data.HookDetails; +import org.jspecify.annotations.NonNull; + +public record AccountHookUpdateRequest( + @NonNull Hbar maxTransactionFee, + @NonNull Duration transactionValidDuration, + @NonNull Account account, + @NonNull List hooksToCreate, + @NonNull List hooksToDelete) + implements TransactionRequest { + + public AccountHookUpdateRequest { + Objects.requireNonNull(maxTransactionFee, "maxTransactionFee is required"); + Objects.requireNonNull(transactionValidDuration, "transactionValidDuration is required"); + Objects.requireNonNull(account, "account is required"); + Objects.requireNonNull(hooksToCreate, "hooksToCreate is required"); + Objects.requireNonNull(hooksToDelete, "hooksToDelete is required"); + if (maxTransactionFee.toTinybars() < 0) { + throw new IllegalArgumentException("maxTransactionFee must be non-negative"); + } + if (transactionValidDuration.isNegative() || transactionValidDuration.isZero()) { + throw new IllegalArgumentException("transactionValidDuration must be positive"); + } + hooksToDelete.forEach( + hookId -> { + Objects.requireNonNull(hookId, "hooksToDelete must not contain null values"); + if (hookId < 0) { + throw new IllegalArgumentException("hook IDs in hooksToDelete must be non-negative"); + } + }); + } + + @NonNull + public static AccountHookUpdateRequest addHook( + @NonNull Account account, @NonNull HookDetails hookToCreate) { + Objects.requireNonNull(hookToCreate, "hookToCreate is required"); + return new AccountHookUpdateRequest( + DEFAULT_MAX_TRANSACTION_FEE, + DEFAULT_TRANSACTION_VALID_DURATION, + account, + List.of(hookToCreate), + List.of()); + } + + @NonNull + public static AccountHookUpdateRequest deleteHook(@NonNull Account account, long hookIdToDelete) { + if (hookIdToDelete < 0) { + throw new IllegalArgumentException("hookIdToDelete must be non-negative"); + } + return new AccountHookUpdateRequest( + DEFAULT_MAX_TRANSACTION_FEE, + DEFAULT_TRANSACTION_VALID_DURATION, + account, + List.of(), + List.of(hookIdToDelete)); + } + + @NonNull + public static AccountHookUpdateRequest of( + @NonNull Account account, + @NonNull List hooksToCreate, + @NonNull List hooksToDelete) { + return new AccountHookUpdateRequest( + DEFAULT_MAX_TRANSACTION_FEE, + DEFAULT_TRANSACTION_VALID_DURATION, + account, + hooksToCreate, + hooksToDelete); + } +} diff --git a/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateResult.java b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateResult.java new file mode 100644 index 00000000..50efb01d --- /dev/null +++ b/hiero-enterprise-base/src/main/java/org/hiero/base/protocol/data/AccountHookUpdateResult.java @@ -0,0 +1,28 @@ +package org.hiero.base.protocol.data; + +import com.hedera.hashgraph.sdk.Hbar; +import com.hedera.hashgraph.sdk.Status; +import com.hedera.hashgraph.sdk.TransactionId; +import java.time.Instant; +import java.util.Objects; +import org.jspecify.annotations.NonNull; + +public record AccountHookUpdateResult( + @NonNull TransactionId transactionId, + @NonNull Status status, + @NonNull byte[] transactionHash, + @NonNull Instant consensusTimestamp, + @NonNull Hbar transactionFee) + implements TransactionRecord { + + public AccountHookUpdateResult { + Objects.requireNonNull(transactionId, "transactionId must not be null"); + Objects.requireNonNull(status, "status must not be null"); + Objects.requireNonNull(transactionHash, "transactionHash must not be null"); + Objects.requireNonNull(consensusTimestamp, "consensusTimestamp must not be null"); + Objects.requireNonNull(transactionFee, "transactionFee must not be null"); + if (transactionFee.toTinybars() < 0) { + throw new IllegalArgumentException("transactionFee must be non-negative"); + } + } +}