diff --git a/CMakeLists.txt b/CMakeLists.txt index bb7646537..19b3402ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,13 +9,14 @@ project(status-desktop VERSION ${STATUS_VERSION} LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + set(PROJECT_ORGANIZATION_DOMAIN "status.im") set(PROJECT_ORGANIZATION_NAME "Status") set(PROJECT_APPLICATION_NAME "Status Desktop") -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) include(CTest) diff --git a/app/README.md b/app/README.md index 4249bea87..d48dbbe75 100644 --- a/app/README.md +++ b/app/README.md @@ -2,6 +2,8 @@ ## Setup dependencies +Note: if not stated otherwise the commands should be run from the root of the source tree + ### 1. conancenter Execute `conan remote list`. It should return this line among the results: @@ -23,28 +25,29 @@ conan profile update settings.compiler.libcxx=libstdc++11 default ### 2. Install dependencies +Install latest Qt release 6.3.X + Platform specific conan profile - Macos: - - Intel: `conan install . --profile=vendor/conan-configs/apple-arm64.ini -s build_type=Release --build=missing -if=build/conan` - - Apple silicon: `conan install . --profile=vendor/conan-configs/apple-x86_64.ini -s build_type=Release --build=missing -if=build/conan` + - Apple silicon: `conan install . --profile=vendor/conan-configs/apple-arm64.ini -s build_type=Release --build=missing -if=build/conan -of=build` + - Intel: `conan install . --profile=vendor/conan-configs/apple-x86_64.ini -s build_type=Release --build=missing -if=build/conan -of=build` - Windows: TODO -- Linux: `conan install . --profile=./vendor/conan-configs/linux.ini -s build_type=Release --build=missing -if=build/conan` - +- Linux: `conan install . --profile=./vendor/conan-configs/linux.ini -s build_type=Release --build=missing -if=build/conan -of=build` ## Buid, test & run -Update `CMake` to the [Latest Release](https://cmake.org/download/) +Update `CMake` to the [Latest Release](https://cmake.org/download/) or use the Qt's "$QTBASE/Tools/CMake/..." ### Build with conan ```bash # linux -CMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/gcc_64" conan build . -if=build/conan -bf=build +CMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/gcc_64" conan build . -if=build/conan -bf=build -# MacOS: CMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/macos" conan build . -if=build/conan -bf=build +# MacOS: CMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/macos" conan build . -if=build/conan -bf=build -# Windows: CMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/mingw_64" conan build . -if=build/conan -bf=build +# Windows: CMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/mingw_64" conan build . -if=build/conan -bf=build ctest -VV -C Release ./status-desktop @@ -54,11 +57,17 @@ ctest -VV -C Release ```bash # linux -cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/gcc_64" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake +cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/gcc_64" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake -# MacOS: cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/macos" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake +# MacOS: cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/macos" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake -# Windows: cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.4.0/mingw_64" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake +# Windows: cmake -B build -S . -DCMAKE_PREFIX_PATH="$HOME/Qt/6.3.2/mingw_64" -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan/conan_toolchain.cmake cmake --build build --config Release ``` + +### Run tests + +```bash +ctest --test-dir ./build +``` diff --git a/conanfile.py b/conanfile.py deleted file mode 100644 index 5ef44c66b..000000000 --- a/conanfile.py +++ /dev/null @@ -1,17 +0,0 @@ -from conans import ConanFile, CMake - - -class StatusDesktop(ConanFile): - name = "status-desktop" - settings = "os", "compiler", "build_type", "arch" - - requires = "gtest/1.11.0", "nlohmann_json/3.10.5" # "fruit/3.6.0", - - # cmake_find_package and cmake_find_package_multi should be substituted with CMakeDeps - # as soon as Conan 2.0 is released and all conan-center packages are adapted - generators = "CMakeToolchain", "cmake_find_package", "cmake_find_package_multi" - - def build(self): - cmake = CMake(self) - cmake.configure() - cmake.build() diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 000000000..1f648bca9 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,10 @@ +[requires] +gtest/1.11.0 +nlohmann_json/3.10.5 +boost/1.79.0 + +#"fruit/3.6.0" + +[generators] +CMakeToolchain +CMakeDeps \ No newline at end of file diff --git a/libs/Helpers/src/Helpers/NamedType.h b/libs/Helpers/src/Helpers/NamedType.h index 0be388fc9..bb5e28c67 100644 --- a/libs/Helpers/src/Helpers/NamedType.h +++ b/libs/Helpers/src/Helpers/NamedType.h @@ -4,6 +4,7 @@ #include #include +#include using json = nlohmann::json; @@ -19,25 +20,38 @@ class NamedType public: using UnderlyingType = T; - // constructor explicit constexpr NamedType(T const& value) : m_value(value) {} template> explicit constexpr NamedType(T&& value) : m_value(std::move(value)) {} explicit constexpr NamedType() = default; - // get - constexpr T& get() { return m_value; } - constexpr std::remove_reference_t const& get() const {return m_value; } + constexpr T& get() { + return m_value; + } - bool operator<(const NamedType &) const = default; - bool operator>(const NamedType &) const = default; - bool operator<=(const NamedType &) const = default; - bool operator>=(const NamedType &) const = default; - bool operator==(const NamedType &) const = default; - bool operator!=(const NamedType &) const = default; + constexpr std::remove_reference_t const& get() const { + return m_value; + } + +#if defined __cpp_impl_three_way_comparison && defined __cpp_lib_three_way_comparison + friend auto operator<=>(const NamedType& l, const NamedType& r) noexcept { + return l.m_value <=> r.m_value; + }; +#else + bool operator<(const NamedType &other) const { return m_value < other.m_value; }; + bool operator>(const NamedType &other) const { return m_value > other.m_value; }; + bool operator<=(const NamedType &other) const { return m_value <= other.m_value; }; + bool operator>=(const NamedType &other) const { return m_value >= other.m_value; }; + bool operator==(const NamedType &other) const { return m_value == other.m_value; }; + bool operator!=(const NamedType &other) const { return m_value != other.m_value; }; +#endif + + T &operator=(NamedType const& rhs) { + return m_value = rhs.m_value; + }; private: - T m_value; + T m_value{}; }; template @@ -59,7 +73,6 @@ template struct hash> { using NamedType = Status::Helpers::NamedType; - using checkIfHashable = typename std::enable_if::type; size_t operator()(NamedType const& x) const { @@ -67,4 +80,4 @@ struct hash> } }; -} +} // namespace std diff --git a/libs/Helpers/src/Helpers/conversions.h b/libs/Helpers/src/Helpers/conversions.h index 5c72d7f77..8a87aff0b 100644 --- a/libs/Helpers/src/Helpers/conversions.h +++ b/libs/Helpers/src/Helpers/conversions.h @@ -1,8 +1,11 @@ #pragma once +#include "helpers.h" #include +#include #include +#include #include @@ -31,6 +34,23 @@ struct adl_serializer { } }; +using namespace std::string_literals; + +template<> +struct adl_serializer { + static void to_json(json& j, const QByteArray& data) { + j = data.toStdString(); + } + + static void from_json(const json& j, QByteArray& data) { + auto str = j.get(); + if(str.size() >= 2 && Status::Helpers::iequals(str, "0x"s, 2)) + data = QByteArray::fromHex(QByteArray::fromRawData(str.c_str() + 2 * sizeof(str[0]), str.size() - 2)); + else + data = QByteArray::fromStdString(str); + } +}; + template<> struct adl_serializer { static void to_json(json& j, const QColor& color) { @@ -38,7 +58,18 @@ struct adl_serializer { } static void from_json(const json& j, QColor& color) { - color = QColor(Status::toQString(j.get())); + color = QColor(QString::fromStdString(j.get())); + } +}; + +template<> +struct adl_serializer { + static void to_json(json& j, const QUrl& url) { + j = url.toString(); + } + + static void from_json(const json& j, QUrl& url) { + url = QUrl(QString::fromStdString(j.get())); } }; diff --git a/libs/Helpers/src/Helpers/helpers.h b/libs/Helpers/src/Helpers/helpers.h index 05447f050..2b1cbac50 100644 --- a/libs/Helpers/src/Helpers/helpers.h +++ b/libs/Helpers/src/Helpers/helpers.h @@ -1,5 +1,9 @@ +#pragma once + #include "Helpers/BuildConfiguration.h" +#include + namespace Status::Helpers { constexpr bool isDebugBuild() @@ -11,4 +15,18 @@ constexpr bool isDebugBuild() #endif } +/// Case insensitive comparision with optional limitation to first \c len characters +/// \note \c T entry type must support \c tolower +/// \todo test me +template +bool iequals(const T& a, const T& b, size_t len = -1) +{ + return len < a.size() && len < b.size() && + std::equal(a.begin(), len >= 0 ? a.end() : a.begin() + len, + b.begin(), len >= 0 ? b.end() : b.begin() + len, + [](auto a, auto b) { + return tolower(a) == tolower(b); + }); +} + } diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp index 32321bcf3..c0be61ad4 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.cpp @@ -139,14 +139,10 @@ QString AccountsService::login(MultiAccount account, const QString& password) const auto hashedPassword(Utils::hashPassword(password)); - const QString installationId(QUuid::createUuid().toString(QUuid::WithoutBraces)); - const QJsonObject nodeConfig(getDefaultNodeConfig(installationId)); - const QString thumbnailImage; const QString largeImage; - // TODO DEV const auto response = StatusGo::Accounts::login(account.name, account.keyUid, hashedPassword, - thumbnailImage, largeImage/*, nodeConfig*/); + thumbnailImage, largeImage); if(response.containsError()) { qWarning() << response.error.message; @@ -310,13 +306,6 @@ QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedMul const QString& installationId, const QString& displayName) const { - try { - auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value(); - const auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value(); - - QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_KEY%", infuraKey); - QJsonArray defaultNetworksJson = QJsonDocument::fromJson(defaultNetworksContent.toUtf8()).array(); - return QJsonObject{ {"key-uid", account.keyUid}, {"mnemonic", account.mnemonic}, @@ -332,22 +321,13 @@ QJsonObject AccountsService::prepareAccountSettingsJsonObject(const GeneratedMul {"log-level", "INFO"}, {"latest-derived-path", 0}, {"currency", "usd"}, - //{"networks/networks", defaultNetworksJson}, {"networks/networks", QJsonArray()}, - //{"networks/current-network", Constants::General::DefaultNetworkName}, {"networks/current-network", ""}, {"wallet/visible-tokens", QJsonObject()}, - //{"wallet/visible-tokens", { - // {Constants::General::DefaultNetworkName, QJsonArray{"SNT"}} - // } - //}, {"waku-enabled", true}, {"appearance", 0}, {"installation-id", installationId} }; - } catch (std::bad_optional_access) { - return QJsonObject(); - } } QJsonObject AccountsService::getAccountSettings(const QString& accountId, const QString& installationId, const QString &displayName) const @@ -388,19 +368,31 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) auto fleetJson = getDataFromFile(":/Status/StaticConfig/fleets.json").value(); auto infuraKey = getDataFromFile(":/Status/StaticConfig/infura_key").value(); - auto nodeConfigJsonStr = templateNodeConfigJsonStr.replace("%INSTALLATIONID%", installationId) - .replace("%INFURA_KEY%", infuraKey); - QJsonObject nodeConfigJson = QJsonDocument::fromJson(nodeConfigJsonStr.toUtf8()).object(); - QJsonObject clusterConfig = nodeConfigJson["ClusterConfig"].toObject(); + auto templateDefaultNetworksJson = getDataFromFile(":/Status/StaticConfig/default-networks.json").value(); + QString defaultNetworksContent = templateDefaultNetworksJson.replace("%INFURA_TOKEN_RESOLVED%", infuraKey); + QJsonArray defaultNetworksJson = QJsonDocument::fromJson(defaultNetworksContent.toUtf8()).array(); + auto DEFAULT_TORRENT_CONFIG_PORT = 9025; + auto DEFAULT_TORRENT_CONFIG_DATADIR = m_statusgoDataDir / "archivedata"; + auto DEFAULT_TORRENT_CONFIG_TORRENTDIR = m_statusgoDataDir / "torrents"; + + auto nodeConfigJsonStr = templateNodeConfigJsonStr + .replace("%INSTALLATIONID%", installationId) + .replace("%INFURA_TOKEN_RESOLVED%", infuraKey) + .replace("%DEFAULT_TORRENT_CONFIG_PORT%", QString::number(DEFAULT_TORRENT_CONFIG_PORT)) + .replace("%DEFAULT_TORRENT_CONFIG_DATADIR%", DEFAULT_TORRENT_CONFIG_DATADIR.c_str()) + .replace("%DEFAULT_TORRENT_CONFIG_TORRENTDIR%", DEFAULT_TORRENT_CONFIG_TORRENTDIR.c_str()); + QJsonObject nodeConfigJson = QJsonDocument::fromJson(nodeConfigJsonStr.toUtf8()).object(); + + QJsonObject clusterConfig = nodeConfigJson["ClusterConfig"].toObject(); QJsonObject fleetsJson = QJsonDocument::fromJson(fleetJson.toUtf8()).object()["fleets"].toObject(); auto fleet = fleetsJson[Constants::Fleet::Prod].toObject(); - clusterConfig["Fleet"] = Constants::Fleet::Prod; clusterConfig["BootNodes"] = getNodes(fleet, Constants::FleetNodes::Bootnodes); clusterConfig["TrustedMailServers"] = getNodes(fleet, Constants::FleetNodes::Mailservers); clusterConfig["StaticNodes"] = getNodes(fleet, Constants::FleetNodes::Whisper); clusterConfig["RendezvousNodes"] = getNodes(fleet, Constants::FleetNodes::Rendezvous); + clusterConfig["DiscV5BootstrapNodes"] = QJsonArray(); clusterConfig["RelayNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); clusterConfig["StoreNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); clusterConfig["FilterNodes"] = getNodes(fleet, Constants::FleetNodes::Waku); @@ -408,6 +400,17 @@ QJsonObject AccountsService::getDefaultNodeConfig(const QString& installationId) nodeConfigJson["ClusterConfig"] = clusterConfig; + nodeConfigJson["NetworkId"] = defaultNetworksJson[0].toObject()["chainId"]; + nodeConfigJson["DataDir"] = "ethereum"; + auto upstreamConfig = nodeConfigJson["UpstreamConfig"].toObject(); + upstreamConfig["Enabled"] = true; + upstreamConfig["URL"] = defaultNetworksJson[0].toObject()["rpcUrl"]; + nodeConfigJson["UpstreamConfig"] = upstreamConfig; + auto shhextConfig = nodeConfigJson["ShhextConfig"].toObject(); + shhextConfig["InstallationID"] = installationId; + nodeConfigJson["ShhextConfig"] = shhextConfig; + nodeConfigJson["Networks"] = defaultNetworksJson; + nodeConfigJson["KeyStoreDir"] = toQString(fs::relative(m_keyStoreDir, m_statusgoDataDir)); return nodeConfigJson; } catch (std::bad_optional_access) { diff --git a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h index 10893e669..ee0d7ba94 100644 --- a/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h +++ b/libs/Onboarding/src/Onboarding/Accounts/AccountsService.h @@ -46,6 +46,8 @@ public: /// \see ServiceInterface bool setKeyStoreDir(const QString &key) override; + /// \todo login@src/app_service/service/accounts/service.nim adds custom configuration for defaults + /// to account for legacy user DBs. See if this is required to replicate or add proper migration logic QString login(MultiAccount account, const QString& password) override; void clear() override; diff --git a/libs/Onboarding/tests/qml_tests/CMakeLists.txt b/libs/Onboarding/tests/qml_tests/CMakeLists.txt index 4d5a6bd56..ef83f00e3 100644 --- a/libs/Onboarding/tests/qml_tests/CMakeLists.txt +++ b/libs/Onboarding/tests/qml_tests/CMakeLists.txt @@ -12,9 +12,6 @@ add_executable(TestOnboardingQml "main.cpp" ) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - # no need to copy around qml test files for shadow builds - just set the respective define add_compile_definitions(QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/libs/StatusGoQt/CMakeLists.txt b/libs/StatusGoQt/CMakeLists.txt index 5cf6ebf52..9f924dc35 100644 --- a/libs/StatusGoQt/CMakeLists.txt +++ b/libs/StatusGoQt/CMakeLists.txt @@ -9,6 +9,7 @@ project(StatusGoQt set(QT_NO_CREATE_VERSIONLESS_FUNCTIONS true) find_package(nlohmann_json 3.10.5 REQUIRED) +find_package(Boost) find_package(Qt6 ${STATUS_QT_VERSION} COMPONENTS Core Concurrent Gui REQUIRED) qt6_standard_project_setup() @@ -21,6 +22,7 @@ set_property(GLOBAL PROPERTY DEBUG_CONFIGURATIONS Debug) target_link_libraries(${PROJECT_NAME} PUBLIC Status::Helpers + Boost::headers PRIVATE Qt6::Gui @@ -90,7 +92,12 @@ target_sources(${PROJECT_NAME} ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/SignalsManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/SignalsManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/BigInt.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/BigInt.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/DerivedAddress.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/NetworkConfiguration.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/Token.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/wallet_types.h ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/WalletApi.h ${CMAKE_CURRENT_SOURCE_DIR}/src/StatusGo/Wallet/WalletApi.cpp ) diff --git a/libs/StatusGoQt/src/StatusGo/Utils.cpp b/libs/StatusGoQt/src/StatusGo/Utils.cpp index 04a381aee..49f192ca9 100644 --- a/libs/StatusGoQt/src/StatusGo/Utils.cpp +++ b/libs/StatusGoQt/src/StatusGo/Utils.cpp @@ -22,8 +22,9 @@ const char* statusGoCallPrivateRPC(const char* inputJSON) { HashedPassword hashPassword(const QString &str) { + // TODO: is utf8 the standard used by NIM also? Will it unlock DBs encrypted with NIM password hashing? return HashedPassword("0x" + QString::fromUtf8(QCryptographicHash::hash(str.toUtf8(), - QCryptographicHash::Keccak_256).toHex())); + QCryptographicHash::Keccak_256).toHex().toUpper())); } std::optional getRPCErrorInJson(const QJsonObject& json) diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.cpp new file mode 100644 index 000000000..d6ab54a59 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.cpp @@ -0,0 +1,44 @@ +#include "BigInt.h" + +#include +#include + +#include + +namespace Status { +namespace StatusGo::Wallet { + +std::string toHexData(const BigInt &num, bool uppercase) +{ + return num.str(0, std::ios_base::showbase | std::ios_base::hex | (uppercase ? std::ios_base::uppercase : 0)); +} + + +using namespace QtLiterals; + +bool has0xPrefix(const QByteArray &in) { + return in.size() >= 2 && Helpers::iequals(in.first(2), "0x"_qba); +} + +BigInt parseHex(const std::string &value) +{ + auto data = QByteArray::fromRawData(value.c_str(), value.size()); + if (!has0xPrefix(data)) + throw std::runtime_error("BigInt::parseHex missing 0x prefix"); + if (data.size() == 2) + throw std::runtime_error("BigInt::parseHex empty number"); + if (data.size() > 3 && data[2] == '0') + throw std::runtime_error("BigInt::parseHex leading zero"); + if (data.size() > 66) + throw std::runtime_error("BigInt::parseHex more than 256 bits"); + return BigInt{data.data()}; +} + +} // namespace StatusGo::Wallet + +QString toQString(const StatusGo::Wallet::BigInt &num) +{ + return QString::fromStdString(num.str(0, std::ios_base::dec)); +} + +} // namespace Status diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h new file mode 100644 index 000000000..ab17bf22d --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/BigInt.h @@ -0,0 +1,49 @@ +#pragma once + +#include + +#include +#include + +#include + +#include + +using json = nlohmann::json; + +namespace Status { +namespace StatusGo::Wallet { + +using BigInt = boost::multiprecision::uint256_t; + +/// Converts into the 0x prefixed hexadecimal display required by status-go (also uppercase) +std::string toHexData(const BigInt &num, bool uppercase = false); + +/// \throws std::runtime_error if value is not a valid status-go hex string +/// or value is higher than 2^64 (numbers larger than 256 bits are not accepted) +/// \see toHexData +BigInt parseHex(const std::string &value); + +} + +/// Human readable form +QString toQString(const StatusGo::Wallet::BigInt &num); + +} // Status::StatusGo::Wallet + +namespace nlohmann { + +namespace GoWallet = Status::StatusGo::Wallet; + +template<> +struct adl_serializer { + static void to_json(json& j, const GoWallet::BigInt& num) { + j = GoWallet::toHexData(num); + } + + static void from_json(const json& j, GoWallet::BigInt& num) { + num = GoWallet::BigInt(j.get()); + } +}; + +} // namespace nlohmann diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h index 81c6b7bc7..3473390bd 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/DerivedAddress.h @@ -16,11 +16,9 @@ using json = nlohmann::json; namespace Status::StatusGo::Wallet { -/*! - * \brief Define a derived address as returned by the corresponding API - * \note equivalent of status-go's DerivedAddress@api.go - * \see \c getDerivedAddressesForPath - */ +/// \brief Define a derived address as returned by the corresponding API +/// \note equivalent of status-go's DerivedAddress@api.go +/// \see \c getDerivedAddressesForPath struct DerivedAddress { Accounts::EOAddress address; diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/NetworkConfiguration.h b/libs/StatusGoQt/src/StatusGo/Wallet/NetworkConfiguration.h new file mode 100644 index 000000000..e4cdf1104 --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/NetworkConfiguration.h @@ -0,0 +1,42 @@ +#pragma once + +#include "wallet_types.h" + +#include + +#include +#include + +#include + +using json = nlohmann::json; + +/// \note not sure if this is the best namespace, ok for now +namespace Status::StatusGo::Wallet { + +/// \note equivalent of status-go's Network@config.go (params package) +struct NetworkConfiguration +{ + ChainID chainId; + QString chainName; + QUrl rpcUrl; + std::optional blockExplorerUrl; + std::optional iconUrl; + std::optional nativeCurrencyName; + std::optional nativeCurrencySymbol; + unsigned int nativeCurrencyDecimals{}; + bool isTest{}; + unsigned int layer{}; + bool enabled{}; + QColor chainColor; + QString shortName; +}; + +using NetworkConfigurations = std::vector; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(NetworkConfiguration, chainId, chainName, rpcUrl, blockExplorerUrl, + iconUrl, nativeCurrencyName, nativeCurrencySymbol, + nativeCurrencyDecimals, isTest, layer, enabled, chainColor, + shortName); + +} diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/Token.h b/libs/StatusGoQt/src/StatusGo/Wallet/Token.h new file mode 100644 index 000000000..8b31ea50b --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/Token.h @@ -0,0 +1,36 @@ +#pragma once + +#include "wallet_types.h" + +#include "Accounts/accounts_types.h" + +#include + +#include + +#include + +namespace Accounts = Status::StatusGo::Accounts; + +using json = nlohmann::json; + +namespace Status::StatusGo::Wallet { + +/// \note equivalent of status-go's Token@token.go +/// \see \c getDerivedAddressesForPath +struct Token +{ + Accounts::EOAddress address; + QString name; + QString symbol; + QColor color; + unsigned int decimals; + ChainID chainId; +}; + +using Tokens = std::vector; + +NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Token, address, name,symbol, + color, decimals, chainId); + +} \ No newline at end of file diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp index 5eb5998ec..356adcade 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.cpp @@ -34,4 +34,67 @@ DerivedAddresses getDerivedAddressesForPath(const HashedPassword &password, cons return resultJson.get().result; } +NetworkConfigurations getEthereumChains(bool onlyEnabled) +{ + std::vector params = {onlyEnabled}; + json inputJson = { + {"jsonrpc", "2.0"}, + {"method", "wallet_getEthereumChains"}, + {"params", params} + }; + + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); + + const auto &data = resultJson.get().result; + return data.is_null() ? nlohmann::json() : data; +} + +Tokens getTokens(const ChainID &chainId) +{ + std::vector params = {chainId}; + json inputJson = { + {"jsonrpc", "2.0"}, + {"method", "wallet_getTokens"}, + {"params", params} + }; + + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); + + const auto &data = resultJson.get().result; + return data.is_null() ? nlohmann::json() : data; +} + +TokenBalances getTokensBalancesForChainIDs(const std::vector &chainIds, + const std::vector accounts, + const std::vector tokens) +{ + std::vector params = {chainIds, accounts, tokens}; + json inputJson = { + {"jsonrpc", "2.0"}, + {"method", "wallet_getTokensBalancesForChainIDs"}, + {"params", params} + }; + + auto result = Utils::statusGoCallPrivateRPC(inputJson.dump().c_str()); + const auto resultJson = json::parse(result); + checkPrivateRpcCallResultAndReportError(resultJson); + + TokenBalances resultData; + const auto &data = resultJson.get().result; + // Workaround to exception "type must be array, but is object" for custom key-types + // TODO: find out why + std::map> dataMap = data.is_null() ? nlohmann::json() : data; + for(const auto &keyIt : dataMap) { + std::map val; + for(const auto &valIt : keyIt.second) + val.emplace(QString::fromStdString(valIt.first), valIt.second); + resultData.emplace(QString::fromStdString(keyIt.first), std::move(val)); + } + return resultData; +} + } // namespaces diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h index 24d260e9c..a1e4994b0 100644 --- a/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h +++ b/libs/StatusGoQt/src/StatusGo/Wallet/WalletApi.h @@ -2,7 +2,12 @@ #include "Accounts/ChatOrWalletAccount.h" #include "Accounts/accounts_types.h" +#include "BigInt.h" + #include "DerivedAddress.h" +#include "NetworkConfiguration.h" +#include "Token.h" + #include "Types.h" #include @@ -16,4 +21,25 @@ namespace Status::StatusGo::Wallet /// \throws \c CallPrivateRpcError DerivedAddresses getDerivedAddressesForPath(const HashedPassword &password, const Accounts::EOAddress &derivedFrom, const Accounts::DerivationPath &path, int pageSize, int pageNumber); +/// \note status-go's GetEthereumChains@api.go which calls +/// NetworkManager@client.go -> network.Manager.get() +/// \throws \c CallPrivateRpcError +NetworkConfigurations getEthereumChains(bool onlyEnabled); + +/// \note status-go's GetEthereumChains@api.go which calls +/// NetworkManager@client.go -> network.Manager.get() +/// \throws \c CallPrivateRpcError +NetworkConfigurations getEthereumChains(bool onlyEnabled); + + +/// \note status-go's GetTokens@api.go -> TokenManager.getTokens@token.go +/// \throws \c CallPrivateRpcError +Tokens getTokens(const ChainID &chainId); + +using TokenBalances = std::map>; +/// \note status-go's @api.go -> @.go +/// \throws \c CallPrivateRpcError +TokenBalances getTokensBalancesForChainIDs(const std::vector &chainIds, + const std::vector accounts, + const std::vector tokens); } // namespaces diff --git a/libs/StatusGoQt/src/StatusGo/Wallet/wallet_types.h b/libs/StatusGoQt/src/StatusGo/Wallet/wallet_types.h new file mode 100644 index 000000000..3155ed8fd --- /dev/null +++ b/libs/StatusGoQt/src/StatusGo/Wallet/wallet_types.h @@ -0,0 +1,16 @@ +#include +#include + +#include + + +#include + +using json = nlohmann::json; + +/// Defines phantom types for strong typing +namespace Status::StatusGo::Wallet { + +using ChainID = Helpers::NamedType; + +} diff --git a/libs/StatusQ/tests/CMakeLists.txt b/libs/StatusQ/tests/CMakeLists.txt index 8fb02f510..626ff3adb 100644 --- a/libs/StatusQ/tests/CMakeLists.txt +++ b/libs/StatusQ/tests/CMakeLists.txt @@ -12,9 +12,6 @@ add_executable(TestStatusQ "main.cpp" ) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - # no need to copy around qml test files for shadow builds - just set the respective define add_definitions(-DQUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") diff --git a/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h b/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h index fc43d029e..ddd76411f 100644 --- a/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h +++ b/libs/Wallet/include/Status/Wallet/NewWalletAccountController.h @@ -34,7 +34,6 @@ public: /// \note On account creation \c accounts are updated with the newly created wallet account NewWalletAccountController(std::shared_ptr accounts); - ~NewWalletAccountController(); /// Called by QML engine to register the instance. QML takes ownership of the instance static NewWalletAccountController *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine); diff --git a/libs/Wallet/qml/Status/Wallet/AssetView.qml b/libs/Wallet/qml/Status/Wallet/AssetView.qml index 53d9cac08..6a540bc6f 100644 --- a/libs/Wallet/qml/Status/Wallet/AssetView.qml +++ b/libs/Wallet/qml/Status/Wallet/AssetView.qml @@ -9,6 +9,6 @@ Item { Label { anchors.centerIn: parent - text: "$$$$$" + text: asset.name } } diff --git a/libs/Wallet/src/NewWalletAccountController.cpp b/libs/Wallet/src/NewWalletAccountController.cpp index 24144f4b9..6467ae49d 100644 --- a/libs/Wallet/src/NewWalletAccountController.cpp +++ b/libs/Wallet/src/NewWalletAccountController.cpp @@ -31,9 +31,6 @@ NewWalletAccountController::NewWalletAccountController(std::shared_ptr(found) == nullptr; - if(!m_customDerivationPath && !std::get<0>(found).get()->alreadyCreated()) - updateSelectedDerivedAddress(std::get<1>(found), std::get<0>(found)); + const auto &[derivedPath, index]= searchDerivationPath(m_derivationPath); + m_customDerivationPath = derivedPath == nullptr; + if(!m_customDerivationPath && !derivedPath.get()->alreadyCreated()) + updateSelectedDerivedAddress(index, derivedPath); if(m_customDerivationPath != oldCustom) emit customDerivationPathChanged(); diff --git a/resources/default-networks.json b/resources/default-networks.json index 830c9e107..63cccdac6 100644 --- a/resources/default-networks.json +++ b/resources/default-networks.json @@ -1,76 +1,122 @@ [ - { - "id": "testnet_rpc", - "etherscan-link": "https://ropsten.etherscan.io/address/", - "name": "Ropsten with upstream RPC", - "config": { - "NetworkId": 3, - "DataDir": "/ethereum/testnet_rpc", - "UpstreamConfig": { - "Enabled": true, - "URL": "https://ropsten.infura.io/v3/%INFURA_KEY%" - } - } - }, - { - "id": "rinkeby_rpc", - "etherscan-link": "https://rinkeby.etherscan.io/address/", - "name": "Rinkeby with upstream RPC", - "config": { - "NetworkId": 4, - "DataDir": "/ethereum/rinkeby_rpc", - "UpstreamConfig": { - "Enabled": true, - "URL": "https://rinkeby.infura.io/v3/%INFURA_KEY%" - } - } - }, - { - "id": "goerli_rpc", - "etherscan-link": "https://goerli.etherscan.io/address/", - "name": "Goerli with upstream RPC", - "config": { - "NetworkId": 5, - "DataDir": "/ethereum/goerli_rpc", - "UpstreamConfig": { - "Enabled": true, - "URL": "https://goerli.blockscout.com/" - } - } - }, - { - "id": "mainnet_rpc", - "etherscan-link": "https://etherscan.io/address/", - "name": "Mainnet with upstream RPC", - "config": { - "NetworkId": 1, - "DataDir": "/ethereum/mainnet_rpc", - "UpstreamConfig": { - "Enabled": true, - "URL": "https://mainnet.infura.io/v3/%INFURA_KEY%" - } - } - }, - { - "id": "xdai_rpc", - "name": "xDai Chain", - "config": { - "NetworkId": 100, - "DataDir": "/ethereum/xdai_rpc", - "UpstreamConfig": { - "Enabled": true, - "URL": "https://dai.poa.network" - } - } - }, - { - "id": "poa_rpc", - "name": "POA Network", - "config": { - "NetworkId": 99, - "DataDir": "/ethereum/poa_rpc", - "UpstreamConfig": { "Enabled": true, "URL": "https://core.poa.network" } - } - } - ] - \ No newline at end of file + { + "chainId": 1, + "chainName": "Mainnet", + "rpcUrl": "https://mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://etherscan.io/", + "iconUrl": "network/Network=Ethereum", + "chainColor": "#627EEA", + "shortName": "eth", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": false, + "layer": 1, + "enabled": true + }, + { + "chainId": 3, + "chainName": "Ropsten", + "rpcUrl": "https://ropsten.infura.io/v3%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://ropsten.etherscan.io/", + "iconUrl": "network/Network=Tetnet", + "chainColor": "#939BA1", + "shortName": "ropEth", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 1, + "enabled": true + }, + { + "chainId": 4, + "chainName": "Rinkeby", + "rpcUrl": "https://rinkeby.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://rinkeby.etherscan.io/", + "iconUrl": "network/Network=Tetnet", + "chainColor": "#939BA1", + "shortName": "rinEth", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 1, + "enabled": false + }, + { + "chainId": 5, + "chainName": "Goerli", + "rpcUrl": "http://goerli.blockscout.com/", + "blockExplorerUrl": "https://goerli.etherscan.io/", + "iconUrl": "network/Network=Tetnet", + "chainColor": "#939BA1", + "shortName": "goeEth", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 1, + "enabled": false + }, + { + "chainId": 10, + "chainName": "Optimism", + "rpcUrl": "https://optimism-mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://optimistic.etherscan.io", + "iconUrl": "network/Network=Optimism", + "chainColor": "#E90101", + "shortName": "opt", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": false, + "layer": 2, + "enabled": true + }, + { + "chainId": 69, + "chainName": "Optimism Kovan", + "rpcUrl": "https://optimism-kovan.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://kovan-optimistic.etherscan.io", + "iconUrl": "network/Network=Tetnet", + "chainColor": "#939BA1", + "shortName": "kovOpt", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 2, + "enabled": false + }, + { + "chainId": 42161, + "chainName": "Arbitrum", + "rpcUrl": "https://arbitrum-mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": "https://arbiscan.io/", + "iconUrl": "network/Network=Arbitrum", + "chainColor": "#51D0F0", + "shortName": "arb", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": false, + "layer": 2, + "enabled": true + }, + { + "chainId": 421611, + "chainName": "Arbitrum Rinkeby", + "rpcUrl": "https://arbitrum-rinkeby.infura.io/v3/%INFURA_TOKEN_RESOLVED%", + "blockExplorerUrl": " https://testnet.arbiscan.io", + "iconUrl": "network/Network=Tetnet", + "chainColor": "#939BA1", + "shortName": "rinArb", + "nativeCurrencyName": "Ether", + "nativeCurrencySymbol": "ETH", + "nativeCurrencyDecimals": 18, + "isTest": true, + "layer": 2, + "enabled": false + } +] \ No newline at end of file diff --git a/resources/node-config.json b/resources/node-config.json index a10947945..a5030a7c0 100644 --- a/resources/node-config.json +++ b/resources/node-config.json @@ -37,20 +37,26 @@ "MaxMessageDeliveryAttempts": 6, "PFSEnabled": true, "VerifyENSContractAddress": "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e", - "VerifyENSURL": "https://mainnet.infura.io/v3/%INFURA_KEY%", + "VerifyENSURL": "https://mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", "VerifyTransactionChainID": 1, - "VerifyTransactionURL": "https://mainnet.infura.io/v3/%INFURA_KEY%", + "VerifyTransactionURL": "https://mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%", "BandwidthStatsEnabled": true }, + "Web3ProviderConfig": { + "Enabled": true + }, + "EnsConfig": { + "Enabled": true + }, "StatusAccountsConfig": { "Enabled": true }, "UpstreamConfig": { "Enabled": true, - "URL": "https://mainnet.infura.io/v3/%INFURA_KEY%" + "URL": "https://mainnet.infura.io/v3/%INFURA_TOKEN_RESOLVED%" }, "WakuConfig": { - "BloomFilterMode": null, + "BloomFilterMode": true, "Enabled": true, "LightClient": true, "MinimumPoW": 0.001 @@ -59,9 +65,25 @@ "Enabled": false, "Host": "0.0.0.0", "Port": 0, - "LightClient": false + "LightClient": false, + "PersistPeers": true, + "EnableDiscV5": true, + "UDPPort": 0, + "PeerExchange": true, + "AutoUpdate": true }, "WalletConfig": { + "Enabled": true, + "OpenseaAPIKey": "" + }, + "EnsConfig": { "Enabled": true + }, + "Networks": [], + "TorrentConfig": { + "Enabled": false, + "Port": %DEFAULT_TORRENT_CONFIG_PORT%, + "DataDir": "%DEFAULT_TORRENT_CONFIG_DATADIR%", + "TorrentDir": "%DEFAULT_TORRENT_CONFIG_TORRENTDIR%" } } diff --git a/test/libs/StatusGoQt/test_wallet.cpp b/test/libs/StatusGoQt/test_wallet.cpp index 67f298792..b76f24132 100644 --- a/test/libs/StatusGoQt/test_wallet.cpp +++ b/test/libs/StatusGoQt/test_wallet.cpp @@ -161,4 +161,117 @@ TEST(WalletApi, TestGetDerivedAddressesForPath_FromWalletAccount_SecondLevel) ASSERT_TRUE(std::all_of(derivedAddresses.begin(), derivedAddresses.end(), [&testPath](const auto& a) { return a.path.get().startsWith(testPath.get()); })); } +TEST(WalletApi, TestGetEthereumChains) +{ + ScopedTestAccount testAccount(test_info_->name()); + + auto networks = Wallet::getEthereumChains(false); + ASSERT_GT(networks.size(), 0); + const auto &network = networks[0]; + ASSERT_FALSE(network.chainName.isEmpty()); + ASSERT_TRUE(network.rpcUrl.isValid()); +} + +TEST(WalletApi, TestGetTokens) +{ + ScopedTestAccount testAccount(test_info_->name()); + + auto networks = Wallet::getEthereumChains(false); + ASSERT_GT(networks.size(), 0); + auto mainNetIt = std::find_if(networks.begin(), networks.end(), [](const auto &n){ return n.chainName == "Mainnet"; }); + ASSERT_NE(mainNetIt, networks.end()); + const auto &mainNet = *mainNetIt; + + auto tokens = Wallet::getTokens(mainNet.chainId); + + auto sntIt = std::find_if(tokens.begin(), tokens.end(), [](const auto &t){ return t.symbol == "SNT"; }); + ASSERT_NE(sntIt, tokens.end()); + const auto &snt = *sntIt; + ASSERT_EQ(snt.chainId, mainNet.chainId); + ASSERT_TRUE(snt.color.isValid()); +} + +TEST(WalletApi, TestGetTokensBalancesForChainIDs) +{ + ScopedTestAccount testAccount(test_info_->name()); + + auto networks = Wallet::getEthereumChains(false); + ASSERT_GT(networks.size(), 1); + + auto mainNetIt = std::find_if(networks.begin(), networks.end(), [](const auto &n){ return n.chainName == "Mainnet"; }); + ASSERT_NE(mainNetIt, networks.end()); + const auto &mainNet = *mainNetIt; + + auto mainTokens = Wallet::getTokens(mainNet.chainId); + auto sntMainIt = std::find_if(mainTokens.begin(), mainTokens.end(), [](const auto &t){ return t.symbol == "SNT"; }); + ASSERT_NE(sntMainIt, mainTokens.end()); + const auto &sntMain = *sntMainIt; + + auto testNetIt = std::find_if(networks.begin(), networks.end(), [](const auto &n){ return n.chainName == "Ropsten"; }); + ASSERT_NE(testNetIt, networks.end()); + const auto &testNet = *testNetIt; + + auto testTokens = Wallet::getTokens(testNet.chainId); + auto sntTestIt = std::find_if(testTokens.begin(), testTokens.end(), [](const auto &t){ return t.symbol == "STT"; }); + ASSERT_NE(sntTestIt, testTokens.end()); + const auto &sntTest = *sntTestIt; + + auto testAddress = testAccount.firstWalletAccount().address; + auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId, testNet.chainId}, + {testAddress}, + {sntMain.address, sntTest.address}); + ASSERT_GT(balances.size(), 0); + + ASSERT_TRUE(balances.contains(testAddress)); + const auto &addressBallance = balances[testAddress]; + ASSERT_GT(addressBallance.size(), 0); + + ASSERT_TRUE(addressBallance.contains(sntMain.address)); + ASSERT_EQ(toQString(addressBallance.at(sntMain.address)), "0"); + + ASSERT_TRUE(addressBallance.contains(sntTest.address)); + ASSERT_EQ(toQString(addressBallance.at(sntTest.address)), "0"); +} + +TEST(WalletApi, TestGetTokensBalancesForChainIDs_WatchOnlyAccount) +{ + ScopedTestAccount testAccount(test_info_->name()); + + const auto newTestAccountName = u"test_watch_only-name"_qs; + Accounts::addAccountWatch(Accounts::EOAddress("0xdb5ac1a559b02e12f29fc0ec0e37be8e046def49"), newTestAccountName, QColor("fuchsia"), u""_qs); + const auto updatedAccounts = Accounts::getAccounts(); + ASSERT_EQ(updatedAccounts.size(), 3); + + const auto newAccountIt = std::find_if(updatedAccounts.begin(), updatedAccounts.end(), + [&newTestAccountName](const auto& a) { + return a.name == newTestAccountName; + }); + ASSERT_NE(newAccountIt, updatedAccounts.end()); + const auto &newAccount = *newAccountIt; + + auto networks = Wallet::getEthereumChains(false); + ASSERT_GT(networks.size(), 1); + + auto mainNetIt = std::find_if(networks.begin(), networks.end(), [](const auto &n){ return n.chainName == "Mainnet"; }); + ASSERT_NE(mainNetIt, networks.end()); + const auto &mainNet = *mainNetIt; + + auto mainTokens = Wallet::getTokens(mainNet.chainId); + auto sntMainIt = std::find_if(mainTokens.begin(), mainTokens.end(), [](const auto &t){ return t.symbol == "SNT"; }); + ASSERT_NE(sntMainIt, mainTokens.end()); + const auto &sntMain = *sntMainIt; + + auto balances = Wallet::getTokensBalancesForChainIDs({mainNet.chainId}, + {newAccount.address}, + {sntMain.address}); + ASSERT_GT(balances.size(), 0); + + ASSERT_TRUE(balances.contains(newAccount.address)); + const auto &addressBallance = balances[newAccount.address]; + ASSERT_GT(addressBallance.size(), 0); + + ASSERT_TRUE(addressBallance.contains(sntMain.address)); + ASSERT_GT(addressBallance.at(sntMain.address), 0); +} + } // namespace