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 bbf5a57ca18f..93705a801990 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 @@ -16,8 +16,10 @@ package com.hedera.node.app.workflows.dispatcher; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED; import static java.util.Objects.requireNonNull; +import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.TopicID; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicRecordBuilder; @@ -26,8 +28,11 @@ import com.hedera.node.app.service.mono.context.properties.GlobalDynamicProperties; import com.hedera.node.app.service.mono.pbj.PbjConverter; import com.hedera.node.app.service.mono.state.validation.UsageLimits; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; +import com.hedera.node.app.service.token.impl.records.CryptoCreateRecordBuilder; import com.hedera.node.app.spi.meta.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; import edu.umd.cs.findbugs.annotations.NonNull; import javax.inject.Inject; import javax.inject.Singleton; @@ -69,6 +74,20 @@ protected void finishConsensusCreateTopic( topicStore.commit(); } + @Override + protected void finishCryptoCreate( + @NonNull final CryptoCreateRecordBuilder recordBuilder, @NonNull final WritableAccountStore accountStore) { + // If accounts can't be created, due to the usage of a price regime, throw an exception + if (!usageLimits.areCreatableAccounts(1)) { + throw new HandleException(MAX_ENTITIES_IN_PRICE_REGIME_HAVE_BEEN_CREATED); + } + // Adapt the record builder outcome for mono-service + txnCtx.setCreated(PbjConverter.fromPbj(AccountID.newBuilder() + .accountNum(recordBuilder.getCreatedAccount()) + .build())); + accountStore.commit(); + } + @Override protected void finishConsensusUpdateTopic(@NonNull WritableTopicStore topicStore) { topicStore.commit(); diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java index 09d8692bcb7d..46b76bbc445d 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/TransactionDispatcher.java @@ -29,8 +29,10 @@ import com.hedera.node.app.service.consensus.impl.records.ConsensusCreateTopicRecordBuilder; import com.hedera.node.app.service.consensus.impl.records.ConsensusSubmitMessageRecordBuilder; import com.hedera.node.app.service.mono.context.properties.GlobalDynamicProperties; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; +import com.hedera.node.app.service.token.impl.records.CryptoCreateRecordBuilder; import com.hedera.node.app.spi.meta.HandleContext; import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; @@ -103,6 +105,7 @@ public void dispatchHandle( txn, writableStoreFactory.createTokenRelStore()); case TOKEN_PAUSE -> dispatchTokenPause(txn, writableStoreFactory.createTokenStore()); case TOKEN_UNPAUSE -> dispatchTokenUnpause(txn, writableStoreFactory.createTokenStore()); + case CRYPTO_CREATE -> dispatchCryptoCreate(txn, writableStoreFactory.createAccountStore()); default -> throw new IllegalArgumentException(TYPE_NOT_SUPPORTED); } } @@ -361,4 +364,30 @@ private void dispatchTokenPause( handler.handle(tokenPause, tokenStore); tokenStore.commit(); } + + /** + * Dispatches the crypto create transaction to the appropriate handler. + * @param cryptoCreate the crypto create transaction body + * @param accountStore the writable account store + */ + private void dispatchCryptoCreate( + @NonNull final TransactionBody cryptoCreate, @NonNull final WritableAccountStore accountStore) { + final var handler = handlers.cryptoCreateHandler(); + final var recordBuilder = handler.newRecordBuilder(); + handler.handle(handleContext, cryptoCreate, accountStore, recordBuilder); + finishCryptoCreate(recordBuilder, accountStore); + } + + /** + * A temporary hook to isolate logic that we expect to move to a workflow, but + * is currently needed when running with facility implementations that are adapters + * for either {@code mono-service} logic or integration tests. + * + * @param recordBuilder the completed record builder for the creation + * @param accountStore the account store used for the creation + */ + protected void finishCryptoCreate( + @NonNull final CryptoCreateRecordBuilder recordBuilder, @NonNull final WritableAccountStore accountStore) { + // No-op by default + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactory.java index cacfdfa34a62..e14562b80688 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactory.java @@ -21,6 +21,7 @@ import com.hedera.node.app.service.consensus.ConsensusService; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; import com.hedera.node.app.service.token.TokenService; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; import com.hedera.node.app.state.HederaState; @@ -74,4 +75,10 @@ public WritableTokenRelationStore createTokenRelStore() { final var tokenStates = stateAccessor.getHederaState().createWritableStates(TokenService.NAME); return new WritableTokenRelationStore(tokenStates); } + + @Override + public WritableAccountStore createAccountStore() { + final var tokenStates = stateAccessor.getHederaState().createWritableStates(TokenService.NAME); + return new WritableAccountStore(tokenStates); + } } diff --git a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WritableStoreFactory.java b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WritableStoreFactory.java index 5ed53a0eefd1..ad5991521f57 100644 --- a/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WritableStoreFactory.java +++ b/hedera-node/hedera-app/src/main/java/com/hedera/node/app/workflows/dispatcher/WritableStoreFactory.java @@ -17,6 +17,7 @@ package com.hedera.node.app.workflows.dispatcher; import com.hedera.node.app.service.consensus.impl.WritableTopicStore; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; @@ -43,5 +44,12 @@ public interface WritableStoreFactory { * * @return a new {@link WritableTokenRelationStore} */ - public WritableTokenRelationStore createTokenRelStore(); + WritableTokenRelationStore createTokenRelStore(); + + /** + * Get a {@link WritableAccountStore}. + * + * @return a new {@link WritableAccountStore} + */ + WritableAccountStore createAccountStore(); } 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 ea5779ba927c..1c328d0977f3 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 @@ -26,6 +26,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @@ -114,6 +115,7 @@ import com.hedera.node.app.service.schedule.impl.handlers.ScheduleDeleteHandler; import com.hedera.node.app.service.schedule.impl.handlers.ScheduleSignHandler; import com.hedera.node.app.service.token.ReadableAccountStore; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.service.token.impl.WritableTokenRelationStore; import com.hedera.node.app.service.token.impl.WritableTokenStore; import com.hedera.node.app.service.token.impl.handlers.CryptoAddLiveHashHandler; @@ -139,9 +141,11 @@ import com.hedera.node.app.service.token.impl.handlers.TokenUnfreezeAccountHandler; import com.hedera.node.app.service.token.impl.handlers.TokenUnpauseHandler; import com.hedera.node.app.service.token.impl.handlers.TokenUpdateHandler; +import com.hedera.node.app.service.token.impl.records.CreateAccountRecordBuilder; import com.hedera.node.app.service.util.impl.handlers.UtilPrngHandler; import com.hedera.node.app.spi.meta.HandleContext; import com.hedera.node.app.spi.state.ReadableStates; +import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; import com.hedera.node.app.spi.workflows.TransactionHandler; import com.hedera.node.app.state.HederaState; @@ -311,7 +315,7 @@ class MonoTransactionDispatcherTest { private GlobalDynamicProperties dynamicProperties; @Mock - private WritableStoreFactory writableStoreFactory; + private WorkingStateWritableStoreFactory writableStoreFactory; @Mock private WritableTopicStore writableTopicStore; @@ -319,6 +323,9 @@ class MonoTransactionDispatcherTest { @Mock private WritableTokenStore writableTokenStore; + @Mock + private WritableAccountStore writableAccountStore; + @Mock private WritableTokenRelationStore writableTokenRelStore; @@ -569,6 +576,41 @@ void dispatchesTokenUnpauseAsExpected() { verify(writableTokenStore).commit(); } + @Test + void dispatchesCryptoCreateAsExpected() { + final var createBuilder = mock(CreateAccountRecordBuilder.class); + + given(cryptoCreateHandler.newRecordBuilder()).willReturn(createBuilder); + given(createBuilder.getCreatedAccount()).willReturn(666L); + given(writableStoreFactory.createAccountStore()).willReturn(writableAccountStore); + given(usageLimits.areCreatableAccounts(1)).willReturn(true); + + dispatcher.dispatchHandle(HederaFunctionality.CRYPTO_CREATE, transactionBody, writableStoreFactory); + + verify(txnCtx) + .setCreated(PbjConverter.fromPbj( + AccountID.newBuilder().accountNum(666L).build())); + verify(writableAccountStore).commit(); + } + + @Test + void doesntCommitWhenUsageLimitsExceeded() { + final var createBuilder = mock(CreateAccountRecordBuilder.class); + + given(cryptoCreateHandler.newRecordBuilder()).willReturn(createBuilder); + given(writableStoreFactory.createAccountStore()).willReturn(writableAccountStore); + given(usageLimits.areCreatableAccounts(1)).willReturn(false); + + assertThatThrownBy(() -> dispatcher.dispatchHandle( + HederaFunctionality.CRYPTO_CREATE, transactionBody, writableStoreFactory)) + .isInstanceOf(HandleException.class); + + verify(txnCtx, never()) + .setCreated(PbjConverter.fromPbj( + AccountID.newBuilder().accountNum(666L).build())); + verify(writableAccountStore, never()).commit(); + } + @Test void cannotDispatchUnsupportedOperations() { assertThatThrownBy(() -> dispatcher.dispatchHandle( diff --git a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactoryTest.java b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactoryTest.java index df9efbf8b573..4a06f586ee4a 100644 --- a/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactoryTest.java +++ b/hedera-node/hedera-app/src/test/java/com/hedera/node/app/workflows/dispatcher/WorkingStateWritableStoreFactoryTest.java @@ -21,6 +21,7 @@ import static org.mockito.BDDMockito.given; import com.hedera.node.app.service.consensus.ConsensusService; +import com.hedera.node.app.service.token.TokenService; import com.hedera.node.app.spi.state.WritableStates; import com.hedera.node.app.state.HederaState; import com.hedera.node.app.state.WorkingStateAccessor; @@ -64,7 +65,7 @@ void createsWritableStore() { @Test void returnsTopicStore() { workingStateAccessor.setHederaState(state); - given(state.createWritableStates("ConsensusService")).willReturn(writableStates); + given(state.createWritableStates(ConsensusService.NAME)).willReturn(writableStates); final var store = subject.createTopicStore(); assertNotNull(store); } @@ -72,7 +73,7 @@ void returnsTopicStore() { @Test void returnsTokenStore() { workingStateAccessor.setHederaState(state); - given(state.createWritableStates("TokenService")).willReturn(writableStates); + given(state.createWritableStates(TokenService.NAME)).willReturn(writableStates); final var store = subject.createTokenStore(); assertNotNull(store); } @@ -80,8 +81,16 @@ void returnsTokenStore() { @Test void returnsTokenRelStore() { workingStateAccessor.setHederaState(state); - given(state.createWritableStates("TokenService")).willReturn(writableStates); + given(state.createWritableStates(TokenService.NAME)).willReturn(writableStates); final var store = subject.createTokenRelStore(); assertNotNull(store); } + + @Test + void returnsAccountStore() { + workingStateAccessor.setHederaState(state); + given(state.createWritableStates(TokenService.NAME)).willReturn(writableStates); + final var store = subject.createAccountStore(); + assertNotNull(store); + } } diff --git a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/AdapterUtils.java b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/AdapterUtils.java index 785a0f8366eb..ade5b6ceb32b 100644 --- a/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/AdapterUtils.java +++ b/hedera-node/hedera-consensus-service-impl/src/test/java/com/hedera/node/app/service/consensus/impl/test/handlers/AdapterUtils.java @@ -69,9 +69,9 @@ import com.hedera.hapi.node.base.AccountID; import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.state.token.AccountCryptoAllowance; import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; -import com.hedera.hapi.node.state.token.AccountTokenAllowance; import com.hedera.node.app.service.mono.state.virtual.EntityNumValue; import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; import com.hedera.node.app.service.token.ReadableAccountStore; @@ -142,21 +142,19 @@ class SigReqAdapterUtils { private static final String ACCOUNTS_KEY = "ACCOUNTS"; private static AccountCryptoAllowance cryptoAllowances = AccountCryptoAllowance.newBuilder() - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(500L) .build(); private static AccountFungibleTokenAllowance fungibleTokenAllowances = AccountFungibleTokenAllowance.newBuilder() - .tokenAllowanceKey(AccountTokenAllowance.newBuilder() - .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) - .build()) + .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(10_000L) .build(); - private static AccountTokenAllowance nftAllowances = AccountTokenAllowance.newBuilder() + private static AccountApprovalForAllAllowance nftAllowances = AccountApprovalForAllAllowance.newBuilder() .tokenNum(KNOWN_TOKEN_WITH_WIPE.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .build(); private static ReadableKVState wellKnownAccountsState() { @@ -261,7 +259,7 @@ private static Account toPbjAccount( boolean receiverSigRequired, List cryptoAllowances, List fungibleTokenAllowances, - List nftTokenAllowances) { + List nftTokenAllowances) { return new Account( number, Bytes.EMPTY, @@ -293,7 +291,8 @@ private static Account toPbjAccount( nftTokenAllowances, fungibleTokenAllowances, 2, - false); + false, + null); } } } diff --git a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/test/handlers/ScheduleDeleteHandlerParityTest.java b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/test/handlers/ScheduleDeleteHandlerParityTest.java index da2604f4a6d1..382c36e25964 100644 --- a/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/test/handlers/ScheduleDeleteHandlerParityTest.java +++ b/hedera-node/hedera-schedule-service-impl/src/test/java/com/hedera/node/app/service/schedule/impl/test/handlers/ScheduleDeleteHandlerParityTest.java @@ -74,9 +74,9 @@ import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.base.ScheduleID; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.state.token.AccountCryptoAllowance; import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; -import com.hedera.hapi.node.state.token.AccountTokenAllowance; import com.hedera.hapi.node.transaction.TransactionBody; import com.hedera.node.app.service.mono.pbj.PbjConverter; import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; @@ -210,21 +210,19 @@ public class SigReqAdapterUtils { private static final String ACCOUNTS_KEY = "ACCOUNTS"; private static AccountCryptoAllowance cryptoAllowances = AccountCryptoAllowance.newBuilder() - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(500L) .build(); private static AccountFungibleTokenAllowance fungibleTokenAllowances = AccountFungibleTokenAllowance.newBuilder() - .tokenAllowanceKey(AccountTokenAllowance.newBuilder() - .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) - .build()) + .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(10_000L) .build(); - private static AccountTokenAllowance nftAllowances = AccountTokenAllowance.newBuilder() + private static AccountApprovalForAllAllowance nftAllowances = AccountApprovalForAllAllowance.newBuilder() .tokenNum(KNOWN_TOKEN_WITH_WIPE.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .build(); public static Map wellKnownAccountStoreAt() { @@ -325,7 +323,7 @@ private static Account toPbjAccount( boolean receiverSigRequired, List cryptoAllowances, List fungibleTokenAllowances, - List nftTokenAllowances) { + List nftTokenAllowances) { return new Account( number, Bytes.EMPTY, @@ -357,7 +355,8 @@ private static Account toPbjAccount( nftTokenAllowances, fungibleTokenAllowances, 2, - false); + false, + null); } } } diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/AdapterUtils.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/AdapterUtils.java index 7b9ff76dfff5..799e372c7ddb 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/AdapterUtils.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/handlers/AdapterUtils.java @@ -72,9 +72,9 @@ import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.state.token.AccountCryptoAllowance; import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; -import com.hedera.hapi.node.state.token.AccountTokenAllowance; import com.hedera.node.app.service.mono.state.virtual.EntityNumValue; import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; import com.hedera.node.app.service.token.ReadableAccountStore; @@ -136,21 +136,19 @@ class SigReqAdapterUtils { private static final String ACCOUNTS_KEY = "ACCOUNTS"; private static AccountCryptoAllowance cryptoAllowances = AccountCryptoAllowance.newBuilder() - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(500L) .build(); private static AccountFungibleTokenAllowance fungibleTokenAllowances = AccountFungibleTokenAllowance.newBuilder() - .tokenAllowanceKey(AccountTokenAllowance.newBuilder() - .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) - .build()) + .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(10_000L) .build(); - private static AccountTokenAllowance nftAllowances = AccountTokenAllowance.newBuilder() + private static AccountApprovalForAllAllowance nftAllowances = AccountApprovalForAllAllowance.newBuilder() .tokenNum(KNOWN_TOKEN_WITH_WIPE.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .build(); static ReadableKVState wellKnownAccountsState() { @@ -291,7 +289,7 @@ private static Account toPbjAccount( boolean receiverSigRequired, List cryptoAllowances, List fungibleTokenAllowances, - List nftTokenAllowances, + List nftTokenAllowances, boolean isSmartContract) { return new Account( number, @@ -324,7 +322,8 @@ private static Account toPbjAccount( nftTokenAllowances, fungibleTokenAllowances, 2, - false); + false, + null); } } } diff --git a/hedera-node/hedera-token-service-impl/build.gradle.kts b/hedera-node/hedera-token-service-impl/build.gradle.kts index 97ee02032355..1de9d472b213 100644 --- a/hedera-node/hedera-token-service-impl/build.gradle.kts +++ b/hedera-node/hedera-token-service-impl/build.gradle.kts @@ -28,6 +28,8 @@ configurations.all { } dependencies { + implementation(project(":hedera-node:hapi")) + testImplementation(project(mapOf("path" to ":hedera-node:hedera-app"))) 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/ReadableAccountStoreImpl.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/ReadableAccountStoreImpl.java index 80cf5d77e82e..7e639c7ceea4 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/ReadableAccountStoreImpl.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/ReadableAccountStoreImpl.java @@ -64,7 +64,7 @@ public ReadableAccountStoreImpl(@NonNull final ReadableStates states) { this.aliases = states.get("ALIASES"); } - private static boolean isMirror(final Bytes bytes) { + protected static boolean isMirror(final Bytes bytes) { return bytes.matchesPrefix(MIRROR_PREFIX); } @@ -92,7 +92,7 @@ public Account getAccountById(@NonNull final AccountID accountID) { * @return merkle leaf for the given account number */ @Nullable - private Account getAccountLeaf(@NonNull final AccountID id) { + protected Account getAccountLeaf(@NonNull final AccountID id) { // Get the account number based on the account identifier. It may be null. final var accountOneOf = id.account(); final Long accountNum = 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 new file mode 100644 index 000000000000..9d1260bcd5cf --- /dev/null +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableAccountStore.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2022-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.service.token.impl; + +import static com.hedera.node.app.service.evm.accounts.HederaEvmContractAliases.EVM_ADDRESS_LEN; +import static java.util.Objects.requireNonNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.node.app.service.mono.state.virtual.EntityNumValue; +import com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; +import com.hedera.node.app.spi.state.WritableKVState; +import com.hedera.node.app.spi.state.WritableKVStateBase; +import com.hedera.node.app.spi.state.WritableStates; +import com.hedera.pbj.runtime.io.buffer.Bytes; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +/** + * Provides write methods for modifying underlying data storage mechanisms for working with + * accounts. + * + *

This class is not exported from the module. It is an internal implementation detail. This + * class is not complete, it will be extended with other methods like remove, update etc., + */ +public class WritableAccountStore extends ReadableAccountStoreImpl { + /** The underlying data storage class that holds the account data. */ + private final WritableKVState accountState; + /** The underlying data storage class that holds the aliases data built from the state. */ + private final WritableKVState aliases; + + /** + * Create a new {@link WritableAccountStore} instance. + * + * @param states The state to use. + */ + public WritableAccountStore(@NonNull final WritableStates states) { + super(states); + requireNonNull(states); + + this.accountState = states.get(TokenServiceImpl.ACCOUNTS_KEY); + this.aliases = states.get(TokenServiceImpl.ALIASES_KEY); + } + + /** + * Persists a new {@link Account} into the state, as well as exporting its ID to the transaction + * receipt. + * + * @param account - the account to be added to modifications in state. + */ + public void put(@NonNull final Account account) { + Objects.requireNonNull(account); + accountState.put(EntityNumVirtualKey.fromLong(account.accountNumber()), Objects.requireNonNull(account)); + } + + /** + * Persists a new alias linked to the account persisted to state + * + * @param alias - the alias to be added to modifications in state. + * @param accountNum - the account number to be added to modifications in state. + */ + public void putAlias(@NonNull final String alias, final long accountNum) { + Objects.requireNonNull(alias); + aliases.put(alias, new EntityNumValue(accountNum)); + } + + /** Commits the changes to the underlying data storage. */ + public void commit() { + ((WritableKVStateBase) accountState).commit(); + ((WritableKVStateBase) aliases).commit(); + } + + /** + * Returns the {@link Account} with the given number. If no such account exists, returns {@code + * Optional.empty()} + * + * @param accountID - the id of the Account to be retrieved. + */ + @NonNull + public Optional get(final AccountID accountID) { + requireNonNull(accountID); + final var account = getAccountLeaf(accountID); + return Optional.ofNullable(account); + } + + /** + * Returns the {@link Account} with the given {@link AccountID}. + * If no such account exists, returns {@code Optional.empty()} + * + * @param id - the number of the account to be retrieved. + */ + @NonNull + public Optional getForModify(final AccountID id) { + // Get the account number based on the account identifier. It may be null. + final var accountOneOf = id.account(); + final Long accountNum = + switch (accountOneOf.kind()) { + case ACCOUNT_NUM -> accountOneOf.as(); + case ALIAS -> { + final Bytes alias = accountOneOf.as(); + if (alias.length() == EVM_ADDRESS_LEN && isMirror(alias)) { + yield fromMirror(alias); + } else { + final var entityNum = aliases.get(alias.asUtf8String()); + yield entityNum == null ? EntityNumValue.DEFAULT.num() : entityNum.num(); + } + } + case UNSET -> EntityNumValue.DEFAULT.num(); + }; + + return Optional.ofNullable(accountState.getForModify(EntityNumVirtualKey.fromLong(accountNum))); + } + + /** + * Returns the number of accounts in the state. It also includes modifications in the {@link + * WritableKVState}. + * + * @return the number of accounts in the state. + */ + public long sizeOfAccountState() { + return accountState.size(); + } + + /** + * Returns the number of aliases in the state. It also includes modifications in the {@link + * WritableKVState}. + * + * @return the number of aliases in the state. + */ + public long sizeOfAliasesState() { + return aliases.size(); + } + + /** + * Returns the set of accounts modified in existing state. + * + * @return the set of accounts modified in existing state + */ + @NonNull + public Set modifiedAccountsInState() { + return accountState.modifiedKeys(); + } + + /** + * Returns the set of aliases modified in existing state. + * + * @return the set of aliases modified in existing state + */ + @NonNull + public Set modifiedAliasesInState() { + return aliases.modifiedKeys(); + } +} diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableTokenStore.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableTokenStore.java index aa91b5baf30c..25e8a0f80c7e 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableTokenStore.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/WritableTokenStore.java @@ -48,7 +48,7 @@ public class WritableTokenStore { public WritableTokenStore(@NonNull final WritableStates states) { requireNonNull(states); - this.tokenState = states.get("TOKENS"); + this.tokenState = states.get(TokenServiceImpl.TOKENS_KEY); } /** diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoCreateHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoCreateHandler.java index 44279cf00998..5e7b80f52cf0 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoCreateHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoCreateHandler.java @@ -16,11 +16,34 @@ 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.INSUFFICIENT_PAYER_BALANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_INITIAL_BALANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RECEIVE_RECORD_THRESHOLD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RENEWAL_PERIOD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SEND_RECORD_THRESHOLD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.OK; +import static com.hedera.hapi.node.base.ResponseCodeEnum.PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED; 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.base.ResponseCodeEnum; +import com.hedera.hapi.node.base.TransactionID; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.token.CryptoCreateTransactionBody; +import com.hedera.hapi.node.token.CryptoCreateTransactionBody.StakedIdOneOfType; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.token.impl.WritableAccountStore; +import com.hedera.node.app.service.token.impl.records.CreateAccountRecordBuilder; +import com.hedera.node.app.service.token.impl.records.CryptoCreateRecordBuilder; +import com.hedera.node.app.spi.meta.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 com.hedera.pbj.runtime.io.buffer.Bytes; import edu.umd.cs.findbugs.annotations.NonNull; import javax.inject.Inject; import javax.inject.Singleton; @@ -37,9 +60,13 @@ public CryptoCreateHandler() { } @Override - public void preHandle(@NonNull final PreHandleContext context) { + public void preHandle(@NonNull final PreHandleContext context) throws PreCheckException { requireNonNull(context); final var op = context.body().cryptoCreateAccountOrThrow(); + final var validationResult = pureChecks(op); + if (validationResult != OK) { + throw new PreCheckException(validationResult); + } if (op.hasKey()) { final var receiverSigReq = op.receiverSigRequired(); if (receiverSigReq && op.hasKey()) { @@ -49,14 +76,202 @@ public void preHandle(@NonNull final PreHandleContext context) { } /** - * This method is called during the handle workflow. It executes the actual transaction. - * - *

Please note: the method signature is just a placeholder which is most likely going to - * change. + * This method is called during the handle workflow. It executes the {@code CryptoCreate} + * transaction, creating a new account with the given properties. + * If the transaction is successful, the account is created and the payer account is charged + * the transaction fee and the initial balance of new account and the balance of the + * new account is set to the initial balance. + * If the transaction is not successful, the account is not created and the payer account is + * charged the transaction fee. * * @throws NullPointerException if one of the arguments is {@code null} + * @throws HandleException if the transaction is not successful due to payer + * account being deleted or has insufficient balance or the account is not created due to + * the usage of a price regime + */ + public void handle( + @NonNull final HandleContext handleContext, + @NonNull final TransactionBody txnBody, + @NonNull final WritableAccountStore accountStore, + @NonNull final CryptoCreateRecordBuilder recordBuilder) { + final var op = txnBody.cryptoCreateAccount(); + + // validate fields in the transaction body that involves checking with + // dynamic properties or state + final ResponseCodeEnum validationResult = validateSemantics(); + if (validationResult != OK) { + throw new HandleException(validationResult); + } + + // FUTURE: Use the config and check if accounts can be created. + // Currently, this check is being done in `finishCryptoCreate` before `commit` + + // validate payer account exists and has enough balance + final var optionalPayer = accountStore.getForModify( + txnBody.transactionIDOrElse(TransactionID.DEFAULT).accountIDOrElse(AccountID.DEFAULT)); + if (optionalPayer.isEmpty()) { + throw new HandleException(INVALID_PAYER_ACCOUNT_ID); + } + final var payer = optionalPayer.get(); + final long newPayerBalance = payer.tinybarBalance() - op.initialBalance(); + validatePayer(payer, newPayerBalance); + + // Change payer's balance to reflect the deduction of the initial balance for the new + // account + final var modifiedPayer = + payer.copyBuilder().tinybarBalance(newPayerBalance).build(); + accountStore.put(modifiedPayer); + + // Build the new account to be persisted based on the transaction body + final var accountCreated = buildAccount(op, handleContext); + accountStore.put(accountCreated); + + // set newly created account number in the record builder + final var createdAccountNum = accountCreated.accountNumber(); + recordBuilder.setCreatedAccount(createdAccountNum); + + // put if any new alias is associated with the account into account store + if (op.alias() != Bytes.EMPTY) { + accountStore.putAlias(op.alias().toString(), createdAccountNum); + } + } + + @Override + public CryptoCreateRecordBuilder newRecordBuilder() { + return new CreateAccountRecordBuilder(); + } + + /* ----------- Helper Methods ----------- */ + + /** + * Validate the basic fields in the transaction body that does not involve checking with dynamic + * properties or state. This check is done as part of the pre-handle workflow. + * @param op the transaction body + * @return OK if the transaction body is valid, otherwise return the appropriate error code + */ + private ResponseCodeEnum pureChecks(@NonNull final CryptoCreateTransactionBody op) { + if (op.initialBalance() < 0L) { + return INVALID_INITIAL_BALANCE; + } + if (!op.hasAutoRenewPeriod()) { + return INVALID_RENEWAL_PERIOD; + } + if (op.sendRecordThreshold() < 0L) { + return INVALID_SEND_RECORD_THRESHOLD; // FUTURE: should this return + // SEND_RECORD_THRESHOLD_FIELD_IS_DEPRECATED + } + if (op.receiveRecordThreshold() < 0L) { + return INVALID_RECEIVE_RECORD_THRESHOLD; // FUTURE: should this return + // RECEIVE_RECORD_THRESHOLD_FIELD_IS_DEPRECATED + } + if (op.hasProxyAccountID() && !op.proxyAccountID().equals(AccountID.DEFAULT)) { + return PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED; + } + return OK; + } + + /** + * Validate the fields in the transaction body that involves checking with dynamic + * properties or state. This check is done as part of the handle workflow. + * @return OK if the transaction body is valid, otherwise return the appropriate error code + */ + private ResponseCodeEnum validateSemantics() { + // FUTURE : Need to add validations that involve dynamic properties or state + return OK; + } + + /** + * Validates the payer account exists and has enough balance to cover the initial balance of the + * account to be created. + * + * @param payer the payer account + * @param newPayerBalance the initial balance of the account to be created + */ + private void validatePayer(@NonNull final Account payer, final long newPayerBalance) { + // If the payer account is deleted, throw an exception + if (payer.deleted()) { + throw new HandleException(ACCOUNT_DELETED); + } + if (newPayerBalance < 0) { + throw new HandleException(INSUFFICIENT_PAYER_BALANCE); + } + // FUTURE: check if payer account is detached when we have started expiring accounts ? + } + + /** + * Builds an account based on the transaction body and the consensus time. + * + * @param op the transaction body + * @param handleContext the handle context + * @return the account created */ - public void handle() { - throw new UnsupportedOperationException("Not implemented"); + private Account buildAccount(CryptoCreateTransactionBody op, HandleContext handleContext) { + long autoRenewPeriod = op.autoRenewPeriodOrThrow().seconds(); + long consensusTime = handleContext.consensusNow().getEpochSecond(); + long expiry = consensusTime + autoRenewPeriod; + var builder = Account.newBuilder() + .memo(op.memo()) + .expiry(expiry) + .autoRenewSecs(autoRenewPeriod) + .receiverSigRequired(op.receiverSigRequired()) + .maxAutoAssociations(op.maxAutomaticTokenAssociations()) + .tinybarBalance(op.initialBalance()) + .declineReward(op.declineReward()); + + if (onlyKeyProvided(op)) { + builder.key(op.keyOrThrow()); + } else if (keyAndAliasProvided(op)) { + builder.key(op.keyOrThrow()).alias(op.alias()); + } + + if (op.hasStakedAccountId() || op.hasStakedNodeId()) { + final var stakeNumber = getStakedId(op); + builder.stakedNumber(stakeNumber); + } + // set the new account number + builder.accountNumber(handleContext.newEntityNumSupplier().getAsLong()); + return builder.build(); + } + + /** + * Checks if only key is provided. + * + * @param op the transaction body + * @return true if only key is provided, false otherwise + */ + private boolean onlyKeyProvided(@NonNull final CryptoCreateTransactionBody op) { + return op.hasKey() && op.alias().equals(Bytes.EMPTY); + } + + /** + * Checks if both key and alias are provided. + * + * @param op the transaction body + * @return true if both key and alias are provided, false otherwise + */ + private boolean keyAndAliasProvided(@NonNull final CryptoCreateTransactionBody op) { + return op.hasKey() && !op.alias().equals(Bytes.EMPTY); + } + + /** + * Gets the stakedId from the provided staked_account_id or staked_node_id. + * When staked_node_id is provided, it is stored as negative number in state to + * distinguish it from staked_account_id. It will be converted back to positive number + * when it is retrieved from state. + * + * To distinguish for node 0, it will be stored as - node_id -1. + * For example, if staked_node_id is 0, it will be stored as -1 in state. + * + * @param op given transaction body + * @return valid staked id + */ + private long getStakedId(final CryptoCreateTransactionBody op) { + if (StakedIdOneOfType.STAKED_ACCOUNT_ID.equals(op.stakedId().kind())) { + return op.stakedAccountIdOrThrow().accountNum(); + } else { + // return a number less than the given node Id, in order to recognize the if nodeId 0 is + // set + return -op.stakedNodeIdOrThrow() - 1; + } } } diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CreateAccountRecordBuilder.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CreateAccountRecordBuilder.java new file mode 100644 index 000000000000..43a0e606ff19 --- /dev/null +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CreateAccountRecordBuilder.java @@ -0,0 +1,56 @@ +/* + * 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.service.token.impl.records; + +import com.hedera.node.app.spi.records.UniversalRecordBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@code RecordBuilder} specialization for tracking the side effects of a {@code CryptoCreate} + * transaction. + */ +public class CreateAccountRecordBuilder extends UniversalRecordBuilder + implements CryptoCreateRecordBuilder { + private long createdAccountNum = 0; + + /** {@inheritDoc} */ + @Override + public CryptoCreateRecordBuilder self() { + return this; + } + + /** {@inheritDoc} */ + @NonNull + @Override + public CryptoCreateRecordBuilder setCreatedAccount(final long num) { + this.createdAccountNum = num; + return this; + } + + /** {@inheritDoc} */ + @Override + public long getCreatedAccount() { + throwIfMissingAccountNum(); + return createdAccountNum; + } + + private void throwIfMissingAccountNum() { + if (createdAccountNum == 0L) { + throw new IllegalStateException("No new account number was recorded"); + } + } +} diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CryptoCreateRecordBuilder.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CryptoCreateRecordBuilder.java new file mode 100644 index 000000000000..1a3d11275b18 --- /dev/null +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/records/CryptoCreateRecordBuilder.java @@ -0,0 +1,44 @@ +/* + * 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.service.token.impl.records; + +import com.hedera.node.app.spi.records.RecordBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; + +/** + * A {@code RecordBuilder} specialization for tracking the side effects of a {@code CryptoCreate} + * transaction. + */ +public interface CryptoCreateRecordBuilder extends RecordBuilder { + /** + * Returns the number of the created account. + * + * @return the number of the created account + */ + long getCreatedAccount(); + + /** + * Tracks creation of a new account by number. Even if someday we support creating multiple + * accounts within a smart contract call, we will still only need to track one created account + * per child record. + * + * @param num the number of the new account + * @return this builder + */ + @NonNull + CryptoCreateRecordBuilder setCreatedAccount(long num); +} 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 8f6069c0ef83..f4a555810ea4 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 @@ -13,6 +13,7 @@ requires com.hedera.pbj.runtime; requires com.github.spotbugs.annotations; requires transitive com.hedera.node.hapi; + requires org.apache.logging.log4j; provides com.hedera.node.app.service.token.TokenService with com.hedera.node.app.service.token.impl.TokenServiceImpl; @@ -22,4 +23,7 @@ com.hedera.node.app; 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 + com.hedera.node.app.service.token.impl.test, + com.hedera.node.app; } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/WritableAccountStoreTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/WritableAccountStoreTest.java new file mode 100644 index 000000000000..e32ba51f92e7 --- /dev/null +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/WritableAccountStoreTest.java @@ -0,0 +1,142 @@ +/* + * 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.service.token.impl.test; + +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 com.hedera.node.app.service.mono.state.virtual.EntityNumVirtualKey; +import com.hedera.node.app.service.token.impl.WritableAccountStore; +import com.hedera.node.app.service.token.impl.test.handlers.CryptoHandlerTestBase; +import java.util.Collections; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class WritableAccountStoreTest extends CryptoHandlerTestBase { + + @BeforeEach + public void setUp() { + super.setUp(); + resetStores(); + } + + @Test + void throwsIfNullValuesAsArgs() { + assertThrows(NullPointerException.class, () -> new WritableAccountStore(null)); + assertThrows(NullPointerException.class, () -> writableStore.put(null)); + assertThrows(NullPointerException.class, () -> writableStore.put(null)); + } + + @Test + void getReturnsImmutableAccount() { + refreshStoresWithCurrentTokenInWritable(); + + writableStore.put(account); + + final var maybeReadAccount = writableStore.get(id); + + assertTrue(maybeReadAccount.isPresent()); + final var readaccount = maybeReadAccount.get(); + assertEquals(account, readaccount); + } + + @Test + void getForModifyReturnsImmutableAccount() { + refreshStoresWithCurrentTokenInWritable(); + + writableStore.put(account); + + final var maybeReadAccount = writableStore.getForModify(id); + + assertTrue(maybeReadAccount.isPresent()); + final var readaccount = maybeReadAccount.get(); + assertEquals(account, readaccount); + } + + @Test + void getForModifyLooksForAlias() { + assertEquals(0, writableStore.sizeOfAliasesState()); + + writableStore.put(account); + writableStore.putAlias(alias.alias().asUtf8String(), accountNum); + + final var maybeReadAccount = writableStore.getForModify(alias); + + assertTrue(maybeReadAccount.isPresent()); + final var readaccount = maybeReadAccount.get(); + assertEquals(account, readaccount); + assertEquals(1, writableStore.sizeOfAliasesState()); + assertEquals(Set.of(alias.alias().asUtf8String()), writableStore.modifiedAliasesInState()); + } + + @Test + void getForModifyReturnEmptyIfAliasNotPresent() { + writableStore.put(account); + + final var maybeReadAccount = writableStore.getForModify(alias); + + assertFalse(maybeReadAccount.isPresent()); + assertEquals(0, writableStore.sizeOfAliasesState()); + } + + @Test + void putsAccountChangesToStateInModifications() { + assertFalse(writableStore.get(id).isPresent()); + + // put, keeps the account in the modifications + writableStore.put(account); + + assertTrue(writableAccounts.contains(accountEntityNumVirtualKey)); + final var writtenaccount = writableAccounts.get(accountEntityNumVirtualKey); + assertEquals(account, writtenaccount); + } + + @Test + void commitsAccountChangesToState() { + assertFalse(writableAccounts.contains(accountEntityNumVirtualKey)); + // put, keeps the account in the modifications. + // Size of state includes modifications and size of backing state. + writableStore.put(account); + + assertTrue(writableAccounts.contains(accountEntityNumVirtualKey)); + final var writtenaccount = writableAccounts.get(accountEntityNumVirtualKey); + assertEquals(1, writableStore.sizeOfAccountState()); + assertTrue(writableStore.modifiedAccountsInState().contains(accountEntityNumVirtualKey)); + assertEquals(account, writtenaccount); + + // commit, pushes modifications to backing store. But the size of state is still 1 + writableStore.commit(); + assertEquals(1, writableStore.sizeOfAccountState()); + assertTrue(writableStore.modifiedAccountsInState().contains(accountEntityNumVirtualKey)); + } + + @Test + void getsSizeOfState() { + assertEquals(0, writableStore.sizeOfAccountState()); + assertEquals(Collections.EMPTY_SET, writableStore.modifiedAccountsInState()); + writableStore.put(account); + + assertEquals(1, writableStore.sizeOfAccountState()); + assertEquals(Set.of(EntityNumVirtualKey.fromLong(3)), writableStore.modifiedAccountsInState()); + } +} diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoCreateHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoCreateHandlerTest.java index 415555d62c6e..e0415fccd18d 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoCreateHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoCreateHandlerTest.java @@ -16,40 +16,145 @@ 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.INSUFFICIENT_PAYER_BALANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_INITIAL_BALANCE; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_PAYER_ACCOUNT_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RECEIVE_RECORD_THRESHOLD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_RENEWAL_PERIOD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_SEND_RECORD_THRESHOLD; +import static com.hedera.hapi.node.base.ResponseCodeEnum.PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.BDDMockito.given; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.Duration; import com.hedera.hapi.node.base.TransactionID; import com.hedera.hapi.node.state.token.Account; import com.hedera.hapi.node.token.CryptoCreateTransactionBody; 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.CryptoCreateHandler; +import com.hedera.node.app.service.token.impl.records.CryptoCreateRecordBuilder; import com.hedera.node.app.spi.fixtures.workflows.FakePreHandleContext; +import com.hedera.node.app.spi.meta.HandleContext; +import com.hedera.node.app.spi.workflows.HandleException; import com.hedera.node.app.spi.workflows.PreCheckException; +import com.hedera.pbj.runtime.io.buffer.Bytes; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +/** + * Unit tests for {@link CryptoCreateHandler}. + */ +@ExtendWith(MockitoExtension.class) class CryptoCreateHandlerTest extends CryptoHandlerTestBase { - private final CryptoCreateHandler subject = new CryptoCreateHandler(); + @Mock + private HandleContext handleContext; + + private CryptoCreateHandler subject = new CryptoCreateHandler(); + private TransactionBody txn; + private CryptoCreateRecordBuilder recordBuilder; + private static final long defaultInitialBalance = 100L; @BeforeEach public void setUp() { super.setUp(); - readableAccounts = emptyReadableAccountStateBuilder() - .value(EntityNumVirtualKey.fromLong(accountNum), account) - .build(); - given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); - readableStore = new ReadableAccountStoreImpl(readableStates); + refreshStoresWithCurrentTokenInWritable(); + txn = new CryptoCreateBuilder().build(); + recordBuilder = subject.newRecordBuilder(); } @Test + @DisplayName("preHandle works when there is a receiverSigRequired") void preHandleCryptoCreateVanilla() throws PreCheckException { - final var txn = createAccountTransaction(true); + final var context = new FakePreHandleContext(readableStore, txn); + subject.preHandle(context); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 1); + assertEquals(key, context.payerKey()); + } + + @Test + @DisplayName("preHandle fails when initial balance is not greater than zero") + void preHandleFailsWhenInitialBalanceIsNegative() throws PreCheckException { + txn = new CryptoCreateBuilder().withInitialBalance(-1L).build(); + final var context = new FakePreHandleContext(readableStore, txn); + final var msg = assertThrows(PreCheckException.class, () -> subject.preHandle(context)); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 0); + assertEquals(key, context.payerKey()); + assertEquals(INVALID_INITIAL_BALANCE, msg.responseCode()); + } + @Test + @DisplayName("preHandle fails without auto-renew period specified") + void preHandleFailsWhenNoAutoRenewPeriodSpecified() throws PreCheckException { + txn = new CryptoCreateBuilder().withNoAutoRenewPeriod().build(); + final var context = new FakePreHandleContext(readableStore, txn); + final var msg = assertThrows(PreCheckException.class, () -> subject.preHandle(context)); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 0); + assertEquals(key, context.payerKey()); + assertEquals(INVALID_RENEWAL_PERIOD, msg.responseCode()); + } + + @Test + @DisplayName("preHandle fails when negative send record threshold is specified") + void preHandleFailsWhenSendRecordThresholdIsNegative() throws PreCheckException { + txn = new CryptoCreateBuilder().withSendRecordThreshold(-1).build(); + final var context = new FakePreHandleContext(readableStore, txn); + final var msg = assertThrows(PreCheckException.class, () -> subject.preHandle(context)); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 0); + assertEquals(key, context.payerKey()); + assertEquals(INVALID_SEND_RECORD_THRESHOLD, msg.responseCode()); + } + + @Test + @DisplayName("preHandle fails when negative receive record threshold is specified") + void preHandleFailsWhenReceiveRecordThresholdIsNegative() throws PreCheckException { + txn = new CryptoCreateBuilder().withReceiveRecordThreshold(-1).build(); + final var context = new FakePreHandleContext(readableStore, txn); + final var msg = assertThrows(PreCheckException.class, () -> subject.preHandle(context)); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 0); + assertEquals(key, context.payerKey()); + assertEquals(INVALID_RECEIVE_RECORD_THRESHOLD, msg.responseCode()); + } + + @Test + @DisplayName("preHandle fails when proxy accounts id is specified") + void preHandleFailsWhenProxyAccountIdIsSpecified() throws PreCheckException { + txn = new CryptoCreateBuilder().withProxyAccountNum(1).build(); + final var context = new FakePreHandleContext(readableStore, txn); + final var msg = assertThrows(PreCheckException.class, () -> subject.preHandle(context)); + + assertEquals(txn, context.body()); + basicMetaAssertions(context, 0); + assertEquals(key, context.payerKey()); + assertEquals(PROXY_ACCOUNT_ID_FIELD_IS_DEPRECATED, msg.responseCode()); + } + + @Test + @DisplayName("preHandle succeeds when initial balance is zero") + void preHandleWorksWhenInitialBalanceIsZero() throws PreCheckException { + txn = new CryptoCreateBuilder().withInitialBalance(0L).build(); final var context = new FakePreHandleContext(readableStore, txn); subject.preHandle(context); @@ -59,11 +164,13 @@ void preHandleCryptoCreateVanilla() throws PreCheckException { } @Test + @DisplayName("preHandle works when there is no receiverSigRequired") void noReceiverSigRequiredPreHandleCryptoCreate() throws PreCheckException { - final var txn = createAccountTransaction(false); - final var expected = new FakePreHandleContext(readableStore, txn); + final var noReceiverSigTxn = + new CryptoCreateBuilder().withReceiverSigReq(false).build(); + final var expected = new FakePreHandleContext(readableStore, noReceiverSigTxn); - final var context = new FakePreHandleContext(readableStore, txn); + final var context = new FakePreHandleContext(readableStore, noReceiverSigTxn); subject.preHandle(context); assertEquals(expected.body(), context.body()); @@ -73,17 +180,341 @@ void noReceiverSigRequiredPreHandleCryptoCreate() throws PreCheckException { assertEquals(key, context.payerKey()); } - private TransactionBody createAccountTransaction(final boolean receiverSigReq) { - final var transactionID = TransactionID.newBuilder().accountID(id).transactionValidStart(consensusTimestamp); - final var createTxnBody = CryptoCreateTransactionBody.newBuilder() - .key(otherKey) - .receiverSigRequired(receiverSigReq) - .memo("Create Account") - .build(); + @Test + @DisplayName("handle works when account can be created without any alias") + void handleCryptoCreateVanilla() { + given(handleContext.consensusNow()).willReturn(consensusInstant); + given(handleContext.newEntityNumSupplier()).willReturn(() -> 1000L); + + // newly created account and payer account are not modified. Validate payers balance + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals(payerBalance, writableStore.get(id).get().tinybarBalance()); + + subject.handle(handleContext, txn, writableStore, recordBuilder); + + // newly created account and payer account are modified + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + + // Validate created account exists and check record builder has created account recorded + final var optionalAccount = + writableStore.get(AccountID.newBuilder().accountNum(1000L).build()); + assertTrue(optionalAccount.isPresent()); + assertEquals(1000L, recordBuilder.getCreatedAccount()); + + // validate fields on created account + final var createdAccount = optionalAccount.get(); - return TransactionBody.newBuilder() - .transactionID(transactionID) - .cryptoCreateAccount(createTxnBody) + assertTrue(createdAccount.receiverSigRequired()); + assertEquals(1000L, createdAccount.accountNumber()); + assertEquals(Bytes.EMPTY, createdAccount.alias()); + assertEquals(otherKey, createdAccount.key()); + assertEquals(consensusTimestamp.seconds() + defaultAutoRenewPeriod, createdAccount.expiry()); + assertEquals(defaultInitialBalance, createdAccount.tinybarBalance()); + assertEquals("Create Account", createdAccount.memo()); + assertFalse(createdAccount.deleted()); + assertEquals(0L, createdAccount.stakedToMe()); + assertEquals(0L, createdAccount.stakePeriodStart()); + // staked node id is stored in state as negative long + assertEquals(-3 - 1, createdAccount.stakedNumber()); + assertFalse(createdAccount.declineReward()); + assertTrue(createdAccount.receiverSigRequired()); + assertEquals(0L, createdAccount.headTokenNumber()); + assertEquals(0L, createdAccount.headNftId()); + assertEquals(0L, createdAccount.headNftSerialNumber()); + assertEquals(0L, createdAccount.numberOwnedNfts()); + assertEquals(0, createdAccount.maxAutoAssociations()); + assertEquals(0, createdAccount.usedAutoAssociations()); + assertEquals(0, createdAccount.numberAssociations()); + assertFalse(createdAccount.smartContract()); + assertEquals(0, createdAccount.numberPositiveBalances()); + assertEquals(0L, createdAccount.ethereumNonce()); + assertEquals(0L, createdAccount.stakeAtStartOfLastRewardedPeriod()); + assertEquals(0L, createdAccount.autoRenewAccountNumber()); + assertEquals(defaultAutoRenewPeriod, createdAccount.autoRenewSecs()); + assertEquals(0, createdAccount.contractKvPairsNumber()); + assertTrue(createdAccount.cryptoAllowances().isEmpty()); + assertTrue(createdAccount.approveForAllNftAllowances().isEmpty()); + assertTrue(createdAccount.tokenAllowances().isEmpty()); + assertEquals(0, createdAccount.numberTreasuryTitles()); + assertFalse(createdAccount.expiredAndPendingRemoval()); + assertNull(createdAccount.firstContractStorageKey()); + + // validate payer balance reduced + assertEquals(9_900L, writableStore.get(id).get().tinybarBalance()); + } + + @Test + @DisplayName("handle works when account can be created without any alias using staked account id") + void handleCryptoCreateVanillaWithStakedAccountId() { + txn = new CryptoCreateBuilder().withStakedAccountId(1000).build(); + given(handleContext.consensusNow()).willReturn(consensusInstant); + given(handleContext.newEntityNumSupplier()).willReturn(() -> 1000L); + + // newly created account and payer account are not modified. Validate payers balance + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals(payerBalance, writableStore.get(id).get().tinybarBalance()); + + subject.handle(handleContext, txn, writableStore, recordBuilder); + + // newly created account and payer account are modified + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + + // Validate created account exists and check record builder has created account recorded + final var optionalAccount = + writableStore.get(AccountID.newBuilder().accountNum(1000L).build()); + assertTrue(optionalAccount.isPresent()); + assertEquals(1000L, recordBuilder.getCreatedAccount()); + + // validate fields on created account + final var createdAccount = optionalAccount.get(); + + assertTrue(createdAccount.receiverSigRequired()); + assertEquals(1000L, createdAccount.accountNumber()); + assertEquals(Bytes.EMPTY, createdAccount.alias()); + assertEquals(otherKey, createdAccount.key()); + assertEquals(consensusTimestamp.seconds() + defaultAutoRenewPeriod, createdAccount.expiry()); + assertEquals(defaultInitialBalance, createdAccount.tinybarBalance()); + assertEquals("Create Account", createdAccount.memo()); + assertFalse(createdAccount.deleted()); + assertEquals(0L, createdAccount.stakedToMe()); + assertEquals(0L, createdAccount.stakePeriodStart()); + // staked node id is stored in state as negative long + assertEquals(1000L, createdAccount.stakedNumber()); + assertFalse(createdAccount.declineReward()); + assertTrue(createdAccount.receiverSigRequired()); + assertEquals(0L, createdAccount.headTokenNumber()); + assertEquals(0L, createdAccount.headNftId()); + assertEquals(0L, createdAccount.headNftSerialNumber()); + assertEquals(0L, createdAccount.numberOwnedNfts()); + assertEquals(0, createdAccount.maxAutoAssociations()); + assertEquals(0, createdAccount.usedAutoAssociations()); + assertEquals(0, createdAccount.numberAssociations()); + assertFalse(createdAccount.smartContract()); + assertEquals(0, createdAccount.numberPositiveBalances()); + assertEquals(0L, createdAccount.ethereumNonce()); + assertEquals(0L, createdAccount.stakeAtStartOfLastRewardedPeriod()); + assertEquals(0L, createdAccount.autoRenewAccountNumber()); + assertEquals(defaultAutoRenewPeriod, createdAccount.autoRenewSecs()); + assertEquals(0, createdAccount.contractKvPairsNumber()); + assertTrue(createdAccount.cryptoAllowances().isEmpty()); + assertTrue(createdAccount.approveForAllNftAllowances().isEmpty()); + assertTrue(createdAccount.tokenAllowances().isEmpty()); + assertEquals(0, createdAccount.numberTreasuryTitles()); + assertFalse(createdAccount.expiredAndPendingRemoval()); + assertNull(createdAccount.firstContractStorageKey()); + + // validate payer balance reduced + assertEquals(9_900L, writableStore.get(id).get().tinybarBalance()); + } + + @Test + @DisplayName("handle fails when autoRenewPeriod is not set. This should not happen as there should" + + " be a semantic check in `preHandle` and handle workflow should reject the " + + "transaction before reaching handle") + void handleFailsWhenAutoRenewPeriodNotSet() { + txn = new CryptoCreateBuilder().withNoAutoRenewPeriod().build(); + // newly created account and payer account are not modified. Validate payers balance + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals(payerBalance, writableStore.get(id).get().tinybarBalance()); + + assertThrows( + NullPointerException.class, () -> subject.handle(handleContext, txn, writableStore, recordBuilder)); + } + + @Test + @DisplayName("handle fails when payer account can't pay for the newly created account initial balance") + void handleFailsWhenPayerHasInsufficientBalance() { + txn = new CryptoCreateBuilder().withInitialBalance(payerBalance + 1L).build(); + + // newly created account and payer account are not modified. Validate payers balance + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals(payerBalance, writableStore.get(id).get().tinybarBalance()); + + final var msg = assertThrows( + HandleException.class, () -> subject.handle(handleContext, txn, writableStore, recordBuilder)); + assertEquals(INSUFFICIENT_PAYER_BALANCE, msg.getStatus()); + + final var recordMsg = assertThrows(IllegalStateException.class, () -> recordBuilder.getCreatedAccount()); + assertEquals("No new account number was recorded", recordMsg.getMessage()); + + // newly created account and payer account are not modified + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + } + + @Test + @DisplayName("handle fails when payer account is deleted") + void handleFailsWhenPayerIsDeleted() { + changeAccountToDeleted(); + + final var msg = assertThrows( + HandleException.class, () -> subject.handle(handleContext, txn, writableStore, recordBuilder)); + assertEquals(ACCOUNT_DELETED, msg.getStatus()); + + final var recordMsg = assertThrows(IllegalStateException.class, () -> recordBuilder.getCreatedAccount()); + assertEquals("No new account number was recorded", recordMsg.getMessage()); + + // newly created account and payer account are not modified + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + } + + @Test + @DisplayName("handle fails when payer account doesn't exist") + void handleFailsWhenPayerInvalid() { + txn = new CryptoCreateBuilder() + .withPayer(AccountID.newBuilder().accountNum(600L).build()) .build(); + + final var msg = assertThrows( + HandleException.class, () -> subject.handle(handleContext, txn, writableStore, recordBuilder)); + assertEquals(INVALID_PAYER_ACCOUNT_ID, msg.getStatus()); + + final var recordMsg = assertThrows(IllegalStateException.class, () -> recordBuilder.getCreatedAccount()); + assertEquals("No new account number was recorded", recordMsg.getMessage()); + + // newly created account and payer account are not modified + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + } + + @Test + @DisplayName("handle commits when any alias is mentioned in the transaction") + void handleCommitsAnyAlias() { + txn = new CryptoCreateBuilder().withAlias(Bytes.wrap("alias")).build(); + + given(handleContext.consensusNow()).willReturn(consensusInstant); + given(handleContext.newEntityNumSupplier()).willReturn(() -> 1000L); + + // newly created account and payer account are not modified. Validate payers balance + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertFalse(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals(payerBalance, writableStore.get(id).get().tinybarBalance()); + + subject.handle(handleContext, txn, writableStore, recordBuilder); + + // newly created account and payer account are modified + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(1000L))); + assertTrue(writableStore.modifiedAccountsInState().contains(EntityNumVirtualKey.fromLong(id.accountNum()))); + assertEquals( + Bytes.wrap("alias"), + writableStore + .get(AccountID.newBuilder().accountNum(1000L).build()) + .get() + .alias()); + } + + private void changeAccountToDeleted() { + final var copy = account.copyBuilder().deleted(true).build(); + writableAccounts.put(EntityNumVirtualKey.fromLong(id.accountNum()), copy); + given(writableStates.get(ACCOUNTS)).willReturn(writableAccounts); + writableStore = new WritableAccountStore(writableStates); + } + + /** + * A builder for {@link TransactionBody} instances. + */ + private class CryptoCreateBuilder { + private AccountID payer = id; + private long initialBalance = defaultInitialBalance; + private long autoRenewPeriod = defaultAutoRenewPeriod; + private boolean receiverSigReq = true; + private Bytes alias = null; + private long sendRecordThreshold = 0; + private long receiveRecordThreshold = 0; + private AccountID proxyAccountId = null; + private long stakeNodeId = 3; + private long stakedAccountId = 0; + + private CryptoCreateBuilder() {} + + public TransactionBody build() { + final var transactionID = + TransactionID.newBuilder().accountID(payer).transactionValidStart(consensusTimestamp); + final var createTxnBody = CryptoCreateTransactionBody.newBuilder() + .key(otherKey) + .receiverSigRequired(receiverSigReq) + .initialBalance(initialBalance) + .memo("Create Account") + .sendRecordThreshold(sendRecordThreshold) + .receiveRecordThreshold(receiveRecordThreshold); + + if (autoRenewPeriod > 0) { + createTxnBody.autoRenewPeriod( + Duration.newBuilder().seconds(autoRenewPeriod).build()); + } + if (alias != null) { + createTxnBody.alias(alias); + } + if (proxyAccountId != null) { + createTxnBody.proxyAccountID(proxyAccountId); + } + if (stakedAccountId > 0) { + createTxnBody.stakedAccountId( + AccountID.newBuilder().accountNum(stakedAccountId).build()); + } else { + createTxnBody.stakedNodeId(stakeNodeId); + } + + return TransactionBody.newBuilder() + .transactionID(transactionID) + .cryptoCreateAccount(createTxnBody.build()) + .build(); + } + + public CryptoCreateBuilder withPayer(final AccountID payer) { + this.payer = payer; + return this; + } + + public CryptoCreateBuilder withInitialBalance(final long initialBalance) { + this.initialBalance = initialBalance; + return this; + } + + public CryptoCreateBuilder withAutoRenewPeriod(final long autoRenewPeriod) { + this.autoRenewPeriod = autoRenewPeriod; + return this; + } + + public CryptoCreateBuilder withProxyAccountNum(final long proxyAccountNum) { + this.proxyAccountId = + AccountID.newBuilder().accountNum(proxyAccountNum).build(); + return this; + } + + public CryptoCreateBuilder withSendRecordThreshold(final long threshold) { + this.sendRecordThreshold = threshold; + return this; + } + + public CryptoCreateBuilder withReceiveRecordThreshold(final long threshold) { + this.receiveRecordThreshold = threshold; + return this; + } + + public CryptoCreateBuilder withAlias(final Bytes alias) { + this.alias = alias; + return this; + } + + public CryptoCreateBuilder withNoAutoRenewPeriod() { + this.autoRenewPeriod = -1; + return this; + } + + public CryptoCreateBuilder withStakedAccountId(final long id) { + this.stakedAccountId = id; + return this; + } + + public CryptoCreateBuilder withReceiverSigReq(final boolean receiverSigReq) { + this.receiverSigReq = receiverSigReq; + return this; + } } } 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 cb0b25dcdb17..a32a91913424 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 @@ -36,8 +36,8 @@ import com.hedera.node.app.service.token.ReadableAccountStore; import com.hedera.node.app.service.token.impl.CryptoSignatureWaiversImpl; import com.hedera.node.app.service.token.impl.ReadableAccountStoreImpl; +import com.hedera.node.app.service.token.impl.WritableAccountStore; import com.hedera.node.app.spi.fixtures.state.MapReadableKVState; -import com.hedera.node.app.spi.fixtures.state.MapReadableKVState.Builder; import com.hedera.node.app.spi.fixtures.state.MapWritableKVState; import com.hedera.node.app.spi.key.HederaKey; import com.hedera.node.app.spi.state.ReadableStates; @@ -46,6 +46,7 @@ import com.hedera.pbj.runtime.io.buffer.Bytes; import com.swirlds.common.utility.CommonUtils; import edu.umd.cs.findbugs.annotations.NonNull; +import java.time.Instant; import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -61,11 +62,12 @@ public class CryptoHandlerTestBase { protected final AccountID id = AccountID.newBuilder().accountNum(3).build(); protected final Timestamp consensusTimestamp = Timestamp.newBuilder().seconds(1_234_567L).build(); + protected final Instant consensusInstant = Instant.ofEpochSecond(consensusTimestamp.seconds()); protected final Key accountKey = A_COMPLEX_KEY; protected final HederaKey accountHederaKey = asHederaKey(accountKey).get(); protected final Long accountNum = id.accountNum(); protected final EntityNumVirtualKey accountEntityNumVirtualKey = new EntityNumVirtualKey(accountNum); - private final AccountID alias = + protected final AccountID alias = AccountID.newBuilder().alias(Bytes.wrap("testAlias")).build(); protected final byte[] evmAddress = CommonUtils.unhex("6aea3773ea468a814d954e6dec795bfee7d76e25"); protected final ContractID contractAlias = @@ -98,12 +100,15 @@ public class CryptoHandlerTestBase { .tokenId(token) .owner(owner) .build(); + 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; protected Account account; protected ReadableAccountStore readableStore; + protected WritableAccountStore writableStore; @Mock protected Account deleteAccount; @@ -130,14 +135,28 @@ protected void basicMetaAssertions(final PreHandleContext context, final int key assertThat(context.requiredNonPayerKeys()).hasSize(keysSize); } + protected void resetStores() { + readableAccounts = emptyReadableAccountStateBuilder().build(); + writableAccounts = emptyWritableAccountStateBuilder().build(); + readableAliases = emptyReadableAliasStateBuilder().build(); + writableAliases = emptyWritableAliasStateBuilder().build(); + given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); + given(readableStates.get(ALIASES)).willReturn(readableAliases); + given(writableStates.get(ACCOUNTS)).willReturn(writableAccounts); + given(writableStates.get(ALIASES)).willReturn(writableAliases); + readableStore = new ReadableAccountStoreImpl(readableStates); + writableStore = new WritableAccountStore(writableStates); + } + protected void refreshStoresWithCurrentTokenOnlyInReadable() { readableAccounts = readableAccountState(); - writableAccounts = emptyWritableAccountState(); + writableAccounts = emptyWritableAccountStateBuilder().build(); readableAliases = readableAliasState(); - writableAliases = emptyWritableAliasState(); + writableAliases = emptyWritableAliasStateBuilder().build(); given(readableStates.get(ACCOUNTS)).willReturn(readableAccounts); given(readableStates.get(ALIASES)).willReturn(readableAliases); readableStore = new ReadableAccountStoreImpl(readableStates); + writableStore = new WritableAccountStore(writableStates); } protected void refreshStoresWithCurrentTokenInWritable() { @@ -150,17 +169,12 @@ protected void refreshStoresWithCurrentTokenInWritable() { given(writableStates.get(ACCOUNTS)).willReturn(writableAccounts); given(writableStates.get(ALIASES)).willReturn(writableAliases); readableStore = new ReadableAccountStoreImpl(readableStates); - } - - @NonNull - protected MapWritableKVState emptyWritableAccountState() { - return MapWritableKVState.builder(ACCOUNTS) - .build(); + writableStore = new WritableAccountStore(writableStates); } @NonNull protected MapWritableKVState writableAccountStateWithOneKey() { - return MapWritableKVState.builder(ACCOUNTS) + return emptyWritableAccountStateBuilder() .value(accountEntityNumVirtualKey, account) .value(EntityNumVirtualKey.fromLong(deleteAccountNum), deleteAccount) .value(EntityNumVirtualKey.fromLong(transferAccountNum), transferAccount) @@ -169,26 +183,16 @@ protected MapWritableKVState writableAccountStateW @NonNull protected MapReadableKVState readableAccountState() { - return MapReadableKVState.builder(ACCOUNTS) + return emptyReadableAccountStateBuilder() .value(accountEntityNumVirtualKey, account) .value(EntityNumVirtualKey.fromLong(deleteAccountNum), deleteAccount) .value(EntityNumVirtualKey.fromLong(transferAccountNum), transferAccount) .build(); } - @NonNull - protected Builder emptyReadableAccountStateBuilder() { - return MapReadableKVState.builder(ACCOUNTS); - } - - @NonNull - protected MapWritableKVState emptyWritableAliasState() { - return MapWritableKVState.builder(ALIASES).build(); - } - @NonNull protected MapWritableKVState writableAliasesStateWithOneKey() { - return MapWritableKVState.builder(ALIASES) + return emptyWritableAliasStateBuilder() .value(alias.toString(), new EntityNumValue(accountNum)) .value(contractAlias.toString(), new EntityNumValue(contract.contractNum())) .build(); @@ -196,19 +200,39 @@ protected MapWritableKVState writableAliasesStateWithOne @NonNull protected MapReadableKVState readableAliasState() { - return MapReadableKVState.builder(ACCOUNTS) + return emptyReadableAliasStateBuilder() .value(alias.toString(), new EntityNumValue(accountNum)) .value(contractAlias.toString(), new EntityNumValue(contract.contractNum())) .build(); } - private void givenValidAccount() { + @NonNull + protected MapReadableKVState.Builder emptyReadableAccountStateBuilder() { + return MapReadableKVState.builder(ACCOUNTS); + } + + @NonNull + protected MapWritableKVState.Builder emptyWritableAccountStateBuilder() { + return MapWritableKVState.builder(ACCOUNTS); + } + + @NonNull + protected MapWritableKVState.Builder emptyWritableAliasStateBuilder() { + return MapWritableKVState.builder(ALIASES); + } + + @NonNull + protected MapReadableKVState.Builder emptyReadableAliasStateBuilder() { + return MapReadableKVState.builder(ALIASES); + } + + protected void givenValidAccount() { account = new Account( accountNum, alias.alias(), key, 1_234_567L, - 10_000, + payerBalance, "testAccount", false, 1_234L, @@ -234,6 +258,7 @@ private void givenValidAccount() { Collections.emptyList(), Collections.emptyList(), 2, - false); + false, + null); } } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenHandlerTestBase.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenHandlerTestBase.java index 2e4c46158f17..d962055f1a58 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenHandlerTestBase.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/TokenHandlerTestBase.java @@ -252,6 +252,7 @@ protected Account newPayerAccount() { Collections.emptyList(), Collections.emptyList(), 2, - false); + false, + null); } } diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/records/CreateAccountRecordBuilderTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/records/CreateAccountRecordBuilderTest.java new file mode 100644 index 000000000000..7eab83149a3e --- /dev/null +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/records/CreateAccountRecordBuilderTest.java @@ -0,0 +1,47 @@ +/* + * 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.service.token.impl.test.records; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.hedera.node.app.service.token.impl.records.CreateAccountRecordBuilder; +import org.junit.jupiter.api.Test; + +class CreateAccountRecordBuilderTest { + @Test + void setterAndGetterWorks() { + var subject = new CreateAccountRecordBuilder(); + + subject.setCreatedAccount(1L); + assertThat(subject.getCreatedAccount()).isEqualTo(1L); + } + + @Test + void getterWithoutSetFails() { + var subject = new CreateAccountRecordBuilder(); + + assertThatThrownBy(() -> subject.getCreatedAccount(), "No new account number was recorded"); + } + + @Test + void selfReturnsSameObject() { + var subject = new CreateAccountRecordBuilder(); + + assertThat(subject.self()).isSameAs(subject); + } +} diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/util/SigReqAdapterUtils.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/util/SigReqAdapterUtils.java index d0cdd8a8dcf7..2c146ecfe689 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/util/SigReqAdapterUtils.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/util/SigReqAdapterUtils.java @@ -73,9 +73,9 @@ import com.hedera.hapi.node.base.Key; import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.AccountApprovalForAllAllowance; import com.hedera.hapi.node.state.token.AccountCryptoAllowance; import com.hedera.hapi.node.state.token.AccountFungibleTokenAllowance; -import com.hedera.hapi.node.state.token.AccountTokenAllowance; import com.hedera.hapi.node.state.token.Token; import com.hedera.hapi.node.transaction.CustomFee; import com.hedera.hapi.node.transaction.TransactionBody; @@ -102,20 +102,18 @@ public class SigReqAdapterUtils { private static final String ACCOUNTS_KEY = "ACCOUNTS"; private static AccountCryptoAllowance cryptoAllowances = AccountCryptoAllowance.newBuilder() - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(500L) .build(); private static AccountFungibleTokenAllowance fungibleTokenAllowances = AccountFungibleTokenAllowance.newBuilder() - .tokenAllowanceKey(AccountTokenAllowance.newBuilder() - .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) - .build()) + .tokenNum(KNOWN_TOKEN_NO_SPECIAL_KEYS.getTokenNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .amount(10_000L) .build(); - private static AccountTokenAllowance nftAllowances = AccountTokenAllowance.newBuilder() + private static AccountApprovalForAllAllowance nftAllowances = AccountApprovalForAllAllowance.newBuilder() .tokenNum(KNOWN_TOKEN_WITH_WIPE.getTokenNum()) - .accountNum(DEFAULT_PAYER.getAccountNum()) + .spenderNum(DEFAULT_PAYER.getAccountNum()) .build(); /** @@ -240,7 +238,7 @@ private static Account toPbjAccount( boolean receiverSigRequired, List cryptoAllowances, List fungibleTokenAllowances, - List nftTokenAllowances) { + List nftTokenAllowances) { return new Account( number, Bytes.EMPTY, @@ -272,7 +270,8 @@ private static Account toPbjAccount( nftTokenAllowances, fungibleTokenAllowances, 2, - false); + false, + null); } @SuppressWarnings("java:S1604") diff --git a/hedera-node/hedera-token-service-impl/src/test/java/module-info.java b/hedera-node/hedera-token-service-impl/src/test/java/module-info.java index 4fa8c172d190..bedd635985eb 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/module-info.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/module-info.java @@ -19,6 +19,7 @@ requires com.hedera.node.app.spi.fixtures; requires static com.github.spotbugs.annotations; requires com.swirlds.merkle; + requires com.hedera.node.app; opens com.hedera.node.app.service.token.impl.test.util to org.junit.platform.commons; @@ -30,4 +31,7 @@ opens com.hedera.node.app.service.token.impl.test.handlers to org.junit.platform.commons, org.mockito; + opens com.hedera.node.app.service.token.impl.test.records to + org.junit.platform.commons, + org.mockito; } diff --git a/settings.gradle.kts b/settings.gradle.kts index b97344dabf71..a020fa5d1b5c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -101,7 +101,8 @@ gitRepositories { uri.set("https://github.com/hashgraph/hedera-protobufs.git") // choose tag or branch of HAPI you would like to test with // this looks for a tag in hedera-protobufs repo - tag.set("v0.38.0") + // This version needs to match tha HAPI version below in versionCatalogs + tag.set("add-missing-account-fields") // do not load project from repo autoInclude.set(false) } @@ -116,8 +117,7 @@ dependencyResolutionManagement { // runtime. create("libs") { // The HAPI API version to use, this need to match the tag set on gitRepositories above - // this looks for a tag in nexus repository manager - version("hapi-version", "0.38.1-SNAPSHOT") + version("hapi-version", "0.38.1-allowance-SNAPSHOT") // Definition of version numbers for all libraries version("pbj-version", "0.5.1")