diff --git a/src/sdk/examples/CMakeLists.txt b/src/sdk/examples/CMakeLists.txt index 6dac723b..0ded71d2 100644 --- a/src/sdk/examples/CMakeLists.txt +++ b/src/sdk/examples/CMakeLists.txt @@ -48,6 +48,7 @@ set(SIGN_TRANSACTION_EXAMPLE_NAME ${PROJECT_NAME}-sign-transaction-example) set(SOLIDITY_PRECOMPILE_EXAMPLE_NAME ${PROJECT_NAME}-solidity-precompile-example) set(STAKING_EXAMPLE_NAME ${PROJECT_NAME}-staking-example) set(STAKING_WITH_UPDATE_EXAMPLE_NAME ${PROJECT_NAME}-staking-with-update-example) +set(TOKEN_METADATA_EXAMPLE_NAME ${PROJECT_NAME}-token-metadata-example) set(TOPIC_WITH_ADMIN_KEY_EXAMPLE_NAME ${PROJECT_NAME}-topic-with-admin-key-example) set(TRANSFER_CRYPTO_EXAMPLE_NAME ${PROJECT_NAME}-transfer-crypto-example) set(TRANSFER_TOKENS_EXAMPLE_NAME ${PROJECT_NAME}-transfer-tokens-example) @@ -98,6 +99,7 @@ add_executable(${SIGN_TRANSACTION_EXAMPLE_NAME} SignTransactionExample.cc) add_executable(${SOLIDITY_PRECOMPILE_EXAMPLE_NAME} SolidityPrecompileExample.cc) add_executable(${STAKING_EXAMPLE_NAME} StakingExample.cc) add_executable(${STAKING_WITH_UPDATE_EXAMPLE_NAME} StakingWithUpdateExample.cc) +add_executable(${TOKEN_METADATA_EXAMPLE_NAME} TokenMetadataExample.cc) add_executable(${TOPIC_WITH_ADMIN_KEY_EXAMPLE_NAME} TopicWithAdminKeyExample.cc) add_executable(${TRANSFER_CRYPTO_EXAMPLE_NAME} TransferCryptoExample.cc) add_executable(${TRANSFER_TOKENS_EXAMPLE_NAME} TransferTokensExample.cc) @@ -170,6 +172,7 @@ target_link_libraries(${SIGN_TRANSACTION_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${SOLIDITY_PRECOMPILE_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${STAKING_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${STAKING_WITH_UPDATE_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) +target_link_libraries(${TOKEN_METADATA_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${TOPIC_WITH_ADMIN_KEY_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${TRANSFER_CRYPTO_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${TRANSFER_TOKENS_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) diff --git a/src/sdk/examples/TokenMetadataExample.cc b/src/sdk/examples/TokenMetadataExample.cc new file mode 100644 index 00000000..93310962 --- /dev/null +++ b/src/sdk/examples/TokenMetadataExample.cc @@ -0,0 +1,163 @@ +/*- + * + * 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 "AccountCreateTransaction.h" +#include "AccountDeleteTransaction.h" +#include "AccountId.h" +#include "Client.h" +#include "ED25519PrivateKey.h" +#include "PrivateKey.h" +#include "TokenCreateTransaction.h" +#include "TokenDeleteTransaction.h" +#include "TokenInfo.h" +#include "TokenInfoQuery.h" +#include "TokenUpdateTransaction.h" +#include "TransactionReceipt.h" +#include "TransactionResponse.h" +#include "exceptions/PrecheckStatusException.h" +#include "exceptions/ReceiptStatusException.h" +#include "impl/Utilities.h" + +#include +#include + +using namespace Hedera; + +int main(int argc, char** argv) +{ + if (argc < 2) + { + std::cerr << "Please provide a parameter -ft, -nft for creating a fungible or non-fungible token" << std::endl; + return 0; + } + + TokenType tokenType; + if (strcmp(argv[1], "-ft") == 0) + { + tokenType = TokenType::FUNGIBLE_COMMON; + } + else if (strcmp(argv[1], "-nft") == 0) + { + tokenType = TokenType::NON_FUNGIBLE_UNIQUE; + } + else + { + std::cerr << "Please provide a parameter -ft, -nft for creating a fungible or non-fungible token" << std::endl; + return 0; + } + + dotenv::init(); + const AccountId operatorAccountId = AccountId::fromString(std::getenv("OPERATOR_ID")); + const std::shared_ptr operatorPrivateKey = ED25519PrivateKey::fromString(std::getenv("OPERATOR_KEY")); + + // Get a client for the Hedera testnet, and set the operator account ID and key such that all generated transactions + // will be paid for by this account and be signed by this key. + Client client = Client::forTestnet(); + client.setOperator(operatorAccountId, operatorPrivateKey); + + // Metadata values for create/update + const std::vector initialMetadata = { std::byte(0xAA), std::byte(0xAB), std::byte(0xAC), std::byte(0xAD) }; + const std::vector updatedMetadata = { std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) }; + + // Create a FT/NFT with metadata and admin key + TokenId mutableTokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setDecimals(3) + .setInitialSupply(100000) + .setTokenType(tokenType) + .setMetadata(initialMetadata) + .setTreasuryAccountId(operatorAccountId) + .setAdminKey(operatorPrivateKey) + .freezeWith(&client) + .sign(operatorPrivateKey) + .execute(client) + .getReceipt(client) + .mTokenId.value(); + + std::cout << "Created a mutable token " << mutableTokenId.toString() << " with metadata: " << std::endl; + + for (std::byte b : initialMetadata) + { + std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b) << " "; + } + std::cout << std::endl; + + // Update the token metadata + TransactionReceipt txReceipt = TokenUpdateTransaction() + .setTokenId(mutableTokenId) + .setMetadata(updatedMetadata) + .freezeWith(&client) + .sign(operatorPrivateKey) + .execute(client) + .getReceipt(client); + + std::cout << "Updated mutable token " << mutableTokenId.toString() << " metadata:" << std::endl; + + TokenInfo tokenInfo = TokenInfoQuery().setTokenId(mutableTokenId).execute(client); + + for (std::byte b : tokenInfo.mMetadata) + { + std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b) << " "; + } + std::cout << std::endl; + + std::shared_ptr metadataKey = ED25519PrivateKey::generatePrivateKey(); + + // Create an immutable FT/NFT with metadata and metadata key + TokenId immutableTokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setDecimals(3) + .setInitialSupply(100000) + .setTokenType(tokenType) + .setMetadata(initialMetadata) + .setTreasuryAccountId(operatorAccountId) + .setMetadataKey(metadataKey) + .execute(client) + .getReceipt(client) + .mTokenId.value(); + + std::cout << "Created a immutable token " << immutableTokenId.toString() << " with metadata: " << std::endl; + + for (std::byte b : initialMetadata) + { + std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b) << " "; + } + std::cout << std::endl; + + // Update the token metadata + txReceipt = TokenUpdateTransaction() + .setTokenId(immutableTokenId) + .setMetadata(updatedMetadata) + .freezeWith(&client) + .sign(metadataKey) + .execute(client) + .getReceipt(client); + + std::cout << "Updated immutable token " << immutableTokenId.toString() << " metadata:" << std::endl; + + tokenInfo = TokenInfoQuery().setTokenId(immutableTokenId).execute(client); + + for (std::byte b : tokenInfo.mMetadata) + { + std::cout << std::hex << std::setw(2) << std::setfill('0') << static_cast(b) << " "; + } + std::cout << std::endl; +} \ No newline at end of file diff --git a/src/sdk/main/CMakeLists.txt b/src/sdk/main/CMakeLists.txt index 5474bb15..bf98de24 100644 --- a/src/sdk/main/CMakeLists.txt +++ b/src/sdk/main/CMakeLists.txt @@ -145,6 +145,7 @@ add_library(${PROJECT_NAME} STATIC src/TokenType.cc src/TokenUnfreezeTransaction.cc src/TokenUnpauseTransaction.cc + src/TokenUpdateNftsTransaction.cc src/TokenUpdateTransaction.cc src/TokenWipeTransaction.cc src/TopicCreateTransaction.cc diff --git a/src/sdk/main/include/RequestType.h b/src/sdk/main/include/RequestType.h index 95bd2ccb..e2fd22cf 100644 --- a/src/sdk/main/include/RequestType.h +++ b/src/sdk/main/include/RequestType.h @@ -107,7 +107,8 @@ enum class RequestType GET_ACCOUNT_DETAILS, ETHEREUM_TRANSACTION, NODE_STAKE_UPDATE, - UTIL_PRNG + UTIL_PRNG, + TOKEN_UPDATE_NFTS }; /** diff --git a/src/sdk/main/include/Status.h b/src/sdk/main/include/Status.h index ccdb2571..a3622ded 100644 --- a/src/sdk/main/include/Status.h +++ b/src/sdk/main/include/Status.h @@ -1501,7 +1501,23 @@ enum class Status /** * An alias that is assigned to an account or contract cannot be assigned to another account or contract. */ - ALIAS_ALREADY_ASSIGNED + ALIAS_ALREADY_ASSIGNED, + + /** + * A provided metadata key was invalid. Verification includes, for example, checking the size of Ed25519 and + * ECDSA(secp256k1) public keys. + */ + INVALID_METADATA_KEY, + + /** + * Token Metadata is not provided + */ + MISSING_TOKEN_METADATA, + + /** + * NFT serial numbers are missing in the TokenUpdateNftsTransactionBody + */ + MISSING_SERIAL_NUMBERS }; /** diff --git a/src/sdk/main/include/TokenCreateTransaction.h b/src/sdk/main/include/TokenCreateTransaction.h index 2982a52e..161d3c68 100644 --- a/src/sdk/main/include/TokenCreateTransaction.h +++ b/src/sdk/main/include/TokenCreateTransaction.h @@ -250,6 +250,22 @@ class TokenCreateTransaction : public Transaction */ TokenCreateTransaction& setPauseKey(const std::shared_ptr& key); + /** + * Set the desired metadata for the new token. + * + * @param metadata The desired metadata for the new token. + * @return A reference to this TokenCreateTransaction with the newly-set metadata. + */ + TokenCreateTransaction& setMetadata(const std::vector& metadata); + + /** + * Set the desired metadata key for the new token. + * + * @param key The desired metadata key for the new token. + * @return A reference to this TokenCreateTransaction with the newly-set metadata key. + */ + TokenCreateTransaction& setMetadataKey(const std::shared_ptr& metadataKey); + /** * Get the desired name for the new token. * @@ -399,6 +415,20 @@ class TokenCreateTransaction : public Transaction */ [[nodiscard]] inline std::shared_ptr getPauseKey() const { return mPauseKey; } + /** + * Get the desired metadata for the new token. + * + * @return The desired metadata for the new token. + */ + [[nodiscard]] inline std::vector getMetadata() const { return mMetadata; } + + /** + * Get the desired metadata key for the new token. + * + * @return The desired metadata key for the new token. + */ + [[nodiscard]] inline std::shared_ptr getMetadataKey() const { return mMetadataKey; } + private: friend class WrappedTransaction; @@ -568,6 +598,16 @@ class TokenCreateTransaction : public Transaction * PauseNotApplicable, otherwise Unpaused. */ std::shared_ptr mPauseKey = nullptr; + + /** + * Metadata of the created token definition. + */ + std::vector mMetadata; + + /** + * The key which can change the metadata of a token (token definition, partition definition, and individual NFTs). + */ + std::shared_ptr mMetadataKey = nullptr; }; } // namespace Hedera diff --git a/src/sdk/main/include/TokenInfo.h b/src/sdk/main/include/TokenInfo.h index 5b7afb1b..71c1ceec 100644 --- a/src/sdk/main/include/TokenInfo.h +++ b/src/sdk/main/include/TokenInfo.h @@ -234,6 +234,16 @@ class TokenInfo * The ID of the ledger from which this response was returned. */ LedgerId mLedgerId; + + /** + * Represents the metadata of the token definition. + */ + std::vector mMetadata; + + /** + * The key which can change the metadata of a token (token definition and individual NFTs). + */ + std::shared_ptr mMetadataKey = nullptr; }; } // namespace Hedera diff --git a/src/sdk/main/include/TokenUpdateNftsTransaction.h b/src/sdk/main/include/TokenUpdateNftsTransaction.h new file mode 100644 index 00000000..f4d8d441 --- /dev/null +++ b/src/sdk/main/include/TokenUpdateNftsTransaction.h @@ -0,0 +1,184 @@ +/*- + * + * 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_UPDATE_NFTS_TRANSACTION_H_ +#define HEDERA_SDK_CPP_TOKEN_UPDATE_NFTS_TRANSACTION_H_ + +#include "AccountId.h" +#include "TokenId.h" +#include "Transaction.h" + +#include +#include + +namespace proto +{ +class TokenUpdateNftsTransactionBody; +class TransactionBody; +} + +namespace Hedera +{ +/** + * At consensus, updates an already created Non Fungible Token to the given values. + * + * If no value is given for a field, that field is left unchanged. + * Only certain fields such as metadata can be updated. + * + * Updating the metadata of an NFT does not affect its ownership or transferability. + * This operation is intended for updating attributes of individual NFTs in a collection. + * + * Transaction Signing Requirements + * - To update metadata of an NFT, the metadata_key of the token should sign the transaction. + */ +class TokenUpdateNftsTransaction : public Transaction +{ +public: + TokenUpdateNftsTransaction() = 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 TokenUpdate transaction. + */ + explicit TokenUpdateNftsTransaction(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 TokenUpdateNftsTransaction( + const std::map>& transactions); + + /** + * Set the ID of the token to update. + * + * @param tokenId The ID of the token to update. + * @return A reference to this TokenUpdateNftsTransaction with the newly-set token ID. + */ + TokenUpdateNftsTransaction& setTokenId(const TokenId& tokenId); + + /** + * Set the token serials. + * + * @param serials The vector of serials. + * @return A reference to this TokenUpdateNftsTransaction with the newly-set token serials. + */ + TokenUpdateNftsTransaction& setSerials(const std::vector& serials); + + /** + * Set the new metadata for the tokens. + * + * @param metadata The new metadata for the tokens. + * @return A reference to this TokenUpdateNftsTransaction with the newly-set token metadata. + */ + TokenUpdateNftsTransaction& setMetadata(const std::vector& metadata); + + /** + * Get the ID of the token to update. + * + * @return The ID of the token to update. + */ + [[nodiscard]] inline TokenId getTokenId() const { return mTokenId; } + + /** + * Get the serials for the token. + * + * @return The serials for the token. Returns empty vector if serials are not set. + */ + [[nodiscard]] inline std::vector getSerials() const { return mSerials; } + + /** + * Get the new metadata for the tokens. + * + * @return The new metadata for the tokens. Returns uninitialized if no new symbol has been set. + */ + [[nodiscard]] inline std::vector getMetadata() const { return mMetadata; } + +private: + friend class WrappedTransaction; + + /** + * Derived from Executable. Submit a Transaction protobuf object which contains this TokenUpdateNftsTransaction'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 TokenUpdateNftsTransaction are valid. + * + * @param client The Client that should be used to validate the checksums. + * @throws BadEntityException This TokenUpdateNftsTransaction's checksums are not valid. + */ + void validateChecksums(const Client& client) const override; + + /** + * Derived from Transaction. Build and add the TokenUpdateNftsTransaction protobuf representation to the Transaction + * protobuf object. + * + * @param body The TransactionBody protobuf object being built. + */ + void addToBody(proto::TransactionBody& body) const override; + + /** + * Initialize this TokenUpdateNftsTransaction from its source TransactionBody protobuf object. + */ + void initFromSourceTransactionBody(); + + /** + * Build a TokenUpdateNftsTransactionBody protobuf object from this TokenUpdateNftsTransaction object. + * + * @return A pointer to a TokenUpdateNftsTransactionBody protobuf object filled with this TokenUpdateNftsTransaction + * object's data. + */ + [[nodiscard]] proto::TokenUpdateNftsTransactionBody* build() const; + + /** + * The token for which to update NFTs. + */ + TokenId mTokenId; + + /** + * The list of serial numbers to be updated. + */ + std::vector mSerials; + + /** + * The new metadata of the NFT(s) + */ + std::vector mMetadata; +}; + +} +// namespace Hedera + +#endif // HEDERA_SDK_CPP_TOKEN_UPDATE_NFTS_TRANSACTION_H_ diff --git a/src/sdk/main/include/TokenUpdateTransaction.h b/src/sdk/main/include/TokenUpdateTransaction.h index 3ce5bbb4..639fc681 100644 --- a/src/sdk/main/include/TokenUpdateTransaction.h +++ b/src/sdk/main/include/TokenUpdateTransaction.h @@ -192,6 +192,22 @@ class TokenUpdateTransaction : public Transaction */ TokenUpdateTransaction& setPauseKey(const std::shared_ptr& key); + /** + * Set the desired metadata for the token. + * + * @param metadata The desired metadata for the token. + * @return A reference to this TokenUpdateTransaction with the newly-set metadata. + */ + TokenUpdateTransaction& setMetadata(const std::vector& metadata); + + /** + * Set a new metadata key for the token. + * + * @param key The new metadata key for the token. + * @return A reference to this TokenUpdateTransaction with the newly-set metadata key. + */ + TokenUpdateTransaction& setMetadataKey(const std::shared_ptr& key); + /** * Get the ID of the token to update. * @@ -305,6 +321,13 @@ class TokenUpdateTransaction : public Transaction */ [[nodiscard]] inline std::shared_ptr getPauseKey() const { return mPauseKey; } + /** + * Get the new metadata key for the token. + * + * @return The new metadata key for the token. Returns nullptr if no new metadata key has been set. + */ + [[nodiscard]] inline std::shared_ptr getMetadataKey() const { return mMetadataKey; } + private: friend class WrappedTransaction; @@ -436,6 +459,19 @@ class TokenUpdateTransaction : public Transaction * The new pause key for the token. */ std::shared_ptr mPauseKey = nullptr; + + /** + * Metadata of the updated token. + */ + std::vector mMetadata; + + /** + * The new metadata key of the token. The metadata key has the ability to + * change the metadata of a token (token definition, partition definition, + * and individual NFTs). If the Token does not currently have a Metadata key, + * the transaction will resolve to TOKEN_HAS_NO_METADATA_KEY. + */ + std::shared_ptr mMetadataKey = nullptr; }; } // namespace Hedera diff --git a/src/sdk/main/include/TransactionType.h b/src/sdk/main/include/TransactionType.h index ac7d799e..56446293 100644 --- a/src/sdk/main/include/TransactionType.h +++ b/src/sdk/main/include/TransactionType.h @@ -62,6 +62,7 @@ enum TransactionType : int TOKEN_REVOKE_KYC_TRANSACTION, TOKEN_UNFREEZE_TRANSACTION, TOKEN_UNPAUSE_TRANSACTION, + TOKEN_UPDATE_NFTS_TRANSACTION, TOKEN_UPDATE_TRANSACTION, TOKEN_WIPE_TRANSACTION, TOPIC_CREATE_TRANSACTION, diff --git a/src/sdk/main/include/WrappedTransaction.h b/src/sdk/main/include/WrappedTransaction.h index bcdf56f7..60a0e3f9 100644 --- a/src/sdk/main/include/WrappedTransaction.h +++ b/src/sdk/main/include/WrappedTransaction.h @@ -54,6 +54,7 @@ #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" +#include "TokenUpdateNftsTransaction.h" #include "TokenUpdateTransaction.h" #include "TokenWipeTransaction.h" #include "TopicCreateTransaction.h" @@ -116,6 +117,7 @@ class WrappedTransaction TokenRevokeKycTransaction, TokenUnfreezeTransaction, TokenUnpauseTransaction, + TokenUpdateNftsTransaction, TokenUpdateTransaction, TokenWipeTransaction, TopicCreateTransaction, diff --git a/src/sdk/main/src/Executable.cc b/src/sdk/main/src/Executable.cc index f2886240..96c5da23 100644 --- a/src/sdk/main/src/Executable.cc +++ b/src/sdk/main/src/Executable.cc @@ -76,6 +76,7 @@ #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" +#include "TokenUpdateNftsTransaction.h" #include "TokenUpdateTransaction.h" #include "TokenWipeTransaction.h" #include "TopicCreateTransaction.h" @@ -664,6 +665,10 @@ template class Executable; template class Executable; +template class Executable; template class Executable; template class Executable; template class Executable; diff --git a/src/sdk/main/src/PrivateKey.cc b/src/sdk/main/src/PrivateKey.cc index 33bd08be..9e94680d 100644 --- a/src/sdk/main/src/PrivateKey.cc +++ b/src/sdk/main/src/PrivateKey.cc @@ -175,6 +175,8 @@ std::vector PrivateKey::signTransaction(WrappedTransaction& transacti return signTransaction(*transaction.getTransaction()); case TOKEN_UNPAUSE_TRANSACTION: return signTransaction(*transaction.getTransaction()); + case TOKEN_UPDATE_NFTS_TRANSACTION: + return signTransaction(*transaction.getTransaction()); case TOKEN_UPDATE_TRANSACTION: return signTransaction(*transaction.getTransaction()); case TOKEN_WIPE_TRANSACTION: diff --git a/src/sdk/main/src/RequestType.cc b/src/sdk/main/src/RequestType.cc index 0689c642..5bcbbbf7 100644 --- a/src/sdk/main/src/RequestType.cc +++ b/src/sdk/main/src/RequestType.cc @@ -98,6 +98,7 @@ const std::unordered_map gProtobufHeder { proto::HederaFunctionality::EthereumTransaction, RequestType::ETHEREUM_TRANSACTION }, { proto::HederaFunctionality::NodeStakeUpdate, RequestType::NODE_STAKE_UPDATE }, { proto::HederaFunctionality::UtilPrng, RequestType::UTIL_PRNG }, + { proto::HederaFunctionality::TokenUpdateNfts, RequestType::TOKEN_UPDATE_NFTS }, }; //----- @@ -174,7 +175,8 @@ const std::unordered_map gRequestTypeTo { RequestType::GET_ACCOUNT_DETAILS, proto::HederaFunctionality::GetAccountDetails }, { RequestType::ETHEREUM_TRANSACTION, proto::HederaFunctionality::EthereumTransaction }, { RequestType::NODE_STAKE_UPDATE, proto::HederaFunctionality::NodeStakeUpdate }, - { RequestType::UTIL_PRNG, proto::HederaFunctionality::UtilPrng } + { RequestType::UTIL_PRNG, proto::HederaFunctionality::UtilPrng }, + { RequestType::TOKEN_UPDATE_NFTS, proto::HederaFunctionality::TokenUpdateNfts }, }; //----- @@ -251,7 +253,8 @@ const std::unordered_map gRequestTypeToString = { { RequestType::GET_ACCOUNT_DETAILS, "GET_ACCOUNT_DETAILS" }, { RequestType::ETHEREUM_TRANSACTION, "ETHEREUM_TRANSACTION" }, { RequestType::NODE_STAKE_UPDATE, "NODE_STAKE_UPDATE" }, - { RequestType::UTIL_PRNG, "UTIL_PRNG" } + { RequestType::UTIL_PRNG, "UTIL_PRNG" }, + { RequestType::TOKEN_UPDATE_NFTS, "TOKEN_UPDATE_NFTS" }, }; } // namespace Hedera diff --git a/src/sdk/main/src/Status.cc b/src/sdk/main/src/Status.cc index 77abcb64..17dd01be 100644 --- a/src/sdk/main/src/Status.cc +++ b/src/sdk/main/src/Status.cc @@ -330,7 +330,10 @@ const std::unordered_map gProtobufResponseCodeT { proto::ResponseCodeEnum::INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES, Status::INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES }, { proto::ResponseCodeEnum::TRANSACTION_HAS_UNKNOWN_FIELDS, Status::TRANSACTION_HAS_UNKNOWN_FIELDS }, { proto::ResponseCodeEnum::ACCOUNT_IS_IMMUTABLE, Status::ACCOUNT_IS_IMMUTABLE }, - { proto::ResponseCodeEnum::ALIAS_ALREADY_ASSIGNED, Status::ALIAS_ALREADY_ASSIGNED } + { proto::ResponseCodeEnum::ALIAS_ALREADY_ASSIGNED, Status::ALIAS_ALREADY_ASSIGNED }, + { 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 } }; //----- @@ -641,6 +644,9 @@ const std::unordered_map gStatusToProtobufRespo { Status::TRANSACTION_HAS_UNKNOWN_FIELDS, proto::ResponseCodeEnum::TRANSACTION_HAS_UNKNOWN_FIELDS }, { Status::ACCOUNT_IS_IMMUTABLE, proto::ResponseCodeEnum::ACCOUNT_IS_IMMUTABLE }, { Status::ALIAS_ALREADY_ASSIGNED, proto::ResponseCodeEnum::ALIAS_ALREADY_ASSIGNED }, + { 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 } }; //----- @@ -932,7 +938,10 @@ const std::unordered_map gStatusToString = { { Status::INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES, "INSUFFICIENT_BALANCES_FOR_RENEWAL_FEES" }, { Status::TRANSACTION_HAS_UNKNOWN_FIELDS, "TRANSACTION_HAS_UNKNOWN_FIELDS" }, { Status::ACCOUNT_IS_IMMUTABLE, "ACCOUNT_IS_IMMUTABLE" }, - { Status::ALIAS_ALREADY_ASSIGNED, "ALIAS_ALREADY_ASSIGNED" } + { Status::ALIAS_ALREADY_ASSIGNED, "ALIAS_ALREADY_ASSIGNED" }, + { Status::INVALID_METADATA_KEY, "INVALID_METADATA_KEY" }, + { Status::MISSING_TOKEN_METADATA, "MISSING_TOKEN_METADATA" }, + { Status::MISSING_SERIAL_NUMBERS, "MISSING_SERIAL_NUMBERS" } }; } // namespace Hedera \ No newline at end of file diff --git a/src/sdk/main/src/TokenCreateTransaction.cc b/src/sdk/main/src/TokenCreateTransaction.cc index c2fa15c7..d78d477c 100644 --- a/src/sdk/main/src/TokenCreateTransaction.cc +++ b/src/sdk/main/src/TokenCreateTransaction.cc @@ -22,6 +22,7 @@ #include "impl/DurationConverter.h" #include "impl/Node.h" #include "impl/TimestampConverter.h" +#include "impl/Utilities.h" #include #include @@ -223,6 +224,22 @@ TokenCreateTransaction& TokenCreateTransaction::setPauseKey(const std::shared_pt return *this; } +//----- +TokenCreateTransaction& TokenCreateTransaction::setMetadata(const std::vector& metadata) +{ + requireNotFrozen(); + mMetadata = metadata; + return *this; +} + +//----- +TokenCreateTransaction& TokenCreateTransaction::setMetadataKey(const std::shared_ptr& key) +{ + requireNotFrozen(); + mMetadataKey = key; + return *this; +} + //----- grpc::Status TokenCreateTransaction::submitRequest(const proto::Transaction& request, const std::shared_ptr& node, @@ -339,6 +356,13 @@ void TokenCreateTransaction::initFromSourceTransactionBody() { mPauseKey = Key::fromProtobuf(body.pause_key()); } + + mMetadata = internal::Utilities::stringToByteVector(body.metadata()); + + if (body.has_metadata_key()) + { + mMetadataKey = Key::fromProtobuf(body.metadata_key()); + } } //----- @@ -409,6 +433,13 @@ proto::TokenCreateTransactionBody* TokenCreateTransaction::build() const body->set_allocated_pause_key(mPauseKey->toProtobufKey().release()); } + body->set_metadata(internal::Utilities::byteVectorToString(mMetadata)); + + if (mMetadataKey) + { + body->set_allocated_metadata_key(mMetadataKey->toProtobufKey().release()); + } + return body.release(); } diff --git a/src/sdk/main/src/TokenInfo.cc b/src/sdk/main/src/TokenInfo.cc index 83f83c24..d271a16c 100644 --- a/src/sdk/main/src/TokenInfo.cc +++ b/src/sdk/main/src/TokenInfo.cc @@ -128,6 +128,13 @@ TokenInfo TokenInfo::fromProtobuf(const proto::TokenInfo& proto) tokenInfo.mLedgerId = LedgerId(internal::Utilities::stringToByteVector(proto.ledger_id())); + tokenInfo.mMetadata = internal::Utilities::stringToByteVector(proto.metadata()); + + if (proto.has_metadata_key()) + { + tokenInfo.mMetadataKey = Key::fromProtobuf(proto.metadata_key()); + } + return tokenInfo; } @@ -203,6 +210,8 @@ std::unique_ptr TokenInfo::toProtobuf() const } protoTokenInfo->set_ledger_id(internal::Utilities::byteVectorToString(mLedgerId.toBytes())); + protoTokenInfo->set_metadata(internal::Utilities::byteVectorToString(mMetadata)); + protoTokenInfo->set_allocated_metadata_key(mMetadataKey->toProtobufKey().release()); return protoTokenInfo; } @@ -288,6 +297,17 @@ std::string TokenInfo::toString() const } json["mLedgerId"] = mLedgerId.toString(); + + if (!mMetadata.empty()) + { + json["mMetadata"] = internal::HexConverter::bytesToHex(mMetadata); + } + + if (mMetadataKey) + { + json["mMetadataKey"] = internal::HexConverter::bytesToHex(mMetadataKey->toBytes()); + } + return json.dump(); } diff --git a/src/sdk/main/src/TokenUpdateNftsTransaction.cc b/src/sdk/main/src/TokenUpdateNftsTransaction.cc new file mode 100644 index 00000000..93890e2b --- /dev/null +++ b/src/sdk/main/src/TokenUpdateNftsTransaction.cc @@ -0,0 +1,135 @@ +/*- + * + * 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 "TokenUpdateNftsTransaction.h" +#include "impl/DurationConverter.h" +#include "impl/Node.h" +#include "impl/TimestampConverter.h" +#include "impl/Utilities.h" + +#include +#include +#include +#include +#include + +namespace Hedera +{ +//----- +TokenUpdateNftsTransaction::TokenUpdateNftsTransaction(const proto::TransactionBody& transactionBody) + : Transaction(transactionBody) +{ + initFromSourceTransactionBody(); +} + +//----- +TokenUpdateNftsTransaction::TokenUpdateNftsTransaction( + const std::map>& transactions) + : Transaction(transactions) +{ + initFromSourceTransactionBody(); +} + +//----- +TokenUpdateNftsTransaction& TokenUpdateNftsTransaction::setTokenId(const TokenId& tokenId) +{ + requireNotFrozen(); + mTokenId = tokenId; + return *this; +} + +//----- +TokenUpdateNftsTransaction& TokenUpdateNftsTransaction::setSerials(const std::vector& serials) +{ + requireNotFrozen(); + mSerials = serials; + return *this; +} + +//----- +TokenUpdateNftsTransaction& TokenUpdateNftsTransaction::setMetadata(const std::vector& metadata) +{ + requireNotFrozen(); + mMetadata = metadata; + return *this; +} + +//----- +grpc::Status TokenUpdateNftsTransaction::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::kTokenUpdateNfts, request, deadline, response); +} + +//----- +void TokenUpdateNftsTransaction::validateChecksums(const Client& client) const +{ + mTokenId.validateChecksum(client); +} + +//----- +void TokenUpdateNftsTransaction::addToBody(proto::TransactionBody& body) const +{ + body.set_allocated_token_update_nfts(build()); +} + +//----- +void TokenUpdateNftsTransaction::initFromSourceTransactionBody() +{ + const proto::TransactionBody transactionBody = getSourceTransactionBody(); + + if (!transactionBody.has_token_update_nfts()) + { + throw std::invalid_argument("Transaction body doesn't contain TokenUpdateNfts data"); + } + + const proto::TokenUpdateNftsTransactionBody& body = transactionBody.token_update_nfts(); + + if (body.has_token()) + { + mTokenId = TokenId::fromProtobuf(body.token()); + } + + for (int i = 0; i < body.serial_numbers_size(); i++) + { + mSerials.push_back(static_cast(body.serial_numbers(i))); + } + + mMetadata = internal::Utilities::stringToByteVector(body.metadata().value()); +} + +//----- +proto::TokenUpdateNftsTransactionBody* TokenUpdateNftsTransaction::build() const +{ + auto body = std::make_unique(); + body->set_allocated_token(mTokenId.toProtobuf().release()); + + for (const uint64_t& serialNumber : mSerials) + { + body->add_serial_numbers(static_cast(serialNumber)); + } + + body->mutable_metadata()->set_value(internal::Utilities::byteVectorToString(mMetadata)); + + return body.release(); +} + +} // namespace Hedera \ No newline at end of file diff --git a/src/sdk/main/src/TokenUpdateTransaction.cc b/src/sdk/main/src/TokenUpdateTransaction.cc index e2bdb76a..bcc66994 100644 --- a/src/sdk/main/src/TokenUpdateTransaction.cc +++ b/src/sdk/main/src/TokenUpdateTransaction.cc @@ -21,6 +21,7 @@ #include "impl/DurationConverter.h" #include "impl/Node.h" #include "impl/TimestampConverter.h" +#include "impl/Utilities.h" #include #include @@ -166,6 +167,22 @@ TokenUpdateTransaction& TokenUpdateTransaction::setPauseKey(const std::shared_pt return *this; } +//----- +TokenUpdateTransaction& TokenUpdateTransaction::setMetadata(const std::vector& metadata) +{ + requireNotFrozen(); + mMetadata = metadata; + return *this; +} + +//----- +TokenUpdateTransaction& TokenUpdateTransaction::setMetadataKey(const std::shared_ptr& key) +{ + requireNotFrozen(); + mMetadataKey = key; + return *this; +} + //----- grpc::Status TokenUpdateTransaction::submitRequest(const proto::Transaction& request, const std::shared_ptr& node, @@ -276,6 +293,13 @@ void TokenUpdateTransaction::initFromSourceTransactionBody() { mPauseKey = Key::fromProtobuf(body.pause_key()); } + + mMetadata = internal::Utilities::stringToByteVector(body.metadata().value()); + + if (body.has_metadata_key()) + { + mMetadataKey = Key::fromProtobuf(body.metadata_key()); + } } //----- @@ -354,6 +378,16 @@ proto::TokenUpdateTransactionBody* TokenUpdateTransaction::build() const body->set_allocated_pause_key(mPauseKey->toProtobufKey().release()); } + if (!mMetadata.empty()) + { + body->mutable_metadata()->set_value(internal::Utilities::byteVectorToString(mMetadata)); + } + + if (mMetadataKey) + { + body->set_allocated_metadata_key(mMetadataKey->toProtobufKey().release()); + } + return body.release(); } diff --git a/src/sdk/main/src/Transaction.cc b/src/sdk/main/src/Transaction.cc index 400fcd80..1888173c 100644 --- a/src/sdk/main/src/Transaction.cc +++ b/src/sdk/main/src/Transaction.cc @@ -57,6 +57,7 @@ #include "TokenRevokeKycTransaction.h" #include "TokenUnfreezeTransaction.h" #include "TokenUnpauseTransaction.h" +#include "TokenUpdateNftsTransaction.h" #include "TokenUpdateTransaction.h" #include "TokenWipeTransaction.h" #include "TopicCreateTransaction.h" @@ -88,34 +89,43 @@ namespace Hedera template struct Transaction::TransactionImpl { - // The source TransactionBody protobuf object from which derived transactions should use to construct themselves. The - // Transaction base class will use this to get the Transaction-specific fields, and then pass it to the derived class - // to pick up its own data. It also acts as the "source of truth" when generating SignedTransaction and Transaction - // protobuf objects to send to the network. + // The source TransactionBody protobuf object from which derived transactions + // should use to construct themselves. The Transaction base class will use + // this to get the Transaction-specific fields, and then pass it to the + // derived class to pick up its own data. It also acts as the "source of + // truth" when generating SignedTransaction and Transaction protobuf objects + // to send to the network. proto::TransactionBody mSourceTransactionBody; - // List of completed Transaction protobuf objects ready to be sent. These are functionally identical, the only - // difference is the node to which they are sent. + // List of completed Transaction protobuf objects ready to be sent. These are + // functionally identical, the only difference is the node to which they are + // sent. std::vector mTransactions; - // List of SignedTransaction protobuf objects. The index of these SignedTransactions match up with their corresponding - // Transaction protobuf object in mTransactions. + // List of SignedTransaction protobuf objects. The index of these + // SignedTransactions match up with their corresponding Transaction protobuf + // object in mTransactions. std::vector mSignedTransactions; - // When submitting a Transaction, the index into mSignedTransactions and mTransactions must be tracked so that a - // proper TransactionResponse can be generated (which must grab the transaction hash and node account ID). + // When submitting a Transaction, the index into mSignedTransactions and + // mTransactions must be tracked so that a proper TransactionResponse can be + // generated (which must grab the transaction hash and node account ID). unsigned int mTransactionIndex = 0U; - // A list of PublicKeys with their signer functions that should sign the TransactionBody protobuf objects this - // Transaction creates. If the signer function associated with a public key is empty, that means that the private key - // associated with that public key has already contributed a signature, but the signer is not available (probably - // because this Transaction was created fromBytes(), or the signature was contributed manually via addSignature()). + // A list of PublicKeys with their signer functions that should sign the + // TransactionBody protobuf objects this Transaction creates. If the signer + // function associated with a public key is empty, that means that the private + // key associated with that public key has already contributed a signature, + // but the signer is not available (probably because this Transaction was + // created fromBytes(), or the signature was contributed manually via + // addSignature()). std::unordered_map, std::function(const std::vector&)>> mSignatories; - // Keep a map of PublicKeys to their associated PrivateKeys. If the Transaction is signed with a PrivateKey, the - // Transaction must make sure the PrivateKey does not go out of scope, otherwise it will crash when trying to generate - // a signature. + // Keep a map of PublicKeys to their associated PrivateKeys. If the + // Transaction is signed with a PrivateKey, the Transaction must make sure the + // PrivateKey does not go out of scope, otherwise it will crash when trying to + // generate a signature. std::unordered_map, std::shared_ptr> mPrivateKeys; // Is this Transaction frozen? @@ -124,12 +134,13 @@ struct Transaction::TransactionImpl // The ID of this Transaction. No value if it has not yet been set. std::optional mTransactionId; - // The maximum transaction fee willing to be paid to execute this Transaction. If not set, this Transaction will use - // the Client's set maximum transaction fee. If that's not set, mDefaultMaxTransactionFee is used. + // The maximum transaction fee willing to be paid to execute this Transaction. + // If not set, this Transaction will use the Client's set maximum transaction + // fee. If that's not set, mDefaultMaxTransactionFee is used. std::optional mMaxTransactionFee; - // The default maximum transaction fee. This can be adjusted by derived Transaction classes if those Transactions - // generally cost more. + // The default maximum transaction fee. This can be adjusted by derived + // Transaction classes if those Transactions generally cost more. Hbar mDefaultMaxTransactionFee = DEFAULT_MAX_TRANSACTION_FEE; // The length of time this Transaction will remain valid. @@ -138,9 +149,10 @@ struct Transaction::TransactionImpl // The memo to be associated with this Transaction. std::string mTransactionMemo; - // Should this Transaction regenerate its TransactionId upon a TRANSACTION_EXPIRED response from the network? If not - // set, this Transaction will use the Client's set transaction ID regeneration policy. If that's not set, the default - // behavior is captured in DEFAULT_REGENERATE_TRANSACTION_ID. + // Should this Transaction regenerate its TransactionId upon a + // TRANSACTION_EXPIRED response from the network? If not set, this Transaction + // will use the Client's set transaction ID regeneration policy. If that's not + // set, the default behavior is captured in DEFAULT_REGENERATE_TRANSACTION_ID. std::optional mTransactionIdRegenerationPolicy; }; @@ -272,6 +284,8 @@ WrappedTransaction Transaction::fromBytes(const std::vector::fromBytes(const std::vector std::vector Transaction::toBytes() const { - // If no nodes have been selected yet, the mSourceTransactionBody can be used to build a Transaction protobuf object. + // If no nodes have been selected yet, the mSourceTransactionBody can be used + // to build a Transaction protobuf object. proto::TransactionList txList; if (Executable:: getNodeAccountIds() @@ -314,13 +329,15 @@ std::vector Transaction::toBytes() const } else { - // Generate the SignedTransaction protobuf objects if the Transaction's not frozen. + // Generate the SignedTransaction protobuf objects if the Transaction's not + // frozen. if (!isFrozen()) { regenerateSignedTransactions(nullptr); } - // Build all the Transaction protobuf objects and add them to the TransactionList protobuf object. + // Build all the Transaction protobuf objects and add them to the + // TransactionList protobuf object. buildAllTransactions(); for (const auto& tx : mImpl->mTransactions) { @@ -367,29 +384,34 @@ template SdkRequestType& Transaction::addSignature(const std::shared_ptr& publicKey, const std::vector& signature) { - // A signature can only be added for Transactions being sent to exactly one node. + // A signature can only be added for Transactions being sent to exactly one + // node. requireOneNodeAccountId(); // A signature can only be added to frozen Transactions. if (!isFrozen()) { - throw IllegalStateException("Adding a signature to a Transaction requires the Transaction to be frozen"); + throw IllegalStateException("Adding a signature to a Transaction requires " + "the Transaction to be frozen"); } - // If this PublicKey has already signed this Transaction, the signature doesn't need to be added again. + // If this PublicKey has already signed this Transaction, the signature + // doesn't need to be added again. if (keyAlreadySigned(publicKey)) { return static_cast(*this); } - // Adding a signature will require all Transaction protobuf objects to be regenerated. + // Adding a signature will require all Transaction protobuf objects to be + // regenerated. mImpl->mTransactions.clear(); mImpl->mTransactions.resize(mImpl->mSignedTransactions.size()); mImpl->mSignatories.emplace(publicKey, std::function(const std::vector&)>()); mImpl->mPrivateKeys.emplace(publicKey, nullptr); - // Add the signature to the SignedTransaction protobuf object. Since there's only one node account ID, there's only - // one SignedTransaction protobuf object in the vector. + // Add the signature to the SignedTransaction protobuf object. Since there's + // only one node account ID, there's only one SignedTransaction protobuf + // object in the vector. *mImpl->mSignedTransactions.begin()->mutable_sigmap()->add_sigpair() = *publicKey->toSignaturePairProtobuf(signature); return static_cast(*this); @@ -410,7 +432,8 @@ Transaction::getSignatures() const return {}; } - // Build all the Transaction protobuf objects to generate the signatures for each key. + // Build all the Transaction protobuf objects to generate the signatures for + // each key. buildAllTransactions(); return getSignaturesInternal(); } @@ -435,13 +458,14 @@ SdkRequestType& Transaction::freezeWith(const Client* client) { if (!client) { - throw IllegalStateException( - "If no client is provided to freeze transaction, the transaction ID must be manually set."); + throw IllegalStateException("If no client is provided to freeze transaction, the transaction ID " + "must be manually set."); } if (!client->getOperatorAccountId().has_value()) { - throw UninitializedException("Client operator has not been initialized and cannot freeze transaction."); + throw UninitializedException("Client operator has not been initialized " + "and cannot freeze transaction."); } // Generate a transaction ID with the client. @@ -454,8 +478,8 @@ SdkRequestType& Transaction::freezeWith(const Client* client) { if (!client) { - throw IllegalStateException( - "If no client is provided to freeze transaction, the node account ID(s) must be manually set."); + throw IllegalStateException("If no client is provided to freeze transaction, the node account " + "ID(s) must be manually set."); } // Make sure the client has a valid network. @@ -464,7 +488,8 @@ SdkRequestType& Transaction::freezeWith(const Client* client) throw UninitializedException("Client has not been initialized with a valid network."); } - // Have the Client's network generate the node account IDs to which to send this Transaction. + // Have the Client's network generate the node account IDs to which to send + // this Transaction. Executable::setNodeAccountIds( client->getClientNetwork()->getNodeAccountIdsForExecute()); } @@ -485,7 +510,8 @@ ScheduleCreateTransaction Transaction::schedule() const getNodeAccountIds() .empty()) { - throw IllegalStateException("Underlying transaction for a scheduled transaction cannot have node account IDs set."); + throw IllegalStateException("Underlying transaction for a scheduled transaction cannot have node " + "account IDs set."); } updateSourceTransactionBody(nullptr); @@ -606,7 +632,8 @@ TransactionId Transaction::getTransactionId() const { if (!mImpl->mTransactionId.has_value()) { - throw UninitializedException("No transaction ID generated yet. Try freezing the transaction or manually setting " + throw UninitializedException("No transaction ID generated yet. Try " + "freezing the transaction or manually setting " "the transaction ID."); } @@ -745,18 +772,22 @@ Transaction::Transaction( return; } - // Set the TransactionId of this Transaction. Transactions only care about the first TransactionId in the map, so grab - // the first TransactionId and set it as this Transaction's TransactionId if it's not a dummy TransactionId. If - // it's a dummy TransactionId, the Transaction should remain incomplete. The other TransactionIds will be looked at by - // the ChunkedTransaction constructor if this Transaction is a ChunkedTransaction. If this Transaction is not a - // ChunkedTransaction or if it's an incomplete Transaction (i.e. has a dummy account ID in its transaction ID), - // there should be only one transaction ID anyway. + // Set the TransactionId of this Transaction. Transactions only care about the + // first TransactionId in the map, so grab the first TransactionId and set it + // as this Transaction's TransactionId if it's not a dummy TransactionId. If + // it's a dummy TransactionId, the Transaction should remain incomplete. The + // other TransactionIds will be looked at by the ChunkedTransaction + // constructor if this Transaction is a ChunkedTransaction. If this + // Transaction is not a ChunkedTransaction or if it's an incomplete + // Transaction (i.e. has a dummy account ID in its transaction ID), there + // should be only one transaction ID anyway. if (!(transactions.cbegin()->first == DUMMY_TRANSACTION_ID)) { mImpl->mTransactionId = transactions.cbegin()->first; } - // If the first account ID is a dummy account ID, then only the source TransactionBody needs to be copied. + // If the first account ID is a dummy account ID, then only the source + // TransactionBody needs to be copied. const std::map& transactionMap = transactions.cbegin()->second; if (!transactionMap.empty() && transactionMap.cbegin()->first == DUMMY_ACCOUNT_ID) { @@ -771,11 +802,13 @@ Transaction::Transaction( else { - // The node account IDs get added as a batch so just add them to a separate vector for now. + // The node account IDs get added as a batch so just add them to a separate + // vector for now. std::vector nodeAccountIds; - // A standard Transaction can only hold information for one Transaction. If this Transaction is a - // ChunkedTransaction, the additional Transaction protobuf objects will be processed there. + // A standard Transaction can only hold information for one Transaction. If + // this Transaction is a ChunkedTransaction, the additional Transaction + // protobuf objects will be processed there. nodeAccountIds.reserve(transactions.cbegin()->second.size()); bool gotSignatures = false; @@ -801,7 +834,8 @@ Transaction::Transaction( mImpl->mPrivateKeys.emplace(publicKey, nullptr); } - // The presence of signatures implies the Transaction should be frozen. + // The presence of signatures implies the Transaction should be + // frozen. mImpl->mIsFrozen = true; } @@ -809,13 +843,15 @@ Transaction::Transaction( } } - // Set the source TransactionBody based on the generated SignedTransaction protobuf objects. + // Set the source TransactionBody based on the generated SignedTransaction + // protobuf objects. proto::TransactionBody txBody; txBody.ParseFromArray(mImpl->mSignedTransactions.cbegin()->bodybytes().data(), static_cast(mImpl->mSignedTransactions.cbegin()->bodybytes().size())); mImpl->mSourceTransactionBody = txBody; - // Now that all node account IDs have been seen, they can all be added at once. + // Now that all node account IDs have been seen, they can all be added at + // once. Executable::setNodeAccountIds( nodeAccountIds); } @@ -849,7 +885,8 @@ proto::Transaction Transaction::makeRequest(unsigned int index) template void Transaction::buildAllTransactions() const { - // Go through each SignedTransaction protobuf object and add all signatures to its SignatureMap protobuf object. + // Go through each SignedTransaction protobuf object and add all signatures to + // its SignatureMap protobuf object. for (unsigned int i = 0; i < mImpl->mSignedTransactions.size(); ++i) { buildTransaction(i); @@ -866,8 +903,8 @@ void Transaction::regenerateSignedTransactions(const Client* cli // Clear out any stale SignedTransaction and/or Transaction protobuf objects. clearTransactions(); - // Add a SignedTransaction protobuf object for each node account ID based off of this Transaction's - // mSourceTransactionBody. + // Add a SignedTransaction protobuf object for each node account ID based off + // of this Transaction's mSourceTransactionBody. addSignedTransactionForEachNode(mImpl->mSourceTransactionBody); } @@ -907,15 +944,18 @@ void Transaction::updateSourceTransactionBody(const Client* clie template void Transaction::addTransaction(const proto::Transaction& transaction) const { - // Add the Transaction protobuf object to the Transaction protobuf object list. + // Add the Transaction protobuf object to the Transaction protobuf object + // list. mImpl->mTransactions.push_back(transaction); - // Parse the Transaction protobuf object into a SignedTransaction protobuf object. + // Parse the Transaction protobuf object into a SignedTransaction protobuf + // object. proto::SignedTransaction signedTx; signedTx.ParseFromArray(transaction.signedtransactionbytes().data(), static_cast(transaction.signedtransactionbytes().size())); - // Add the SignedTransaction protobuf object to the SignedTransaction protobuf object list. + // Add the SignedTransaction protobuf object to the SignedTransaction protobuf + // object list. mImpl->mSignedTransactions.push_back(signedTx); } @@ -983,7 +1023,8 @@ template std::map, std::vector>> Transaction::getSignaturesInternal(size_t offset) const { - // Get each node account ID that the Transaction protobuf objects will be sent. + // Get each node account ID that the Transaction protobuf objects will be + // sent. const std::vector nodeAccountIds = Executable:: getNodeAccountIds(); @@ -1026,12 +1067,14 @@ proto::Transaction Transaction::getTransactionProtobufObject(uns template proto::TransactionBody Transaction::getSourceTransactionBody() const { - // mSourceTransactionBody should not be updated in this call because updateSourceTransactionBody() makes a - // virtual call to addBody(), which will produce undefined behavior in the construction of derived Transactions. In - // the constructors of derived Transactions, mSourceTransactionBody already contains all the correct data and doesn't - // need an update. If this function is called anywhere else, a call to updateSourceTransactionBody() should be made - // before calling this to make sure any and all recent changes to this Transaction are grabbed and used to update - // mSourceTransactionBody. + // mSourceTransactionBody should not be updated in this call because + // updateSourceTransactionBody() makes a virtual call to addBody(), which will + // produce undefined behavior in the construction of derived Transactions. In + // the constructors of derived Transactions, mSourceTransactionBody already + // contains all the correct data and doesn't need an update. If this function + // is called anywhere else, a call to updateSourceTransactionBody() should be + // made before calling this to make sure any and all recent changes to this + // Transaction are grabbed and used to update mSourceTransactionBody. return mImpl->mSourceTransactionBody; } @@ -1087,8 +1130,8 @@ typename ExecutablemTransactionIdRegenerationPolicy.value(); } - // Follow the Client's policy if this Transaction's policy hasn't been explicitly set and the Client's policy has - // been. + // Follow the Client's policy if this Transaction's policy hasn't been + // explicitly set and the Client's policy has been. else if (client.getTransactionIdRegenerationPolicy().has_value()) { shouldRegenerate = client.getTransactionIdRegenerationPolicy().value(); @@ -1096,8 +1139,8 @@ typename ExecutablemTransactionId = TransactionId::generate(mImpl->mTransactionId->mAccountId); // Regenerate the SignedTransaction protobuf objects. @@ -1108,7 +1151,8 @@ typename Executable:: ExecutionStatus::REQUEST_ERROR; } @@ -1128,7 +1172,8 @@ void Transaction::onExecute(const Client& client) validateChecksums(client); } - // Sign with the operator if the operator's present, and if it's paying for the Transaction. + // Sign with the operator if the operator's present, and if it's paying for + // the Transaction. if (client.getOperatorAccountId().has_value() && client.getOperatorAccountId().value() == mImpl->mTransactionId->mAccountId) { @@ -1140,19 +1185,22 @@ void Transaction::onExecute(const Client& client) template void Transaction::buildTransaction(unsigned int index) const { - // If the Transaction protobuf object is already built for this index, there's no need to do anything else. + // If the Transaction protobuf object is already built for this index, there's + // no need to do anything else. if (!getTransactionProtobufObject(index).signedtransactionbytes().empty()) { return; } - // For each PublicKey and signer function, generate a signature of the TransactionBody protobuf object bytes held in - // the SignedTransaction protobuf object at the provided index. + // For each PublicKey and signer function, generate a signature of the + // TransactionBody protobuf object bytes held in the SignedTransaction + // protobuf object at the provided index. proto::SignedTransaction& signedTransaction = mImpl->mSignedTransactions[index]; for (const auto& [publicKey, signer] : mImpl->mSignatories) { - // If there is no signer function, the signature has already been generated for the SignedTransaction (either - // added manually with addSignature() or this Transaction came from fromBytes()). + // If there is no signer function, the signature has already been generated + // for the SignedTransaction (either added manually with addSignature() or + // this Transaction came from fromBytes()). if (signer) { *signedTransaction.mutable_sigmap()->add_sigpair() = *publicKey->toSignaturePairProtobuf( @@ -1198,7 +1246,8 @@ SdkRequestType& Transaction::signInternal( if (!keyAlreadySigned(publicKey)) { - // Adding a signature will require all Transaction protobuf objects to be regenerated. + // Adding a signature will require all Transaction protobuf objects to be + // regenerated. mImpl->mTransactions.clear(); mImpl->mTransactions.resize(mImpl->mSignedTransactions.size()); mImpl->mSignatories.emplace(publicKey, signer); @@ -1245,6 +1294,7 @@ template class Transaction; 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 a1a91a7f..63b127b8 100644 --- a/src/sdk/main/src/WrappedTransaction.cc +++ b/src/sdk/main/src/WrappedTransaction.cc @@ -170,6 +170,10 @@ WrappedTransaction WrappedTransaction::fromProtobuf(const proto::TransactionBody { return WrappedTransaction(TokenUnpauseTransaction(proto)); } + else if (proto.has_token_update_nfts()) + { + return WrappedTransaction(TokenUpdateNftsTransaction(proto)); + } else if (proto.has_tokenwipe()) { return WrappedTransaction(TokenWipeTransaction(proto)); @@ -362,6 +366,11 @@ WrappedTransaction WrappedTransaction::fromProtobuf(const proto::SchedulableTran *txBody.mutable_token_unpause() = proto.token_unpause(); return WrappedTransaction(TokenUnpauseTransaction(txBody)); } + else if (proto.has_token_update_nfts()) + { + *txBody.mutable_token_update_nfts() = proto.token_update_nfts(); + return WrappedTransaction(TokenUpdateNftsTransaction(txBody)); + } else if (proto.has_tokenwipe()) { *txBody.mutable_tokenwipe() = proto.tokenwipe(); @@ -613,6 +622,12 @@ std::unique_ptr WrappedTransaction::toProtobuf() const transaction->updateSourceTransactionBody(nullptr); return std::make_unique(transaction->getSourceTransactionBody()); } + case TOKEN_UPDATE_NFTS_TRANSACTION: + { + const auto transaction = getTransaction(); + transaction->updateSourceTransactionBody(nullptr); + return std::make_unique(transaction->getSourceTransactionBody()); + } case TOKEN_WIPE_TRANSACTION: { const auto transaction = getTransaction(); diff --git a/src/sdk/main/src/impl/Node.cc b/src/sdk/main/src/impl/Node.cc index 46ad194e..1ea92f5a 100644 --- a/src/sdk/main/src/impl/Node.cc +++ b/src/sdk/main/src/impl/Node.cc @@ -187,6 +187,8 @@ grpc::Status Node::submitTransaction(proto::TransactionBody::DataCase funcEnum, return mTokenStub->unpauseToken(&context, transaction, response); case proto::TransactionBody::DataCase::kTokenUpdate: return mTokenStub->updateToken(&context, transaction, response); + case proto::TransactionBody::DataCase::kTokenUpdateNfts: + return mTokenStub->updateToken(&context, transaction, response); case proto::TransactionBody::DataCase::kTokenWipe: return mTokenStub->wipeTokenAccount(&context, transaction, response); case proto::TransactionBody::DataCase::kUtilPrng: diff --git a/src/sdk/tests/integration/CMakeLists.txt b/src/sdk/tests/integration/CMakeLists.txt index c7429591..749c7680 100644 --- a/src/sdk/tests/integration/CMakeLists.txt +++ b/src/sdk/tests/integration/CMakeLists.txt @@ -57,6 +57,7 @@ add_executable(${TEST_PROJECT_NAME} TokenRevokeKycTransactionIntegrationTests.cc TokenUnfreezeTransactionIntegrationTests.cc TokenUnpauseTransactionIntegrationTests.cc + TokenUpdateNftsTransactionIntegrationTests.cc TokenUpdateTransactionIntegrationTests.cc TokenWipeTransactionIntegrationTests.cc TopicCreateTransactionIntegrationTests.cc diff --git a/src/sdk/tests/integration/TokenCreateTransactionIntegrationTests.cc b/src/sdk/tests/integration/TokenCreateTransactionIntegrationTests.cc index f5707afc..a19abf5f 100644 --- a/src/sdk/tests/integration/TokenCreateTransactionIntegrationTests.cc +++ b/src/sdk/tests/integration/TokenCreateTransactionIntegrationTests.cc @@ -68,6 +68,8 @@ TEST_F(TokenCreateTransactionIntegrationTests, ExecuteTokenCreateTransaction) .setKycKey(operatorKey) .setSupplyKey(operatorKey) .setFeeScheduleKey(operatorKey) + .setPauseKey(operatorKey) + .setMetadataKey(operatorKey) .execute(getTestClient()) .getReceipt(getTestClient())); diff --git a/src/sdk/tests/integration/TokenUpdateNftsTransactionIntegrationTests.cc b/src/sdk/tests/integration/TokenUpdateNftsTransactionIntegrationTests.cc new file mode 100644 index 00000000..0364b132 --- /dev/null +++ b/src/sdk/tests/integration/TokenUpdateNftsTransactionIntegrationTests.cc @@ -0,0 +1,214 @@ +/*- + * + * 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 "BaseIntegrationTest.h" +#include "ED25519PrivateKey.h" +#include "NftId.h" +#include "PrivateKey.h" +#include "TokenCreateTransaction.h" +#include "TokenId.h" +#include "TokenMintTransaction.h" +#include "TokenNftInfo.h" +#include "TokenNftInfoQuery.h" +#include "TokenUpdateNftsTransaction.h" +#include "TransactionReceipt.h" +#include "TransactionResponse.h" +#include "exceptions/ReceiptStatusException.h" + +#include + +using namespace Hedera; + +/** + * @notice Integration-HIP-657 + * @url https://hips.hedera.com/hip/hip-657 + */ +class TokenUpdateNftsTransactionIntegrationTests : public BaseIntegrationTest +{ +protected: + [[nodiscard]] inline const std::vector getTestMetadata() const { return mMetadata; } + [[nodiscard]] inline const std::vector> generateTestMetadataRecords( + std::vector metadata, + int count) const + { + return std::vector>(count, metadata); + } + +private: + const std::vector mMetadata = { std::byte(0xAA), std::byte(0xAB), std::byte(0xAC), std::byte(0xAD) }; +}; + +//----- +TEST_F(TokenUpdateNftsTransactionIntegrationTests, UpdateNFTMetadata) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = std::shared_ptr( + ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137") + .release())); + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + const int nftCount = 4; + const std::vector> initialMetadataRecords = + generateTestMetadataRecords(getTestMetadata(), nftCount); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + const std::vector> updatedMetadataRecords = + generateTestMetadataRecords(updatedMetadataRecord, nftCount / 2); + const std::vector> notUpdatedMetadataRecords = + generateTestMetadataRecords(getTestMetadata(), nftCount / 2); + + // create a token with metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(AccountId(2ULL)) + .setAdminKey(operatorKey) + .setSupplyKey(operatorKey) + .setMetadataKey(metadataKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // mint tokens + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId) + .setMetadata(initialMetadataRecords) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // check that metadata was set correctly + std::vector serials = txReceipt.mSerialNumbers; + + std::vector> metadataRecordsAfterMint; + + for (uint64_t serial : serials) + { + NftId nftId(tokenId, serial); + TokenNftInfo nftInfo; + ASSERT_NO_THROW(nftInfo = TokenNftInfoQuery().setNftId(nftId).execute(getTestClient());); + metadataRecordsAfterMint.push_back(nftInfo.mMetadata); + } + + EXPECT_EQ(initialMetadataRecords, metadataRecordsAfterMint); + + // When + + // update metadata of the first two minted NFTs + int middleIndex = serials.size() / 2; + std::vector serialsToUpdate(serials.begin(), serials.begin() + middleIndex); + + ASSERT_NO_THROW(txReceipt = TokenUpdateNftsTransaction() + .setTokenId(tokenId) + .setSerials(serialsToUpdate) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .sign(metadataKey) + .execute(getTestClient()) + .getReceipt(getTestClient());); + + // Then + + // check updated NFTs' metadata + std::vector> metadataRecordsAfterUpdate; + + for (uint64_t serial : serialsToUpdate) + { + NftId nftId(tokenId, serial); + TokenNftInfo nftInfo; + ASSERT_NO_THROW(nftInfo = TokenNftInfoQuery().setNftId(nftId).execute(getTestClient());); + metadataRecordsAfterUpdate.push_back(nftInfo.mMetadata); + } + + EXPECT_EQ(updatedMetadataRecords, metadataRecordsAfterUpdate); + + // check that remaining NFTs were not updated + std::vector notUpdatedSerials(serials.begin() + middleIndex, serials.end()); + + metadataRecordsAfterUpdate.clear(); + for (uint64_t serial : notUpdatedSerials) + { + NftId nftId(tokenId, serial); + TokenNftInfo nftInfo; + ASSERT_NO_THROW(nftInfo = TokenNftInfoQuery().setNftId(nftId).execute(getTestClient());); + metadataRecordsAfterUpdate.push_back(nftInfo.mMetadata); + } + + EXPECT_EQ(notUpdatedMetadataRecords, metadataRecordsAfterUpdate); +} + +//----- +TEST_F(TokenUpdateNftsTransactionIntegrationTests, CannotUpdateNFTMetadataWhenNotSignedWithMetadataKey) +{ + // Given + std::shared_ptr operatorKey; + ASSERT_NO_THROW( + operatorKey = std::shared_ptr( + ED25519PrivateKey::fromString( + "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137") + .release())); + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + const int nftCount = 4; + const std::vector> initialMetadataRecords = + generateTestMetadataRecords(getTestMetadata(), nftCount); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a token with metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(AccountId(2ULL)) + .setAdminKey(operatorKey) + .setSupplyKey(operatorKey) + .setMetadataKey(metadataKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // mint tokens + TransactionReceipt txReceipt; + ASSERT_NO_THROW(txReceipt = TokenMintTransaction() + .setTokenId(tokenId) + .setMetadata(initialMetadataRecords) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + std::vector serials = txReceipt.mSerialNumbers; + + // When / Then + ASSERT_THROW(txReceipt = TokenUpdateNftsTransaction() + .setTokenId(tokenId) + .setSerials(serials) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // INVALID_SIGNATURE +} diff --git a/src/sdk/tests/integration/TokenUpdateTransactionIntegrationTests.cc b/src/sdk/tests/integration/TokenUpdateTransactionIntegrationTests.cc index cc1dda44..39d51990 100644 --- a/src/sdk/tests/integration/TokenUpdateTransactionIntegrationTests.cc +++ b/src/sdk/tests/integration/TokenUpdateTransactionIntegrationTests.cc @@ -26,6 +26,8 @@ #include "PrivateKey.h" #include "TokenCreateTransaction.h" #include "TokenDeleteTransaction.h" +#include "TokenInfo.h" +#include "TokenInfoQuery.h" #include "TokenUpdateTransaction.h" #include "TransactionReceipt.h" #include "TransactionResponse.h" @@ -39,6 +41,11 @@ using namespace Hedera; class TokenUpdateTransactionIntegrationTests : public BaseIntegrationTest { +protected: + [[nodiscard]] inline const std::vector& getTestMetadata() const { return mMetadata; } + +private: + const std::vector mMetadata = { std::byte(0xAA), std::byte(0xAB), std::byte(0xAC), std::byte(0xAD) }; }; //----- @@ -49,6 +56,8 @@ TEST_F(TokenUpdateTransactionIntegrationTests, ExecuteTokenUpdateTransaction) ASSERT_NO_THROW( operatorKey = ED25519PrivateKey::fromString( "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + const std::string updatedTokenName = "Token"; + const std::string updatedTokenSymbol = "T"; TokenId tokenId; EXPECT_NO_THROW(tokenId = TokenCreateTransaction() @@ -71,13 +80,17 @@ TEST_F(TokenUpdateTransactionIntegrationTests, ExecuteTokenUpdateTransaction) TransactionReceipt txReceipt; EXPECT_NO_THROW(txReceipt = TokenUpdateTransaction() .setTokenId(tokenId) - .setTokenName("aaaa") - .setTokenSymbol("A") + .setTokenName(updatedTokenName) + .setTokenSymbol(updatedTokenSymbol) .execute(getTestClient()) .getReceipt(getTestClient())); // Then - // TODO: TokenInfoQuery + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + + EXPECT_EQ(tokenInfo.mTokenName, updatedTokenName); + EXPECT_EQ(tokenInfo.mTokenSymbol, updatedTokenSymbol); // Clean up ASSERT_NO_THROW(txReceipt = @@ -92,6 +105,8 @@ TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateImmutableKey) ASSERT_NO_THROW( operatorKey = ED25519PrivateKey::fromString( "302e020100300506032b65700422042091132178e72057a1d7528025956fe39b0b847f200ab59b2fdd367017f3087137")); + const std::string updatedTokenName = "Token"; + const std::string updatedTokenSymbol = "T"; TokenId tokenId; EXPECT_NO_THROW(tokenId = TokenCreateTransaction() @@ -105,9 +120,344 @@ TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateImmutableKey) // When / Then EXPECT_THROW(const TransactionReceipt txReceipt = TokenUpdateTransaction() .setTokenId(tokenId) - .setTokenName("aaaa") - .setTokenSymbol("A") + .setTokenName(updatedTokenName) + .setTokenSymbol(updatedTokenSymbol) .execute(getTestClient()) .getReceipt(getTestClient()), ReceiptStatusException); // TOKEN_IS_IMMUTABLE } + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, CanUpdateFungibleTokenMetadata) +{ + // Given + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a fungible token with metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::FUNGIBLE_COMMON) + .setDecimals(3) + .setInitialSupply(1000000) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(getTestClient().getOperatorPublicKey()) + .setMetadataKey(metadataKey) + .setFreezeDefault(false) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if token created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); + EXPECT_EQ(tokenInfo.mMetadataKey->toBytes(), metadataKey->getPublicKey()->toBytes()); + + // When + // update token's metadata + ASSERT_NO_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .sign(metadataKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, updatedMetadataRecord); +} + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, CanUpdateNonFungibleTokenMetadata) +{ + // Given + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a NFT with metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(getTestClient().getOperatorPublicKey()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .setMetadataKey(metadataKey) + .setFreezeDefault(false) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if NFT created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); + EXPECT_EQ(tokenInfo.mMetadataKey->toBytes(), metadataKey->getPublicKey()->toBytes()); + + // When + // update token's metadata + ASSERT_NO_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .sign(metadataKey) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, updatedMetadataRecord); +} + +//----- +// This test verifies that metadata is not updated silently. This test is kept for regression +// because previously the metadata got updated silently +TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateFungibleTokenMetadataKeyNotSet) +{ + // Given + // create a NFT without metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::FUNGIBLE_COMMON) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(getTestClient().getOperatorPublicKey()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .setFreezeDefault(false) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if FT created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); + EXPECT_EQ(tokenInfo.mMetadataKey, nullptr); + + // When + // update token's metadata + ASSERT_NO_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .freezeWith(&getTestClient()) + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); +} + +//----- +// This test verifies that metadata is not updated silently. This test is kept for regression +// because previously the metadata got updated silently +TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateNonFungibleTokenMetadataKeyNotSet) +{ + // Given + // create a NFT without metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(getTestClient().getOperatorPublicKey()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .setFreezeDefault(false) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if NFT created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); + EXPECT_EQ(tokenInfo.mMetadataKey, nullptr); + + // When + // update token's metadata + ASSERT_NO_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setTokenMemo("abc") + .execute(getTestClient()) + .getReceipt(getTestClient())); + + // Then + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); +} + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, + CannotUpdateFungibleTokenMetadataWhenTransactionNotSignedWithMetadataOrAdminKey) +{ + // Given + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + std::shared_ptr adminKey; + ASSERT_NO_THROW(adminKey = ED25519PrivateKey::generatePrivateKey()); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a FT with metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::FUNGIBLE_COMMON) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(adminKey) + .setMetadataKey(metadataKey) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .freezeWith(&getTestClient()) + .sign(adminKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if FT created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + + // When / Then + // update token's metadata + EXPECT_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // INVALID_SIGNATURE +} + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, + CannotUpdateNonFungibleTokenMetadataWhenTransactionNotSignedWithMetadataOrAdminKey) +{ + // Given + std::shared_ptr metadataKey; + ASSERT_NO_THROW(metadataKey = ED25519PrivateKey::generatePrivateKey()); + std::shared_ptr adminKey; + ASSERT_NO_THROW(adminKey = ED25519PrivateKey::generatePrivateKey()); + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a NFT with metadata and metadata key + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setAdminKey(adminKey) + .setMetadataKey(metadataKey) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .freezeWith(&getTestClient()) + .sign(adminKey) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if NFT created successfully with metadata and metadata key + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + + // When / Then + // update token's metadata + EXPECT_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .freezeWith(&getTestClient()) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // INVALID_SIGNATURE +} + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateNonFungibleTokenMetadataWhenMetadataKeyNotSet) +{ + // Given + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a NFT with metadata + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::NON_FUNGIBLE_UNIQUE) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if NFT created successfully with metadata + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + + // When / Then + // update token's metadata + EXPECT_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // TOKEN_IS_IMMUTABLE +} + +//----- +TEST_F(TokenUpdateTransactionIntegrationTests, CannotUpdateFungibleTokenMetadataWhenMetadataKeyNotSet) +{ + // Given + const std::vector updatedMetadataRecord = { + std::byte(0xBA), std::byte(0xBB), std::byte(0xBC), std::byte(0xBD) + }; + + // create a NFT with metadata + TokenId tokenId; + ASSERT_NO_THROW(tokenId = TokenCreateTransaction() + .setTokenName("ffff") + .setTokenSymbol("F") + .setMetadata(getTestMetadata()) + .setTokenType(TokenType::FUNGIBLE_COMMON) + .setTreasuryAccountId(getTestClient().getOperatorAccountId().value()) + .setSupplyKey(getTestClient().getOperatorPublicKey()) + .execute(getTestClient()) + .getReceipt(getTestClient()) + .mTokenId.value()); + + // check if NFT created successfully with metadata + TokenInfo tokenInfo; + ASSERT_NO_THROW(tokenInfo = TokenInfoQuery().setTokenId(tokenId).execute(getTestClient())); + + // When / Then + // update token's metadata + EXPECT_THROW(TokenUpdateTransaction() + .setTokenId(tokenId) + .setMetadata(updatedMetadataRecord) + .execute(getTestClient()) + .getReceipt(getTestClient()), + ReceiptStatusException); // TOKEN_IS_IMMUTABLE +} \ No newline at end of file diff --git a/src/sdk/tests/unit/TokenInfoUnitTests.cc b/src/sdk/tests/unit/TokenInfoUnitTests.cc index 1db837f0..891d0430 100644 --- a/src/sdk/tests/unit/TokenInfoUnitTests.cc +++ b/src/sdk/tests/unit/TokenInfoUnitTests.cc @@ -72,6 +72,8 @@ class TokenInfoUnitTests : public ::testing::Test [[nodiscard]] inline const std::shared_ptr& getTestPauseKey() const { return mTestPauseKey; } [[nodiscard]] inline const std::optional& getTestPauseStatus() const { return mTestPauseStatus; } [[nodiscard]] inline const LedgerId& getTestLedgerId() const { return mTestLedgerId; } + [[nodiscard]] inline const std::vector getTestMetadata() const { return mTestMetadata; } + [[nodiscard]] inline const std::shared_ptr& getTestMetadataKey() const { return mTestMetadataKey; } private: const TokenId mTestTokenId = TokenId(1ULL, 2ULL, 3ULL); @@ -102,6 +104,8 @@ class TokenInfoUnitTests : public ::testing::Test const std::shared_ptr mTestPauseKey = ECDSAsecp256k1PrivateKey::generatePrivateKey()->getPublicKey(); const std::optional mTestPauseStatus = true; const LedgerId mTestLedgerId = LedgerId({ std::byte(0x0E), std::byte(0x0F) }); + const std::vector mTestMetadata = { std::byte(0x0E), std::byte(0x0F) }; + const std::shared_ptr mTestMetadataKey = ECDSAsecp256k1PrivateKey::generatePrivateKey()->getPublicKey(); }; //----- @@ -169,6 +173,8 @@ TEST_F(TokenInfoUnitTests, FromProtobuf) } protoTokenInfo.set_ledger_id(internal::Utilities::byteVectorToString(getTestLedgerId().toBytes())); + protoTokenInfo.set_metadata(internal::Utilities::byteVectorToString(getTestMetadata())); + protoTokenInfo.set_allocated_metadata_key(getTestMetadataKey()->toProtobufKey().release()); // When const TokenInfo tokenInfo = TokenInfo::fromProtobuf(protoTokenInfo); @@ -200,6 +206,8 @@ TEST_F(TokenInfoUnitTests, FromProtobuf) EXPECT_EQ(tokenInfo.mPauseKey->toBytes(), getTestPauseKey()->toBytes()); EXPECT_EQ(tokenInfo.mPauseStatus, getTestPauseStatus()); EXPECT_EQ(tokenInfo.mLedgerId.toBytes(), getTestLedgerId().toBytes()); + EXPECT_EQ(tokenInfo.mMetadata, getTestMetadata()); + EXPECT_EQ(tokenInfo.mMetadataKey->toBytes(), getTestMetadataKey()->toBytes()); } //----- @@ -267,6 +275,8 @@ TEST_F(TokenInfoUnitTests, FromBytes) } protoTokenInfo.set_ledger_id(internal::Utilities::byteVectorToString(getTestLedgerId().toBytes())); + protoTokenInfo.set_metadata(internal::Utilities::byteVectorToString(getTestMetadata())); + protoTokenInfo.set_allocated_metadata_key(getTestMetadataKey()->toProtobufKey().release()); // When const TokenInfo tokenInfo = @@ -332,6 +342,8 @@ TEST_F(TokenInfoUnitTests, ToProtobuf) tokenInfo.mPauseKey = getTestPauseKey(); tokenInfo.mPauseStatus = getTestPauseStatus(); tokenInfo.mLedgerId = getTestLedgerId(); + tokenInfo.mMetadata = getTestMetadata(); + tokenInfo.mMetadataKey = getTestMetadataKey(); // When const std::unique_ptr protoTokenInfo = tokenInfo.toProtobuf(); @@ -389,6 +401,9 @@ TEST_F(TokenInfoUnitTests, ToProtobuf) ? proto::TokenPauseStatus::PauseNotApplicable : (getTestPauseStatus().value() ? proto::TokenPauseStatus::Paused : proto::TokenPauseStatus::Unpaused))); EXPECT_EQ(protoTokenInfo->ledger_id(), internal::Utilities::byteVectorToString(getTestLedgerId().toBytes())); + EXPECT_EQ(protoTokenInfo->metadata(), internal::Utilities::byteVectorToString(getTestMetadata())); + EXPECT_EQ(protoTokenInfo->metadata_key().ecdsa_secp256k1(), + internal::Utilities::byteVectorToString(getTestMetadataKey()->toBytesRaw())); } //----- @@ -422,6 +437,8 @@ TEST_F(TokenInfoUnitTests, ToBytes) tokenInfo.mPauseKey = getTestPauseKey(); tokenInfo.mPauseStatus = getTestPauseStatus(); tokenInfo.mLedgerId = getTestLedgerId(); + tokenInfo.mMetadata = getTestMetadata(); + tokenInfo.mMetadataKey = getTestMetadataKey(); // When const std::vector bytes = tokenInfo.toBytes();