From 4261ad295745256032c5f9dd485a557cc910f226 Mon Sep 17 00:00:00 2001 From: gsstoykov Date: Mon, 22 Jul 2024 17:16:43 +0300 Subject: [PATCH] feat(HIP-904): TokenRejectTransaction (#743) Signed-off-by: gsstoykov --- .editorconfig | 0 src/sdk/main/CMakeLists.txt | 2 + src/sdk/main/include/Status.h | 18 +- src/sdk/main/include/TokenRejectFlow.h | 217 +++++ src/sdk/main/include/TokenRejectTransaction.h | 188 ++++ src/sdk/main/include/TransactionType.h | 1 + src/sdk/main/include/WrappedTransaction.h | 2 + src/sdk/main/src/AccountInfo.cc | 1 + src/sdk/main/src/Executable.cc | 2 + src/sdk/main/src/PrivateKey.cc | 2 + src/sdk/main/src/Status.cc | 15 +- src/sdk/main/src/TokenRejectFlow.cc | 162 ++++ src/sdk/main/src/TokenRejectTransaction.cc | 159 +++ src/sdk/main/src/Transaction.cc | 4 + src/sdk/main/src/WrappedTransaction.cc | 15 + src/sdk/main/src/impl/Node.cc | 2 + src/sdk/tests/integration/CMakeLists.txt | 1 + .../TokenRejectTransactionIntegrationTests.cc | 913 ++++++++++++++++++ src/sdk/tests/unit/CMakeLists.txt | 1 + .../unit/TokenRejectTransactionUnitTests.cc | 102 ++ 20 files changed, 1803 insertions(+), 4 deletions(-) create mode 100644 .editorconfig create mode 100644 src/sdk/main/include/TokenRejectFlow.h create mode 100644 src/sdk/main/include/TokenRejectTransaction.h create mode 100644 src/sdk/main/src/TokenRejectFlow.cc create mode 100644 src/sdk/main/src/TokenRejectTransaction.cc create mode 100644 src/sdk/tests/integration/TokenRejectTransactionIntegrationTests.cc create mode 100644 src/sdk/tests/unit/TokenRejectTransactionUnitTests.cc diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..e69de29b diff --git a/src/sdk/main/CMakeLists.txt b/src/sdk/main/CMakeLists.txt index a70efda4..972e6193 100644 --- a/src/sdk/main/CMakeLists.txt +++ b/src/sdk/main/CMakeLists.txt @@ -139,6 +139,8 @@ add_library(${PROJECT_NAME} STATIC src/TokenNftInfoQuery.cc src/TokenNftTransfer.cc src/TokenPauseTransaction.cc + src/TokenRejectFlow.cc + src/TokenRejectTransaction.cc src/TokenRelationship.cc src/TokenRevokeKycTransaction.cc src/TokenSupplyType.cc diff --git a/src/sdk/main/include/Status.h b/src/sdk/main/include/Status.h index 14c044b4..20fafaa6 100644 --- a/src/sdk/main/include/Status.h +++ b/src/sdk/main/include/Status.h @@ -1523,7 +1523,23 @@ enum class Status * The maximum automatic associations value is not valid. The most common cause for this error is a value less than * `-1`. */ - INVALID_MAX_AUTO_ASSOCIATIONS + INVALID_MAX_AUTO_ASSOCIATIONS, + + /** + * The transaction attempted to use duplicate `TokenReference`.
+ * This affects `TokenReject` attempting to reject same token reference more than once. + */ + TOKEN_REFERENCE_REPEATED, + + /** + * The account id specified as the owner in `TokenReject` is invalid or does not exist. + */ + INVALID_OWNER_ID, + + /** + * The transaction attempted to use empty `TokenReference` list. + */ + EMPTY_TOKEN_REFERENCE_LIST }; /** diff --git a/src/sdk/main/include/TokenRejectFlow.h b/src/sdk/main/include/TokenRejectFlow.h new file mode 100644 index 00000000..2a34cf91 --- /dev/null +++ b/src/sdk/main/include/TokenRejectFlow.h @@ -0,0 +1,217 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#ifndef HEDERA_SDK_CPP_TOKEN_REJECT_FLOW_H_ +#define HEDERA_SDK_CPP_TOKEN_REJECT_FLOW_H_ + +#include "AccountId.h" +#include "NftId.h" +#include "TokenId.h" +#include "TransactionResponse.h" + +namespace Hedera +{ +class Client; +class PrivateKey; +} + +namespace Hedera +{ +/** + * Reject undesired token(s) and dissociate in a single flow. + */ +class TokenRejectFlow +{ +public: + /** + * Execute the Transactions in this flow (TokenRejectTransaction and TokenDissociateTransaction). + * + * @param client The Client to use to submit these Transactions. + * @return The TransactionResponse object of the flow. + * @throws MaxAttemptsExceededException If this Executable attempts to execute past the number of allowable attempts. + * @throws PrecheckStatusException If this Executable fails its pre-check. + * @throws UninitializedException If the input Client has not yet been initialized. + */ + TransactionResponse execute(const Client& client); + + /** + * Execute the Transactions in this flow (TokenRejectTransaction and TokenDissociateTransaction) with timeout. + * + * @param client The Client to use to submit these Transactions. + * @param timeout The desired timeout for the execution of these Transactions. + * @return The TransactionResponse object of the flow. + * @throws MaxAttemptsExceededException If this Executable attempts to execute past the number of allowable attempts. + * @throws PrecheckStatusException If this Executable fails its pre-check. + * @throws UninitializedException If the input Client has not yet been initialized. + */ + TransactionResponse execute(const Client& client, const std::chrono::system_clock::duration& timeout); + + /** + * Freeze the TokenRejectTransaction with a Client. The Client's operator will be used to generate a transaction + * ID, and the client's network will be used to generate a list of node account IDs. + * + * NOTE: Since Client's can't be copied, this TokenRejectFlow will store a pointer to the input Client. It is the + * responsibility of the user to make sure that the Client does not go out of scope or get destroyed until this + * TokenRejectFlow is done executing, otherwise this will crash upon execution. + * + * @param client The Client with which to freeze the TokenRejectTransaction. + * @return A reference to this TokenRejectFlow object with the newly-set freeze Client. + * @throws UninitializedException If Client operator has not been initialized. + */ + TokenRejectFlow& freezeWith(const Client* client); + + /** + * Set the PrivateKey with which the TokenRejectTransaction will be signed. + * + * @param key The PrivateKey with which to sign the TokenRejectTransaction. + * @return A reference to this TokenRejectFlow object with the newly-set signing PrivateKey. + */ + TokenRejectFlow& sign(const std::shared_ptr& key); + + /** + * Set the PublicKey and signer function with which the TokenRejectTransaction will be signed. + * + * @param key The PublicKey with which to sign the TokenRejectTransaction. + * @param signer The callback function to use to sign the TokenRejectTransaction. + * @return A reference to this TokenRejectFlow object with the newly-set public key and signer function. + */ + TokenRejectFlow& signWith(const std::shared_ptr& key, + const std::function(const std::vector&)>& signer); + + /** + * Set the Client operator with which the TokenRejectTransaction will be signed. + * + * @param client The Client operator to sign the TokenRejectTransaction. + * @return A reference to this TokenRejectFlow object with the newly-set signing operator. + * @throws UninitializedException If the Client operator has not yet been set. + */ + TokenRejectFlow& signWithOperator(const Client& client); + + /** + * Get the list of account IDs for nodes with which execution will be attempted. + * + * @return The list of account IDs of nodes this TokenRejectFlow would attempt request submission. + */ + [[nodiscard]] inline std::vector getNodeAccountIds() const { return mNodeAccountIds; } + + /** + * Get the account holding tokens to be rejected. + * + * @return Optional containing the account Id of the owner. + */ + [[nodiscard]] std::optional getOwner() const { return mOwner; }; + + /** + * Get the list of fungible tokens to be rejected. + * + * @return A vector of TokenId objects. + */ + [[nodiscard]] const std::vector& getFts() const { return mFts; }; + + /** + * Get the list of non-fungible tokens to be rejected. + * + * @return A vector of NftId objects. + */ + [[nodiscard]] const std::vector& getNfts() const { return mNfts; }; + + /** + * Set the desired account IDs of nodes to which this transaction will be submitted. + * + * @param nodeAccountIds The desired list of account IDs of nodes to submit this request. + * @return A reference to this TokenRejectFlow object with the newly-set node account IDs. + */ + TokenRejectFlow& setNodeAccountIds(const std::vector& nodeAccountIds); + + /** + * Set a new account holding tokens to be rejected. + * + * @param owner Account Id of the account. + * @return A reference to this TokenRejectFlow with the newly-set owner. + */ + TokenRejectFlow& setOwner(const AccountId& owner); + + /** + * Set a new fungible tokens list of tokens to be rejected. + * + * @param fts List of token ids. + * @return A reference to this TokenRejectFlow with the newly-set tokens list. + */ + TokenRejectFlow& setFts(const std::vector& fts); + + /** + * Set a new non-fungible tokens list of tokens to be rejected. + * + * @param nfts List of nft ids. + * @return A reference to this TokenRejectFlow with the newly-set nfts list. + */ + TokenRejectFlow& setNfts(const std::vector& nfts); + +private: + /** + * The Client with which to freeze the TokenRejectTransaction. + */ + const Client* mFreezeWithClient = nullptr; + + /** + * The PrivateKey with which to sign the TokenRejectTransaction. + */ + std::shared_ptr mPrivateKey = nullptr; + + /** + * The PublicKey associated with the signer function to sign the TokenRejectTransaction. + */ + std::shared_ptr mPublicKey = nullptr; + + /** + * The signer function to use to sign the TokenRejectTransaction. + */ + std::optional(const std::vector&)>> mSigner; + + /** + * The list of account IDs of the nodes with which execution should be attempted. + */ + std::vector mNodeAccountIds; + + /** + * An account holding the tokens to be rejected. + * If set, this account MUST sign this transaction. + * If not set, the payer for this transaction SHALL be the account rejecting tokens. + */ + std::optional mOwner; + + /** + * On success each rejected token serial number or balance SHALL be transferred from + * the requesting account to the treasury account for that token type. + * After rejection the requesting account SHALL continue to be associated with the token. + * if dissociation is desired then a separate TokenDissociate transaction MUST be submitted to remove the association. + * + * A list of one or more fungible token rejections. + */ + std::vector mFts; + + /** + * A list of one or more non-fungible token rejections. + */ + std::vector mNfts; +}; + +} // namespace Hedera + +#endif // HEDERA_SDK_CPP_TOKEN_REJECT_FLOW_H_ diff --git a/src/sdk/main/include/TokenRejectTransaction.h b/src/sdk/main/include/TokenRejectTransaction.h new file mode 100644 index 00000000..6dfe1c5d --- /dev/null +++ b/src/sdk/main/include/TokenRejectTransaction.h @@ -0,0 +1,188 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#ifndef HEDERA_SDK_CPP_TOKEN_REJECT_TRANSACTION_H_ +#define HEDERA_SDK_CPP_TOKEN_REJECT_TRANSACTION_H_ + +#include "AccountId.h" +#include "NftId.h" +#include "TokenId.h" +#include "Transaction.h" + +namespace proto +{ +class TokenRejectTransactionBody; +class TransactionBody; +class TokenReference; +} + +namespace Hedera +{ +/** + * Reject undesired token(s).Transfer one or more token balances held by the requesting account to the treasury for each + * token type. Each transfer SHALL be one of the following: + * - A single non-fungible/unique token. + * - The full balance held for a fungible/common token type. + * + * A single tokenReject transaction SHALL support a maximum of 10 transfers. + * + * Transaction Record Effects: + * - Each successful transfer from `payer` to `treasury` SHALL be recorded in `token_transfer_list` for the transaction + * record. + */ +class TokenRejectTransaction : public Transaction +{ +public: + TokenRejectTransaction() = default; + + /** + * Construct from a TransactionBody protobuf object. + * + * @param transactionBody The TransactionBody protobuf object from which to construct. + * @throws std::invalid_argument If the input TransactionBody does not represent a TokenReject transaction. + */ + explicit TokenRejectTransaction(const proto::TransactionBody& transactionBody); + + /** + * Construct from a map of TransactionIds to node account IDs and their respective Transaction protobuf objects. + * + * @param transactions The map of TransactionIds to node account IDs and their respective Transaction protobuf + * objects. + */ + explicit TokenRejectTransaction(const std::map>& transactions); + + /** + * Get the account holding tokens to be rejected. + * + * @return Optional containing the account Id of the owner. + */ + [[nodiscard]] std::optional getOwner() const { return mOwner; }; + + /** + * Get the list of fungible tokens to be rejected. + * + * @return A vector of TokenId objects. + */ + [[nodiscard]] const std::vector& getFts() const { return mFts; }; + + /** + * Get the list of non-fungible tokens to be rejected. + * + * @return A vector of NftId objects. + */ + [[nodiscard]] const std::vector& getNfts() const { return mNfts; }; + + /** + * Set a new account holding tokens to be rejected. + * + * @param owner Account Id of the account. + * @return A reference to this TokenRejectTransaction with the newly-set owner. + */ + TokenRejectTransaction& setOwner(const AccountId& owner); + + /** + * Set a new fungible tokens list of tokens to be rejected. + * + * @param fts List of token ids. + * @return A reference to this TokenRejectTransaction with the newly-set tokens list. + */ + TokenRejectTransaction& setFts(const std::vector& fts); + + /** + * Set a new non-fungible tokens list of tokens to be rejected. + * + * @param nfts List of nft ids. + * @return A reference to this TokenRejectTransaction with the newly-set nfts list. + */ + TokenRejectTransaction& setNfts(const std::vector& nfts); + +private: + friend class WrappedTransaction; + + /** + * Derived from Executable. Submit a Transaction protobuf object which contains this TokenRejectTransaction's data to + * a Node. + * + * @param request The Transaction protobuf object to submit. + * @param node The Node to which to submit the request. + * @param deadline The deadline for submitting the request. + * @param response Pointer to the ProtoResponseType object that gRPC should populate with the response information + * from the gRPC server. + * @return The gRPC status of the submission. + */ + [[nodiscard]] grpc::Status submitRequest(const proto::Transaction& request, + const std::shared_ptr& node, + const std::chrono::system_clock::time_point& deadline, + proto::TransactionResponse* response) const override; + + /** + * Derived from Transaction. Verify that all the checksums in this TokenRejectTransaction are valid. + * + * @param client The Client that should be used to validate the checksums. + * @throws BadEntityException This TokenRejectTransaction's checksums are not valid. + */ + void validateChecksums(const Client& client) const override; + + /** + * Derived from Transaction. Build and add the TokenRejectTransaction protobuf representation to the Transaction + * protobuf object. + * + * @param body The TransactionBody protobuf object being built. + */ + void addToBody(proto::TransactionBody& body) const override; + + /** + * Initialize this TokenRejectTransaction from its source TransactionBody protobuf object. + */ + void initFromSourceTransactionBody(); + + /** + * Build a TokenRejectTransactionBody protobuf object from this TokenRejectTransaction object. + * + * @return A pointer to a TokenRejectTransactionBody protobuf object filled with this TokenRejectTransaction object's + * data. + */ + [[nodiscard]] proto::TokenRejectTransactionBody* build() const; + + /** + * An account holding the tokens to be rejected. + * If set, this account MUST sign this transaction. + * If not set, the payer for this transaction SHALL be the account rejecting tokens. + */ + std::optional mOwner; + + /** + * On success each rejected token serial number or balance SHALL be transferred from + * the requesting account to the treasury account for that token type. + * After rejection the requesting account SHALL continue to be associated with the token. + * if dissociation is desired then a separate TokenDissociate transaction MUST be submitted to remove the association. + * + * A list of one or more fungible token rejections. + */ + std::vector mFts; + + /** + * A list of one or more non-fungible token rejections. + */ + std::vector mNfts; +}; + +} // namespace Hedera + +#endif // HEDERA_SDK_CPP_TOKEN_REJECT_TRANSACTION_H_ diff --git a/src/sdk/main/include/TransactionType.h b/src/sdk/main/include/TransactionType.h index 56446293..14b41873 100644 --- a/src/sdk/main/include/TransactionType.h +++ b/src/sdk/main/include/TransactionType.h @@ -59,6 +59,7 @@ enum TransactionType : int TOKEN_GRANT_KYC_TRANSACTION, TOKEN_MINT_TRANSACTION, TOKEN_PAUSE_TRANSACTION, + TOKEN_REJECT_TRANSACTION, TOKEN_REVOKE_KYC_TRANSACTION, TOKEN_UNFREEZE_TRANSACTION, TOKEN_UNPAUSE_TRANSACTION, diff --git a/src/sdk/main/include/WrappedTransaction.h b/src/sdk/main/include/WrappedTransaction.h index 60a0e3f9..82c25175 100644 --- a/src/sdk/main/include/WrappedTransaction.h +++ b/src/sdk/main/include/WrappedTransaction.h @@ -51,6 +51,7 @@ #include "TokenGrantKycTransaction.h" #include "TokenMintTransaction.h" #include "TokenPauseTransaction.h" +#include "TokenRejectTransaction.h" #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" @@ -114,6 +115,7 @@ class WrappedTransaction TokenGrantKycTransaction, TokenMintTransaction, TokenPauseTransaction, + TokenRejectTransaction, TokenRevokeKycTransaction, TokenUnfreezeTransaction, TokenUnpauseTransaction, diff --git a/src/sdk/main/src/AccountInfo.cc b/src/sdk/main/src/AccountInfo.cc index 1ce6e0f6..5b05720c 100644 --- a/src/sdk/main/src/AccountInfo.cc +++ b/src/sdk/main/src/AccountInfo.cc @@ -137,6 +137,7 @@ std::unique_ptr AccountInfo::toProtobu proto->set_ledger_id(internal::Utilities::byteVectorToString(mLedgerId.toBytes())); proto->set_allocated_staking_info(mStakingInfo.toProtobuf().release()); + // TODO: reference and type for (auto tr : mTokenRelationships) { proto->mutable_tokenrelationships()->AddAllocated(tr.second.toProtobuf().release()); diff --git a/src/sdk/main/src/Executable.cc b/src/sdk/main/src/Executable.cc index 96c5da23..d700afa4 100644 --- a/src/sdk/main/src/Executable.cc +++ b/src/sdk/main/src/Executable.cc @@ -73,6 +73,7 @@ #include "TokenNftInfo.h" #include "TokenNftInfoQuery.h" #include "TokenPauseTransaction.h" +#include "TokenRejectTransaction.h" #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" @@ -656,6 +657,7 @@ template class Executable; template class Executable; template class Executable; +template class Executable; template class Executable PrivateKey::signTransaction(WrappedTransaction& transacti return signTransaction(*transaction.getTransaction()); case TOKEN_PAUSE_TRANSACTION: return signTransaction(*transaction.getTransaction()); + case TOKEN_REJECT_TRANSACTION: + return signTransaction(*transaction.getTransaction()); case TOKEN_REVOKE_KYC_TRANSACTION: return signTransaction(*transaction.getTransaction()); case TOKEN_UNFREEZE_TRANSACTION: diff --git a/src/sdk/main/src/Status.cc b/src/sdk/main/src/Status.cc index 544cfe06..1b8dc6d5 100644 --- a/src/sdk/main/src/Status.cc +++ b/src/sdk/main/src/Status.cc @@ -334,7 +334,10 @@ const std::unordered_map gProtobufResponseCodeT { proto::ResponseCodeEnum::INVALID_METADATA_KEY, Status::INVALID_METADATA_KEY }, { proto::ResponseCodeEnum::MISSING_TOKEN_METADATA, Status::MISSING_TOKEN_METADATA }, { proto::ResponseCodeEnum::MISSING_SERIAL_NUMBERS, Status::MISSING_SERIAL_NUMBERS }, - { proto::ResponseCodeEnum::INVALID_MAX_AUTO_ASSOCIATIONS, Status::INVALID_MAX_AUTO_ASSOCIATIONS } + { proto::ResponseCodeEnum::INVALID_MAX_AUTO_ASSOCIATIONS, Status::INVALID_MAX_AUTO_ASSOCIATIONS }, + { proto::ResponseCodeEnum::TOKEN_REFERENCE_REPEATED, Status::TOKEN_REFERENCE_REPEATED }, + { proto::ResponseCodeEnum::INVALID_OWNER_ID, Status::INVALID_OWNER_ID }, + { proto::ResponseCodeEnum::EMPTY_TOKEN_REFERENCE_LIST, Status::EMPTY_TOKEN_REFERENCE_LIST } }; //----- @@ -648,7 +651,10 @@ const std::unordered_map gStatusToProtobufRespo { Status::INVALID_METADATA_KEY, proto::ResponseCodeEnum::INVALID_METADATA_KEY }, { Status::MISSING_TOKEN_METADATA, proto::ResponseCodeEnum::MISSING_TOKEN_METADATA }, { Status::MISSING_SERIAL_NUMBERS, proto::ResponseCodeEnum::MISSING_SERIAL_NUMBERS }, - { Status::INVALID_MAX_AUTO_ASSOCIATIONS, proto::ResponseCodeEnum::INVALID_MAX_AUTO_ASSOCIATIONS } + { Status::INVALID_MAX_AUTO_ASSOCIATIONS, proto::ResponseCodeEnum::INVALID_MAX_AUTO_ASSOCIATIONS }, + { Status::TOKEN_REFERENCE_REPEATED, proto::ResponseCodeEnum::TOKEN_REFERENCE_REPEATED }, + { Status::INVALID_OWNER_ID, proto::ResponseCodeEnum::INVALID_OWNER_ID }, + { Status::EMPTY_TOKEN_REFERENCE_LIST, proto::ResponseCodeEnum::EMPTY_TOKEN_REFERENCE_LIST } }; //----- @@ -944,7 +950,10 @@ const std::unordered_map gStatusToString = { { Status::INVALID_METADATA_KEY, "INVALID_METADATA_KEY" }, { Status::MISSING_TOKEN_METADATA, "MISSING_TOKEN_METADATA" }, { Status::MISSING_SERIAL_NUMBERS, "MISSING_SERIAL_NUMBERS" }, - { Status::INVALID_MAX_AUTO_ASSOCIATIONS, "INVALID_MAX_AUTO_ASSOCIATIONS" } + { Status::INVALID_MAX_AUTO_ASSOCIATIONS, "INVALID_MAX_AUTO_ASSOCIATIONS" }, + { Status::TOKEN_REFERENCE_REPEATED, "TOKEN_REFERENCE_REPEATED" }, + { Status::INVALID_OWNER_ID, "INVALID_OWNER_ID" }, + { Status::EMPTY_TOKEN_REFERENCE_LIST, "EMPTY_TOKEN_REFERENCE_LIST" } }; } // namespace Hedera \ No newline at end of file diff --git a/src/sdk/main/src/TokenRejectFlow.cc b/src/sdk/main/src/TokenRejectFlow.cc new file mode 100644 index 00000000..a80da84b --- /dev/null +++ b/src/sdk/main/src/TokenRejectFlow.cc @@ -0,0 +1,162 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#include "TokenRejectFlow.h" +#include "AccountId.h" +#include "Client.h" +#include "NftId.h" +#include "TokenDissociateTransaction.h" +#include "TokenId.h" +#include "TokenRejectTransaction.h" +#include "TransactionReceipt.h" +#include "exceptions/UninitializedException.h" + +namespace Hedera +{ +//----- +TransactionResponse TokenRejectFlow::execute(const Client& client) +{ + return execute(client, client.getRequestTimeout()); +} + +//----- +TransactionResponse TokenRejectFlow::execute(const Client& client, const std::chrono::system_clock::duration& timeout) +{ + TokenRejectTransaction tokenRejectTransaction; + + if (mOwner.has_value()) + { + tokenRejectTransaction.setOwner(mOwner.value()); + } + + tokenRejectTransaction.setFts(mFts).setNfts(mNfts); + + if (!mNodeAccountIds.empty()) + { + tokenRejectTransaction.setNodeAccountIds(mNodeAccountIds); + } + + if (mFreezeWithClient != nullptr) + { + tokenRejectTransaction.freezeWith(mFreezeWithClient); + } + + if (mPrivateKey) + { + tokenRejectTransaction.sign(mPrivateKey); + } + else if (mPublicKey && mSigner.has_value()) + { + tokenRejectTransaction.signWith(mPublicKey, mSigner.value()); + } + + // Submit the TokenRejectTransaction. + TransactionResponse txResponse = tokenRejectTransaction.execute(client, timeout); + + // Make sure the transaction reaches consensus. + TransactionReceipt txReceipt = txResponse.getReceipt(client, timeout); + + TokenDissociateTransaction tokenDissociateTransaction; + + if (mOwner.has_value()) + { + tokenDissociateTransaction.setAccountId(mOwner.value()); + } + + // build dissociate vector from rejected nfts + std::vector toDissociate; + std::for_each( + mNfts.cbegin(), mNfts.cend(), [&toDissociate](const NftId& nftId) { toDissociate.push_back(nftId.mTokenId); }); + + // Make sure the transaction reaches consensus. + txReceipt = tokenDissociateTransaction.setTokenIds(toDissociate).execute(client, timeout).getReceipt(client, timeout); + + return txResponse; +} + +//----- +TokenRejectFlow& TokenRejectFlow::freezeWith(const Client* client) +{ + mFreezeWithClient = client; + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::sign(const std::shared_ptr& key) +{ + mPrivateKey = key; + mPublicKey = nullptr; + mSigner.reset(); + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::signWith( + const std::shared_ptr& key, + const std::function(const std::vector&)>& signer) +{ + mPrivateKey = nullptr; + mPublicKey = key; + mSigner = signer; + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::signWithOperator(const Client& client) +{ + if (!client.getOperatorPublicKey()) + { + throw UninitializedException("Client operator has not yet been set"); + } + + mPrivateKey = nullptr; + mPublicKey = client.getOperatorPublicKey(); + mSigner = client.getOperatorSigner(); + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::setNodeAccountIds(const std::vector& nodeAccountIds) +{ + mNodeAccountIds = nodeAccountIds; + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::setOwner(const AccountId& owner) +{ + mOwner = owner; + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::setFts(const std::vector& fts) +{ + mFts = fts; + return *this; +} + +//----- +TokenRejectFlow& TokenRejectFlow::setNfts(const std::vector& nfts) +{ + mNfts = nfts; + return *this; +} + +} // namespace Hedera \ No newline at end of file diff --git a/src/sdk/main/src/TokenRejectTransaction.cc b/src/sdk/main/src/TokenRejectTransaction.cc new file mode 100644 index 00000000..a3fc55d5 --- /dev/null +++ b/src/sdk/main/src/TokenRejectTransaction.cc @@ -0,0 +1,159 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#include "TokenRejectTransaction.h" +#include "AccountId.h" +#include "NftId.h" +#include "TokenId.h" +#include "impl/Node.h" + +#include +#include +#include +#include +#include + +namespace Hedera +{ +//----- +TokenRejectTransaction::TokenRejectTransaction(const proto::TransactionBody& transactionBody) + : Transaction(transactionBody) +{ + initFromSourceTransactionBody(); +} + +//----- +TokenRejectTransaction::TokenRejectTransaction( + const std::map>& transactions) + : Transaction(transactions) +{ + initFromSourceTransactionBody(); +} + +//----- +TokenRejectTransaction& TokenRejectTransaction::setOwner(const AccountId& owner) +{ + requireNotFrozen(); + mOwner = owner; + return *this; +} + +//----- +TokenRejectTransaction& TokenRejectTransaction::setFts(const std::vector& fts) +{ + requireNotFrozen(); + mFts = fts; + return *this; +} + +//----- +TokenRejectTransaction& TokenRejectTransaction::setNfts(const std::vector& nfts) +{ + requireNotFrozen(); + mNfts = nfts; + return *this; +} + +//----- +grpc::Status TokenRejectTransaction::submitRequest(const proto::Transaction& request, + const std::shared_ptr& node, + const std::chrono::system_clock::time_point& deadline, + proto::TransactionResponse* response) const +{ + return node->submitTransaction(proto::TransactionBody::DataCase::kTokenReject, request, deadline, response); +} + +//----- +void TokenRejectTransaction::validateChecksums(const Client& client) const +{ + if (mOwner.has_value()) + { + mOwner.value().validateChecksum(client); + } + + std::for_each(mFts.cbegin(), mFts.cend(), [&client](const TokenId& tokenId) { tokenId.validateChecksum(client); }); + std::for_each( + mNfts.cbegin(), mNfts.cend(), [&client](const NftId& nftId) { nftId.mTokenId.validateChecksum(client); }); +} + +//----- +void TokenRejectTransaction::addToBody(proto::TransactionBody& body) const +{ + body.set_allocated_tokenreject(build()); +} + +//----- +void TokenRejectTransaction::initFromSourceTransactionBody() +{ + const proto::TransactionBody transactionBody = getSourceTransactionBody(); + + if (!transactionBody.has_tokenreject()) + { + throw std::invalid_argument("Transaction body doesn't contain TokenReject data"); + } + + const proto::TokenRejectTransactionBody& body = transactionBody.tokenreject(); + + if (body.has_owner()) + { + mOwner = AccountId::fromProtobuf(body.owner()); + } + + for (int i = 0; i < body.rejections_size(); i++) + { + if (body.rejections(i).has_fungible_token()) + { + mFts.push_back(TokenId::fromProtobuf(body.rejections(i).fungible_token())); + } + + if (body.rejections(i).has_nft()) + { + mNfts.push_back(NftId::fromProtobuf(body.rejections(i).nft())); + } + } +} + +//----- +proto::TokenRejectTransactionBody* TokenRejectTransaction::build() const +{ + auto body = std::make_unique(); + + if (mOwner.has_value()) + { + body->set_allocated_owner(mOwner.value().toProtobuf().release()); + } + + for (const TokenId& ft : mFts) + { + auto tr = std::make_unique(); + tr->set_allocated_fungible_token(ft.toProtobuf().release()); + body->mutable_rejections()->AddAllocated(tr.release()); + } + + for (const NftId& nft : mNfts) + { + auto tr = std::make_unique(); + tr->set_allocated_nft(nft.toProtobuf().release()); + body->mutable_rejections()->AddAllocated(tr.release()); + } + + return body.release(); +} + +} // namespace Hedera \ No newline at end of file diff --git a/src/sdk/main/src/Transaction.cc b/src/sdk/main/src/Transaction.cc index 1888173c..6bd32cfb 100644 --- a/src/sdk/main/src/Transaction.cc +++ b/src/sdk/main/src/Transaction.cc @@ -54,6 +54,7 @@ #include "TokenGrantKycTransaction.h" #include "TokenMintTransaction.h" #include "TokenPauseTransaction.h" +#include "TokenRejectTransaction.h" #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" @@ -278,6 +279,8 @@ WrappedTransaction Transaction::fromBytes(const std::vector; template class Transaction; template class Transaction; template class Transaction; +template class Transaction; template class Transaction; template class Transaction; template class Transaction; diff --git a/src/sdk/main/src/WrappedTransaction.cc b/src/sdk/main/src/WrappedTransaction.cc index 63b127b8..28d489f5 100644 --- a/src/sdk/main/src/WrappedTransaction.cc +++ b/src/sdk/main/src/WrappedTransaction.cc @@ -158,6 +158,10 @@ WrappedTransaction WrappedTransaction::fromProtobuf(const proto::TransactionBody { return WrappedTransaction(TokenPauseTransaction(proto)); } + else if (proto.has_tokenreject()) + { + return WrappedTransaction(TokenRejectTransaction(proto)); + } else if (proto.has_tokenrevokekyc()) { return WrappedTransaction(TokenRevokeKycTransaction(proto)); @@ -351,6 +355,11 @@ WrappedTransaction WrappedTransaction::fromProtobuf(const proto::SchedulableTran *txBody.mutable_token_pause() = proto.token_pause(); return WrappedTransaction(TokenPauseTransaction(txBody)); } + else if (proto.has_tokenreject()) + { + *txBody.mutable_tokenreject() = proto.tokenreject(); + return WrappedTransaction(TokenRejectTransaction(txBody)); + } else if (proto.has_tokenrevokekyc()) { *txBody.mutable_tokenrevokekyc() = proto.tokenrevokekyc(); @@ -598,6 +607,12 @@ std::unique_ptr WrappedTransaction::toProtobuf() const transaction->updateSourceTransactionBody(nullptr); return std::make_unique(transaction->getSourceTransactionBody()); } + case TOKEN_REJECT_TRANSACTION: + { + const auto transaction = getTransaction(); + transaction->updateSourceTransactionBody(nullptr); + return std::make_unique(transaction->getSourceTransactionBody()); + } case TOKEN_REVOKE_KYC_TRANSACTION: { const auto transaction = getTransaction(); diff --git a/src/sdk/main/src/impl/Node.cc b/src/sdk/main/src/impl/Node.cc index 1ea92f5a..83ca3444 100644 --- a/src/sdk/main/src/impl/Node.cc +++ b/src/sdk/main/src/impl/Node.cc @@ -179,6 +179,8 @@ grpc::Status Node::submitTransaction(proto::TransactionBody::DataCase funcEnum, return mTokenStub->mintToken(&context, transaction, response); case proto::TransactionBody::DataCase::kTokenPause: return mTokenStub->pauseToken(&context, transaction, response); + case proto::TransactionBody::DataCase::kTokenReject: + return mTokenStub->revokeKycFromTokenAccount(&context, transaction, response); case proto::TransactionBody::DataCase::kTokenRevokeKyc: return mTokenStub->revokeKycFromTokenAccount(&context, transaction, response); case proto::TransactionBody::DataCase::kTokenUnfreeze: diff --git a/src/sdk/tests/integration/CMakeLists.txt b/src/sdk/tests/integration/CMakeLists.txt index 749c7680..b2f7c003 100644 --- a/src/sdk/tests/integration/CMakeLists.txt +++ b/src/sdk/tests/integration/CMakeLists.txt @@ -54,6 +54,7 @@ add_executable(${TEST_PROJECT_NAME} TokenMintTransactionIntegrationTests.cc TokenNftInfoQueryIntegrationTests.cc TokenPauseTransactionIntegrationTests.cc + TokenRejectTransactionIntegrationTests.cc TokenRevokeKycTransactionIntegrationTests.cc TokenUnfreezeTransactionIntegrationTests.cc TokenUnpauseTransactionIntegrationTests.cc diff --git a/src/sdk/tests/integration/TokenRejectTransactionIntegrationTests.cc b/src/sdk/tests/integration/TokenRejectTransactionIntegrationTests.cc new file mode 100644 index 00000000..31241995 --- /dev/null +++ b/src/sdk/tests/integration/TokenRejectTransactionIntegrationTests.cc @@ -0,0 +1,913 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#include "AccountAllowanceApproveTransaction.h" +#include "AccountBalance.h" +#include "AccountBalanceQuery.h" +#include "AccountCreateTransaction.h" +#include "BaseIntegrationTest.h" +#include "ED25519PrivateKey.h" +#include "TokenAssociateTransaction.h" +#include "TokenCreateTransaction.h" +#include "TokenFreezeTransaction.h" +#include "TokenMintTransaction.h" +#include "TokenPauseTransaction.h" +#include "TokenRejectTransaction.h" +#include "TransactionReceipt.h" +#include "TransactionResponse.h" +#include "TransferTransaction.h" + +#include "exceptions/PrecheckStatusException.h" +#include "exceptions/ReceiptStatusException.h" + +#include + +using namespace Hedera; + +class TokenRejectTransactionIntegrationTests : public BaseIntegrationTest +{ +protected: + TokenId createFt(const std::shared_ptr& operatorKey, bool pause = false) + { + return TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setTokenType(TokenType::FUNGIBLE_COMMON) + .setInitialSupply(100000) + .setTreasuryAccountId(AccountId(2ULL)) + .setAdminKey(operatorKey) + .setFreezeKey(operatorKey) + .setWipeKey(operatorKey) + .setPauseKey(pause ? operatorKey : nullptr) + .setSupplyKey(operatorKey) + .setFeeScheduleKey(operatorKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value(); + } + + TokenId createNft(const std::shared_ptr& operatorKey, bool pause = false) + { + return TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(AccountId(2ULL)) + .setSupplyType(TokenSupplyType::FINITE) + .setMaxSupply(10) + .setAdminKey(operatorKey) + .setFreezeKey(operatorKey) + .setWipeKey(operatorKey) + .setPauseKey(pause ? operatorKey : nullptr) + .setSupplyKey(operatorKey) + .setFeeScheduleKey(operatorKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value(); + } + + AccountId createAccount(const std::shared_ptr& accountKey, bool treasury = false) + { + AccountCreateTransaction tx = + AccountCreateTransaction().setKey(accountKey).setInitialBalance(Hbar(5)).setMaxAutomaticTokenAssociations(1000); + + if (treasury) + { + tx.setInitialBalance(Hbar(0)) + .setReceiverSignatureRequired(treasury) + .freezeWith(&getTestClient()) + .sign(accountKey); + } + + return tx.execute(getTestClient()).getReceipt(getTestClient()).mAccountId.value(); + } + +private: +}; + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, CanExecuteForFT) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create two fts + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createFt(operatorKey)); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // When + // Transfer fts to the receiver + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId1, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId1, receiver, 10) + .addTokenTransfer(tokenId2, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId2, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the tokens + EXPECT_NO_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId1, tokenId2 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Verify the balance of the receiver is 0 + AccountBalance balance; + EXPECT_NO_THROW(balance = AccountBalanceQuery().setAccountId(receiver).execute(getTestClient())); + + EXPECT_EQ(0, balance.mTokens[tokenId1]); + EXPECT_EQ(0, balance.mTokens[tokenId2]); + + // Verify the tokens are transferred back to the treasury + EXPECT_NO_THROW( + balance = + AccountBalanceQuery().setAccountId(getTestClient().getOperatorAccountId().value()).execute(getTestClient())); + + EXPECT_EQ(100000, balance.mTokens[tokenId1]); + EXPECT_EQ(100000, balance.mTokens[tokenId2]); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, CanExecuteForNFT) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create two nfts + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createNft(operatorKey)); + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId1) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // When + // Transfer nfts to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId1.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .addNftTransfer(tokenId2.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the nfts + EXPECT_NO_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId1.nft(1), tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Verify the balance of the receiver is 0 + AccountBalance balance; + EXPECT_NO_THROW(balance = AccountBalanceQuery().setAccountId(receiver).execute(getTestClient())); + + EXPECT_EQ(0, balance.mTokens[tokenId1]); + EXPECT_EQ(0, balance.mTokens[tokenId2]); + + // Verify the tokens are transferred back to the treasury + EXPECT_NO_THROW( + balance = + AccountBalanceQuery().setAccountId(getTestClient().getOperatorAccountId().value()).execute(getTestClient())); + + EXPECT_EQ(1, balance.mTokens[tokenId1]); + EXPECT_EQ(1, balance.mTokens[tokenId2]); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, CanExecuteForFTAndNFT) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create two fts + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createFt(operatorKey)); + + // Create two nfts + TokenId tokenId3; + ASSERT_NO_THROW(tokenId3 = createNft(operatorKey)); + TokenId tokenId4; + ASSERT_NO_THROW(tokenId4 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId3) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId4) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // When + // Transfer fts to the receiver + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId1, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId1, receiver, 10) + .addTokenTransfer(tokenId2, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId2, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Transfer nfts to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId3.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .addNftTransfer(tokenId4.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the fts and nfts + EXPECT_NO_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId3.nft(1), tokenId4.nft(1) }) + .setFts({ tokenId1, tokenId2 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Verify the ft balance of the receiver is 0 + AccountBalance balance; + EXPECT_NO_THROW(balance = AccountBalanceQuery().setAccountId(receiver).execute(getTestClient())); + + EXPECT_EQ(0, balance.mTokens[tokenId1]); + EXPECT_EQ(0, balance.mTokens[tokenId2]); + + // Verify the tokens are transferred back to the treasury + EXPECT_NO_THROW( + balance = + AccountBalanceQuery().setAccountId(getTestClient().getOperatorAccountId().value()).execute(getTestClient())); + + EXPECT_EQ(100000, balance.mTokens[tokenId1]); + EXPECT_EQ(100000, balance.mTokens[tokenId2]); + + // Verify the ft balance of the receiver is 0 + EXPECT_NO_THROW(balance = AccountBalanceQuery().setAccountId(receiver).execute(getTestClient())); + + EXPECT_EQ(0, balance.mTokens[tokenId3]); + EXPECT_EQ(0, balance.mTokens[tokenId4]); + + // Verify the tokens are transferred back to the treasury + EXPECT_NO_THROW( + balance = + AccountBalanceQuery().setAccountId(getTestClient().getOperatorAccountId().value()).execute(getTestClient())); + + EXPECT_EQ(1, balance.mTokens[tokenId3]); + EXPECT_EQ(1, balance.mTokens[tokenId4]); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, ReceiverSigRequired) +{ + // Given + // Create treasury account with auto associations + AccountId treasury; + std::shared_ptr treasuryKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(treasury = createAccount(treasuryKey, true)); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create ft for + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("Test Fungible Token") + .setTokenSymbol("TFT") + .setTokenMemo("I was created for integration tests") + .setInitialSupply(100000) + .setTreasuryAccountId(treasury) + .setAdminKey(getTestClient().getOperatorPublicKey()) + .setFreezeKey(getTestClient().getOperatorPublicKey()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .setMetadataKey(getTestClient().getOperatorPublicKey()) + .freezeWith(&getTestClient()) + .sign(treasuryKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // When + // Transfer fts to the receiver + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId, treasury, -10) + .addTokenTransfer(tokenId, receiver, 10) + .freezeWith(&getTestClient()) + .sign(treasuryKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the ft + EXPECT_NO_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, TokenFrozen) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create an ft + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + + // Create an nft + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // When + // Transfer fts to the receiver + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId1, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId1, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Transfer nfts to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId2.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Freeze the ft + ASSERT_NO_THROW(txReceipt = TokenFreezeTransaction() + .setTokenId(tokenId1) + .setAccountId(receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Reject the ft + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId1 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); + + // Freeze the nft + ASSERT_NO_THROW(txReceipt = TokenFreezeTransaction() + .setTokenId(tokenId2) + .setAccountId(receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the nfts + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, TokenPaused) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create an ft + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey, true)); + + // Create an nft + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey, true)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // When + // Transfer fts to the receiver + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId1, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId1, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Transfer nfts to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId2.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Pause the ft + ASSERT_NO_THROW(txReceipt = + TokenPauseTransaction().setTokenId(tokenId1).execute(getTestClient()).getReceipt(getTestClient())); + + // Pause the nft + ASSERT_NO_THROW(txReceipt = + TokenPauseTransaction().setTokenId(tokenId2).execute(getTestClient()).getReceipt(getTestClient())); + + // Then + // Reject the ft + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId1 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); + // Reject the nft + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, RemovesAllowance) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create spender account with auto associations + AccountId spender; + std::shared_ptr spenderKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(spender = createAccount(spenderKey)); + + // Create an nft + TokenId tokenId; + ASSERT_NO_THROW(tokenId = createNft(operatorKey, true)); + + // Mint the nft + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId) + .setMetadata({ { std::byte(0x01) }, { std::byte(0x02) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // When + // Transfer nft to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .addNftTransfer(tokenId.nft(2), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // approve allowance to the spender + ASSERT_NO_THROW(txReceipt = AccountAllowanceApproveTransaction() + .approveTokenNftAllowance(tokenId.nft(1), receiver, spender) + .approveTokenNftAllowance(tokenId.nft(2), receiver, spender) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Verify that the spender has allowance + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addApprovedNftTransfer(tokenId.nft(1), receiver, spender) + .setTransactionId(TransactionId::generate(spender)) + .freezeWith(&getTestClient()) + .sign(spenderKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject nfts + EXPECT_NO_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId.nft(2) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Verify no further allowance + ASSERT_THROW(txReceipt = TransferTransaction() + .addApprovedNftTransfer(tokenId.nft(2), receiver, spender) + .setTransactionId(TransactionId::generate(spender)) + .freezeWith(&getTestClient()) + .sign(spenderKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWhenRejectingNFTWithTokenID) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create spender account with auto associations + AccountId spender; + std::shared_ptr spenderKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(spender = createAccount(spenderKey)); + + // Create an nft + TokenId tokenId; + ASSERT_NO_THROW(tokenId = createNft(operatorKey, true)); + + // Mint the nft + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId) + .setMetadata({ { std::byte(0x01) }, { std::byte(0x02) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + // When + // Transfer nft to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .addNftTransfer(tokenId.nft(2), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject nft from fts collection + ASSERT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWithTokenReferenceRepeated) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create an ft + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + + // Create an nft + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + // When + // Transfer fts to the receiver + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId1, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId1, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Transfer nfts to the receiver + ASSERT_NO_THROW(txReceipt = + TransferTransaction() + .addNftTransfer(tokenId2.nft(1), getTestClient().getOperatorAccountId().value(), receiver) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the ft repeated + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId1, tokenId1 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + PrecheckStatusException); // TOKEN_REFERENCE_REPEATED + + // Reject the nft repeated + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId2.nft(1), tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + PrecheckStatusException); // TOKEN_REFERENCE_REPEATED +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWhenOwnerHasNoBalance) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create an ft + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + + // Create an nft + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + // When + // Associate the receiver + EXPECT_NO_THROW(txReceipt = TokenAssociateTransaction() + .setAccountId(receiver) + .setTokenIds({ tokenId1, tokenId2 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + // Reject the ft with invalid balance + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId1 }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // INSUFFICIENT_BALANCE + + // Reject the nft with invalid owner + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setNfts({ tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // INVALID_OWNER_ID +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsTreasuryRejects) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create an ft + TokenId tokenId1; + ASSERT_NO_THROW(tokenId1 = createFt(operatorKey)); + + // Create an nft + TokenId tokenId2; + ASSERT_NO_THROW(tokenId2 = createNft(operatorKey)); + + // Mint the nfts + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId2) + .setMetadata({ { std::byte(0xAB) } }) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // When / Then + // Reject the ft and nft for treasury + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(AccountId(2ULL)) + .setFts({ tokenId1 }) + .setNfts({ tokenId2.nft(1) }) + .freezeWith(&getTestClient()) + .sign(operatorKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // ACCOUNT_IS_TREASURY +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWithInvalidToken) +{ + // Given + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // When / Then + // Reject invalid ft or nft + TransactionReceipt txReceipt; + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ TokenId(2ULL) }) + .setNfts({ TokenId(2ULL).nft(1) }) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWhenNoFTsOrNFTsProvided) +{ + // Given + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // When / Then + // Reject with empty fts or nfts + TransactionReceipt txReceipt; + EXPECT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .freezeWith(&getTestClient()) + .sign(receiverKey) + .execute(getTestClient()) + .getReceipt(getTestClient()), + PrecheckStatusException); // EMPTY_TOKEN_REFERENCE_LIST +} + +//----- +TEST_F(TokenRejectTransactionIntegrationTests, FailsWithInvalidSignature) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + + // Create receiver account with auto associations + AccountId receiver; + std::shared_ptr receiverKey = ED25519PrivateKey::generatePrivateKey(); + ASSERT_NO_THROW(receiver = createAccount(receiverKey)); + + // Create an ft + TokenId tokenId; + ASSERT_NO_THROW(tokenId = createFt(operatorKey)); + + // When + // Transfer fts to the receiver + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TransferTransaction() + .addTokenTransfer(tokenId, getTestClient().getOperatorAccountId().value(), -10) + .addTokenTransfer(tokenId, receiver, 10) + .execute(getTestClient()) + .getReceipt(getTestClient())); + // Then + // Reject ft with invalid signature + ASSERT_THROW(txReceipt = TokenRejectTransaction() + .setOwner(receiver) + .setFts({ tokenId }) + .freezeWith(&getTestClient()) + .sign(ED25519PrivateKey::generatePrivateKey()) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); +} diff --git a/src/sdk/tests/unit/CMakeLists.txt b/src/sdk/tests/unit/CMakeLists.txt index cda7990e..58433a16 100644 --- a/src/sdk/tests/unit/CMakeLists.txt +++ b/src/sdk/tests/unit/CMakeLists.txt @@ -95,6 +95,7 @@ add_executable(${TEST_PROJECT_NAME} TokenNftInfoQueryUnitTests.cc TokenNftTransferUnitTests.cc TokenPauseTransactionUnitTests.cc + TokenRejectTransactionUnitTests.cc TokenRevokeKycTransactionUnitTests.cc TokenSupplyTypeUnitTests.cc TokenTransferUnitTests.cc diff --git a/src/sdk/tests/unit/TokenRejectTransactionUnitTests.cc b/src/sdk/tests/unit/TokenRejectTransactionUnitTests.cc new file mode 100644 index 00000000..67207260 --- /dev/null +++ b/src/sdk/tests/unit/TokenRejectTransactionUnitTests.cc @@ -0,0 +1,102 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2024 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. + * + */ +#include "AccountId.h" +#include "NftId.h" +#include "TokenId.h" +#include "TokenRejectTransaction.h" + +#include +#include +#include + +using namespace Hedera; + +class TokenRejectTransactionUnitTests : public ::testing::Test +{ +protected: + TokenRejectTransaction transaction; +}; + +TEST_F(TokenRejectTransactionUnitTests, ConstructTokenRejectTransactionFromTransactionBodyProtobuf) +{ + // Given + proto::TransactionBody transactionBody; + proto::TokenRejectTransactionBody* body = transactionBody.mutable_tokenreject(); + + proto::TokenID* fungibleTokenId = body->add_rejections()->mutable_fungible_token(); + fungibleTokenId->set_shardnum(1); + fungibleTokenId->set_realmnum(2); + fungibleTokenId->set_tokennum(3); + + proto::TokenID* nonFungibleTokenId = body->add_rejections()->mutable_nft()->mutable_token_id(); + nonFungibleTokenId->set_shardnum(4); + nonFungibleTokenId->set_realmnum(5); + nonFungibleTokenId->set_tokennum(6); + + // When + TokenRejectTransaction tokenRejectTransaction(transactionBody); + + // Then + EXPECT_EQ(tokenRejectTransaction.getFts().size(), 1); + EXPECT_EQ(tokenRejectTransaction.getNfts().size(), 1); + EXPECT_EQ(tokenRejectTransaction.getFts().at(0), TokenId(1, 2, 3)); + EXPECT_EQ(tokenRejectTransaction.getNfts().at(0), NftId(TokenId(4, 5, 6), 0)); // Assuming serial number 0 by default +} + +TEST_F(TokenRejectTransactionUnitTests, SetOwner) +{ + // Given + AccountId ownerId(1, 2, 3); + + // When + transaction.setOwner(ownerId); + + // Then + EXPECT_TRUE(transaction.getOwner().has_value()); + EXPECT_EQ(transaction.getOwner().value(), ownerId); +} + +TEST_F(TokenRejectTransactionUnitTests, SetFts) +{ + // Given + std::vector fts = { TokenId(1, 2, 3), TokenId(4, 5, 6) }; + + // When + transaction.setFts(fts); + + // Then + EXPECT_EQ(transaction.getFts().size(), 2); + EXPECT_EQ(transaction.getFts().at(0), TokenId(1, 2, 3)); + EXPECT_EQ(transaction.getFts().at(1), TokenId(4, 5, 6)); +} + +TEST_F(TokenRejectTransactionUnitTests, SetNfts) +{ + // Given + std::vector nfts = { NftId(TokenId(1, 2, 3), 1), NftId(TokenId(4, 5, 6), 2) }; + + // When + transaction.setNfts(nfts); + + // Then + EXPECT_EQ(transaction.getNfts().size(), 2); + EXPECT_EQ(transaction.getNfts().at(0), NftId(TokenId(1, 2, 3), 1)); + EXPECT_EQ(transaction.getNfts().at(1), NftId(TokenId(4, 5, 6), 2)); +}