diff --git a/sdk/examples/CMakeLists.txt b/sdk/examples/CMakeLists.txt index 5766b7f25..9196126f6 100644 --- a/sdk/examples/CMakeLists.txt +++ b/sdk/examples/CMakeLists.txt @@ -6,6 +6,7 @@ set(AUTO_CREATE_ACCOUNT_TRANSFER_TRANSACTION_EXAMPLE_NAME ${PROJECT_NAME}-auto-c set(CONSENSUS_PUB_SUB_EXAMPLE_NAME ${PROJECT_NAME}-consensus-pub-sub-example) set(CONSENSUS_PUB_SUB_CHUNKED_EXAMPLE_NAME ${PROJECT_NAME}-consensus-pub-sub-chunked-example) set(CONSENSUS_PUB_SUB_WITH_SUBMIT_KEY_EXAMPLE_NAME ${PROJECT_NAME}-consensus-pub-sub-with-submit-key-example) +set(CONSTRUCT_CLIENT_EXAMPLE_NAME ${PROJECT_NAME}-construct-client-example) set(CREATE_ACCOUNT_EXAMPLE_NAME ${PROJECT_NAME}-create-account-example) set(CREATE_SIMPLE_CONTRACT_EXAMPLE_NAME ${PROJECT_NAME}-create-simple-contract-example) set(CREATE_STATEFUL_CONTRACT_EXAMPLE_NAME ${PROJECT_NAME}-create-stateful-contract-example) @@ -39,6 +40,7 @@ add_executable(${AUTO_CREATE_ACCOUNT_TRANSFER_TRANSACTION_EXAMPLE_NAME} AutoCrea add_executable(${CONSENSUS_PUB_SUB_EXAMPLE_NAME} ConsensusPubSubExample.cc) add_executable(${CONSENSUS_PUB_SUB_CHUNKED_EXAMPLE_NAME} ConsensusPubSubChunkedExample.cc) add_executable(${CONSENSUS_PUB_SUB_WITH_SUBMIT_KEY_EXAMPLE_NAME} ConsensusPubSubWithSubmitKeyExample.cc) +add_executable(${CONSTRUCT_CLIENT_EXAMPLE_NAME} ConstructClientExample.cc) add_executable(${CREATE_ACCOUNT_EXAMPLE_NAME} CreateAccountExample.cc) add_executable(${CREATE_SIMPLE_CONTRACT_EXAMPLE_NAME} CreateSimpleContractExample.cc) add_executable(${CREATE_STATEFUL_CONTRACT_EXAMPLE_NAME} CreateStatefulContractExample.cc) @@ -73,6 +75,8 @@ file(COPY ${PROJECT_SOURCE_DIR}/addressbook/testnet.pb file(COPY ${PROJECT_SOURCE_DIR}/config/hello_world.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}) +file(COPY ${PROJECT_SOURCE_DIR}/config/local_node.json + DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}) file(COPY ${PROJECT_SOURCE_DIR}/config/stateful.json DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_BUILD_TYPE}) @@ -90,6 +94,7 @@ target_link_libraries(${AUTO_CREATE_ACCOUNT_TRANSFER_TRANSACTION_EXAMPLE_NAME} P target_link_libraries(${CONSENSUS_PUB_SUB_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${CONSENSUS_PUB_SUB_CHUNKED_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${CONSENSUS_PUB_SUB_WITH_SUBMIT_KEY_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) +target_link_libraries(${CONSTRUCT_CLIENT_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${CREATE_ACCOUNT_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${CREATE_SIMPLE_CONTRACT_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) target_link_libraries(${CREATE_STATEFUL_CONTRACT_EXAMPLE_NAME} PUBLIC ${PROJECT_NAME}) @@ -126,6 +131,7 @@ install(TARGETS ${CONSENSUS_PUB_SUB_EXAMPLE_NAME} ${CONSENSUS_PUB_SUB_CHUNKED_EXAMPLE_NAME} ${CONSENSUS_PUB_SUB_WITH_SUBMIT_KEY_EXAMPLE_NAME} + ${CONSTRUCT_CLIENT_EXAMPLE_NAME} ${CREATE_ACCOUNT_EXAMPLE_NAME} ${CREATE_SIMPLE_CONTRACT_EXAMPLE_NAME} ${CREATE_STATEFUL_CONTRACT_EXAMPLE_NAME} diff --git a/sdk/examples/ConstructClientExample.cc b/sdk/examples/ConstructClientExample.cc new file mode 100644 index 000000000..2d1ce8109 --- /dev/null +++ b/sdk/examples/ConstructClientExample.cc @@ -0,0 +1,104 @@ +/*- + * + * Hedera C++ SDK + * + * Copyright (C) 2020 - 2023 Hedera Hashgraph, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License") + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +#include "AccountId.h" +#include "Client.h" +#include "ED25519PrivateKey.h" +#include "LedgerId.h" + +#include +#include +#include + +using namespace Hedera; + +int main(int argc, char** argv) +{ + if (argc < 3) + { + std::cout << "Please enter a network name and a configuration filepath" << std::endl; + return 1; + } + + /* + * Here are some ways you can construct and configure a client. A client has a network and an operator. + * + * A Hedera network is made up of nodes -- individual servers who participate in the process of reaching consensus + * on the order and validity of transactions on the network. Three networks you likely know of are previewnet, + * testnet, and mainnet. + * + * For the purpose of connecting to it, each node has an IP address or URL and a port number. Each node also has an + * AccountId used to refer to that node for several purposes, including the paying of fees to that node when a + * client submits requests to it. + * + * You can configure what network you want a client to use -- in other words, you can specify a list of URLS and + * port numbers with associated AccountIds, and when that client is used to execute queries and transactions, the + * client will submit requests only to nodes in that list. + * + * A Client has an operator, which has an AccountId and a PublicKey, and which can sign requests. A client's + * operator can also be configured. + */ + + // Here's the simplest way to construct a client. These clients' networks are filled with default lists of nodes that + // are baked into the SDK. Their operators are not yet set, and trying to use them now will result in exceptions. + Client previewClient = Client::forPreviewnet(); + Client testClient = Client::forTestnet(); + Client mainClient = Client::forMainnet(); + + // We can also construct a client for previewnet, testnet, or mainnet depending on the value of a network name string. + // If, for example, the input string equals "testnet", this client will be configured to connect to the Hedera + // Testnet. + Client namedNetworkClient = Client::forName(argv[1]); + + // Set the operator on testClient (the AccountId and PrivateKey here are fake, this is just an example). + testClient.setOperator( + AccountId::fromString("0.0.3"), + ED25519PrivateKey::fromString( + "302e020100300506032b657004220420db484b828e64b2d8f12ce3c0a0e93a0b8cce7af1bb8f39c97732394482538e10")); + + // Create a Client with a custom network. + const std::unordered_map network = { + {"2.testnet.hedera.com:50211", AccountId(5ULL)}, + { "3.testnet.hedera.com:50211", AccountId(6ULL)} + }; + Client customClient = Client::forNetwork(network); + + // Since the customClient's network is in this case a subset of the Hedera Testnet, we should set the LedgerId of the + // Client to testnet's LedgerId. If we don't do this, checksum validation won't work (See ValidateChecksumExample.cc). + // You can use customClient.getLedgerId() to check the ledger ID. If you attempt to validate a checksum against a + // client whose ledger ID is not set, an IllegalStateException will be thrown. + customClient.setLedgerId(LedgerId::TESTNET); + + // Let's generate a client from a config.json file. A config file may specify a network by name, or it may provide a + // custom network in the form of a list of nodes. The config file should specify the operator, so you can use a client + // constructed using fromConfigFile() immediately. + Client configClient = Client::fromConfigFile(argv[2]); + configClient.close(); + + // Always close a Client when you're done with it. + previewClient.close(); + testClient.close(); + mainClient.close(); + namedNetworkClient.close(); + customClient.close(); + + std::cout << "Success!" << std::endl; + + return 0; +} diff --git a/sdk/main/include/Client.h b/sdk/main/include/Client.h index 24f912c66..d7bcc8ade 100644 --- a/sdk/main/include/Client.h +++ b/sdk/main/include/Client.h @@ -21,9 +21,13 @@ #define HEDERA_SDK_CPP_CLIENT_H_ #include +#include #include #include +#include #include +#include +#include #include #include @@ -73,6 +77,15 @@ class Client */ [[nodiscard]] static Client forNetwork(const std::unordered_map& networkMap); + /** + * Construct a Client by a name. The name must be one of "mainnet", "testnet", or "previewnet", otherwise this will + * throw std::invalid_argument. + * + * @param name The name of the Client to construct. + * @return A Client object that is set-up to communicate with the input network name. + */ + [[nodiscard]] static Client forName(std::string_view name); + /** * Construct a Client pre-configured for Hedera Mainnet access. * @@ -94,6 +107,30 @@ class Client */ [[nodiscard]] static Client forPreviewnet(); + /** + * Construct a Client from a JSON configuration string. + * + * @param json The JSON configuration string. + * @return A Client object initialized with the properties specified in the JSON configuration string. + */ + [[nodiscard]] static Client fromConfig(std::string_view json); + + /** + * Construct a Client from a JSON configuration object. + * + * @param json The JSON configuration object. + * @return A Client object initialized with the properties specified in the JSON configuration object. + */ + [[nodiscard]] static Client fromConfig(const nlohmann::json& json); + + /** + * Construct a Client from a JSON configuration file. + * + * @param path The filepath to the JSON configuration file. + * @return A Client object initialized with the properties specified in the JSON configuration file. + */ + [[nodiscard]] static Client fromConfigFile(std::string_view path); + /** * Set the mirror network with which this Client should communicate. * diff --git a/sdk/main/include/PrivateKey.h b/sdk/main/include/PrivateKey.h index 2fe0ce68b..47a26cc07 100644 --- a/sdk/main/include/PrivateKey.h +++ b/sdk/main/include/PrivateKey.h @@ -55,6 +55,26 @@ class PrivateKey : public Key */ ~PrivateKey() override; + /** + * Construct a PrivateKey object from a hex-encoded, DER-encoded key string. + * + * @param key The DER-encoded hex string from which to construct a PrivateKey. + * @return A pointer to an PrivateKey representing the input DER-encoded hex string. + * @throws BadKeyException If the private key type (ED25519 or ECDSAsecp256k1) is unable to be determined or realized + * from the input hex string. + */ + [[nodiscard]] static std::unique_ptr fromStringDer(std::string_view key); + + /** + * Construct a PrivateKey object from a DER-encoded byte vector. + * + * @param bytes The vector of DER-encoded bytes from which to construct a PrivateKey. + * @return A pointer to a PrivateKey representing the input DER-encoded bytes. + * @throws BadKeyException If the private key type (ED25519 or ECDSAsecp256k1) is unable to be determined or realized + * from the input byte array. + */ + [[nodiscard]] static std::unique_ptr fromBytesDer(const std::vector& bytes); + /** * Derive a child PrivateKey from this PrivateKey. * diff --git a/sdk/main/src/Client.cc b/sdk/main/src/Client.cc index 8a1f6ae93..0f6127efa 100644 --- a/sdk/main/src/Client.cc +++ b/sdk/main/src/Client.cc @@ -32,7 +32,7 @@ #include "impl/TLSBehavior.h" #include -#include +#include #include #include @@ -145,6 +145,25 @@ Client& Client::operator=(Client&& other) noexcept return *this; } +//----- +Client Client::forNetwork(const std::unordered_map& networkMap) +{ + Client client; + client.mImpl->mNetwork = std::make_shared(internal::Network::forNetwork(networkMap)); + return client; +} + +//----- +Client Client::forName(std::string_view name) +{ + // clang-format off + if (name == "mainnet") return Client::forMainnet(); + else if (name == "testnet") return Client::forTestnet(); + else if (name == "previewnet") return Client::forPreviewnet(); + else throw std::invalid_argument("Unknown Client name"); + // clang-format on +} + //----- Client Client::forMainnet() { @@ -173,14 +192,200 @@ Client Client::forPreviewnet() } //----- -Client Client::forNetwork(const std::unordered_map& networkMap) +Client Client::fromConfig(std::string_view json) +{ + // Make sure the input string is valid JSON. + nlohmann::json jsonObj; + try + { + jsonObj = nlohmann::json::parse(json); + } + catch (const std::exception& ex) + { + throw std::invalid_argument(std::string("Cannot parse JSON: ") + ex.what()); + } + + return fromConfig(jsonObj); +} + +//----- +Client Client::fromConfig(const nlohmann::json& json) { Client client; - client.mImpl->mNetwork = std::make_shared(internal::Network::forNetwork(networkMap)); - client.mImpl->mMirrorNetwork = nullptr; + + // A "network" tag should always be specified. + if (!json.contains("network")) + { + throw std::invalid_argument("Network tag is not set in JSON"); + } + + // If the network tags specifies a dictionary of nodes, parse each one. + if (const nlohmann::json& jsonNetwork = json["network"]; jsonNetwork.is_object()) + { + std::unordered_map networkMap; + for (const auto& [accountId, url] : jsonNetwork.items()) + { + networkMap.try_emplace(url, AccountId::fromString(accountId)); + } + + client = Client::forNetwork(networkMap); + + // If a network name is provided, set the ledger ID based on it. + if (json.contains("networkName")) + { + try + { + client.setLedgerId(LedgerId::fromString(jsonNetwork["networkName"].get())); + } + catch (const std::exception&) + { + throw std::invalid_argument( + R"(Invalid argument for network name. Should be one of "mainnet", "testnet", or "previewnet")"); + } + } + } + + // If the network tag specifies a name, get the Client for that name. + else if (jsonNetwork.is_string()) + { + try + { + client = Client::forName(jsonNetwork.get()); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid argument for network tag "); + } + } + + // If the network tag type is unclear, throw. + else + { + throw std::invalid_argument("Invalid argument for network tag"); + } + + // Check if there's an operator configured. + if (json.contains("operator")) + { + const nlohmann::json& jsonOperator = json["operator"]; + + if (!jsonOperator.is_object()) + { + throw std::invalid_argument("Invalid argument for operator"); + } + + // If an operator is configured, an AccountId and PrivateKey must also be configured. + if (!jsonOperator.contains("accountId") || !jsonOperator.contains("privateKey")) + { + throw std::invalid_argument("An operator must have an accountId and privateKey"); + } + + // Verify the account ID is valid. + AccountId operatorAccountId; + try + { + operatorAccountId = AccountId::fromString(jsonOperator["accountId"].get()); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid argument for operator accountId"); + } + + // Verify the private key is valid. + std::shared_ptr operatorPrivateKey; + try + { + operatorPrivateKey = PrivateKey::fromStringDer(jsonOperator["privateKey"].get()); + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid argument for operator privateKey"); + } + + // Set the operator. + client.setOperator(operatorAccountId, operatorPrivateKey); + } + + // Check if there's a mirror network configured. + if (json.contains("mirrorNetwork")) + { + // If the mirror network tags specifies a list of nodes, parse each one. + if (const nlohmann::json& jsonMirrorNetwork = json["mirrorNetwork"]; jsonMirrorNetwork.is_array()) + { + std::vector mirrorNetwork; + for (const auto& url : jsonMirrorNetwork) + { + mirrorNetwork.push_back(url); + } + + client.setMirrorNetwork(mirrorNetwork); + } + + // If the mirror network tag specifies a name, get the mirror network for that name. + else if (jsonMirrorNetwork.is_string()) + { + try + { + const std::string_view mirrorNetworkName = jsonMirrorNetwork.get(); + if (mirrorNetworkName == "mainnet") + { + client.mImpl->mMirrorNetwork = + std::make_shared(internal::MirrorNetwork::forMainnet()); + } + else if (mirrorNetworkName == "testnet") + { + client.mImpl->mMirrorNetwork = + std::make_shared(internal::MirrorNetwork::forTestnet()); + } + else if (mirrorNetworkName == "previewnet") + { + client.mImpl->mMirrorNetwork = + std::make_shared(internal::MirrorNetwork::forPreviewnet()); + } + else + { + throw std::invalid_argument("Invalid argument for mirrorNetwork tag"); + } + } + catch (const std::exception&) + { + throw std::invalid_argument("Invalid argument for mirrorNetwork tag"); + } + } + + // If the mirrorNetwork tag type is unclear, throw. + else + { + throw std::invalid_argument("Invalid argument for mirrorNetwork tag"); + } + } + return client; } +//----- +Client Client::fromConfigFile(std::string_view path) +{ + std::ifstream infile(path.data()); + if (!infile.is_open()) + { + throw std::invalid_argument(std::string("File cannot be found at ") + path.data()); + } + + // Make sure the input file is valid JSON. + nlohmann::json jsonObj; + try + { + jsonObj = nlohmann::json::parse(infile); + } + catch (const std::exception& ex) + { + throw std::invalid_argument(std::string("Cannot parse JSON: ") + ex.what()); + } + + return fromConfig(jsonObj); +} + //----- Client& Client::setMirrorNetwork(const std::vector& network) { diff --git a/sdk/main/src/PrivateKey.cc b/sdk/main/src/PrivateKey.cc index ec25dc4ad..8a646a8d0 100644 --- a/sdk/main/src/PrivateKey.cc +++ b/sdk/main/src/PrivateKey.cc @@ -18,9 +18,12 @@ * */ #include "PrivateKey.h" +#include "ECDSAsecp256k1PrivateKey.h" +#include "ED25519PrivateKey.h" #include "PublicKey.h" #include "exceptions/BadKeyException.h" #include "exceptions/OpenSSLException.h" +#include "impl/HexConverter.h" #include "impl/PrivateKeyImpl.h" #include "impl/Utilities.h" #include "impl/openssl_utils/OpenSSLUtils.h" @@ -32,6 +35,40 @@ namespace Hedera //----- PrivateKey::~PrivateKey() = default; +//----- +std::unique_ptr PrivateKey::fromStringDer(std::string_view key) +{ + if (const std::string_view prefix = "0x"; key.substr(0, prefix.size()) == prefix) + { + key.remove_prefix(prefix.size()); + } + + try + { + return PrivateKey::fromBytesDer(internal::HexConverter::hexToBytes(key)); + } + catch (const OpenSSLException&) + { + throw BadKeyException(std::string("Unable to decode input key string ") + key.data()); + } +} + +//----- +std::unique_ptr PrivateKey::fromBytesDer(const std::vector& bytes) +{ + if (internal::Utilities::isPrefixOf(bytes, ED25519PrivateKey::DER_ENCODED_PREFIX_BYTES)) + { + return ED25519PrivateKey::fromBytes(bytes); + } + + else if (internal::Utilities::isPrefixOf(bytes, ECDSAsecp256k1PrivateKey::DER_ENCODED_PREFIX_BYTES)) + { + return ECDSAsecp256k1PrivateKey::fromBytes(bytes); + } + + throw BadKeyException("Key type cannot be determined from input DER-encoded byte array"); +} + //----- std::vector PrivateKey::getChainCode() const { @@ -97,7 +134,7 @@ PrivateKey::PrivateKey(internal::OpenSSLUtils::EVP_PKEY&& key, std::vector keyBytes(i2d_PUBKEY(mImpl->mKey.get(), nullptr)); - if (unsigned char* rawPublicKeyBytes = internal::Utilities::toTypePtr(keyBytes.data()); + if (auto* rawPublicKeyBytes = internal::Utilities::toTypePtr(keyBytes.data()); i2d_PUBKEY(mImpl->mKey.get(), &rawPublicKeyBytes) <= 0) { throw OpenSSLException(internal::OpenSSLUtils::getErrorMessage("i2d_PUBKEY")); diff --git a/sdk/tests/integration/BaseIntegrationTest.cc b/sdk/tests/integration/BaseIntegrationTest.cc index b442cd4e0..d320a5961 100644 --- a/sdk/tests/integration/BaseIntegrationTest.cc +++ b/sdk/tests/integration/BaseIntegrationTest.cc @@ -37,46 +37,7 @@ namespace Hedera //----- void BaseIntegrationTest::SetUp() { - const string_view networkTag = "network"; - const string_view operatorTag = "operator"; - const string_view accountIdTag = "accountId"; - const string_view privateKeyTag = "privateKey"; - - const string testPathToJSON = (filesystem::current_path() / "local_node.json").string(); - - ifstream testInputFile(testPathToJSON, ios::in); - - unordered_map networksMap; - unordered_map networkAccountsMap; - unordered_map::iterator it; - - json jsonData = json::parse(testInputFile); - jsonData[networkTag].get_to(networksMap); - - for (it = networksMap.begin(); it != networksMap.end(); it++) - { - const string_view accountIdStr = (*it).first; - const string nodeAddressString = (*it).second; - - networkAccountsMap.try_emplace(nodeAddressString, AccountId::fromString(accountIdStr)); - } - - AccountId operatorAccountId; - string operatorAccountPrivateKey; - - if (jsonData[operatorTag][accountIdTag].is_string() && jsonData[operatorTag][privateKeyTag].is_string()) - { - string operatorAccountIdStr = jsonData[operatorTag][accountIdTag]; - operatorAccountPrivateKey = jsonData[operatorTag][privateKeyTag]; - - operatorAccountId = AccountId::fromString(operatorAccountIdStr); - } - - testInputFile.close(); - - mClient = Client::forNetwork(networkAccountsMap); - mClient.setOperator(operatorAccountId, ED25519PrivateKey::fromString(operatorAccountPrivateKey)); - mClient.setMirrorNetwork({ "127.0.0.1:5600" }); + mClient = Client::fromConfigFile((filesystem::current_path() / "local_node.json").string()); mClient.setNetworkUpdatePeriod(std::chrono::hours(24)); mFileContent = internal::Utilities::stringToByteVector( diff --git a/sdk/tests/unit/ClientTest.cc b/sdk/tests/unit/ClientTest.cc index 112bc42dd..ce8665e8e 100644 --- a/sdk/tests/unit/ClientTest.cc +++ b/sdk/tests/unit/ClientTest.cc @@ -37,10 +37,10 @@ class ClientTest : public ::testing::Test return mTestNetworkUpdatePeriod; } - [[nodiscard]] inline const std::chrono::milliseconds getNegativeBackoffTime() const { return mNegativeBackoffTime; } - [[nodiscard]] inline const std::chrono::milliseconds getZeroBackoffTime() const { return mZeroBackoffTime; } - [[nodiscard]] inline const std::chrono::milliseconds getBelowMinBackoffTime() const { return mBelowMinBackoffTime; } - [[nodiscard]] inline const std::chrono::milliseconds getAboveMaxBackoffTime() const { return mAboveMaxBackoffTime; } + [[nodiscard]] inline const std::chrono::milliseconds& getNegativeBackoffTime() const { return mNegativeBackoffTime; } + [[nodiscard]] inline const std::chrono::milliseconds& getZeroBackoffTime() const { return mZeroBackoffTime; } + [[nodiscard]] inline const std::chrono::milliseconds& getBelowMinBackoffTime() const { return mBelowMinBackoffTime; } + [[nodiscard]] inline const std::chrono::milliseconds& getAboveMaxBackoffTime() const { return mAboveMaxBackoffTime; } private: const AccountId mAccountId = AccountId(10ULL);