diff --git a/core/idl/wallet/configuration.djinni b/core/idl/wallet/configuration.djinni index 49a5e40d00..a3f0a335fb 100644 --- a/core/idl/wallet/configuration.djinni +++ b/core/idl/wallet/configuration.djinni @@ -5,6 +5,7 @@ BlockchainExplorerEngines = interface +c { const RIPPLE_NODE: string = "RIPPLE_NODE"; const TEZOS_NODE: string = "TEZOS_NODE"; const TZSTATS_API: string = "TZSTATS_API"; + const TZSTATS_RPC_NODE: string = "TZSTATS_RPC_NODE"; } # Available API to use with observers. diff --git a/core/idl/wallet/tezos/tezos_like_wallet.djinni b/core/idl/wallet/tezos/tezos_like_wallet.djinni index 6340899e13..77fd6fb75c 100644 --- a/core/idl/wallet/tezos/tezos_like_wallet.djinni +++ b/core/idl/wallet/tezos/tezos_like_wallet.djinni @@ -160,6 +160,7 @@ TezosConfigurationDefaults = interface +c { const TEZOS_DEFAULT_API_ENDPOINT: string = "https://explorers.api.live.ledger.com"; const TEZOS_DEFAULT_API_VERSION: string = "v3"; const TZSTATS_API_ENDPOINT: string = "https://api.tzstats.com/explorer"; + const TEZOS_RPC_ENDPOINT: string = "https://mainnet.tezrpc.me"; const TEZOS_OBSERVER_NODE_ENDPOINT_S3: string = "https://s3.tezos.com"; const TEZOS_OBSERVER_WS_ENDPOINT_S2: string = "wss://s2.tezos.com"; const TEZOS_OBSERVER_WS_ENDPOINT_S3: string = "wss://s3.tezos.com"; diff --git a/core/src/api/BlockchainExplorerEngines.cpp b/core/src/api/BlockchainExplorerEngines.cpp index fe33069c98..888963a878 100644 --- a/core/src/api/BlockchainExplorerEngines.cpp +++ b/core/src/api/BlockchainExplorerEngines.cpp @@ -15,4 +15,6 @@ std::string const BlockchainExplorerEngines::TEZOS_NODE = {"TEZOS_NODE"}; std::string const BlockchainExplorerEngines::TZSTATS_API = {"TZSTATS_API"}; +std::string const BlockchainExplorerEngines::TZSTATS_RPC_NODE = {"TZSTATS_RPC_NODE"}; + } } } // namespace ledger::core::api diff --git a/core/src/api/BlockchainExplorerEngines.hpp b/core/src/api/BlockchainExplorerEngines.hpp index 2db707cadf..84ed982b68 100644 --- a/core/src/api/BlockchainExplorerEngines.hpp +++ b/core/src/api/BlockchainExplorerEngines.hpp @@ -29,6 +29,8 @@ class LIBCORE_EXPORT BlockchainExplorerEngines { static std::string const TEZOS_NODE; static std::string const TZSTATS_API; + + static std::string const TZSTATS_RPC_NODE; }; } } } // namespace ledger::core::api diff --git a/core/src/api/TezosConfigurationDefaults.cpp b/core/src/api/TezosConfigurationDefaults.cpp index 73aac41900..1b38a4cf05 100644 --- a/core/src/api/TezosConfigurationDefaults.cpp +++ b/core/src/api/TezosConfigurationDefaults.cpp @@ -11,6 +11,8 @@ std::string const TezosConfigurationDefaults::TEZOS_DEFAULT_API_VERSION = {"v3"} std::string const TezosConfigurationDefaults::TZSTATS_API_ENDPOINT = {"https://api.tzstats.com/explorer"}; +std::string const TezosConfigurationDefaults::TEZOS_RPC_ENDPOINT = {"https://mainnet.tezrpc.me"}; + std::string const TezosConfigurationDefaults::TEZOS_OBSERVER_NODE_ENDPOINT_S3 = {"https://s3.tezos.com"}; std::string const TezosConfigurationDefaults::TEZOS_OBSERVER_WS_ENDPOINT_S2 = {"wss://s2.tezos.com"}; diff --git a/core/src/api/TezosConfigurationDefaults.hpp b/core/src/api/TezosConfigurationDefaults.hpp index 911f11efda..22b3628b21 100644 --- a/core/src/api/TezosConfigurationDefaults.hpp +++ b/core/src/api/TezosConfigurationDefaults.hpp @@ -25,6 +25,8 @@ class LIBCORE_EXPORT TezosConfigurationDefaults { static std::string const TZSTATS_API_ENDPOINT; + static std::string const TEZOS_RPC_ENDPOINT; + static std::string const TEZOS_OBSERVER_NODE_ENDPOINT_S3; static std::string const TEZOS_OBSERVER_WS_ENDPOINT_S2; diff --git a/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.cpp b/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.cpp new file mode 100644 index 0000000000..8fd4fb94d2 --- /dev/null +++ b/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.cpp @@ -0,0 +1,235 @@ +/* + * + * RpcNodeTezosLikeBlockchainExplorer + * + * Created by El Khalil Bellakrid on 22/10/2019. + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Ledger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + + +#include "RpcNodeTezosLikeBlockchainExplorer.h" +#include +#include +#include +#include + +namespace ledger { + namespace core { + RpcNodeTezosLikeBlockchainExplorer::RpcNodeTezosLikeBlockchainExplorer( + const std::shared_ptr &context, + const std::shared_ptr &http, + const api::TezosLikeNetworkParameters ¶meters, + const std::shared_ptr &configuration) : + DedicatedContext(context), + TezosLikeBlockchainExplorer(configuration, {api::Configuration::BLOCKCHAIN_EXPLORER_API_ENDPOINT}) { + _http = http; + _parameters = parameters; + } + + + Future> + RpcNodeTezosLikeBlockchainExplorer::getBalance(const std::vector &addresses) { + auto size = addresses.size(); + if (size != 1) { + throw make_exception(api::ErrorCode::INVALID_ARGUMENT, + "Can only get balance of 1 address from Tezos Node, but got {} addresses", + addresses.size()); + } + return getHelper(fmt::format("chains/main/blocks/head/context/contracts/{}/balance", addresses[0]->toString()), + "", + std::unordered_map{}, + "0" + ); + } + + Future> + RpcNodeTezosLikeBlockchainExplorer::getFees() { + return FuturePtr::successful( + std::make_shared(api::TezosConfigurationDefaults::TEZOS_DEFAULT_FEES) + ); + } + + Future + RpcNodeTezosLikeBlockchainExplorer::pushLedgerApiTransaction(const std::vector &transaction) { + std::stringstream body; + body << '"' << hex::toString(transaction) << '"'; + auto bodyString = body.str(); + return _http->POST("injection/operation?chain=main", + std::vector(bodyString.begin(), bodyString.end())) + .json().template map(getExplorerContext(), + [](const HttpRequest::JsonResult &result) -> String { + auto &json = *std::get<1>(result); + if (!json.IsString()) { + throw make_exception(api::ErrorCode::HTTP_ERROR, + "Failed to parse broadcast transaction response, missing transaction hash"); + } + return json.GetString(); + }); + } + + Future RpcNodeTezosLikeBlockchainExplorer::startSession() { + return Future::successful(new std::string()); + } + + Future RpcNodeTezosLikeBlockchainExplorer::killSession(void *session) { + return Future::successful(unit); + } + + Future RpcNodeTezosLikeBlockchainExplorer::getRawTransaction(const String &transactionHash) { + // WARNING: not implemented + throw make_exception(api::ErrorCode::IMPLEMENTATION_IS_MISSING, + "Endpoint to get raw transactions is not implemented."); + } + + Future RpcNodeTezosLikeBlockchainExplorer::pushTransaction(const std::vector &transaction) { + return pushLedgerApiTransaction(transaction); + } + + FuturePtr + RpcNodeTezosLikeBlockchainExplorer::getTransactions(const std::vector &addresses, + Option fromBlockHash, + Option session) { + if (addresses.size() != 1) { + throw make_exception(api::ErrorCode::INVALID_ARGUMENT, + "Can only get transactions for 1 address from Tezos Node, but got {} addresses", + addresses.size()); + } + using EitherTransactionsBulk = Either>; + return _http->GET(fmt::format("/operations/{}", addresses[0]), + std::unordered_map{}, + "https://mystique.tzkt.io/v3/") + .template json( + LedgerApiParser()) + .template mapPtr(getExplorerContext(), + [](const EitherTransactionsBulk &result) { + if (result.isLeft()) { + // Because it fails when there are no ops + return std::make_shared(); + } else { + return result.getRight(); + } + }); + } + + FuturePtr RpcNodeTezosLikeBlockchainExplorer::getCurrentBlock() const { + return _http->GET("chains/main/blocks/head") + .template json(LedgerApiParser()) + .template mapPtr(getExplorerContext(), + [](const Either> &result) { + if (result.isLeft()) { + throw result.getLeft(); + } else { + return result.getRight(); + } + }); + } + + FuturePtr + RpcNodeTezosLikeBlockchainExplorer::getTransactionByHash(const String &transactionHash) const { + return getLedgerApiTransactionByHash(transactionHash); + } + + Future RpcNodeTezosLikeBlockchainExplorer::getTimestamp() const { + return getLedgerApiTimestamp(); + } + + std::shared_ptr RpcNodeTezosLikeBlockchainExplorer::getExplorerContext() const { + return _executionContext; + } + + api::TezosLikeNetworkParameters RpcNodeTezosLikeBlockchainExplorer::getNetworkParameters() const { + return _parameters; + } + + std::string RpcNodeTezosLikeBlockchainExplorer::getExplorerVersion() const { + return ""; + } + + Future> + RpcNodeTezosLikeBlockchainExplorer::getHelper(const std::string &url, + const std::string &field, + const std::unordered_map ¶ms, + const std::string &fallbackValue) { + const bool parseNumbersAsString = true; + auto networkId = getNetworkParameters().Identifier; + + std::string p, separator = "?"; + for (auto ¶m : params) { + p += fmt::format("{}{}={}", separator, param.first, param.second); + separator = "&"; + } + + return _http->GET(url + p, + std::unordered_map()) + .json(parseNumbersAsString) + .mapPtr(getContext(), + [field, networkId, fallbackValue](const HttpRequest::JsonResult &result) { + auto &json = *std::get<1>(result); + if ((!json.IsObject() || + !json.HasMember(field.c_str()) || + !json[field.c_str()].IsString()) && !json.IsString()) { + throw make_exception(api::ErrorCode::HTTP_ERROR, + fmt::format("Failed to get {} for {}", field, + networkId)); + } + std::string value = json.IsString() ? json.GetString() : json[field.c_str()].GetString(); + if (value == "0" && !fallbackValue.empty()) { + value = fallbackValue; + } else if (value.find('.') != std::string::npos) { + value = api::BigInt::fromDecimalString(value, 6, ".")->toString(10); + } + return std::make_shared(value); + }) + .recover(getContext(), [fallbackValue] (const Exception &exception) { + return std::make_shared(!fallbackValue.empty() ? fallbackValue : "0"); + }); + } + + Future> + RpcNodeTezosLikeBlockchainExplorer::getEstimatedGasLimit(const std::string &address) { + return FuturePtr::successful( + std::make_shared(api::TezosConfigurationDefaults::TEZOS_DEFAULT_GAS_LIMIT) + ); + } + + Future> + RpcNodeTezosLikeBlockchainExplorer::getStorage(const std::string &address) { + return getHelper(fmt::format("chains/main/blocks/head/context/contracts/{}/storage", address), + "", + std::unordered_map{}, + api::TezosConfigurationDefaults::TEZOS_DEFAULT_STORAGE_LIMIT + ); + } + + Future> + RpcNodeTezosLikeBlockchainExplorer::getCounter(const std::string &address) { + return getHelper(fmt::format("chains/main/blocks/head/context/contracts/{}/counter", address), + "", + std::unordered_map{}, + "0" + ); + } + } +} \ No newline at end of file diff --git a/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.h b/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.h new file mode 100644 index 0000000000..c3c57dbc7e --- /dev/null +++ b/core/src/wallet/tezos/explorers/RpcNodeTezosLikeBlockchainExplorer.h @@ -0,0 +1,118 @@ +/* + * + * RpcNodeTezosLikeBlockchainExplorer + * + * Created by El Khalil Bellakrid on 22/10/2019. + * + * The MIT License (MIT) + * + * Copyright (c) 2019 Ledger + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace ledger { + namespace core { + using RpcNodeBlockchainExplorer = AbstractLedgerApiBlockchainExplorer< + TezosLikeBlockchainExplorerTransaction, + TezosLikeBlockchainExplorer::TransactionsBulk, + TezosLikeTransactionsParser, + TezosLikeTransactionsBulkParser, + TezosLikeBlockParser, + api::TezosLikeNetworkParameters>; + + class RpcNodeTezosLikeBlockchainExplorer : public TezosLikeBlockchainExplorer, + public RpcNodeBlockchainExplorer, + public DedicatedContext, + public std::enable_shared_from_this { + public: + RpcNodeTezosLikeBlockchainExplorer(const std::shared_ptr &context, + const std::shared_ptr &http, + const api::TezosLikeNetworkParameters ¶meters, + const std::shared_ptr &configuration); + + Future> + getBalance(const std::vector &addresses) override; + + Future> + getFees() override; + + Future pushLedgerApiTransaction(const std::vector &transaction) override; + + Future startSession() override; + + Future killSession(void *session) override; + + Future getRawTransaction(const String &transactionHash) override; + + Future pushTransaction(const std::vector &transaction) override; + + FuturePtr + getTransactions(const std::vector &addresses, + Option fromBlockHash = Option(), + Option session = Option()) override; + + FuturePtr getCurrentBlock() const override; + + FuturePtr + getTransactionByHash(const String &transactionHash) const override; + + Future getTimestamp() const override; + + std::shared_ptr getExplorerContext() const override; + + api::TezosLikeNetworkParameters getNetworkParameters() const override; + + std::string getExplorerVersion() const override; + + Future> + getEstimatedGasLimit(const std::string &address) override; + + Future> + getStorage(const std::string &address) override; + + Future> getCounter(const std::string &address) override; + + private: + /* + * Helper to a get specific field's value from given url + * WARNING: this is only useful for fields with an integer (decimal representation) value (with a string type) + * @param url : base url to fetch the value on, + * @param field: name of field we are interested into, + * @param params: additional params to query value of field + * @return BigInt representing the value of targetted field + */ + Future> + getHelper(const std::string &url, + const std::string &field, + const std::unordered_map ¶ms = std::unordered_map(), + const std::string &fallbackValue = ""); + + api::TezosLikeNetworkParameters _parameters; + }; + } +} \ No newline at end of file diff --git a/core/src/wallet/tezos/factories/TezosLikeWalletFactory.cpp b/core/src/wallet/tezos/factories/TezosLikeWalletFactory.cpp index ba08292832..c4ca4cdb46 100644 --- a/core/src/wallet/tezos/factories/TezosLikeWalletFactory.cpp +++ b/core/src/wallet/tezos/factories/TezosLikeWalletFactory.cpp @@ -38,6 +38,7 @@ #include #include +#include #include #include #include @@ -135,33 +136,37 @@ namespace ledger { auto pool = getPool(); auto engine = configuration->getString(api::Configuration::BLOCKCHAIN_EXPLORER_ENGINE) .value_or(api::BlockchainExplorerEngines::TEZOS_NODE); + std::shared_ptr explorer = nullptr; - auto isTzStats = engine == api::BlockchainExplorerEngines::TZSTATS_API; - if (engine == api::BlockchainExplorerEngines::TEZOS_NODE || - isTzStats) { - auto defaultValue = isTzStats ? - api::TezosConfigurationDefaults::TZSTATS_API_ENDPOINT : - api::TezosConfigurationDefaults::TEZOS_DEFAULT_API_ENDPOINT; - auto http = pool->getHttpClient(fmt::format("{}", - configuration->getString( - api::Configuration::BLOCKCHAIN_EXPLORER_API_ENDPOINT - ).value_or(defaultValue)) - ); - auto context = pool->getDispatcher() - ->getSerialExecutionContext(api::BlockchainObserverEngines::TEZOS_NODE); - auto &networkParams = getCurrency().tezosLikeNetworkParameters.value(); + auto context = pool->getDispatcher()->getSerialExecutionContext(api::BlockchainObserverEngines::TEZOS_NODE); + auto &networkParams = getCurrency().tezosLikeNetworkParameters.value(); + auto optionalEndpoint = configuration->getString(api::Configuration::BLOCKCHAIN_EXPLORER_API_ENDPOINT); - if (isTzStats) { - explorer = std::make_shared(context, - http, - networkParams, - configuration); - } else { - explorer = std::make_shared(context, + if (engine == api::BlockchainExplorerEngines::TEZOS_NODE) { + auto http = pool->getHttpClient(optionalEndpoint.value_or( + api::TezosConfigurationDefaults::TEZOS_DEFAULT_API_ENDPOINT + )); + explorer = std::make_shared(context, http, networkParams, configuration); - } + } else if (engine == api::BlockchainExplorerEngines::TZSTATS_API) { + auto http = pool->getHttpClient(optionalEndpoint.value_or( + api::TezosConfigurationDefaults::TZSTATS_API_ENDPOINT + )); + explorer = std::make_shared(context, + http, + networkParams, + configuration); + } else { + // Fallback on RPC node if unknown engine + auto http = pool->getHttpClient(optionalEndpoint.value_or( + api::TezosConfigurationDefaults::TEZOS_RPC_ENDPOINT + )); + explorer = std::make_shared(context, + http, + networkParams, + configuration); } if (explorer) _runningExplorers.push_back(explorer); diff --git a/core/test/integration/synchronization/tezos_synchronization.cpp b/core/test/integration/synchronization/tezos_synchronization.cpp index 54db3e540e..66f85af036 100644 --- a/core/test/integration/synchronization/tezos_synchronization.cpp +++ b/core/test/integration/synchronization/tezos_synchronization.cpp @@ -55,7 +55,7 @@ TEST_F(TezosLikeWalletSynchronization, MediumXpubSynchronization) { auto configuration = DynamicObject::newInstance(); configuration->putString(api::Configuration::KEYCHAIN_DERIVATION_SCHEME,"44'/'/'/'/
"); configuration->putString(api::TezosConfiguration::TEZOS_XPUB_CURVE, api::TezosConfigurationDefaults::TEZOS_XPUB_CURVE_SECP256K1); - configuration->putString(api::Configuration::BLOCKCHAIN_EXPLORER_ENGINE, api::BlockchainExplorerEngines::TZSTATS_API); + configuration->putString(api::Configuration::BLOCKCHAIN_EXPLORER_ENGINE, api::BlockchainExplorerEngines::TZSTATS_RPC_NODE); auto wallet = wait(pool->createWallet("e847815f-488a-4301-b67c-378a5e9c8a61", "tezos", configuration)); std::set emittedOperations; {