diff --git a/.github/workflows/platform-zxcron-release-jrs-regression.yaml b/.github/workflows/platform-zxcron-release-jrs-regression.yaml index 50c63b349e0..fe4a1377154 100644 --- a/.github/workflows/platform-zxcron-release-jrs-regression.yaml +++ b/.github/workflows/platform-zxcron-release-jrs-regression.yaml @@ -47,10 +47,10 @@ jobs: while IFS= read -r line; do [[ "${line}" =~ ^release/([0-9]+).([0-9]+) ]] || continue - + major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" - + if [[ "${major}" -eq 0 && "${minor}" -lt 38 ]]; then continue fi diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/EntityType.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/EntityType.java new file mode 100644 index 00000000000..23f732db566 --- /dev/null +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/EntityType.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.hedera.node.app.spi.validation; + +/** + * Various entity types in all services + */ +public enum EntityType { + ACCOUNT, + CONTRACT, + FILE, + NFT, + SCHEDULE, + TOKEN, + TOKEN_ASSOCIATION, + TOPIC +} diff --git a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/ExpiryValidator.java b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/ExpiryValidator.java index 273b6568b2d..a45101596d6 100644 --- a/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/ExpiryValidator.java +++ b/hedera-node/hedera-app-spi/src/main/java/com/hedera/node/app/spi/validation/ExpiryValidator.java @@ -16,8 +16,10 @@ package com.hedera.node.app.spi.validation; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.TransactionHandler; +import edu.umd.cs.findbugs.annotations.NonNull; /** * A type that any {@link TransactionHandler} can use to validate the expiry @@ -32,7 +34,8 @@ public interface ExpiryValidator { * @param creationMetadata the expiry metadata for the attempted creation * @throws HandleException if the metadata is invalid */ - ExpiryMeta resolveCreationAttempt(boolean entityCanSelfFundRenewal, ExpiryMeta creationMetadata); + @NonNull + ExpiryMeta resolveCreationAttempt(boolean entityCanSelfFundRenewal, @NonNull ExpiryMeta creationMetadata); /** * Validates the expiry metadata for an attempt to update an entity, and returns the @@ -44,5 +47,37 @@ public interface ExpiryValidator { * @return the expiry metadata that will result from the update * @throws HandleException if the metadata is invalid */ - ExpiryMeta resolveUpdateAttempt(ExpiryMeta currentMetadata, ExpiryMeta updateMetadata); + @NonNull + ExpiryMeta resolveUpdateAttempt(@NonNull ExpiryMeta currentMetadata, @NonNull ExpiryMeta updateMetadata); + + /** + * + * @return OK if the account is not expired, otherwise the appropriate error code + */ + /** + * Gets the expiration status of an entity based on the {@link EntityType}. + * @param entityType entity type + * @param isMarkedExpired if the entity is marked as expired and pending removal + * @param balanceAvailableForSelfRenewal if balance is available for self renewal + * @return OK if the entity is not expired, otherwise the appropriate error code + */ + @NonNull + ResponseCodeEnum expirationStatus( + @NonNull final EntityType entityType, + final boolean isMarkedExpired, + final long balanceAvailableForSelfRenewal); + + /** + * Gets the expiration status of an account and returns if the account is detached + * @param entityType entity type + * @param isMarkedExpired if the entity is marked as expired and pending removal + * @param balanceAvailableForSelfRenewal if balance is available for self renewal + * @return true if the account is detached, otherwise false + */ + default boolean isDetached( + @NonNull final EntityType entityType, + final boolean isMarkedExpired, + final long balanceAvailableForSelfRenewal) { + return expirationStatus(entityType, isMarkedExpired, balanceAvailableForSelfRenewal) != ResponseCodeEnum.OK; + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcher.java index 2973387882b..6ffb5a840f6 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcher.java @@ -85,6 +85,7 @@ public void dispatchHandle(@NonNull final HandleContext context) { case CONSENSUS_DELETE_TOPIC -> dispatchConsensusDeleteTopic(context); case CONSENSUS_SUBMIT_MESSAGE -> dispatchConsensusSubmitMessage(context); case CRYPTO_CREATE_ACCOUNT -> dispatchCryptoCreate(context); + case CRYPTO_DELETE -> dispatchCryptoDelete(context); case TOKEN_ASSOCIATE -> dispatchTokenAssociate(context); case TOKEN_FREEZE -> dispatchTokenFreeze(context); case TOKEN_UNFREEZE -> dispatchTokenUnfreeze(context); @@ -175,6 +176,17 @@ private void dispatchTokenGrantKycToAccount(@NonNull final HandleContext handleC finishTokenGrantKycToAccount(handleContext); } + private void dispatchCryptoDelete(@NonNull final HandleContext handleContext) { + final var handler = handlers.cryptoDeleteHandler(); + handler.handle(handleContext); + finishCryptoDelete(handleContext); + } + + protected void finishCryptoDelete(@NonNull final HandleContext handleContext) { + final var accountStore = handleContext.writableStore(WritableAccountStore.class); + accountStore.commit(); + } + private void finishTokenGrantKycToAccount(@NonNull final HandleContext handleContext) { final var tokenRelStore = handleContext.writableStore(WritableTokenRelationStore.class); tokenRelStore.commit(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidator.java index 134b2fd66d4..f17624938bf 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidator.java @@ -26,6 +26,7 @@ import com.hedera.node.app.spi.validation.AttributeValidator; import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.config.ConfigProvider; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.function.LongSupplier; import javax.inject.Inject; @@ -43,7 +44,8 @@ public MonoExpiryValidator( @NonNull final AccountStore accountStore, @NonNull final AttributeValidator attributeValidator, @NonNull final LongSupplier consensusSecondNow, - @NonNull final HederaNumbers numbers) { + @NonNull final HederaNumbers numbers, + @NonNull final ConfigProvider configProvider) { super( id -> { try { @@ -54,6 +56,7 @@ public MonoExpiryValidator( }, attributeValidator, consensusSecondNow, - numbers); + numbers, + configProvider); } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/StandardizedExpiryValidator.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/StandardizedExpiryValidator.java index b6a739b709c..8f0ba8878e1 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/StandardizedExpiryValidator.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/handle/validation/StandardizedExpiryValidator.java @@ -16,21 +16,29 @@ package com.hedera.node.app.workflows.handle.validation; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_EXPIRED_AND_PENDING_REMOVAL; import static com.hedera.hapi.node.base.ResponseCodeEnum.EXPIRATION_REDUCTION_NOT_ALLOWED; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; +import com.hedera.hapi.node.base.ResponseCodeEnum; import com.hedera.node.app.service.mono.config.HederaNumbers; import com.hedera.node.app.service.mono.store.models.Id; import com.hedera.node.app.spi.validation.AttributeValidator; +import com.hedera.node.app.spi.validation.EntityType; import com.hedera.node.app.spi.validation.ExpiryMeta; import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.data.AutoRenewConfig; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Objects; import java.util.function.Consumer; import java.util.function.LongSupplier; +import javax.inject.Inject; /** * An implementation of {@link ExpiryValidator} that encapsulates the current policies @@ -42,16 +50,20 @@ public class StandardizedExpiryValidator implements ExpiryValidator { private final LongSupplier consensusSecondNow; private final AttributeValidator attributeValidator; private final HederaNumbers numbers; + private final ConfigProvider configProvider; + @Inject public StandardizedExpiryValidator( @NonNull final Consumer idValidator, @NonNull final AttributeValidator attributeValidator, @NonNull final LongSupplier consensusSecondNow, - @NonNull final HederaNumbers numbers) { + @NonNull final HederaNumbers numbers, + @NonNull final ConfigProvider configProvider) { this.attributeValidator = Objects.requireNonNull(attributeValidator); this.consensusSecondNow = Objects.requireNonNull(consensusSecondNow); this.numbers = Objects.requireNonNull(numbers); this.idValidator = Objects.requireNonNull(idValidator); + this.configProvider = Objects.requireNonNull(configProvider); } /** @@ -115,6 +127,27 @@ public ExpiryMeta resolveUpdateAttempt(final ExpiryMeta currentMeta, final Expir return new ExpiryMeta(resolvedExpiry, resolvedAutoRenewPeriod, resolvedAutoRenewNum); } + /** + * {@inheritDoc} + */ + @Override + public ResponseCodeEnum expirationStatus( + @NonNull final EntityType entityType, + final boolean isMarkedExpired, + final long balanceAvailableForSelfRenewal) { + final var isSmartContract = entityType.equals(EntityType.CONTRACT); + final var autoRenewConfig = configProvider.getConfiguration().getConfigData(AutoRenewConfig.class); + if (!autoRenewConfig.isAutoRenewEnabled() + || balanceAvailableForSelfRenewal > 0 + || !isMarkedExpired + || isExpiryDisabled( + isSmartContract, autoRenewConfig.expireAccounts(), autoRenewConfig.expireContracts())) { + return OK; + } + + return isSmartContract ? CONTRACT_EXPIRED_AND_PENDING_REMOVAL : ACCOUNT_EXPIRED_AND_PENDING_REMOVAL; + } + /** * Helper to check if an entity with the given metadata has a completely specified * auto-renew configuration. This is true if either the {@link ExpiryMeta} includes @@ -148,4 +181,8 @@ private void validateAutoRenewAccount(final long shard, final long realm, final final var autoRenewId = new Id(numbers.shard(), numbers.realm(), num); idValidator.accept(autoRenewId); } + + private boolean isExpiryDisabled(boolean smartContract, boolean expireAccounts, boolean expireContracts) { + return (smartContract && !expireContracts) || (!smartContract && !expireAccounts); + } } diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcherTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcherTest.java index 454764cc9e9..a548dbf1914 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcherTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/MonoTransactionDispatcherTest.java @@ -665,6 +665,19 @@ void dispatchesCryptoCreateAsExpected() { verify(writableAccountStore).commit(); } + @Test + void dispatchesCryptoDeleteAsExpected() { + final var txnBody = TransactionBody.newBuilder() + .cryptoDelete(CryptoDeleteTransactionBody.DEFAULT) + .build(); + + given(handleContext.body()).willReturn(txnBody); + given(handleContext.writableStore(WritableAccountStore.class)).willReturn(writableAccountStore); + + dispatcher.dispatchHandle(handleContext); + verify(writableAccountStore).commit(); + } + @Test void doesntCommitWhenUsageLimitsExceeded() { final var txnBody = TransactionBody.newBuilder() diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidatorTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidatorTest.java index 3d7e8b3986a..b48ff94b01e 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidatorTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/handle/validation/MonoExpiryValidatorTest.java @@ -16,28 +16,40 @@ package com.hedera.node.app.workflows.handle.validation; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.CONTRACT_EXPIRED_AND_PENDING_REMOVAL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; import static com.hedera.node.app.spi.validation.ExpiryMeta.NA; import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INVALID_AUTORENEW_ACCOUNT; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.willThrow; import com.hedera.hapi.node.base.ResponseCodeEnum; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.node.app.config.VersionedConfigImpl; import com.hedera.node.app.service.evm.exceptions.InvalidTransactionException; import com.hedera.node.app.service.mono.config.HederaNumbers; import com.hedera.node.app.service.mono.store.AccountStore; import com.hedera.node.app.service.mono.store.models.Id; import com.hedera.node.app.spi.validation.AttributeValidator; +import com.hedera.node.app.spi.validation.EntityType; import com.hedera.node.app.spi.validation.ExpiryMeta; import com.hedera.node.app.spi.workflows.HandleException; +import com.hedera.node.config.ConfigProvider; +import com.hedera.node.config.VersionedConfiguration; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import java.util.function.LongSupplier; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Mock.Strictness; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) @@ -48,6 +60,7 @@ class MonoExpiryValidatorTest { private static final long aPeriod = 666_666L; private static final long bPeriod = 777_777L; private static final long anAutoRenewNum = 888; + private static final long DEFAULT_CONFIG_VERSION = 1; @Mock private AttributeValidator attributeValidator; @@ -61,11 +74,23 @@ class MonoExpiryValidatorTest { @Mock private HederaNumbers numbers; + @Mock + private Account account; + + @Mock(strictness = Strictness.LENIENT) + private ConfigProvider configProvider; + + private VersionedConfiguration configuration; + private MonoExpiryValidator subject; @BeforeEach void setUp() { - subject = new MonoExpiryValidator(accountStore, attributeValidator, consensusSecondNow, numbers); + subject = + new MonoExpiryValidator(accountStore, attributeValidator, consensusSecondNow, numbers, configProvider); + configuration = + new VersionedConfigImpl(new HederaTestConfigBuilder().getOrCreateConfig(), DEFAULT_CONFIG_VERSION); + given(configProvider.getConfiguration()).willReturn(configuration); } @Test @@ -279,6 +304,33 @@ void canUseWildcardForRemovingAutoRenewAccount() { assertEquals(update, subject.resolveUpdateAttempt(current, update)); } + @Test + void checksIfAccountIsDetachedIfBalanceZero() { + assertEquals(OK, subject.expirationStatus(EntityType.ACCOUNT, false, 0)); + assertFalse(subject.isDetached(EntityType.ACCOUNT, false, 0)); + } + + @Test + void failsIfAccountExpiredAndPendingRemoval() { + assertEquals(ACCOUNT_EXPIRED_AND_PENDING_REMOVAL, subject.expirationStatus(EntityType.ACCOUNT, true, 0L)); + assertTrue(subject.isDetached(EntityType.ACCOUNT, true, 0)); + + assertEquals(CONTRACT_EXPIRED_AND_PENDING_REMOVAL, subject.expirationStatus(EntityType.CONTRACT, true, 0L)); + assertTrue(subject.isDetached(EntityType.CONTRACT, true, 0)); + } + + @Test + void notDetachedIfAccountNotExpired() { + assertEquals(OK, subject.expirationStatus(EntityType.ACCOUNT, false, 0L)); + assertFalse(subject.isDetached(EntityType.ACCOUNT, false, 10)); + } + + @Test + void notDetachedIfAutoRenewDisabled() { + assertEquals(OK, subject.expirationStatus(EntityType.ACCOUNT, false, 0L)); + assertFalse(subject.isDetached(EntityType.ACCOUNT, false, 0)); + } + private static void assertFailsWith(final ResponseCodeEnum expected, final Runnable runnable) { final var e = assertThrows(HandleException.class, runnable::run); assertEquals(expected, e.getStatus()); diff --git a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/AutoRenewConfig.java b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/AutoRenewConfig.java index 0b9272fe3ba..c7044a666c4 100644 --- a/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/AutoRenewConfig.java +++ b/hedera-node/hedera-config/src/main/java/com/hedera/node/config/data/AutoRenewConfig.java @@ -17,7 +17,22 @@ package com.hedera.node.config.data; import com.swirlds.config.api.ConfigData; +import com.swirlds.config.api.ConfigProperty; +import java.util.Set; @ConfigData("autoRenew") -public record AutoRenewConfig( // @ConfigProperty(defaultValue = "") Set targetTypes - ) {} +public record AutoRenewConfig( + // @ConfigProperty(defaultValue = "") Set targetTypes + @ConfigProperty(defaultValue = "CONTRACT,ACCOUNT") Set targetTypes) { + public boolean expireContracts() { + return targetTypes.contains("CONTRACT"); + } + + public boolean expireAccounts() { + return targetTypes.contains("ACCOUNT"); + } + + public boolean isAutoRenewEnabled() { + return !targetTypes.isEmpty(); + } +} diff --git a/hedera-node/hedera-token-service-impl/build.gradle.kts b/hedera-node/hedera-token-service-impl/build.gradle.kts index c9afac2cb6e..b6741d4091d 100644 --- a/hedera-node/hedera-token-service-impl/build.gradle.kts +++ b/hedera-node/hedera-token-service-impl/build.gradle.kts @@ -30,7 +30,9 @@ configurations.all { dependencies { implementation(project(":hedera-node:hapi")) implementation(project(":hedera-node:hedera-config")) - testImplementation(project(mapOf("path" to ":hedera-node:hedera-app"))) + implementation(project(":hedera-node:hedera-config")) + testImplementation(project(":hedera-node:hedera-app")) + testImplementation(testFixtures(project(":hedera-node:hedera-config"))) annotationProcessor(libs.dagger.compiler) api(project(":hedera-node:hedera-token-service")) implementation(project(":hedera-node:hapi")) diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableAccountStore.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableAccountStore.java index 9d1260bcd5c..32ed9dceb3e 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableAccountStore.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableAccountStore.java @@ -127,6 +127,15 @@ public Optional getForModify(final AccountID id) { return Optional.ofNullable(accountState.getForModify(EntityNumVirtualKey.fromLong(accountNum))); } + /** + * Removes the {@link Account} with the given {@link AccountID} from the state. + * This will add value of the accountId to num in the modifications in state. + * @param accountID - the account id of the account to be removed. + */ + public void remove(@NonNull final AccountID accountID) { + accountState.remove(EntityNumVirtualKey.fromLong(accountID.accountNum())); + } + /** * Returns the number of accounts in the state. It also includes modifications in the {@link * WritableKVState}. diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteHandler.java index 33edd5ee1b1..7093cd7d37e 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoDeleteHandler.java @@ -16,20 +16,35 @@ package com.hedera.node.app.service.token.impl.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_IS_TREASURY; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INSUFFICIENT_ACCOUNT_BALANCE; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSFER_ACCOUNT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT; +import static com.hedera.node.app.spi.workflows.HandleException.validateFalse; +import static com.hedera.node.app.spi.workflows.HandleException.validateTrue; import static java.util.Objects.requireNonNull; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.HederaFunctionality; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.token.CryptoDeleteTransactionBody; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.token.impl.WritableAccountStore; +import com.hedera.node.app.spi.validation.EntityType; +import com.hedera.node.app.spi.validation.ExpiryValidator; import com.hedera.node.app.spi.workflows.HandleContext; -import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.PreHandleContext; import com.hedera.node.app.spi.workflows.TransactionHandler; import edu.umd.cs.findbugs.annotations.NonNull; import javax.inject.Inject; import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; /** * This class contains all workflow-related functionality regarding {@link @@ -42,9 +57,23 @@ public CryptoDeleteHandler() { // Exists for injection } + @Override + public void pureChecks(@NonNull final TransactionBody txn) throws PreCheckException { + final var op = txn.cryptoDeleteOrThrow(); + + if (!op.hasDeleteAccountID() || !op.hasTransferAccountID()) { + throw new PreCheckException(ACCOUNT_ID_DOES_NOT_EXIST); + } + + if (op.deleteAccountIDOrThrow().equals(op.transferAccountIDOrThrow())) { + throw new PreCheckException(TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT); + } + } + @Override public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { requireNonNull(context); + pureChecks(context.body()); final var op = context.body().cryptoDeleteOrThrow(); final var deleteAccountId = op.deleteAccountIDOrElse(AccountID.DEFAULT); final var transferAccountId = op.transferAccountIDOrElse(AccountID.DEFAULT); @@ -53,7 +82,119 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx } @Override - public void handle(@NonNull final HandleContext context) throws HandleException { - throw new UnsupportedOperationException("Not implemented"); + public void handle(@NonNull final HandleContext context) { + requireNonNull(context); + + final var txn = context.body(); + final var accountStore = context.writableStore(WritableAccountStore.class); + + final var op = txn.cryptoDelete(); + + // validate the semantics involving dynamic properties and state. + // Gets delete and transfer accounts from state + final var deleteAndTransferAccounts = validateSemantics(op, accountStore, context.expiryValidator()); + transferToBeneficiary(context.expiryValidator(), deleteAndTransferAccounts, accountStore); + + // get the account from account store that has all balance changes + // commit the account with deleted flag set to true + final var updatedDeleteAccount = accountStore.get(op.deleteAccountID()).get(); + accountStore.put(updatedDeleteAccount.copyBuilder().deleted(true).build()); + } + + /* ----------------------------- Helper methods -------------------------------- */ + /** + * Validate the expiration on delete and transfer accounts. Transfer balance from delete account + * to transfer account if valid. + * @param expiryValidator expiry validator + * @param deleteAndTransferAccounts pair of delete and transfer accounts + * @param accountStore writable account store + */ + private void transferToBeneficiary( + @NonNull final ExpiryValidator expiryValidator, + @NonNull final Pair deleteAndTransferAccounts, + @NonNull final WritableAccountStore accountStore) { + final var fromAccount = deleteAndTransferAccounts.getLeft(); + final var toAccount = deleteAndTransferAccounts.getRight(); + final var adjustment = fromAccount.tinybarBalance(); + // + final long newFromBalance = computeNewBalance(expiryValidator, fromAccount, -1 * adjustment); + final long newToBalance = computeNewBalance(expiryValidator, toAccount, adjustment); + + accountStore.put( + fromAccount.copyBuilder().tinybarBalance(newFromBalance).build()); + accountStore.put(toAccount.copyBuilder().tinybarBalance(newToBalance).build()); + } + + /** + * Computes new balance for the account based on adjustment. Also validates expiration checks. + * @param expiryValidator expiry validator + * @param account account whose balance should be adjusted + * @param adjustment adjustment amount + * @return new balance + */ + private long computeNewBalance( + final ExpiryValidator expiryValidator, final Account account, final long adjustment) { + validateTrue(!account.deleted(), ACCOUNT_DELETED); + validateTrue( + !expiryValidator.isDetached( + EntityType.ACCOUNT, account.expiredAndPendingRemoval(), account.tinybarBalance()), + ACCOUNT_EXPIRED_AND_PENDING_REMOVAL); + final long balance = account.tinybarBalance(); + validateTrue(balance + adjustment >= 0, INSUFFICIENT_ACCOUNT_BALANCE); + return balance + adjustment; + } + + private Pair validateSemantics( + @NonNull final CryptoDeleteTransactionBody op, + @NonNull final WritableAccountStore accountStore, + @NonNull final ExpiryValidator expiryValidator) { + final var deleteAccountId = op.deleteAccountID(); + final var transferAccountId = op.transferAccountID(); + + // validate if accounts exist + final var optDeleteAccount = accountStore.get(deleteAccountId); + validateTrue(optDeleteAccount.isPresent(), INVALID_ACCOUNT_ID); + + final var optTransferAccount = accountStore.get(transferAccountId); + validateTrue(optTransferAccount.isPresent(), INVALID_TRANSFER_ACCOUNT_ID); + + // if the account is treasury for any other token, it can't be deleted + final var deletedAccount = optDeleteAccount.get(); + final var transferAccount = optTransferAccount.get(); + validateFalse(deletedAccount.numberTreasuryTitles() > 0, ACCOUNT_IS_TREASURY); + + // checks if accounts are detached + final var isExpired = areAccountsDetached(deletedAccount, transferAccount, expiryValidator); + validateFalse(isExpired, ACCOUNT_EXPIRED_AND_PENDING_REMOVAL); + + // An account can't be deleted if there are any tokens associated with this account + validateTrue(deletedAccount.numberPositiveBalances() == 0, TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES); + + return Pair.of(deletedAccount, transferAccount); + } + + /** + * Checks if delete account and transfer account are detached + * @param deleteAccount account to be deleted + * @param transferAccount beneficiary account + * @param expiryValidator expiry validator + * @return true if any on of the accounts is detached, false otherwise + */ + private boolean areAccountsDetached( + @NonNull Account deleteAccount, + @NonNull Account transferAccount, + @NonNull final ExpiryValidator expiryValidator) { + return expiryValidator.isDetached( + getEntityType(deleteAccount), + deleteAccount.expiredAndPendingRemoval(), + deleteAccount.tinybarBalance()) + || expiryValidator.isDetached( + getEntityType(transferAccount), + transferAccount.expiredAndPendingRemoval(), + transferAccount.tinybarBalance()); + } + + private EntityType getEntityType(@NonNull final Account account) { + return account.smartContract() ? EntityType.CONTRACT : EntityType.ACCOUNT; } } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java index af270ca3f42..80e252413a8 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/module-info.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/module-info.java @@ -22,7 +22,8 @@ com.hedera.node.app.service.token.impl.TokenServiceImpl; exports com.hedera.node.app.service.token.impl.handlers to - com.hedera.node.app; + com.hedera.node.app, + com.hedera.node.app.service.token.impl.test; exports com.hedera.node.app.service.token.impl.serdes; exports com.hedera.node.app.service.token.impl; exports com.hedera.node.app.service.token.impl.records to diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteHandlerTest.java index e8ea2c2aff8..a9966bfc2bb 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoDeleteHandlerTest.java @@ -16,13 +16,27 @@ package com.hedera.node.app.service.token.impl.test.handlers; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_DELETED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_EXPIRED_AND_PENDING_REMOVAL; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_ID_DOES_NOT_EXIST; +import static com.hedera.hapi.node.base.ResponseCodeEnum.ACCOUNT_IS_TREASURY; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID; import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_TRANSFER_ACCOUNT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT; import static com.hedera.node.app.spi.fixtures.Assertions.assertThrowsPreCheck; +import static com.hedera.node.app.spi.fixtures.workflows.ExceptionConditions.responseCode; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertIterableEquals; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.lenient; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.TransactionID; @@ -31,33 +45,51 @@ import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; import com.hedera.node.app.service.token.impl.ReadableAccountStoreImpl; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.handlers.CryptoDeleteHandler; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; +import com.hedera.node.app.spi.validation.EntityType; +import com.hedera.node.app.spi.validation.ExpiryValidator; +import com.hedera.node.app.spi.workflows.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; +import com.swirlds.config.api.Configuration; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class CryptoDeleteHandlerTest extends CryptoHandlerTestBase { + @Mock + private HandleContext handleContext; + + @Mock + private ExpiryValidator expiryValidator; + + private Configuration configuration; + private CryptoDeleteHandler subject = new CryptoDeleteHandler(); @BeforeEach public void setUp() { super.setUp(); - readableAccounts = emptyReadableAccountStateBuilder() - .value(EntityNumVirtualKey.fromLong(accountNum), account) - .value(EntityNumVirtualKey.fromLong(deleteAccountNum), deleteAccount) - .value(EntityNumVirtualKey.fromLong(transferAccountNum), transferAccount) - .build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); + configuration = new HederaTestConfigBuilder().getOrCreateConfig(); + updateReadableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); + updateWritableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); + + lenient().when(handleContext.configuration()).thenReturn(configuration); + lenient().when(handleContext.writableStore(WritableAccountStore.class)).thenReturn(writableStore); } @Test void preHandlesCryptoDeleteIfNoReceiverSigRequired() throws PreCheckException { - given(transferAccount.receiverSigRequired()).willReturn(false); - given(deleteAccount.key()).willReturn(accountKey); - final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); final var context = new FakePreHandleContext(readableStore, txn); @@ -70,10 +102,6 @@ void preHandlesCryptoDeleteIfNoReceiverSigRequired() throws PreCheckException { @Test void preHandlesCryptoDeleteIfReceiverSigRequiredVanilla() throws PreCheckException { - given(transferAccount.key()).willReturn(key); - given(transferAccount.receiverSigRequired()).willReturn(true); - given(deleteAccount.key()).willReturn(accountKey); - final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); final var context = new FakePreHandleContext(readableStore, txn); @@ -86,7 +114,15 @@ void preHandlesCryptoDeleteIfReceiverSigRequiredVanilla() throws PreCheckExcepti @Test void doesntAddBothKeysAccountsSameAsPayerForCryptoDelete() throws PreCheckException { - final var txn = deleteAccountTransaction(id, id); + final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); + + updateReadableStore(Map.of( + accountNum, + account, + deleteAccountNum, + deleteAccount.copyBuilder().key(key).build(), + transferAccountNum, + transferAccount)); final var context = new FakePreHandleContext(readableStore, txn); subject.preHandle(context); @@ -101,14 +137,8 @@ void doesntAddBothKeysAccountsSameAsPayerForCryptoDelete() throws PreCheckExcept void doesntAddTransferKeyIfAccountSameAsPayerForCryptoDelete() throws PreCheckException { final var txn = deleteAccountTransaction(deleteAccountId, id); - readableAccounts = emptyReadableAccountStateBuilder() - .value(EntityNumVirtualKey.fromLong(accountNum), account) - .value(EntityNumVirtualKey.fromLong(deleteAccountNum), deleteAccount) - .value(EntityNumVirtualKey.fromLong(transferAccountNum), transferAccount) - .build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); - given(deleteAccount.key()).willReturn(accountKey); + updateReadableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); final var context = new FakePreHandleContext(readableStore, txn); subject.preHandle(context); @@ -135,31 +165,18 @@ void doesntAddDeleteKeyIfAccountSameAsPayerForCryptoDelete() throws PreCheckExce void failsWithResponseCodeIfAnyAccountMissingForCryptoDelete() throws PreCheckException { /* ------ payerAccount missing, so deleteAccount and transferAccount will not be added ------ */ final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); - readableAccounts = emptyReadableAccountStateBuilder().build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); - given(deleteAccount.key()).willReturn(accountKey); + updateReadableStore(Map.of()); assertThrowsPreCheck(() -> new FakePreHandleContext(readableStore, txn), INVALID_PAYER_ACCOUNT_ID); /* ------ deleteAccount missing, so transferAccount will not be added ------ */ - readableAccounts = emptyReadableAccountStateBuilder() - .value(EntityNumVirtualKey.fromLong(accountNum), account) - .value(EntityNumVirtualKey.fromLong(transferAccountNum), transferAccount) - .build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); + updateReadableStore(Map.of(accountNum, account, transferAccountNum, transferAccount)); final var context2 = new FakePreHandleContext(readableStore, txn); assertThrowsPreCheck(() -> subject.preHandle(context2), INVALID_ACCOUNT_ID); /* ------ transferAccount missing ------ */ - readableAccounts = emptyReadableAccountStateBuilder() - .value(EntityNumVirtualKey.fromLong(accountNum), account) - .value(EntityNumVirtualKey.fromLong(deleteAccountNum), deleteAccount) - .build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); + updateReadableStore(Map.of(accountNum, account, deleteAccountNum, deleteAccount)); final var context3 = new FakePreHandleContext(readableStore, txn); assertThrowsPreCheck(() -> subject.preHandle(context3), INVALID_TRANSFER_ACCOUNT_ID); @@ -167,8 +184,6 @@ void failsWithResponseCodeIfAnyAccountMissingForCryptoDelete() throws PreCheckEx @Test void doesntExecuteIfAccountIdIsDefaultInstance() throws PreCheckException { - given(deleteAccount.key()).willReturn(key); - final var txn = deleteAccountTransaction(deleteAccountId, AccountID.DEFAULT); final var context = new FakePreHandleContext(readableStore, txn); @@ -180,6 +195,162 @@ void doesntExecuteIfAccountIdIsDefaultInstance() throws PreCheckException { assertIterableEquals(List.of(), context.requiredNonPayerKeys()); } + @Test + void pureChecksFailWhenTargetSameAsBeneficiary() throws PreCheckException { + final var txn = deleteAccountTransaction(deleteAccountId, deleteAccountId); + + final var context = new FakePreHandleContext(readableStore, txn); + + assertThatThrownBy(() -> subject.preHandle(context)) + .isInstanceOf(PreCheckException.class) + .has(responseCode(TRANSFER_ACCOUNT_SAME_AS_DELETE_ACCOUNT)); + } + + @Test + void pureChecksPassForValidTxn() { + final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); + + assertThatNoException().isThrownBy(() -> subject.pureChecks(txn)); + } + + @Test + void handleFailsIfDeleteAccountAccountMissing() { + updateWritableStore(Map.of(accountNum, account, transferAccountNum, transferAccount)); + givenTxnWith(deleteAccountId, transferAccountId); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(INVALID_ACCOUNT_ID)); + } + + @Test + void handleFailsIfTransferAccountAccountMissing() { + updateWritableStore(Map.of(accountNum, account, deleteAccountNum, deleteAccount)); + + givenTxnWith(deleteAccountId, transferAccountId); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(INVALID_TRANSFER_ACCOUNT_ID)); + } + + @Test + void failsIfAccountIsAlreadyDeleted() { + updateWritableStore(Map.of( + accountNum, + account, + deleteAccountNum, + deleteAccount.copyBuilder().deleted(true).build(), + transferAccountNum, + transferAccount)); + + givenTxnWith(deleteAccountId, transferAccountId); + given(expiryValidator.isDetached(eq(EntityType.ACCOUNT), anyBoolean(), anyLong())) + .willReturn(false); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(ACCOUNT_DELETED)); + } + + @Test + void happyPathWorks() { + updateWritableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); + + givenTxnWith(deleteAccountId, transferAccountId); + given(expiryValidator.isDetached(eq(EntityType.ACCOUNT), anyBoolean(), anyLong())) + .willReturn(false); + + subject.handle(handleContext); + + // When an account is deleted, marks the value of the account deleted flag to true + assertThat(writableStore.get(deleteAccountId).get().deleted()).isTrue(); + } + + @Test + void failsIfDeleteAccountIsDetached() { + updateWritableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); + given(handleContext.writableStore(WritableAccountStore.class)).willReturn(writableStore); + + givenTxnWith(deleteAccountId, transferAccountId); + given(expiryValidator.isDetached(eq(EntityType.ACCOUNT), anyBoolean(), anyLong())) + .willReturn(true); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(ACCOUNT_EXPIRED_AND_PENDING_REMOVAL)); + } + + @Test + void failsIfTransferAccountIsDetached() { + updateWritableStore( + Map.of(accountNum, account, deleteAccountNum, deleteAccount, transferAccountNum, transferAccount)); + + givenTxnWith(deleteAccountId, transferAccountId); + given(expiryValidator.isDetached(eq(EntityType.ACCOUNT), anyBoolean(), anyLong())) + .willReturn(true); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(ACCOUNT_EXPIRED_AND_PENDING_REMOVAL)); + } + + @Test + void failsIfDeleteAccountIsTreasury() { + updateWritableStore(Map.of( + accountNum, + account, + deleteAccountNum, + deleteAccount.copyBuilder().numberTreasuryTitles(2).build(), + transferAccountNum, + transferAccount)); + + givenTxnWith(deleteAccountId, transferAccountId); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(ACCOUNT_IS_TREASURY)); + } + + @Test + void failsIfTargetHasNonZeroBalances() { + updateWritableStore(Map.of( + accountNum, + account, + deleteAccountNum, + deleteAccount.copyBuilder().numberPositiveBalances(2).build(), + transferAccountNum, + transferAccount)); + givenTxnWith(deleteAccountId, transferAccountId); + + assertThatThrownBy(() -> subject.handle(handleContext)) + .isInstanceOf(HandleException.class) + .has(responseCode(TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES)); + } + + @Test + void failsIfEitherDeleteOrTransferAccountDoesntExist() throws PreCheckException { + var txn = deleteAccountTransaction(null, transferAccountId); + final var context = new FakePreHandleContext(readableStore, txn); + assertThatThrownBy(() -> subject.preHandle(context)) + .isInstanceOf(PreCheckException.class) + .has(responseCode(ACCOUNT_ID_DOES_NOT_EXIST)); + + txn = deleteAccountTransaction(deleteAccountId, null); + final var context1 = new FakePreHandleContext(readableStore, txn); + assertThatThrownBy(() -> subject.preHandle(context1)) + .isInstanceOf(PreCheckException.class) + .has(responseCode(ACCOUNT_ID_DOES_NOT_EXIST)); + + txn = deleteAccountTransaction(null, null); + final var context2 = new FakePreHandleContext(readableStore, txn); + assertThatThrownBy(() -> subject.preHandle(context2)) + .isInstanceOf(PreCheckException.class) + .has(responseCode(ACCOUNT_ID_DOES_NOT_EXIST)); + } + private TransactionBody deleteAccountTransaction( final AccountID deleteAccountId, final AccountID transferAccountId) { final var transactionID = TransactionID.newBuilder().accountID(id).transactionValidStart(consensusTimestamp); @@ -192,4 +363,31 @@ private TransactionBody deleteAccountTransaction( .cryptoDelete(deleteTxBody) .build(); } + + private void updateReadableStore(Map accountsToAdd) { + final var emptyStateBuilder = emptyReadableAccountStateBuilder(); + for (final var entry : accountsToAdd.entrySet()) { + emptyStateBuilder.value(EntityNumVirtualKey.fromLong(entry.getKey()), entry.getValue()); + } + readableAccounts = emptyStateBuilder.build(); + given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); + readableStore = new ReadableAccountStoreImpl(readableStates); + } + + private void updateWritableStore(Map accountsToAdd) { + final var emptyStateBuilder = emptyWritableAccountStateBuilder(); + for (final var entry : accountsToAdd.entrySet()) { + emptyStateBuilder.value(EntityNumVirtualKey.fromLong(entry.getKey()), entry.getValue()); + } + writableAccounts = emptyStateBuilder.build(); + given(writableStates.get(ACCOUNTS)).willReturn(writableAccounts); + writableStore = new WritableAccountStore(writableStates); + } + + private void givenTxnWith(AccountID deleteAccountId, AccountID transferAccountId) { + final var txn = deleteAccountTransaction(deleteAccountId, transferAccountId); + given(handleContext.body()).willReturn(txn); + given(handleContext.expiryValidator()).willReturn(expiryValidator); + given(handleContext.writableStore(WritableAccountStore.class)).willReturn(writableStore); + } } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoHandlerTestBase.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoHandlerTestBase.java index 16586ca8b5c..c5449b7e9d2 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoHandlerTestBase.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoHandlerTestBase.java @@ -22,6 +22,7 @@ import static com.hedera.test.utils.KeyUtils.C_COMPLEX_KEY; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.ContractID; @@ -104,6 +105,7 @@ public class CryptoHandlerTestBase { protected static final long defaultAutoRenewPeriod = 720000L; protected static final long payerBalance = 10_000L; protected MapReadableKVState readableAliases; + protected MapReadableKVState readableAccounts; protected MapWritableKVState writableAliases; protected MapWritableKVState writableAccounts; @@ -111,16 +113,14 @@ public class CryptoHandlerTestBase { protected ReadableAccountStore readableStore; protected WritableAccountStore writableStore; - @Mock protected Account deleteAccount; - @Mock protected Account transferAccount; @Mock protected ReadableStates readableStates; - @Mock + @Mock(strictness = LENIENT) protected WritableStates writableStates; @Mock @@ -128,7 +128,19 @@ public class CryptoHandlerTestBase { @BeforeEach public void setUp() { - givenValidAccount(); + account = givenValidAccount(); + deleteAccount = givenValidAccount() + .copyBuilder() + .accountNumber(deleteAccountNum) + .key(accountKey) + .numberPositiveBalances(0) + .numberTreasuryTitles(0) + .build(); + transferAccount = givenValidAccount() + .copyBuilder() + .accountNumber(transferAccountNum) + .key(key) + .build(); refreshStoresWithCurrentTokenOnlyInReadable(); } @@ -227,8 +239,8 @@ protected MapReadableKVState.Builder emptyReadableAliasS return MapReadableKVState.builder(ALIASES); } - protected void givenValidAccount() { - account = new Account( + protected Account givenValidAccount() { + return new Account( accountNum, alias.alias(), key, diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTokenHandlerTestBase.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTokenHandlerTestBase.java index 47172c7f725..420e48728ab 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTokenHandlerTestBase.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoTokenHandlerTestBase.java @@ -61,8 +61,11 @@ import com.hedera.node.app.spi.state.ReadableStates; import com.hedera.node.app.spi.state.WritableStates; import com.hedera.node.app.spi.workflows.PreHandleContext; +import com.hedera.node.config.data.TokensConfig; +import com.hedera.node.config.testfixtures.HederaTestConfigBuilder; import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.utility.CommonUtils; +import com.swirlds.config.api.Configuration; import edu.umd.cs.findbugs.annotations.NonNull; import java.util.Collections; import java.util.List; @@ -172,6 +175,7 @@ public class CryptoTokenHandlerTestBase { .build(); protected List customFees = List.of(withFixedFee(fixedFee), withFractionalFee(fractionalFee)); + protected TokensConfig tokensConfig; protected MapReadableKVState readableAliases; protected MapReadableKVState readableAccounts; protected MapWritableKVState writableAliases; @@ -208,8 +212,12 @@ public class CryptoTokenHandlerTestBase { @Mock protected CryptoSignatureWaiversImpl waivers; + protected Configuration configuration; + @BeforeEach public void setUp() { + configuration = new HederaTestConfigBuilder().getOrCreateConfig(); + tokensConfig = configuration.getConfigData(TokensConfig.class); givenValidAccount(); givenValidFungibleToken(); givenValidNonFungibleToken();