From e1ba045bec4dd27e99201902a92cf0ea7b45b614 Mon Sep 17 00:00:00 2001 From: Valentin Valkanov <32802706+MrValioBg@users.noreply.github.com> Date: Fri, 29 Sep 2023 18:45:12 +0300 Subject: [PATCH] Implementation of SetApprovalForAll system contract (#8723) Signed-off-by: Valentin Valkanov Co-authored-by: Mustafa Uzun --- .../java/contract/SetApprovalForAllXTest.java | 270 ++++++++++++++++++ .../xtest/java/contract/XTestConstants.java | 2 + .../exec/processors/HtsTranslatorsModule.java | 8 + .../setapproval/SetApprovalForAllDecoder.java | 91 ++++++ .../SetApprovalForAllTranslator.java | 77 +++++ .../SetApprovalForAllDecoderTest.java | 130 +++++++++ .../SetApprovalForAllTranslatorTest.java | 69 +++++ .../CryptoApproveAllowanceHandler.java | 3 +- .../CryptoApproveAllowanceHandlerTest.java | 8 +- 9 files changed, 655 insertions(+), 3 deletions(-) create mode 100644 hedera-node/hedera-app/src/xtest/java/contract/SetApprovalForAllXTest.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoder.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslator.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoderTest.java create mode 100644 hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslatorTest.java diff --git a/hedera-node/hedera-app/src/xtest/java/contract/SetApprovalForAllXTest.java b/hedera-node/hedera-app/src/xtest/java/contract/SetApprovalForAllXTest.java new file mode 100644 index 000000000000..a6769fb496ed --- /dev/null +++ b/hedera-node/hedera-app/src/xtest/java/contract/SetApprovalForAllXTest.java @@ -0,0 +1,270 @@ +/* + * 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 contract; + +import static com.hedera.hapi.node.base.ResponseCodeEnum.SPENDER_DOES_NOT_HAVE_ALLOWANCE; +import static contract.HtsErc721TransferXTestConstants.APPROVED_ID; +import static contract.HtsErc721TransferXTestConstants.UNAUTHORIZED_SPENDER_ID; +import static contract.MiscClassicTransfersXTestConstants.INITIAL_RECEIVER_AUTO_ASSOCIATIONS; +import static contract.MiscClassicTransfersXTestConstants.NEXT_ENTITY_NUM; +import static contract.XTestConstants.AN_ED25519_KEY; +import static contract.XTestConstants.ERC721_TOKEN_ADDRESS; +import static contract.XTestConstants.ERC721_TOKEN_ID; +import static contract.XTestConstants.OWNER_ADDRESS; +import static contract.XTestConstants.OWNER_BESU_ADDRESS; +import static contract.XTestConstants.OWNER_HEADLONG_ADDRESS; +import static contract.XTestConstants.OWNER_ID; +import static contract.XTestConstants.RECEIVER_HEADLONG_ADDRESS; +import static contract.XTestConstants.RECEIVER_ID; +import static contract.XTestConstants.SENDER_ADDRESS; +import static contract.XTestConstants.SENDER_BESU_ADDRESS; +import static contract.XTestConstants.SENDER_HEADLONG_ADDRESS; +import static contract.XTestConstants.SENDER_ID; +import static contract.XTestConstants.SN_1234; +import static contract.XTestConstants.SN_1234_METADATA; +import static contract.XTestConstants.SN_2345; +import static contract.XTestConstants.SN_2345_METADATA; +import static contract.XTestConstants.addErc721Relation; +import static contract.XTestConstants.assertSuccess; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.NftID; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.base.TokenType; +import com.hedera.hapi.node.state.common.EntityIDPair; +import com.hedera.hapi.node.state.primitives.ProtoBytes; +import com.hedera.hapi.node.state.token.Account; +import com.hedera.hapi.node.state.token.Nft; +import com.hedera.hapi.node.state.token.Token; +import com.hedera.hapi.node.state.token.TokenRelation; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ClassicTransfersTranslator; +import com.hedera.node.app.spi.state.ReadableKVState; +import java.util.HashMap; +import java.util.Map; +import org.apache.tuweni.bytes.Bytes; +import org.jetbrains.annotations.NotNull; + +/** + * Exercises setApprovalForAll via the following steps relative to an {@code OWNER} account: + *
    + *
  1. Transfer {@code ERC721_TOKEN} serialN 1234 from Owner's account. Should fail with SPENDER_DOES_NOT_HAVE_ALLOWANCE
  2. + *
  3. SetApprovalForAll to true and verify successful operation
  4. + *
  5. Transfer {@code ERC721_TOKEN} serialN 1234 from Owner's account and verify successful operation
  6. + *
  7. SetApprovalForAll to false and verify successful operation
  8. + *
  9. Transfer {@code ERC721_TOKEN} serialN 2345 from Owner's account. Should fail with SPENDER_DOES_NOT_HAVE_ALLOWANCE
  10. + *
  11. SetApprovalForAll with ERC call to true and verify successful operation
  12. + *
  13. Transfer {@code ERC721_TOKEN} serialN 2345 from Owner's account and verify successful operation
  14. + *
  15. SetApprovalForAll with ERC call to false and verify successful operation
  16. + *
  17. Via {@code assertExpectedAccounts} verify that 2 NFTs have been transferred from the OWNER to the RECEIVER
  18. + *
+ */ +public class SetApprovalForAllXTest extends AbstractContractXTest { + + private static final long NUMBER_OWNED_NFT = 3L; + + @Override + protected void doScenarioOperations() { + // Transfer series 1234 of ERC721_TOKEN to RECEIVER + runHtsCallAndExpectOnSuccess( + SENDER_BESU_ADDRESS, + Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT + .encodeCallWithArgs( + ERC721_TOKEN_ADDRESS, + OWNER_HEADLONG_ADDRESS, + RECEIVER_HEADLONG_ADDRESS, + SN_1234.serialNumber()) + .array()), + output -> assertEquals( + Bytes.wrap(ReturnTypes.encodedRc(SPENDER_DOES_NOT_HAVE_ALLOWANCE) + .array()), + output)); + + // Set approval for all to true + runHtsCallAndExpectOnSuccess( + OWNER_BESU_ADDRESS, + Bytes.wrap(SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(ERC721_TOKEN_ADDRESS, SENDER_HEADLONG_ADDRESS, true) + .array()), + assertSuccess()); + + // Transfer series 1234 of ERC721_TOKEN to RECEIVER + runHtsCallAndExpectOnSuccess( + SENDER_BESU_ADDRESS, + Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT + .encodeCallWithArgs( + ERC721_TOKEN_ADDRESS, + OWNER_HEADLONG_ADDRESS, + RECEIVER_HEADLONG_ADDRESS, + SN_1234.serialNumber()) + .array()), + assertSuccess()); + + // Set approval for all to false + runHtsCallAndExpectOnSuccess( + OWNER_BESU_ADDRESS, + Bytes.wrap(SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(ERC721_TOKEN_ADDRESS, SENDER_HEADLONG_ADDRESS, false) + .array()), + assertSuccess()); + + // Attempt to transfer series 2345 of ERC721_TOKEN to RECEIVER + runHtsCallAndExpectOnSuccess( + SENDER_BESU_ADDRESS, + Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT + .encodeCallWithArgs( + ERC721_TOKEN_ADDRESS, + OWNER_HEADLONG_ADDRESS, + RECEIVER_HEADLONG_ADDRESS, + SN_2345.serialNumber()) + .array()), + output -> assertEquals( + Bytes.wrap(ReturnTypes.encodedRc(SPENDER_DOES_NOT_HAVE_ALLOWANCE) + .array()), + output)); + + // Set approval for all to true via ERC call + runHtsCallAndExpectOnSuccess( + OWNER_BESU_ADDRESS, + bytesForRedirect( + SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.encodeCallWithArgs( + SENDER_HEADLONG_ADDRESS, true), + ERC721_TOKEN_ID), + assertSuccess()); + + // Transfer series 2345 of ERC721_TOKEN to RECEIVER + runHtsCallAndExpectOnSuccess( + SENDER_BESU_ADDRESS, + Bytes.wrap(ClassicTransfersTranslator.TRANSFER_NFT + .encodeCallWithArgs( + ERC721_TOKEN_ADDRESS, + OWNER_HEADLONG_ADDRESS, + RECEIVER_HEADLONG_ADDRESS, + SN_2345.serialNumber()) + .array()), + assertSuccess()); + + // Set approval for all to false via ERC call + runHtsCallAndExpectOnSuccess( + OWNER_BESU_ADDRESS, + bytesForRedirect( + SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.encodeCallWithArgs( + SENDER_HEADLONG_ADDRESS, false), + ERC721_TOKEN_ID), + assertSuccess()); + } + + @Override + protected long initialEntityNum() { + return NEXT_ENTITY_NUM - 1; + } + + @Override + protected Map initialAliases() { + final var aliases = new HashMap(); + aliases.put(ProtoBytes.newBuilder().value(SENDER_ADDRESS).build(), SENDER_ID); + aliases.put(ProtoBytes.newBuilder().value(OWNER_ADDRESS).build(), OWNER_ID); + return aliases; + } + + @Override + protected Map initialTokens() { + final var tokens = new HashMap(); + tokens.put( + ERC721_TOKEN_ID, + Token.newBuilder() + .tokenId(ERC721_TOKEN_ID) + .treasuryAccountId(UNAUTHORIZED_SPENDER_ID) + .tokenType(TokenType.NON_FUNGIBLE_UNIQUE) + .build()); + return tokens; + } + + @Override + protected Map initialTokenRelationships() { + final var tokenRelationships = new HashMap(); + addErc721Relation(tokenRelationships, OWNER_ID, NUMBER_OWNED_NFT); + return tokenRelationships; + } + + @Override + protected Map initialNfts() { + final var nfts = new HashMap(); + nfts.put( + SN_1234, + Nft.newBuilder() + .nftId(SN_1234) + .ownerId(OWNER_ID) + .spenderId(APPROVED_ID) + .metadata(SN_1234_METADATA) + .build()); + nfts.put( + SN_2345, + Nft.newBuilder() + .nftId(SN_2345) + .ownerId(OWNER_ID) + .metadata(SN_2345_METADATA) + .build()); + return nfts; + } + + @Override + protected Map initialAccounts() { + final var accounts = new HashMap(); + accounts.put( + SENDER_ID, + Account.newBuilder() + .accountId(SENDER_ID) + .alias(SENDER_ADDRESS) + .smartContract(true) + .build()); + accounts.put( + OWNER_ID, + Account.newBuilder() + .accountId(OWNER_ID) + .alias(OWNER_ADDRESS) + .key(AN_ED25519_KEY) + .numberOwnedNfts(NUMBER_OWNED_NFT) + .build()); + accounts.put( + RECEIVER_ID, + Account.newBuilder() + .accountId(RECEIVER_ID) + .maxAutoAssociations(INITIAL_RECEIVER_AUTO_ASSOCIATIONS) + .build()); + accounts.put( + UNAUTHORIZED_SPENDER_ID, + Account.newBuilder().accountId(UNAUTHORIZED_SPENDER_ID).build()); + return accounts; + } + + @Override + protected void assertExpectedAccounts(@NotNull final ReadableKVState accounts) { + final var ownerAccount = accounts.get(OWNER_ID); + final var receiverAccount = accounts.get(RECEIVER_ID); + assertNotNull(ownerAccount); + assertNotNull(receiverAccount); + + // Number of owned NFTs should be decreased by 2 + // Number of receiver NFTs should be 2 + assertEquals(NUMBER_OWNED_NFT - 2, ownerAccount.numberOwnedNfts()); + assertEquals(2, receiverAccount.numberOwnedNfts()); + } +} diff --git a/hedera-node/hedera-app/src/xtest/java/contract/XTestConstants.java b/hedera-node/hedera-app/src/xtest/java/contract/XTestConstants.java index 5c91bfdf3508..2b0d318a9e4e 100644 --- a/hedera-node/hedera-app/src/xtest/java/contract/XTestConstants.java +++ b/hedera-node/hedera-app/src/xtest/java/contract/XTestConstants.java @@ -80,6 +80,8 @@ class XTestConstants { .build(); static final Bytes SENDER_ADDRESS = com.hedera.pbj.runtime.io.buffer.Bytes.fromHex("f91e624b8b8ea7244e8159ba7c0deeea2b6be990"); + static final com.esaulpaugh.headlong.abi.Address SENDER_HEADLONG_ADDRESS = + asHeadlongAddress(SENDER_ADDRESS.toByteArray()); static final Address SENDER_BESU_ADDRESS = pbjToBesuAddress(SENDER_ADDRESS); static final AccountID RECEIVER_ID = AccountID.newBuilder().accountNum(987654321L).build(); diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java index c5d16f2ff47c..ad54452a1e88 100644 --- a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/processors/HtsTranslatorsModule.java @@ -29,6 +29,7 @@ import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.name.NameTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ownerof.OwnerOfTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.pauses.PausesTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.symbol.SymbolTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.tokenuri.TokenUriTranslator; import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.totalsupply.TotalSupplyTranslator; @@ -140,6 +141,13 @@ static HtsCallTranslator provideOwnerOfTranslator(@NonNull final OwnerOfTranslat return translator; } + @Provides + @Singleton + @IntoSet + static HtsCallTranslator provideSetApprovalForAllTranslator(@NonNull final SetApprovalForAllTranslator translator) { + return translator; + } + @Provides @Singleton @IntoSet diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoder.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoder.java new file mode 100644 index 000000000000..821e847f457e --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoder.java @@ -0,0 +1,91 @@ +/* + * 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.contract.impl.exec.systemcontracts.hts.setapproval; + +import static com.hedera.node.app.service.contract.impl.utils.ConversionUtils.asTokenId; + +import com.esaulpaugh.headlong.abi.Address; +import com.hedera.hapi.node.base.AccountID; +import com.hedera.hapi.node.base.TokenID; +import com.hedera.hapi.node.token.CryptoApproveAllowanceTransactionBody; +import com.hedera.hapi.node.token.NftAllowance; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Objects; +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class SetApprovalForAllDecoder { + + @Inject + public SetApprovalForAllDecoder() { + // Dagger 2 constructor + } + + /** + * Decodes the given {@code attempt} into a {@link TransactionBody} for a setApprovalForAll function call. + * + * @param attempt the attempt to decode + * @return a {@link TransactionBody} + */ + public TransactionBody decodeSetApprovalForAll(@NonNull final HtsCallAttempt attempt) { + final var call = SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL.decodeCall(attempt.inputBytes()); + return bodyOf(approveAllAllowanceNFTBody( + attempt.addressIdConverter(), attempt.senderId(), asTokenId(call.get(0)), call.get(1), call.get(2))); + } + + /** + * Decodes the given {@code attempt} into a {@link TransactionBody} for a setApprovalForAll ERC function call. + * + * @param attempt the attempt to decode + * @return a {@link TransactionBody} + */ + public TransactionBody decodeSetApprovalForAllERC(@NonNull final HtsCallAttempt attempt) { + final var call = SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.decodeCall(attempt.inputBytes()); + final var tokenId = attempt.redirectTokenId(); + Objects.requireNonNull(tokenId, "Redirect Token ID is null."); + + return bodyOf(approveAllAllowanceNFTBody( + attempt.addressIdConverter(), attempt.senderId(), tokenId, call.get(0), call.get(1))); + } + + private CryptoApproveAllowanceTransactionBody approveAllAllowanceNFTBody( + @NonNull final AddressIdConverter addressIdConverter, + @NonNull final AccountID senderId, + @NonNull final TokenID tokenID, + @NonNull final Address operatorAddress, + final boolean approved) { + return CryptoApproveAllowanceTransactionBody.newBuilder() + .nftAllowances(NftAllowance.newBuilder() + .tokenId(tokenID) + .owner(senderId) + .spender(addressIdConverter.convert(operatorAddress)) + .approvedForAll(approved) + .build()) + .build(); + } + + private TransactionBody bodyOf( + @NonNull final CryptoApproveAllowanceTransactionBody approveAllowanceTransactionBody) { + return TransactionBody.newBuilder() + .cryptoApproveAllowance(approveAllowanceTransactionBody) + .build(); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslator.java b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslator.java new file mode 100644 index 000000000000..a48b2219f972 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/main/java/com/hedera/node/app/service/contract/impl/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslator.java @@ -0,0 +1,77 @@ +/* + * 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.contract.impl.exec.systemcontracts.hts.setapproval; + +import com.esaulpaugh.headlong.abi.Function; +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AbstractHtsCallTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.DispatchForResponseCodeHtsCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCall; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.ReturnTypes; +import com.hedera.node.app.spi.workflows.record.SingleTransactionRecordBuilder; +import edu.umd.cs.findbugs.annotations.NonNull; +import java.util.Arrays; +import javax.inject.Inject; + +/** + * Translates setApprovalForAll (including ERC) call to the HTS system contract. There are no special cases for these + * calls, so the returned {@link HtsCall} is simply an instance of {@link DispatchForResponseCodeHtsCall}. + */ +public class SetApprovalForAllTranslator extends AbstractHtsCallTranslator { + + public static final Function SET_APPROVAL_FOR_ALL = + new Function("setApprovalForAll(address,address,bool)", ReturnTypes.INT); + public static final Function ERC721_SET_APPROVAL_FOR_ALL = + new Function("setApprovalForAll(address,bool)", ReturnTypes.INT); + + private final SetApprovalForAllDecoder decoder; + + @Inject + public SetApprovalForAllTranslator(final SetApprovalForAllDecoder decoder) { + this.decoder = decoder; + } + + /** + * {@inheritDoc} + */ + @Override + public boolean matches(@NonNull final HtsCallAttempt attempt) { + return selectorMatches(attempt, SET_APPROVAL_FOR_ALL) + || (attempt.isTokenRedirect() && selectorMatches(attempt, ERC721_SET_APPROVAL_FOR_ALL)); + } + + /** + * {@inheritDoc} + */ + @Override + public HtsCall callFrom(@NonNull final HtsCallAttempt attempt) { + return new DispatchForResponseCodeHtsCall<>( + attempt, bodyForClassic(attempt), SingleTransactionRecordBuilder.class); + } + + private TransactionBody bodyForClassic(@NonNull final HtsCallAttempt attempt) { + if (selectorMatches(attempt, SET_APPROVAL_FOR_ALL)) { + return decoder.decodeSetApprovalForAll(attempt); + } + return decoder.decodeSetApprovalForAllERC(attempt); + } + + private boolean selectorMatches(final HtsCallAttempt attempt, final Function function) { + return Arrays.equals(attempt.selector(), function.selector()); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoderTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoderTest.java new file mode 100644 index 000000000000..991daca361a8 --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllDecoderTest.java @@ -0,0 +1,130 @@ +/* + * 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.contract.impl.test.exec.systemcontracts.hts.setapproval; + +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.APPROVED_HEADLONG_ADDRESS; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.APPROVED_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.NON_FUNGIBLE_TOKEN_ID; +import static com.hedera.node.app.service.contract.impl.test.TestHelpers.SENDER_ID; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; + +import com.hedera.hapi.node.transaction.TransactionBody; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.AddressIdConverter; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllDecoder; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator; +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) +public class SetApprovalForAllDecoderTest { + + @Mock + private HtsCallAttempt attempt; + + @Mock + private AddressIdConverter addressIdConverter; + + private final SetApprovalForAllDecoder subject = new SetApprovalForAllDecoder(); + + @BeforeEach + void setUp() { + given(attempt.addressIdConverter()).willReturn(addressIdConverter); + given(attempt.senderId()).willReturn(SENDER_ID); + given(addressIdConverter.convert(APPROVED_HEADLONG_ADDRESS)).willReturn(APPROVED_ID); + } + + @Test + void setApprovalForAll_true_Works() { + // given + final var encodedInput = SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, APPROVED_HEADLONG_ADDRESS, true) + .array(); + given(attempt.inputBytes()).willReturn(encodedInput); + + // when + final var transactionBody = subject.decodeSetApprovalForAll(attempt); + + // then + verifyDecoding(true, transactionBody); + } + + @Test + void setApprovalForAllToken_false_Works() { + // given + final var encodedInput = SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(NON_FUNGIBLE_TOKEN_HEADLONG_ADDRESS, APPROVED_HEADLONG_ADDRESS, false) + .array(); + given(attempt.inputBytes()).willReturn(encodedInput); + + // when + final var transactionBody = subject.decodeSetApprovalForAll(attempt); + + // then + verifyDecoding(false, transactionBody); + } + + @Test + void setApprovalForAllTokenERC_true_Works() { + // given + final var encodedInput = SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(APPROVED_HEADLONG_ADDRESS, true) + .array(); + given(attempt.inputBytes()).willReturn(encodedInput); + given(attempt.redirectTokenId()).willReturn(NON_FUNGIBLE_TOKEN_ID); + + // when + final var transactionBody = subject.decodeSetApprovalForAllERC(attempt); + + // then + verifyDecoding(true, transactionBody); + } + + @Test + void setApprovalForAllTokenERC_false_Works() { + // given + final var encodedInput = SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL + .encodeCallWithArgs(APPROVED_HEADLONG_ADDRESS, false) + .array(); + given(attempt.inputBytes()).willReturn(encodedInput); + given(attempt.redirectTokenId()).willReturn(NON_FUNGIBLE_TOKEN_ID); + + // when + final var transactionBody = subject.decodeSetApprovalForAllERC(attempt); + + // then + verifyDecoding(false, transactionBody); + } + + private void verifyDecoding(final boolean expectedApproval, final TransactionBody body) { + final var cryptoApproveAllowance = body.cryptoApproveAllowance(); + assertThat(cryptoApproveAllowance).isNotNull(); + + final var nftAllowancesList = cryptoApproveAllowance.nftAllowances(); + assertThat(nftAllowancesList).isNotEmpty(); + + assertThat(nftAllowancesList.get(0).tokenId()).isEqualTo(NON_FUNGIBLE_TOKEN_ID); + assertThat(nftAllowancesList.get(0).spender()).isEqualTo(APPROVED_ID); + assertThat(nftAllowancesList.get(0).owner()).isEqualTo(SENDER_ID); + assertThat(nftAllowancesList.get(0).approvedForAll()).isEqualTo(expectedApproval); + } +} diff --git a/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslatorTest.java b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslatorTest.java new file mode 100644 index 000000000000..39389986d03d --- /dev/null +++ b/hedera-node/hedera-smart-contract-service-impl/src/test/java/com/hedera/node/app/service/contract/impl/test/exec/systemcontracts/hts/setapproval/SetApprovalForAllTranslatorTest.java @@ -0,0 +1,69 @@ +/* + * 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.contract.impl.test.exec.systemcontracts.hts.setapproval; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.BDDMockito.given; + +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.HtsCallAttempt; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllDecoder; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.setapproval.SetApprovalForAllTranslator; +import com.hedera.node.app.service.contract.impl.exec.systemcontracts.hts.transfer.ClassicTransfersTranslator; +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) +public class SetApprovalForAllTranslatorTest { + + @Mock + private HtsCallAttempt attempt; + + private final SetApprovalForAllDecoder decoder = new SetApprovalForAllDecoder(); + + private SetApprovalForAllTranslator subject; + + @BeforeEach + void setUp() { + subject = new SetApprovalForAllTranslator(decoder); + } + + @Test + void matchesClassicalSelectorTest() { + given(attempt.selector()).willReturn(SetApprovalForAllTranslator.SET_APPROVAL_FOR_ALL.selector()); + final var matches = subject.matches(attempt); + assertThat(matches).isTrue(); + } + + @Test + void matchesERCSelectorTest() { + given(attempt.selector()).willReturn(SetApprovalForAllTranslator.ERC721_SET_APPROVAL_FOR_ALL.selector()); + given(attempt.isTokenRedirect()).willReturn(true); + final var matches = subject.matches(attempt); + assertThat(matches).isTrue(); + } + + @Test + void falseOnInvalidSelector() { + given(attempt.selector()).willReturn(ClassicTransfersTranslator.CRYPTO_TRANSFER.selector()); + final var matches = subject.matches(attempt); + assertFalse(matches); + } +} diff --git a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java index fc541e82fec5..fca3464c053f 100644 --- a/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java +++ b/hedera-node/hedera-token-service-impl/src/main/java/com/hedera/node/app/service/token/impl/handlers/CryptoApproveAllowanceHandler.java @@ -115,8 +115,7 @@ public void preHandle(@NonNull final PreHandleContext context) throws PreCheckEx @Override public void handle(@NonNull final HandleContext context) throws HandleException { - final var txn = context.body(); - final var payer = txn.transactionIDOrThrow().accountIDOrThrow(); + final var payer = context.payer(); final var accountStore = context.writableStore(WritableAccountStore.class); diff --git a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoApproveAllowanceHandlerTest.java b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoApproveAllowanceHandlerTest.java index f0bafefe4c9c..6042a223bbdd 100644 --- a/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoApproveAllowanceHandlerTest.java +++ b/hedera-node/hedera-token-service-impl/src/test/java/com/hedera/node/app/service/token/impl/test/handlers/CryptoApproveAllowanceHandlerTest.java @@ -16,7 +16,11 @@ package com.hedera.node.app.service.token.impl.test.handlers; -import static com.hedera.hapi.node.base.ResponseCodeEnum.*; +import static com.hedera.hapi.node.base.ResponseCodeEnum.EMPTY_ALLOWANCES; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_ALLOWANCE_OWNER_ID; +import static com.hedera.hapi.node.base.ResponseCodeEnum.INVALID_DELEGATING_SPENDER; +import static com.hedera.hapi.node.base.ResponseCodeEnum.MAX_ALLOWANCES_EXCEEDED; +import static com.hedera.hapi.node.base.ResponseCodeEnum.SENDER_DOES_NOT_OWN_NFT_SERIAL_NO; 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.assertThat; @@ -60,12 +64,14 @@ class CryptoApproveAllowanceHandlerTest extends CryptoTokenHandlerTestBase { private CryptoApproveAllowanceHandler subject; + @Override @BeforeEach public void setUp() { super.setUp(); refreshWritableStores(); final var validator = new ApproveAllowanceValidator(); givenStoresAndConfig(handleContext); + given(handleContext.payer()).willReturn(payerId); subject = new CryptoApproveAllowanceHandler(validator); }