chore(CPP): integrate tokens balance status-go API (POC)

Exposes status-go API for retrieving networks and tokens
Tests for the exposed API
Introduced boost for the multiprecision library and 256 bits support
for balance.
Update build instructions

Updates: #6321
This commit is contained in:
Stefan 2022-07-21 20:04:11 +02:00 committed by Stefan Dunca
parent 9f6feb67dd
commit 0ba35d3812
27 changed files with 695 additions and 172 deletions

View File

@ -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)

View File

@ -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
```

View File

@ -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()

10
conanfile.txt Normal file
View File

@ -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

View File

@ -4,6 +4,7 @@
#include <type_traits>
#include <utility>
#include <compare>
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<typename T_ = T, typename = IsNotReference<T_>>
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<T> const& get() const {return m_value; }
constexpr T& get() {
return m_value;
}
bool operator<(const NamedType<T, Parameter> &) const = default;
bool operator>(const NamedType<T, Parameter> &) const = default;
bool operator<=(const NamedType<T, Parameter> &) const = default;
bool operator>=(const NamedType<T, Parameter> &) const = default;
bool operator==(const NamedType<T, Parameter> &) const = default;
bool operator!=(const NamedType<T, Parameter> &) const = default;
constexpr std::remove_reference_t<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<T, Parameter>& l, const NamedType<T, Parameter>& r) noexcept {
return l.m_value <=> r.m_value;
};
#else
bool operator<(const NamedType<T, Parameter> &other) const { return m_value < other.m_value; };
bool operator>(const NamedType<T, Parameter> &other) const { return m_value > other.m_value; };
bool operator<=(const NamedType<T, Parameter> &other) const { return m_value <= other.m_value; };
bool operator>=(const NamedType<T, Parameter> &other) const { return m_value >= other.m_value; };
bool operator==(const NamedType<T, Parameter> &other) const { return m_value == other.m_value; };
bool operator!=(const NamedType<T, Parameter> &other) const { return m_value != other.m_value; };
#endif
T &operator=(NamedType<T, Parameter> const& rhs) {
return m_value = rhs.m_value;
};
private:
T m_value;
T m_value{};
};
template <typename T, typename P>
@ -59,7 +73,6 @@ template <typename T, typename Parameter>
struct hash<Status::Helpers::NamedType<T, Parameter>>
{
using NamedType = Status::Helpers::NamedType<T, Parameter>;
using checkIfHashable = typename std::enable_if<NamedType::is_hashable, void>::type;
size_t operator()(NamedType const& x) const
{
@ -67,4 +80,4 @@ struct hash<Status::Helpers::NamedType<T, Parameter>>
}
};
}
} // namespace std

View File

@ -1,8 +1,11 @@
#pragma once
#include "helpers.h"
#include <QString>
#include <QByteArray>
#include <QColor>
#include <QUrl>
#include <filesystem>
@ -31,6 +34,23 @@ struct adl_serializer<QString> {
}
};
using namespace std::string_literals;
template<>
struct adl_serializer<QByteArray> {
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<std::string>();
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<QColor> {
static void to_json(json& j, const QColor& color) {
@ -38,7 +58,18 @@ struct adl_serializer<QColor> {
}
static void from_json(const json& j, QColor& color) {
color = QColor(Status::toQString(j.get<std::string>()));
color = QColor(QString::fromStdString(j.get<std::string>()));
}
};
template<>
struct adl_serializer<QUrl> {
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<std::string>()));
}
};

View File

@ -1,5 +1,9 @@
#pragma once
#include "Helpers/BuildConfiguration.h"
#include <string>
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<typename T>
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);
});
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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}")

View File

@ -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
)

View File

@ -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<RpcError> getRPCErrorInJson(const QJsonObject& json)

View File

@ -0,0 +1,44 @@
#include "BigInt.h"
#include <locale>
#include <iostream>
#include <Helpers/helpers.h>
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

View File

@ -0,0 +1,49 @@
#pragma once
#include <Helpers/conversions.h>
#include <QByteArray>
#include <QString>
#include <nlohmann/json.hpp>
#include <boost/multiprecision/cpp_int.hpp>
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<GoWallet::BigInt> {
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<std::string>());
}
};
} // namespace nlohmann

View File

@ -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;

View File

@ -0,0 +1,42 @@
#pragma once
#include "wallet_types.h"
#include <nlohmann/json.hpp>
#include <QString>
#include <QUrl>
#include <vector>
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<QUrl> blockExplorerUrl;
std::optional<QUrl> iconUrl;
std::optional<QString> nativeCurrencyName;
std::optional<QString> nativeCurrencySymbol;
unsigned int nativeCurrencyDecimals{};
bool isTest{};
unsigned int layer{};
bool enabled{};
QColor chainColor;
QString shortName;
};
using NetworkConfigurations = std::vector<NetworkConfiguration>;
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(NetworkConfiguration, chainId, chainName, rpcUrl, blockExplorerUrl,
iconUrl, nativeCurrencyName, nativeCurrencySymbol,
nativeCurrencyDecimals, isTest, layer, enabled, chainColor,
shortName);
}

View File

@ -0,0 +1,36 @@
#pragma once
#include "wallet_types.h"
#include "Accounts/accounts_types.h"
#include <QColor>
#include <nlohmann/json.hpp>
#include <vector>
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<Token>;
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Token, address, name,symbol,
color, decimals, chainId);
}

View File

@ -34,4 +34,67 @@ DerivedAddresses getDerivedAddressesForPath(const HashedPassword &password, cons
return resultJson.get<CallPrivateRpcResponse>().result;
}
NetworkConfigurations getEthereumChains(bool onlyEnabled)
{
std::vector<json> 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<CallPrivateRpcResponse>().result;
return data.is_null() ? nlohmann::json() : data;
}
Tokens getTokens(const ChainID &chainId)
{
std::vector<json> 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<CallPrivateRpcResponse>().result;
return data.is_null() ? nlohmann::json() : data;
}
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID> &chainIds,
const std::vector<Accounts::EOAddress> accounts,
const std::vector<Accounts::EOAddress> tokens)
{
std::vector<json> 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<CallPrivateRpcResponse>().result;
// Workaround to exception "type must be array, but is object" for custom key-types
// TODO: find out why
std::map<std::string, std::map<std::string, BigInt>> dataMap = data.is_null() ? nlohmann::json() : data;
for(const auto &keyIt : dataMap) {
std::map<Accounts::EOAddress, BigInt> 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

View File

@ -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 <vector>
@ -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<Accounts::EOAddress, std::map<Accounts::EOAddress, BigInt>>;
/// \note status-go's @api.go -> <xx>@<xx>.go
/// \throws \c CallPrivateRpcError
TokenBalances getTokensBalancesForChainIDs(const std::vector<ChainID> &chainIds,
const std::vector<Accounts::EOAddress> accounts,
const std::vector<Accounts::EOAddress> tokens);
} // namespaces

View File

@ -0,0 +1,16 @@
#include <Helpers/NamedType.h>
#include <Helpers/conversions.h>
#include <nlohmann/json.hpp>
#include <QString>
using json = nlohmann::json;
/// Defines phantom types for strong typing
namespace Status::StatusGo::Wallet {
using ChainID = Helpers::NamedType<unsigned long long int, struct ChainIDTag>;
}

View File

@ -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}")

View File

@ -34,7 +34,6 @@ public:
/// \note On account creation \c accounts are updated with the newly created wallet account
NewWalletAccountController(std::shared_ptr<AccountsModel> accounts);
~NewWalletAccountController();
/// Called by QML engine to register the instance. QML takes ownership of the instance
static NewWalletAccountController *create(QQmlEngine *qmlEngine, QJSEngine *jsEngine);

View File

@ -9,6 +9,6 @@ Item {
Label {
anchors.centerIn: parent
text: "$$$$$"
text: asset.name
}
}

View File

@ -31,9 +31,6 @@ NewWalletAccountController::NewWalletAccountController(std::shared_ptr<Helpers::
{
}
NewWalletAccountController::~NewWalletAccountController()
{
}
QAbstractListModel* NewWalletAccountController::mainAccountsModel()
{
@ -58,10 +55,10 @@ void NewWalletAccountController::setDerivationPath(const QString &newDerivationP
emit derivationPathChanged();
auto oldCustom = m_customDerivationPath;
auto found = searchDerivationPath(m_derivationPath);
m_customDerivationPath = std::get<0>(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();

View File

@ -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" }
}
}
]
{
"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
}
]

View File

@ -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%"
}
}

View File

@ -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