diff --git a/.gitignore b/.gitignore index f436280..445ab4e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ src/tor/* !src/tor/.gitkeep src/config-feather.h src/assets/exec/* +feather.AppDir/* diff --git a/BUILDING.md b/BUILDING.md index 89229b8..68e130d 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -22,7 +22,7 @@ git clone --branch master --recursive https://git.featherwallet.org/feather/feat cd feather ``` -Replace `master` with the desired version tag (e.g. `beta-4`) to build the release binary. +Replace `master` with the desired version tag (e.g. `beta-6`) to build the release binary. #### 2. Base image @@ -37,7 +37,7 @@ Building the base image takes a while. You only need to build the base image onc ##### Standalone binary ```bash -docker run --rm -it -v $PWD:/feather -w /feather feather:linux sh -c 'make release-static -j4' +docker run --rm -it -v $PWD:/feather -w /feather feather:linux sh -c 'CHECK_UPDATES=On make release-static -j4' ``` If you're re-running a build make sure to `rm -rf build/` first. @@ -75,7 +75,7 @@ Building the base image takes a while. You only need to build the base image onc #### 3. Build ```bash -docker run --rm -it -v $PWD:/feather -w /feather feather:win sh -c 'make depends root=/depends target=x86_64-w64-mingw32 tag=win-x64 -j4' +docker run --rm -it -v $PWD:/feather -w /feather feather:win sh -c 'CHECK_UPDATES=On make depends root=/depends target=x86_64-w64-mingw32 tag=win-x64 -j4' ``` If you're re-running a build make sure to `rm -rf build/` first. diff --git a/CMakeLists.txt b/CMakeLists.txt index de325cc..2a77d0d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,11 +7,13 @@ set(THREADS_PREFER_PTHREAD_FLAG ON) set(VERSION_MAJOR "0") set(VERSION_MINOR "1") set(VERSION_REVISION "0") -set(VERSION "beta-5") +set(VERSION "beta-6") option(FETCH_DEPS "Download dependencies if they are not found" ON) +option(LOCALMONERO "Include LocalMonero module" ON) option(XMRIG "Include XMRig module" ON) option(TOR_BIN "Path to Tor binary to embed inside Feather" OFF) +option(CHECK_UPDATES "Enable checking for application updates" OFF) option(STATIC "Link libraries statically, requires static Qt") option(USE_DEVICE_TREZOR "Trezor support compilation" OFF) @@ -29,7 +31,7 @@ if(DEBUG) set(CMAKE_VERBOSE_MAKEFILE ON) endif() -set(MONERO_HEAD "41327974116dedccc2f9709d8ad3a8a1f591faed") +set(MONERO_HEAD "36fb05da3394505f8033ceb8806b28909617696f") set(BUILD_GUI_DEPS ON) set(ARCH "x86-64") set(BUILD_64 ON) @@ -139,6 +141,11 @@ if(NOT monero-seed_FOUND) endif() endif() +# libzip +find_package(zlib CONFIG) +find_path(LIBZIP_INCLUDE_DIRS zip.h) +find_library(LIBZIP_LIBRARIES zip) + # Boost if(DEBUG) set(Boost_DEBUG ON) diff --git a/Dockerfile b/Dockerfile index 1520660..9ad2090 100644 --- a/Dockerfile +++ b/Dockerfile @@ -323,17 +323,17 @@ RUN rm /usr/lib/x86_64-linux-gnu/libX11.a && \ cd ../../../.. && \ rm -rf $(pwd) -RUN git clone -b v1.0.23 --depth 1 https://github.com/libusb/libusb && \ +RUN git clone -b v1.0.24 --depth 1 https://github.com/libusb/libusb && \ cd libusb && \ - git reset --hard e782eeb2514266f6738e242cdcb18e3ae1ed06fa && \ + git reset --hard c6a35c56016ea2ab2f19115d2ea1e85e0edae155 && \ ./autogen.sh --disable-shared --enable-static && \ make -j$THREADS && \ make -j$THREADS install && \ rm -rf $(pwd) -RUN git clone -b hidapi-0.9.0 --depth 1 https://github.com/libusb/hidapi && \ +RUN git clone -b hidapi-0.10.1 --depth 1 https://github.com/libusb/hidapi && \ cd hidapi && \ - git reset --hard 7da5cc91fc0d2dbe4df4f08cd31f6ca1a262418f && \ + git reset --hard f6d0073fcddbdda24549199445e844971d3c9cef && \ ./bootstrap && \ ./configure --disable-shared --enable-static && \ make -j$THREADS && \ @@ -384,9 +384,9 @@ RUN git clone -b v3.18.4 --depth 1 https://github.com/Kitware/CMake && \ make -j$THREADS install && \ rm -rf $(pwd) -RUN git clone -b v4.0.2 --depth 1 https://github.com/fukuchi/libqrencode.git && \ +RUN git clone -b v4.1.1 --depth 1 https://github.com/fukuchi/libqrencode.git && \ cd libqrencode && \ - git reset --hard 59ee597f913fcfda7a010a6e106fbee2595f68e4 && \ + git reset --hard 715e29fd4cd71b6e452ae0f4e36d917b43122ce8 && \ cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=/usr . && \ make -j$THREADS && \ make -j$THREADS install && \ @@ -414,3 +414,10 @@ RUN mkdir linuxdeployqt && \ chmod +x linuxdeployqt-7-x86_64.AppImage && \ ./linuxdeployqt-7-x86_64.AppImage --appimage-extract && \ rm linuxdeployqt-7-x86_64.AppImage + +RUN git clone -b v1.7.3 --depth 1 https://github.com/nih-at/libzip.git && \ + cd libzip && \ + git reset --hard 66e496489bdae81bfda8b0088172871d8fda0032 && \ + cmake -DBUILD_SHARED_LIBS=OFF -DCMAKE_INSTALL_PREFIX=/usr . && \ + make -j$THREADS && \ + make -j$THREADS install diff --git a/Dockerfile.windows b/Dockerfile.windows index d85f037..da29f88 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -15,9 +15,9 @@ RUN apt update && \ RUN update-alternatives --set x86_64-w64-mingw32-g++ $(which x86_64-w64-mingw32-g++-posix) && \ update-alternatives --set x86_64-w64-mingw32-gcc $(which x86_64-w64-mingw32-gcc-posix) -RUN git clone -b v0.17.1.9 --depth 1 https://github.com/monero-project/monero && \ +RUN git clone -b v0.17.2.0 --depth 1 https://github.com/monero-project/monero && \ cd monero && \ - git reset --hard 8fef32e45c80aec41f25be9d1d8fb75adc883c64 && \ + git reset --hard f6e63ef260e795aacd408c28008398785b84103a && \ cp -a contrib/depends / && \ cd .. && \ rm -rf monero @@ -34,12 +34,12 @@ RUN git clone git://code.qt.io/qt/qt5.git -b ${QT_VERSION} --depth 1 && \ git clone git://code.qt.io/qt/qttranslations.git -b ${QT_VERSION} --depth 1 && \ git clone git://code.qt.io/qt/qtxmlpatterns.git -b ${QT_VERSION} --depth 1 && \ git clone git://code.qt.io/qt/qtwebsockets.git -b ${QT_VERSION} --depth 1 && \ - OPENSSL_LIBS="-lssl -lcrypto -lpthread -ldl" \ + OPENSSL_LIBS="-lssl -lcrypto -lws2_32" \ ./configure --prefix=/depends/x86_64-w64-mingw32 -xplatform win32-g++ \ -device-option CROSS_COMPILE=/usr/bin/x86_64-w64-mingw32- \ -I $(pwd)/qtbase/src/3rdparty/angle/include \ -opensource -confirm-license -release -static -static-runtime -no-opengl \ - -no-avx -openssl -I /depends/x86_64-w64-mingw32/include -L /depends/x86_64-w64-mingw32/lib \ + -no-avx -openssl-linked -I /depends/x86_64-w64-mingw32/include -L /depends/x86_64-w64-mingw32/lib \ -qt-freetype -qt-harfbuzz -qt-libjpeg -qt-libpng -qt-pcre -qt-zlib \ -skip gamepad -skip location -skip qt3d -skip qtactiveqt -skip qtandroidextras \ -skip qtcanvas3d -skip qtcharts -skip qtconnectivity -skip qtdatavis3d -skip qtdoc \ @@ -122,11 +122,11 @@ RUN wget https://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.16.tar.gz && \ rm -rf $(pwd) # OpenSSL -> Tor -RUN wget https://www.openssl.org/source/openssl-1.1.1.k.tar.gz && \ - echo "892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5 openssl-1.1.1.k.tar.gz" | sha256sum -c && \ - tar -xzf openssl-1.1.1.k.tar.gz && \ - rm openssl-1.1.1.k.tar.gz && \ - cd openssl-1.1.1.k && \ +RUN wget https://www.openssl.org/source/openssl-1.1.1k.tar.gz && \ + echo "892a0875b9872acd04a9fde79b1f943075d5ea162415de3047c327df33fbaee5 openssl-1.1.1k.tar.gz" | sha256sum -c && \ + tar -xzf openssl-1.1.1k.tar.gz && \ + rm openssl-1.1.1k.tar.gz && \ + cd openssl-1.1.1k && \ ./Configure mingw64 no-shared no-dso --cross-compile-prefix=x86_64-w64-mingw32- --prefix=/usr/local/openssl && \ make -j$THREADS && \ make -j$THREADS install_sw && \ @@ -180,3 +180,13 @@ RUN git clone https://git.featherwallet.org/feather/monero-seed.git && \ make -Cbuild -j$THREADS && \ make -Cbuild install && \ rm -rf $(pwd) + +RUN git clone https://github.com/nih-at/libzip.git && \ + cd libzip && \ + git reset --hard 66e496489bdae81bfda8b0088172871d8fda0032 && \ + cmake -DCMAKE_INSTALL_PREFIX=/depends/x86_64-w64-mingw32 \ + -DCMAKE_TOOLCHAIN_FILE=/depends/x86_64-w64-mingw32/share/toolchain.cmake \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_PREFIX_PATH=/usr/x86_64-w64-mingw32 && \ + make -j$THREADS && \ + make -j$THREADS install \ No newline at end of file diff --git a/HACKING.md b/HACKING.md index 01e8155..451c486 100644 --- a/HACKING.md +++ b/HACKING.md @@ -41,9 +41,11 @@ via the `CMAKE_PREFIX_PATH` definition. For me this is: There are some Monero/Feather related options/definitions that you may pass: +- `-DLOCALMONERO=OFF` - disable LocalMonero feature - `-DXMRIG=OFF` - disable XMRig feature - `-DTOR_BIN=/path/to/tor` - Embed a Tor executable inside Feather - `-DDONATE_BEG=OFF` - disable the dreaded donate requests +- `-DCHECK_UPDATES=ON` - enable checking for updates, only for standalone binaries And: diff --git a/Makefile b/Makefile index d1f2073..bde8936 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,9 @@ CMAKEFLAGS = \ -DARCH=x86_64 \ -DBUILD_64=On \ -DBUILD_TESTS=Off \ + -DLOCALMONERO=On \ -DXMRIG=On \ + -DCHECK_UPDATES=Off \ -DTOR_BIN=Off \ -DCMAKE_CXX_STANDARD=11 \ -DCMAKE_VERBOSE_MAKEFILE=On \ @@ -42,6 +44,7 @@ CMAKEFLAGS = \ release-static: CMAKEFLAGS += -DBUILD_TAG="linux-x64" release-static: CMAKEFLAGS += -DTOR_BIN=$(or ${TOR_BIN},OFF) +release-static: CMAKEFLAGS += -DCHECK_UPDATES=$(or ${CHECK_UPDATES}, Off) release-static: CMAKEFLAGS += -DCMAKE_BUILD_TYPE=Release release-static: CMAKEFLAGS += -DREPRODUCIBLE=$(or ${SOURCE_DATE_EPOCH},OFF) release-static: @@ -50,10 +53,11 @@ release-static: depends: mkdir -p build/$(target)/release - cd build/$(target)/release && cmake -D STATIC=ON -DREPRODUCIBLE=$(or ${SOURCE_DATE_EPOCH},OFF) -DTOR_VERSION=$(or ${TOR_VERSION}, OFF) -DTOR_BIN=$(or ${TOR_BIN},OFF) -D DEV_MODE=$(or ${DEV_MODE},OFF) -D BUILD_TAG=$(tag) -D CMAKE_BUILD_TYPE=Release -D CMAKE_TOOLCHAIN_FILE=$(root)/$(target)/share/toolchain.cmake ../../.. && $(MAKE) + cd build/$(target)/release && cmake -D STATIC=ON -DREPRODUCIBLE=$(or ${SOURCE_DATE_EPOCH},OFF) -DTOR_VERSION=$(or ${TOR_VERSION}, OFF) -DTOR_BIN=$(or ${TOR_BIN},OFF) -DCHECK_UPDATES=$(or ${CHECK_UPDATES}, OFF) -D DEV_MODE=$(or ${DEV_MODE},OFF) -D BUILD_TAG=$(tag) -D CMAKE_BUILD_TYPE=Release -D CMAKE_TOOLCHAIN_FILE=$(root)/$(target)/share/toolchain.cmake ../../.. && $(MAKE) mac-release: CMAKEFLAGS += -DSTATIC=Off mac-release: CMAKEFLAGS += -DTOR_BIN=$(or ${TOR_BIN},OFF) +mac-release: CMAKEFLAGS += -DCHECK_UPDATES=$(or ${CHECK_UPDATES}, Off) mac-release: CMAKEFLAGS += -DBUILD_TAG="mac-x64" mac-release: CMAKEFLAGS += -DCMAKE_BUILD_TYPE=Release mac-release: diff --git a/PKGBUILD b/PKGBUILD index c8f7f04..cd94c5d 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -2,24 +2,19 @@ # Contributor: wowario pkgname='monero-feather-git' -pkgver=0.1.0.925ef5683 +pkgver=0.5.0.d1bb4c143f pkgrel=1 pkgdesc='a free Monero desktop wallet' license=('BSD') arch=('x86_64') url="https://featherwallet.org" -depends=('boost-libs' 'libunwind' 'openssl' 'readline' 'pcsclite' 'hidapi' 'protobuf' 'miniupnpc' 'libgcrypt' 'qrencode' 'libsodium' 'libpgm' 'expat' 'qt5-base' 'qt5-websockets' 'tor') +depends=('boost-libs' 'libunwind' 'openssl' 'readline' 'zeromq' 'pcsclite' 'hidapi' 'protobuf' 'libusb' 'libudev.so' 'miniupnpc' 'libgcrypt' 'qrencode' 'libsodium' 'libpgm' 'expat' 'qt5-base' 'qt5-websockets' 'qt5-svg' 'tor' 'libzip') makedepends=('git' 'cmake' 'boost') source=("${pkgname}"::"git+https://git.featherwallet.org/feather/feather") sha256sums=('SKIP') -pkgver() { - cd "${srcdir}/${pkgname}" - printf "%s.%s" "$(git describe --tags --abbrev=0)" "$(git rev-parse --short=9 HEAD)" -} - build() { cd "${srcdir}/${pkgname}" git submodule update --init --recursive @@ -32,4 +27,6 @@ build() { package_monero-feather-git() { install -Dm644 "${srcdir}/${pkgname}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm755 "${srcdir}/${pkgname}/build/bin/feather" "${pkgdir}/usr/bin/feather" + install -Dm644 "${srcdir}/${pkgname}/src/assets/feather.desktop" "${pkgdir}/usr/share/applications/feather.desktop" + install -Dm644 "${srcdir}/${pkgname}/src/assets/images/feather.png" "${pkgdir}/usr/share/pixmaps/feather.png" } diff --git a/cmake/FindHIDAPI.cmake b/cmake/FindHIDAPI.cmake index e69de29..ca1c8e4 100644 --- a/cmake/FindHIDAPI.cmake +++ b/cmake/FindHIDAPI.cmake @@ -0,0 +1,60 @@ +# - try to find HIDAPI library +# from http://www.signal11.us/oss/hidapi/ +# +# Cache Variables: (probably not for direct use in your scripts) +# HIDAPI_INCLUDE_DIR +# HIDAPI_LIBRARY +# +# Non-cache variables you might use in your CMakeLists.txt: +# HIDAPI_FOUND +# HIDAPI_INCLUDE_DIRS +# HIDAPI_LIBRARIES +# +# Requires these CMake modules: +# FindPackageHandleStandardArgs (known included with CMake >=2.6.2) +# +# Original Author: +# 2009-2010 Ryan Pavlik +# http://academic.cleardefinition.com +# Iowa State University HCI Graduate Program/VRAC +# +# Copyright Iowa State University 2009-2010. +# Distributed under the Boost Software License, Version 1.0. +# (See accompanying file LICENSE_1_0.txt or copy at +# http://www.boost.org/LICENSE_1_0.txt) + +find_library(HIDAPI_LIBRARY + NAMES hidapi hidapi-libusb) + +find_path(HIDAPI_INCLUDE_DIR + NAMES hidapi.h + PATH_SUFFIXES + hidapi) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(HIDAPI + DEFAULT_MSG + HIDAPI_LIBRARY + HIDAPI_INCLUDE_DIR) + +if(HIDAPI_FOUND) + set(HIDAPI_LIBRARIES "${HIDAPI_LIBRARY}") + if((STATIC AND UNIX AND NOT APPLE) OR (DEPENDS AND CMAKE_SYSTEM_NAME STREQUAL "Linux")) + find_library(LIBUSB-1.0_LIBRARY usb-1.0) + find_library(LIBUDEV_LIBRARY udev) + if(LIBUSB-1.0_LIBRARY) + set(HIDAPI_LIBRARIES "${HIDAPI_LIBRARIES};${LIBUSB-1.0_LIBRARY}") + if(LIBUDEV_LIBRARY) + set(HIDAPI_LIBRARIES "${HIDAPI_LIBRARIES};${LIBUDEV_LIBRARY}") + else() + message(WARNING "libudev library not found, binaries may fail to link.") + endif() + else() + message(WARNING "libusb-1.0 library not found, binaries may fail to link.") + endif() + endif() + + set(HIDAPI_INCLUDE_DIRS "${HIDAPI_INCLUDE_DIR}") +endif() + +mark_as_advanced(HIDAPI_INCLUDE_DIR HIDAPI_LIBRARY) \ No newline at end of file diff --git a/monero b/monero index 4132797..36fb05d 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 41327974116dedccc2f9709d8ad3a8a1f591faed +Subproject commit 36fb05da3394505f8033ceb8806b28909617696f diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 48312ce..94b8968 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,8 @@ qt5_add_resources(RESOURCES assets.qrc) file(GLOB SOURCE_FILES "*.h" "*.cpp" + "api/*.h" + "api/*.cpp" "utils/*.h" "utils/*.cpp" "libwalletqt/*.h" @@ -116,12 +118,22 @@ target_include_directories(feather PUBLIC ${Qt5Svg_INCLUDE_DIRS} ${Qt5Xml_INCLUDE_DIRS} ${Qt5WebSockets_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} + ${LIBZIP_INCLUDE_DIRS} ) if(DONATE_BEG) target_compile_definitions(feather PRIVATE DONATE_BEG=1) endif() +if (CHECK_UPDATES) + target_compile_definitions(feather PRIVATE CHECK_UPDATES=1) +endif() + +if(LOCALMONERO) + target_compile_definitions(feather PRIVATE HAS_LOCALMONERO=1) +endif() + if(TOR_BIN) target_compile_definitions(feather PRIVATE HAS_TOR_BIN=1) endif() @@ -130,6 +142,9 @@ if(XMRIG) target_compile_definitions(feather PRIVATE HAS_XMRIG=1) endif() +# TODO: PLACEHOLDER +target_compile_definitions(feather PRIVATE HAS_WEBSOCKET=1) + if(HAVE_SYS_PRCTL_H) target_compile_definitions(feather PRIVATE HAVE_SYS_PRCTL_H=1) endif() @@ -191,6 +206,8 @@ target_link_libraries(feather openpgp Threads::Threads ${QRENCODE_LIBRARY} + ${ZLIB_LIBRARIES} + ${LIBZIP_LIBRARIES} ) if(APPLE) diff --git a/src/api/LocalMoneroApi.cpp b/src/api/LocalMoneroApi.cpp new file mode 100644 index 0000000..a047a87 --- /dev/null +++ b/src/api/LocalMoneroApi.cpp @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "LocalMoneroApi.h" + +LocalMoneroApi::LocalMoneroApi(QObject *parent, UtilsNetworking *network, const QString &baseUrl) + : QObject(parent) + , m_network(network) + , m_baseUrl(baseUrl) +{ +} + +void LocalMoneroApi::countryCodes() { + QString url = QString("%1/countrycodes").arg(m_baseUrl); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::COUNTRY_CODES)); +} + +void LocalMoneroApi::currencies() { + QString url = QString("%1/currencies").arg(m_baseUrl); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::CURRENCIES)); +} + +void LocalMoneroApi::paymentMethods(const QString &countryCode) { + QString url; + if (countryCode.isEmpty()) { + url = QString("%1/payment_methods").arg(m_baseUrl); + } else { + url = QString("%1/payment_methods/%2").arg(m_baseUrl, countryCode); + } + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::PAYMENT_METHODS)); +} + +void LocalMoneroApi::buyMoneroOnline(const QString ¤cyCode, const QString &countryCode, + const QString &paymentMethod, const QString &amount, int page) +{ + QString url = this->getBuySellUrl(true, currencyCode, countryCode, paymentMethod, amount, page); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::BUY_MONERO_ONLINE)); +} + +void LocalMoneroApi::sellMoneroOnline(const QString ¤cyCode, const QString &countryCode, + const QString &paymentMethod, const QString &amount, int page) +{ + QString url = this->getBuySellUrl(false, currencyCode, countryCode, paymentMethod, amount, page); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::SELL_MONERO_ONLINE)); +} + +void LocalMoneroApi::accountInfo(const QString &username) { + QString url = QString("%1/account_info/%2").arg(m_baseUrl, username); + QNetworkReply *reply = m_network->getJson(url); + connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::ACCOUNT_INFO)); +} + +void LocalMoneroApi::onResponse(QNetworkReply *reply, LocalMoneroApi::Endpoint endpoint) { + const bool ok = reply->error() == QNetworkReply::NoError; + const QString err = reply->errorString(); + + QByteArray data = reply->readAll(); + + qDebug() << "Response"; + qDebug() << data; + for (const auto header : reply->rawHeaderList()) { + qDebug() << header << ": " << reply->rawHeader(header); + } + + qDebug() << reply->rawHeaderPairs(); + + qDebug() << "Request"; + for (const auto header : reply->request().rawHeaderList()) { + qDebug() << "header: " << header << ": " << reply->request().rawHeader(header); + } + qDebug() << reply->request().url(); + + reply->deleteLater(); + + QJsonObject obj; + if (!data.isEmpty() && Utils::validateJSON(data)) { + auto doc = QJsonDocument::fromJson(data); + obj = doc.object(); + } + else if (!ok) { + emit ApiResponse(LocalMoneroResponse{false, endpoint, err, {}}); + return; + } + else { + emit ApiResponse(LocalMoneroResponse{false, endpoint, "Invalid response from LocalMonero", {}}); + return; + } + + if (obj.contains("error")) { + QString errorStr = QJsonDocument(obj["error"].toObject()).toJson(QJsonDocument::Compact); + emit ApiResponse(LocalMoneroResponse{false, endpoint, errorStr, obj}); + return; + } + + emit ApiResponse(LocalMoneroResponse{true, endpoint, "", obj}); +} + +QString LocalMoneroApi::getBuySellUrl(bool buy, const QString ¤cyCode, const QString &countryCode, + const QString &paymentMethod, const QString &amount, int page) +{ + QString url = QString("%1/%2-monero-online/%3").arg(m_baseUrl, buy ? "buy" : "sell", currencyCode); + if (!countryCode.isEmpty() && paymentMethod.isEmpty()) + url += QString("/%1").arg(countryCode); + else if (countryCode.isEmpty() && !paymentMethod.isEmpty()) + url += QString("/%1").arg(paymentMethod); + else if (!countryCode.isEmpty() && !paymentMethod.isEmpty()) + url += QString("/%1/%2").arg(countryCode, paymentMethod); + + QUrlQuery query; + if (!amount.isEmpty() && amount != "0") + query.addQueryItem("amount", amount); + if (page > 0) + query.addQueryItem("page", QString::number(page)); + url += "?" + query.toString(); + return url; +} \ No newline at end of file diff --git a/src/api/LocalMoneroApi.h b/src/api/LocalMoneroApi.h new file mode 100644 index 0000000..14b6c82 --- /dev/null +++ b/src/api/LocalMoneroApi.h @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_LOCALMONEROAPI_H +#define FEATHER_LOCALMONEROAPI_H + +#include +#include "utils/networking.h" + +class LocalMoneroApi : public QObject { + Q_OBJECT + +public: + enum Endpoint { + COUNTRY_CODES, + CURRENCIES, + PAYMENT_METHODS, + BUY_MONERO_ONLINE, + SELL_MONERO_ONLINE, + ACCOUNT_INFO + }; + + struct LocalMoneroResponse { + bool ok; + Endpoint endpoint; + QString message; + QJsonObject obj; + }; + + explicit LocalMoneroApi(QObject *parent, UtilsNetworking *network, const QString &baseUrl = "https://agoradesk.com/api/v1"); + + void countryCodes(); + void currencies(); + void paymentMethods(const QString &countryCode = ""); + void buyMoneroOnline(const QString ¤cyCode, const QString &countryCode="", const QString &paymentMethod="", const QString &amount = "", int page = 0); + void sellMoneroOnline(const QString ¤cyCode, const QString &countryCode="", const QString &paymentMethod="", const QString &amount = "", int page = 0); + void accountInfo(const QString &username); + +signals: + void ApiResponse(LocalMoneroResponse resp); + +private slots: + void onResponse(QNetworkReply *reply, Endpoint endpoint); + +private: + QString getBuySellUrl(bool buy, const QString ¤cyCode, const QString &countryCode="", const QString &paymentMethod="", const QString &amount = "", int page = 0); + + QString m_baseUrl; + UtilsNetworking *m_network; +}; + + +#endif //FEATHER_LOCALMONEROAPI_H diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 34ef1e0..de5c2e1 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -13,20 +13,16 @@ #include "libwalletqt/Coins.h" #include "model/TransactionHistoryModel.h" #include "model/SubaddressModel.h" +#include "utils/NetworkManager.h" +#include "utils/WebsocketClient.h" +#include "utils/WebsocketNotifier.h" - -Prices *AppContext::prices = nullptr; WalletKeysFilesModel *AppContext::wallets = nullptr; -TxFiatHistory *AppContext::txFiatHistory = nullptr; -double AppContext::balance = 0; QMap AppContext::txCache; AppContext::AppContext(QCommandLineParser *cmdargs) { - this->network = new QNetworkAccessManager(); - this->networkClearnet = new QNetworkAccessManager(); this->cmdargs = cmdargs; - this->isTorSocks = Utils::isTorsocks(); this->isTails = TailsOS::detect(); this->isWhonix = WhonixOS::detect(); @@ -45,21 +41,20 @@ AppContext::AppContext(QCommandLineParser *cmdargs) { // ----------------- Network Type ----------------- - if (this->cmdargs->isSet("stagenet")) + if (this->cmdargs->isSet("stagenet")) { this->networkType = NetworkType::STAGENET; - else if (this->cmdargs->isSet("testnet")) + config()->set(Config::networkType, NetworkType::STAGENET); + } + else if (this->cmdargs->isSet("testnet")) { this->networkType = NetworkType::TESTNET; - else + config()->set(Config::networkType, NetworkType::TESTNET); + } + else { this->networkType = NetworkType::MAINNET; + config()->set(Config::networkType, NetworkType::MAINNET); + } - - this->nodes = new Nodes(this, this->networkClearnet); - connect(this, &AppContext::nodeSourceChanged, this->nodes, &Nodes::onNodeSourceChanged); - connect(this, &AppContext::setCustomNodes, this->nodes, &Nodes::setCustomNodes); - - // Tor & socks proxy - this->ws = new WSClient(this, globals::websocketUrl); - connect(this->ws, &WSClient::WSMessage, this, &AppContext::onWSMessage); + this->nodes = new Nodes(this, this); // Store the wallet every 2 minutes m_storeTimer.start(2 * 60 * 1000); @@ -67,31 +62,6 @@ AppContext::AppContext(QCommandLineParser *cmdargs) { this->storeWallet(); }); - // restore height lookup - this->initRestoreHeights(); - - // price history lookup - auto genesis_timestamp = this->restoreHeights[NetworkType::Type::MAINNET]->data.firstKey(); - AppContext::txFiatHistory = new TxFiatHistory(genesis_timestamp, configDir); - connect(this->ws, &WSClient::connectionEstablished, AppContext::txFiatHistory, &TxFiatHistory::onUpdateDatabase); - connect(AppContext::txFiatHistory, &TxFiatHistory::requestYear, [=](int year){ - QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1}})").arg(year).toUtf8(); - this->ws->sendMsg(data); - }); - connect(AppContext::txFiatHistory, &TxFiatHistory::requestYearMonth, [=](int year, int month) { - QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1, "month": %2}})").arg(year).arg(month).toUtf8(); - this->ws->sendMsg(data); - }); - - // fiat/crypto lookup - AppContext::prices = new Prices(); - - // XMRig -#ifdef HAS_XMRIG - this->XMRig = new XmRig(configDir, this); - this->XMRig->prepare(); -#endif - this->walletManager = WalletManager::instance(); QString logPath = QString("%1/daemon.log").arg(configDir); Monero::Utils::onStartup(); @@ -108,23 +78,34 @@ AppContext::AppContext(QCommandLineParser *cmdargs) { // libwallet connects connect(this->walletManager, &WalletManager::walletOpened, this, &AppContext::onWalletOpened); + connect(this->walletManager, &WalletManager::walletCreated, this, &AppContext::onWalletCreated); + connect(this->walletManager, &WalletManager::deviceButtonRequest, this, &AppContext::onDeviceButtonRequest); + connect(this->walletManager, &WalletManager::deviceError, this, &AppContext::onDeviceError); + + // TODO: move me + connect(websocketNotifier(), &WebsocketNotifier::NodesReceived, this->nodes, &Nodes::onWSNodesReceived); + + m_rpc = new DaemonRpc{this, getNetworkTor(), ""}; } void AppContext::initTor() { - this->tor = new Tor(this, this); - this->tor->start(); + if (this->cmdargs->isSet("tor-host")) + config()->set(Config::socks5Host, this->cmdargs->value("tor-host")); + if (this->cmdargs->isSet("tor-port")) + config()->set(Config::socks5Port, this->cmdargs->value("tor-port")); + if (this->cmdargs->isSet("use-local-tor")) + config()->set(Config::useLocalTor, true); - if (!(isWhonix) && !(isTorSocks)) { - this->networkProxy = new QNetworkProxy(QNetworkProxy::Socks5Proxy, Tor::torHost, Tor::torPort); - this->network->setProxy(*networkProxy); - if (globals::websocketUrl.host().endsWith(".onion")) { - this->ws->webSocket.setProxy(*networkProxy); - } - } + torManager()->init(); + torManager()->start(); + + connect(torManager(), &TorManager::connectionStateChanged, &websocketNotifier()->websocketClient, &WebsocketClient::onToggleConnect); + + this->onTorSettingsChanged(); } void AppContext::initWS() { - this->ws->start(); + websocketNotifier()->websocketClient.start(); } void AppContext::onCancelTransaction(PendingTransaction *tx, const QVector &address) { @@ -134,7 +115,7 @@ void AppContext::onCancelTransaction(PendingTransaction *tx, const QVectorcurrentWallet->disposeTransaction(tx); } -void AppContext::onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const { +void AppContext::onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) { if(this->currentWallet == nullptr){ qCritical() << "Cannot create transaction; no wallet loaded"; return; @@ -146,6 +127,8 @@ void AppContext::onSweepOutput(const QString &keyImage, QString address, bool ch qCritical() << "Creating transaction"; this->currentWallet->createTransactionSingleAsync(keyImage, address, outputs, this->tx_priority); + + emit initiateTransaction(); } void AppContext::onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all) { @@ -249,6 +232,17 @@ void AppContext::onOpenWallet(const QString &path, const QString &password){ this->walletManager->openWalletAsync(path, password, this->networkType, 1); } +void AppContext::onWalletCreated(Wallet * wallet) { + // Currently only called when a wallet is created from device. + auto state = wallet->status(); + if (state != Wallet::Status_Ok) { + emit walletCreatedError(wallet->errorString()); + return; + } + + this->onWalletOpened(wallet); +} + void AppContext::onPreferredFiatCurrencyChanged(const QString &symbol) { if(this->currentWallet) { auto *model = this->currentWallet->transactionHistoryModel(); @@ -276,9 +270,6 @@ void AppContext::commitTransaction(PendingTransaction *tx) { } void AppContext::onMultiBroadcast(PendingTransaction *tx) { - UtilsNetworking *net = new UtilsNetworking(this->network, this); - DaemonRpc *rpc = new DaemonRpc(this, net, ""); - int count = tx->txCount(); for (int i = 0; i < count; i++) { QString txData = tx->signedTxToHex(i); @@ -286,40 +277,81 @@ void AppContext::onMultiBroadcast(PendingTransaction *tx) { for (const auto& node: this->nodes->websocketNodes()) { if (!node.online) continue; - QString address = node.as_url(); + QString address = node.toURL(); qDebug() << QString("Relaying %1 to: %2").arg(tx->txid()[i], address); - rpc->setDaemonAddress(address); - rpc->sendRawTransaction(txData); + m_rpc->setDaemonAddress(address); + m_rpc->sendRawTransaction(txData); } } } +void AppContext::onDeviceButtonRequest(quint64 code) { + emit deviceButtonRequest(code); +} + +void AppContext::onDeviceError(const QString &message) { + qCritical() << "Device error: " << message; + emit deviceError(message); +} + +void AppContext::onTorSettingsChanged() { + if (WhonixOS::detect() || Utils::isTorsocks()) { + return; + } + + // use local tor -> bundled tor + QString host = config()->get(Config::socks5Host).toString(); + quint16 port = config()->get(Config::socks5Port).toString().toUShort(); + if (!torManager()->isLocalTor()) { + host = torManager()->featherTorHost; + port = torManager()->featherTorPort; + } + + QNetworkProxy proxy{QNetworkProxy::Socks5Proxy, host, port}; + getNetworkTor()->setProxy(proxy); + websocketNotifier()->websocketClient.webSocket.setProxy(proxy); + + this->nodes->connectToNode(); + + auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); + qDebug() << "Changed privacyLevel to " << privacyLevel; +} + +void AppContext::onInitialNetworkConfigured() { + this->initTor(); + this->initWS(); +} + void AppContext::onWalletOpened(Wallet *wallet) { auto state = wallet->status(); if (state != Wallet::Status_Ok) { auto errMsg = wallet->errorString(); - if(errMsg == QString("basic_string::_M_replace_aux") || errMsg == QString("std::bad_alloc")) { + if (state == Wallet::Status_BadPassword) { + this->closeWallet(false); + // Don't show incorrect password when we try with empty password for the first time + bool showIncorrectPassword = m_openWalletTriedOnce; + m_openWalletTriedOnce = true; + emit walletOpenPasswordNeeded(showIncorrectPassword, wallet->path()); + } + else if (errMsg == QString("basic_string::_M_replace_aux") || errMsg == QString("std::bad_alloc")) { qCritical() << errMsg; this->walletManager->clearWalletCache(this->walletPath); errMsg = QString("%1\n\nAttempted to clean wallet cache. Please restart Feather.").arg(errMsg); this->closeWallet(false); emit walletOpenedError(errMsg); - } else if(errMsg.contains("wallet cannot be opened as")) { - this->closeWallet(false); - emit walletOpenedError(errMsg); - } else if(errMsg.contains("is opened by another wallet program")) { - this->closeWallet(false); - emit walletOpenedError(errMsg); } else { this->closeWallet(false); - emit walletOpenPasswordNeeded(!this->walletPassword.isEmpty(), wallet->path()); + emit walletOpenedError(errMsg); } + return; } + m_openWalletTriedOnce = false; this->refreshed = false; this->currentWallet = wallet; this->walletPath = this->currentWallet->path() + ".keys"; + this->walletPassword = this->currentWallet->getPassword(); config()->set(Config::walletPath, this->walletPath); connect(this->currentWallet, &Wallet::moneySpent, this, &AppContext::onMoneySpent); @@ -331,6 +363,8 @@ void AppContext::onWalletOpened(Wallet *wallet) { connect(this->currentWallet, &Wallet::transactionCommitted, this, &AppContext::onTransactionCommitted); connect(this->currentWallet, &Wallet::heightRefreshed, this, &AppContext::onHeightRefreshed); connect(this->currentWallet, &Wallet::transactionCreated, this, &AppContext::onTransactionCreated); + connect(this->currentWallet, &Wallet::deviceError, this, &AppContext::onDeviceError); + connect(this->currentWallet, &Wallet::deviceButtonRequest, this, &AppContext::onDeviceButtonRequest); emit walletOpened(); @@ -346,142 +380,6 @@ void AppContext::onWalletOpened(Wallet *wallet) { // force trigger preferredFiat signal for history model this->onPreferredFiatCurrencyChanged(config()->get(Config::preferredFiatCurrency).toString()); - this->setWindowTitle(); -} - -void AppContext::setWindowTitle(bool mining) { - QFileInfo fileInfo(this->walletPath); - auto title = QString("Feather - [%1]").arg(fileInfo.fileName()); - if(this->currentWallet && this->currentWallet->viewOnly()) - title += " [view-only]"; - if(mining) - title += " [mining]"; - - emit setTitle(title); -} - -void AppContext::onWSMessage(const QJsonObject &msg) { - QString cmd = msg.value("cmd").toString(); - - if (cmd == "blockheights") { - QJsonObject data = msg.value("data").toObject(); - int mainnet = data.value("mainnet").toInt(); - int stagenet = data.value("stagenet").toInt(); - - this->heights[NetworkType::MAINNET] = mainnet; - this->heights[NetworkType::STAGENET] = stagenet; - } - - else if(cmd == "nodes") { - this->onWSNodes(msg.value("data").toArray()); - } -#if defined(HAS_XMRIG) - else if(cmd == "xmrig") { - this->XMRigDownloads(msg.value("data").toObject()); - } -#endif - else if(cmd == "crypto_rates") { - QJsonArray crypto_rates = msg.value("data").toArray(); - AppContext::prices->cryptoPricesReceived(crypto_rates); - } - - else if(cmd == "fiat_rates") { - QJsonObject fiat_rates = msg.value("data").toObject(); - AppContext::prices->fiatPricesReceived(fiat_rates); - } - else if(cmd == "reddit") { - QJsonArray reddit_data = msg.value("data").toArray(); - this->onWSReddit(reddit_data); - } - - else if(cmd == "ccs") { - auto ccs_data = msg.value("data").toArray(); - this->onWSCCS(ccs_data); - } - - else if(cmd == "txFiatHistory") { - auto txFiatHistory_data = msg.value("data").toObject(); - AppContext::txFiatHistory->onWSData(txFiatHistory_data); - } -} - -void AppContext::onWSNodes(const QJsonArray &nodes) { - QList> l; - for (auto &&entry: nodes) { - auto obj = entry.toObject(); - auto nettype = obj.value("nettype"); - auto type = obj.value("type"); - - // filter remote node network types - if(nettype == "mainnet" && this->networkType != NetworkType::MAINNET) - continue; - if(nettype == "stagenet" && this->networkType != NetworkType::STAGENET) - continue; - if(nettype == "testnet" && this->networkType != NetworkType::TESTNET) - continue; - - if(type == "clearnet" && (this->isTails || this->isWhonix || this->isTorSocks)) - continue; - if(type == "tor" && (!(this->isTails || this->isWhonix || this->isTorSocks))) - continue; - - auto node = new FeatherNode( - obj.value("address").toString(), - obj.value("height").toInt(), - obj.value("target_height").toInt(), - obj.value("online").toBool()); - QSharedPointer r = QSharedPointer(node); - l.append(r); - } - this->nodes->onWSNodesReceived(l); -} - -void AppContext::onWSReddit(const QJsonArray& reddit_data) { - QList> l; - - for (auto &&entry: reddit_data) { - auto obj = entry.toObject(); - auto redditPost = new RedditPost( - obj.value("title").toString(), - obj.value("author").toString(), - obj.value("permalink").toString(), - obj.value("comments").toInt()); - QSharedPointer r = QSharedPointer(redditPost); - l.append(r); - } - - emit redditUpdated(l); -} - -void AppContext::onWSCCS(const QJsonArray &ccs_data) { - QList> l; - - - QStringList fonts = {"state", "address", "author", "date", - "title", "target_amount", "raised_amount", - "percentage_funded", "contributions"}; - - for (auto &&entry: ccs_data) { - auto obj = entry.toObject(); - auto c = QSharedPointer(new CCSEntry()); - - if (obj.value("state").toString() != "FUNDING-REQUIRED") - continue; - - c->state = obj.value("state").toString(); - c->address = obj.value("address").toString(); - c->author = obj.value("author").toString(); - c->date = obj.value("date").toString(); - c->title = obj.value("title").toString(); - c->url = obj.value("url").toString(); - c->target_amount = obj.value("target_amount").toDouble(); - c->raised_amount = obj.value("raised_amount").toDouble(); - c->percentage_funded = obj.value("percentage_funded").toDouble(); - c->contributions = obj.value("contributions").toInt(); - l.append(c); - } - - emit ccsUpdated(l); } void AppContext::createConfigDirectory(const QString &dir) { @@ -528,7 +426,18 @@ void AppContext::createWallet(FeatherSeed seed, const QString &path, const QStri return; } - this->createWalletFinish(password); + this->onWalletOpened(wallet); +} + +void AppContext::createWalletFromDevice(const QString &path, const QString &password, int restoreHeight) { + if(Utils::fileExists(path)) { + auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path); + qCritical() << err; + emit walletCreatedError(err); + return; + } + + this->walletManager->createWalletFromDeviceAsync(path, password, this->networkType, "Ledger", restoreHeight); } void AppContext::createWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, bool deterministic) { @@ -539,7 +448,7 @@ void AppContext::createWalletFromKeys(const QString &path, const QString &passwo return; } - if(!this->walletManager->addressValid(address, this->networkType)) { + if(!WalletManager::addressValid(address, this->networkType)) { auto err = QString("Failed to create wallet. Invalid address provided.").arg(path); qCritical() << err; emit walletCreatedError(err); @@ -560,21 +469,8 @@ void AppContext::createWalletFromKeys(const QString &path, const QString &passwo return; } - this->currentWallet = this->walletManager->createWalletFromKeys(path, this->seedLanguage, this->networkType, address, viewkey, spendkey, restoreHeight); - this->createWalletFinish(password); -} - -void AppContext::createWalletFinish(const QString &password) { - this->currentWallet->setPassword(password); - this->currentWallet->store(); - this->walletPassword = password; - emit walletCreated(this->currentWallet); -} - -void AppContext::initRestoreHeights() { - restoreHeights[NetworkType::TESTNET] = new RestoreHeightLookup(NetworkType::TESTNET); - restoreHeights[NetworkType::STAGENET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_stagenet.txt", NetworkType::STAGENET); - restoreHeights[NetworkType::MAINNET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_mainnet.txt", NetworkType::MAINNET); + Wallet *wallet = this->walletManager->createWalletFromKeys(path, password, this->seedLanguage, this->networkType, address, viewkey, spendkey, restoreHeight); + this->walletManager->walletOpened(wallet); } void AppContext::onSetRestoreHeight(quint64 height){ @@ -643,7 +539,7 @@ void AppContext::donateBeg() { if (this->currentWallet->viewOnly()) return; auto donationCounter = config()->get(Config::donateBeg).toInt(); - if(donationCounter == -1) + if (donationCounter == -1) return; // previously donated donationCounter += 1; @@ -688,7 +584,13 @@ void AppContext::onWalletUpdate() { this->updateBalance(); } -void AppContext::onWalletRefreshed(bool success) { +void AppContext::onWalletRefreshed(bool success, const QString &message) { + if (!success) { + // Something went wrong during refresh, in some cases we need to notify the user + qCritical() << "Exception during refresh: " << message; // Can't use ->errorString() here, other SLOT might snipe it first + return; + } + if (!this->refreshed) { refreshModels(); this->refreshed = true; @@ -727,6 +629,8 @@ void AppContext::onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, q } void AppContext::onTransactionCreated(PendingTransaction *tx, const QVector &address) { + qDebug() << Q_FUNC_INFO; + for (auto &addr : address) { if (addr == globals::donationAddress) { this->donationSending = true; @@ -778,11 +682,10 @@ void AppContext::updateBalance() { if (!this->currentWallet) return; - quint64 balance_u = this->currentWallet->balance(); - AppContext::balance = balance_u / globals::cdiv; - double spendable = this->currentWallet->unlockedBalance(); + quint64 balance = this->currentWallet->balance(); + quint64 spendable = this->currentWallet->unlockedBalance(); - emit balanceUpdated(balance_u, spendable); + emit balanceUpdated(balance, spendable); } void AppContext::syncStatusUpdated(quint64 height, quint64 target) { diff --git a/src/appcontext.h b/src/appcontext.h index 9500a03..35435b7 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -5,16 +5,13 @@ #define FEATHER_APPCONTEXT_H #include -#include -#include #include #include "utils/tails.h" #include "utils/whonix.h" #include "utils/prices.h" #include "utils/networking.h" -#include "utils/tor.h" -#include "utils/xmrig.h" +#include "utils/TorManager.h" #include "utils/wsclient.h" #include "utils/txfiathistory.h" #include "utils/FeatherSeed.h" @@ -28,9 +25,6 @@ #include "utils/keysfiles.h" #include "PendingTransaction.h" -#define SUBADDRESS_LOOKAHEAD_MINOR 200 - - class AppContext : public QObject { Q_OBJECT @@ -43,7 +37,6 @@ public: bool isTails = false; bool isWhonix = false; - bool isTorSocks = false; bool donationSending = false; @@ -54,25 +47,13 @@ public: QString walletPassword = ""; NetworkType::Type networkType; - QMap heights; - QMap restoreHeights; PendingTransaction::Priority tx_priority = PendingTransaction::Priority::Priority_Low; QString seedLanguage = "English"; // 14 word `monero-seed` only has English - QNetworkAccessManager *network; - QNetworkAccessManager *networkClearnet; - QNetworkProxy *networkProxy{}; + Nodes *nodes; // TODO: move this to mainwindow (?) - Tor *tor{}; - WSClient *ws; - XmRig *XMRig; - Nodes *nodes; - DaemonRpc *daemonRpc; - static Prices *prices; static WalletKeysFilesModel *wallets; - static double balance; static QMap txCache; - static TxFiatHistory *txFiatHistory; static void createConfigDirectory(const QString &dir); @@ -81,17 +62,15 @@ public: WalletManager *walletManager; Wallet *currentWallet = nullptr; void createWallet(FeatherSeed seed, const QString &path, const QString &password, const QString &seedOffset = ""); + void createWalletFromDevice(const QString &path, const QString &password, int restoreHeight); void createWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, bool deterministic = false); - void createWalletFinish(const QString &password); void commitTransaction(PendingTransaction *tx); void syncStatusUpdated(quint64 height, quint64 target); void updateBalance(); void initTor(); - void initRestoreHeights(); void initWS(); void donateBeg(); void refreshModels(); - void setWindowTitle(bool mining = false); // Closes the currently opened wallet void closeWallet(bool emitClosedSignal = true, bool storeWallet = false); @@ -99,28 +78,28 @@ public: public slots: void onOpenWallet(const QString& path, const QString &password); + void onWalletCreated(Wallet * wallet); void onCreateTransaction(const QString &address, quint64 amount, const QString &description, bool all); void onCreateTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); void onCancelTransaction(PendingTransaction *tx, const QVector &address); - void onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs) const; + void onSweepOutput(const QString &keyImage, QString address, bool churn, int outputs); void onCreateTransactionError(const QString &msg); void onOpenAliasResolve(const QString &openAlias); void onSetRestoreHeight(quint64 height); void onPreferredFiatCurrencyChanged(const QString &symbol); void onAmountPrecisionChanged(int precision); void onMultiBroadcast(PendingTransaction *tx); + void onDeviceButtonRequest(quint64 code); + void onTorSettingsChanged(); + void onInitialNetworkConfigured(); + void onDeviceError(const QString &message); private slots: - void onWSNodes(const QJsonArray &nodes); - void onWSMessage(const QJsonObject& msg); - void onWSCCS(const QJsonArray &ccs_data); - void onWSReddit(const QJsonArray& reddit_data); - void onMoneySpent(const QString &txId, quint64 amount); void onMoneyReceived(const QString &txId, quint64 amount); void onUnconfirmedMoneyReceived(const QString &txId, quint64 amount); void onWalletUpdate(); - void onWalletRefreshed(bool success); + void onWalletRefreshed(bool success, const QString &message); void onWalletOpened(Wallet *wallet); void onWalletNewBlock(quint64 blockheight, quint64 targetHeight); void onHeightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight); @@ -148,12 +127,6 @@ signals: void createTransactionError(QString message); void createTransactionCancelled(const QVector &address, double amount); void createTransactionSuccess(PendingTransaction *tx, const QVector &address); - void redditUpdated(QList> &posts); - void nodesUpdated(QList> &nodes); - void ccsUpdated(QList> &entries); - void nodeSourceChanged(NodeSource nodeSource); - void XMRigDownloads(const QJsonObject &data); - void setCustomNodes(QList nodes); void openAliasResolveError(const QString &msg); void openAliasResolved(const QString &address, const QString &openAlias); void setRestoreHeightError(const QString &msg); @@ -162,10 +135,14 @@ signals: void donationNag(); void initiateTransaction(); void endTransaction(); - void setTitle(const QString &title); // set window title + void deviceButtonRequest(quint64 code); + void updatesAvailable(const QJsonObject &updates); + void deviceError(const QString &message); private: + DaemonRpc *m_rpc; QTimer m_storeTimer; + bool m_openWalletTriedOnce = false; }; #endif //FEATHER_APPCONTEXT_H diff --git a/src/assets.qrc b/src/assets.qrc index 5a341e7..8f7488a 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -5,6 +5,7 @@ assets/contributors.txt assets/feather.desktop assets/nodes.json + assets/gpg_keys/featherwallet.asc assets/images/appicons/32x32.png assets/images/appicons/48x48.png assets/images/appicons/64x64.png @@ -44,10 +45,18 @@ assets/images/gnome-calc.png assets/images/history.png assets/images/info.png + assets/images/info2.svg assets/images/key.png assets/images/ledger.png assets/images/ledger_unpaired.png assets/images/lightning.png + assets/images/localMonero_search.svg + assets/images/localMonero_buy.svg + assets/images/localMonero_buy_white.svg + assets/images/localMonero_sell.svg + assets/images/localMonero_sell_white.svg + assets/images/localMonero_logo.png + assets/images/localMonero_register.svg assets/images/lock.png assets/images/lock_icon.png assets/images/lock.svg @@ -63,6 +72,12 @@ assets/images/revealer_c.png assets/images/revealer.png assets/images/seal.png + assets/images/securityLevelSafer.png + assets/images/securityLevelSafest.png + assets/images/securityLevelStandard.png + assets/images/securityLevelSaferWhite.png + assets/images/securityLevelSafestWhite.png + assets/images/securityLevelStandardWhite.png assets/images/seed.png assets/images/speaker.png assets/images/status_connected_fork.png diff --git a/src/assets/gpg_keys/featherwallet.asc b/src/assets/gpg_keys/featherwallet.asc new file mode 100644 index 0000000..c047b2f --- /dev/null +++ b/src/assets/gpg_keys/featherwallet.asc @@ -0,0 +1,52 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQINBF/HogkBEAChsjCJUsZhDxOx5FrnRA3X5/mJd2xdKskLSPFtnYiQUtKvpRW6 +i/RVNMkTwFovzbXB6ucKJtY+OoEMu7xDhIkDWp//UlfHuP9AWAvqbhq6V5xVrZ41 +9oQ7JNN4gwAI8+ZjcNq3IVFQQ9mZ3py9t1IUdgWtWM3P/SD7vWiPIjG0D3Bt3Ptl +/mZjIFZZWUtFBItJLkiTpW0Ue4t98XMP6mvQiQ/LhP82OtSyCZ6agj4Wa3ve5KjA +pdEqamBGytx2kmN+AQFgMt66yOvr+97zzuEzI6mlWYORzOc1CFMsmPd6bu/dtQ4Z +96T8PNI6i1Lv5VqvqC7RBErvD7hO36JZb8j+PnbE1YADTKrw0HmgpI6d3RLyVop3 +n6ZQri0+nZ+TH0JG74MiihyZIz826zJO5OIwltexRcW0ZiRSpRCxZekU894lEs5Q +SxacRLeqM8ZVawB+9brqbeU3IJxmOCZgXLkkns0dBiSWGxtt+Tji+KXjogNfghmA +dVw9NQoBS+W5+pBtKEORD0YIGiUou9a7ukyMe2uvsl7rT+7BCOdvYtMBRbsfV5NP +s644wfJNIGa7OOjkWhuGwy6BVKTohDhJdKeZUpiTPKLV7ZLHjT4pkjuJgGQB7c+w +v7QYeUpwARwQNi8ZHuij2loG3Fb4l+3ejkcvivw0DLnDDhvUY57ezq53JwARAQAB +tCVGZWF0aGVyV2FsbGV0IDxkZXZAZmVhdGhlcndhbGxldC5vcmc+iQJOBBMBCgA4 +FiEEgYXhWKMzMMf9YbwNH3bhVc77pxwFAl/HogkCGwMFCwkIBwMFFQoJCAsFFgID +AQACHgECF4AACgkQH3bhVc77pxzAxw/9GYXGm71lUlZl2yfBPmo91euSc3w/irEC +88X1kFBsdKwL19B8HUaksCOQJRG8fJQmKvJmFnRZg3NK/GLIHam+1WVObFZc1MTv +y2ERzX5ILr9sb7FptB0Wr9gk0y0Nv032ZKci3wn1j2nA87o40uopDoQTaadDTKXa +s3M2+y6zM4dCmCaV6ylJromTzIaL2Q+tWSHDD8EDF2GbnfSeeEV6TV4xj3vqfT5P +34rK4vuVNxEy/YvRQJVRYntveNMJu9C4KJvIpo8onauUHEgBu4m+qfFpixDLwQzq +bJiJQaCUrwJ3liKMolBKiPqjGNl5JRRDy+YR1Dgsj6CRobWg1fDNnrGXUwDLaBwx +zVdCB0VSmcjXpt+FKTxw1mbY+6i6trUfJSjaaawXJbktOkO6sl0bVX83oQxEgod1 +aHwuo+eFCAW5zF0r+8R9Lk97Y5jkLWRKjXMFnMIyHaRhPdc24fOfojIQrXzQBMEO +lDhbWVd5vdOALhqvSOGYvjGjxBd9TE0pGzayNfPaee6kFEbxO3wZgF/QLPABl8i9 +b6hHJewpY5W9mM9/yP4lHL2TRcEMzk6I7XxPQUGEb3fzTAEHRM+My4SLwaUBIFvM +L8+hRhbfNnLZPd0xDAmvH6wToL3qgK/xSl9SYwuZkzaynblmyXE4+dCFp+T2XTam +FIbphOl8Yt+5Ag0EX8eiCQEQAKv0XnHtGhWTaq/sQ4lulYWNRjBsFQRMqwSFIosO +PfzWwATQeHxxIgRlWkc25w8W0O//t8x0UcNA5rU4R+C7kVrchVSYYYl9PY0vBhKP +3efVtPgntl/VgGH8LAdShHEt3H8ZDMFjqT6gx4xnpgt3C5OdGOA3bIWuvSZ1P7qp +SYiFZakrDfPeCdI/ifucipd+EnZhFv7ivnaoIGs+jgaImQH/5uEEVxpA89Bpxoju +gXlEKSVkVAanZsUwQkc/xzhsh8dzuEF5yKomVbwTYmXDTYmpff02ycdUP7gHw0Qg +WrWaQ2M0Xq1qcZL3ZpoaWUa/A92OfuncCSDNq1pRLqwJrExqQUP9cHGwGbqeGl8K +n2tFds8Pnnv+57ZKiO8E1VTDyBey1J3/Y1hOzctfEz6BzrL52Vj4vPWh2WNNh5fL +u1ZEIdykflH/Kho0zQkRfBfD93FbN/nH1xL3V7pO/wXVGqHSD3HbFLIcJ9Ax+Jgc +Z9fm9Bvc2RkXC8lJU5+htQ+YwHPLDExvUKrBL8b8xksODCvJSWLKcTPooFQyKgbK +EnPW5kmn3eT0SHHHOArn6EHoQttkR0pV2Lrgpfg+uhy3LSTmKbtRWo7VgDY0kfVL +hsatIUqYAVdDTBzsuMhehaoWwtLAsJ01OqxAoc6+0velLddLBuLxtzGtsF0u2mEF +QJmBABEBAAGJAjYEGAEKACAWIQSBheFYozMwx/1hvA0fduFVzvunHAUCX8eiCQIb +DAAKCRAfduFVzvunHDx1D/45GVAtIP1X640PR6N8qa4Iysc/crKepgDqm8zzvpQ8 +58MdeJZ9oPFEHDMkIMM8FGK9GbK4UE5mJzWJ2y5acMDOwvX4C9M206YaWQW9jPZt +fTfElP1KdAfTWz2/1UeOZKtOUuq9Wq+QlZGYg532JlX09TMyvINRM/w0+f4IBDlE +XIeRzRI6UQfz3BxpFpfWtMq/ayJnmJPrDsKQBPalai01OsbC+h4BUysZf1n7eTRF +DVaAKkSeOu+4gOVguE9PgKr11lDlKOI38tR6xBXzidBe3cPdun6vQbd1Bdfdmx3J +yFtlQo16kwwG2ZiVicXXugASBsrOFJa2/0lrtAPOnUWJsp2+1Ea6IzpRN8d1mNqr +6ND+CLxBsWj16UXq34GW6vt/QM7N1Br4/6SuPtv8OmDGRkRH7h2pz5yMf5GOwQFq +kgvOHt/x/sFPwk0GMgGn8aFr3vPH2YDg90mPn306Kv12e0JGkYVl4KqdL7u51gxT +3z5C/4+hhPVGHSPkf+g0VY/eY136kuuAZjV3P36M6UaBeCyqeD7b3fJ5IJcLwD9N +R0ustnn8IJ9zEwn+LY8kjRG8J3V57t2qAVGkMCiXnwFu3Vb+AYozOYi2ibu/N9QX +V4dTHarw64HUtLu/HEtcYuzuM5nGOXYvWPz3pQBtlqsyrhIfeaywQ+O55h5/KBo8 +Ig== +=2rq8 +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/assets/images/info2.svg b/src/assets/images/info2.svg new file mode 100644 index 0000000..0842a79 --- /dev/null +++ b/src/assets/images/info2.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/images/localMonero_buy.svg b/src/assets/images/localMonero_buy.svg new file mode 100644 index 0000000..7cfcdb1 --- /dev/null +++ b/src/assets/images/localMonero_buy.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/localMonero_buy_white.svg b/src/assets/images/localMonero_buy_white.svg new file mode 100644 index 0000000..a64fda4 --- /dev/null +++ b/src/assets/images/localMonero_buy_white.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/localMonero_logo.png b/src/assets/images/localMonero_logo.png new file mode 100644 index 0000000..1751ce7 Binary files /dev/null and b/src/assets/images/localMonero_logo.png differ diff --git a/src/assets/images/localMonero_register.svg b/src/assets/images/localMonero_register.svg new file mode 100644 index 0000000..6f7c63c --- /dev/null +++ b/src/assets/images/localMonero_register.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/localMonero_search.svg b/src/assets/images/localMonero_search.svg new file mode 100644 index 0000000..17f5218 --- /dev/null +++ b/src/assets/images/localMonero_search.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/localMonero_sell.svg b/src/assets/images/localMonero_sell.svg new file mode 100644 index 0000000..9b5f91b --- /dev/null +++ b/src/assets/images/localMonero_sell.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/localMonero_sell_white.svg b/src/assets/images/localMonero_sell_white.svg new file mode 100644 index 0000000..fe2cac3 --- /dev/null +++ b/src/assets/images/localMonero_sell_white.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/securityLevelSafer.png b/src/assets/images/securityLevelSafer.png new file mode 100644 index 0000000..efe8060 Binary files /dev/null and b/src/assets/images/securityLevelSafer.png differ diff --git a/src/assets/images/securityLevelSafer.svg b/src/assets/images/securityLevelSafer.svg new file mode 100644 index 0000000..ec3ed45 --- /dev/null +++ b/src/assets/images/securityLevelSafer.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/assets/images/securityLevelSaferWhite.png b/src/assets/images/securityLevelSaferWhite.png new file mode 100644 index 0000000..6ae6c25 Binary files /dev/null and b/src/assets/images/securityLevelSaferWhite.png differ diff --git a/src/assets/images/securityLevelSafest.png b/src/assets/images/securityLevelSafest.png new file mode 100644 index 0000000..4b8d3e6 Binary files /dev/null and b/src/assets/images/securityLevelSafest.png differ diff --git a/src/assets/images/securityLevelSafest.svg b/src/assets/images/securityLevelSafest.svg new file mode 100644 index 0000000..b064d0f --- /dev/null +++ b/src/assets/images/securityLevelSafest.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/assets/images/securityLevelSafestWhite.png b/src/assets/images/securityLevelSafestWhite.png new file mode 100644 index 0000000..b494a97 Binary files /dev/null and b/src/assets/images/securityLevelSafestWhite.png differ diff --git a/src/assets/images/securityLevelStandard.png b/src/assets/images/securityLevelStandard.png new file mode 100644 index 0000000..9afa1a4 Binary files /dev/null and b/src/assets/images/securityLevelStandard.png differ diff --git a/src/assets/images/securityLevelStandard.svg b/src/assets/images/securityLevelStandard.svg new file mode 100644 index 0000000..cdea04d --- /dev/null +++ b/src/assets/images/securityLevelStandard.svg @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/src/assets/images/securityLevelStandardWhite.png b/src/assets/images/securityLevelStandardWhite.png new file mode 100644 index 0000000..bacfdcd Binary files /dev/null and b/src/assets/images/securityLevelStandardWhite.png differ diff --git a/src/calcwidget.cpp b/src/calcwidget.cpp index 93bd7b9..48e8574 100644 --- a/src/calcwidget.cpp +++ b/src/calcwidget.cpp @@ -5,16 +5,15 @@ #include "calcwidget.h" #include "ui_calcwidget.h" -#include "mainwindow.h" -#include "components.h" #include "utils/ColorScheme.h" +#include "utils/AppData.h" +#include "utils/config.h" CalcWidget::CalcWidget(QWidget *parent) : QWidget(parent), ui(new Ui::CalcWidget) { ui->setupUi(this); - m_ctx = MainWindow::getContext(); ui->imageExchange->setBackgroundRole(QPalette::Base); ui->imageExchange->setAssets(":/assets/images/exchange.png", ":/assets/images/exchange_white.png"); @@ -30,8 +29,8 @@ CalcWidget::CalcWidget(QWidget *parent) : ui->lineFrom->setValidator(dv); ui->lineTo->setValidator(dv); - connect(AppContext::prices, &Prices::fiatPricesUpdated, this, &CalcWidget::initFiat); - connect(AppContext::prices, &Prices::cryptoPricesUpdated, this, &CalcWidget::initCrypto); + connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &CalcWidget::initFiat); + connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &CalcWidget::initCrypto);; } void CalcWidget::fromChanged(const QString &data) { @@ -56,12 +55,12 @@ void CalcWidget::fromChanged(const QString &data) { } double amount = amount_str.toDouble(); - double result = AppContext::prices->convert(symbolFrom, symbolTo, amount); + double result = appData()->prices.convert(symbolFrom, symbolTo, amount); this->m_changing = true; int precision = 10; - if(AppContext::prices->rates.contains(symbolTo)) + if (appData()->prices.rates.contains(symbolTo)) precision = 2; ui->lineTo->setText(QString::number(result, 'f', precision)); @@ -91,12 +90,12 @@ void CalcWidget::toChanged(const QString &data) { } double amount = amount_str.toDouble(); - double result = AppContext::prices->convert(symbolTo, symbolFrom, amount); + double result = appData()->prices.convert(symbolTo, symbolFrom, amount); this->m_changing = true; int precision = 10; - if(AppContext::prices->rates.contains(symbolFrom)) + if(appData()->prices.rates.contains(symbolFrom)) precision = 2; ui->lineFrom->setText(QString::number(result, 'f', precision)); @@ -116,8 +115,8 @@ void CalcWidget::initFiat() { void CalcWidget::initComboBox() { if(m_comboBoxInit) return; - QList marketsKeys = AppContext::prices->markets.keys(); - QList ratesKeys = AppContext::prices->rates.keys(); + QList marketsKeys = appData()->prices.markets.keys(); + QList ratesKeys = appData()->prices.rates.keys(); if(marketsKeys.count() <= 0 || ratesKeys.count() <= 0) return; ui->comboCalcFrom->addItems(marketsKeys); diff --git a/src/calcwidget.h b/src/calcwidget.h index 42f94bf..9ad2134 100644 --- a/src/calcwidget.h +++ b/src/calcwidget.h @@ -4,8 +4,7 @@ #ifndef CALC_H #define CALC_H -#include -#include "appcontext.h" +#include namespace Ui { class CalcWidget; @@ -32,7 +31,7 @@ public slots: private: Ui::CalcWidget *ui; - AppContext *m_ctx; + bool m_comboBoxInit = false; void initComboBox(); bool m_changing = false; diff --git a/src/calcwindow.cpp b/src/calcwindow.cpp index ba6dff1..f4ccbfc 100644 --- a/src/calcwindow.cpp +++ b/src/calcwindow.cpp @@ -3,6 +3,8 @@ #include "calcwindow.h" #include "mainwindow.h" +#include "utils/Icons.h" +#include "utils/AppData.h" #include "ui_calcwindow.h" @@ -14,10 +16,10 @@ CalcWindow::CalcWindow(QWidget *parent) : this->setWindowFlags(flags|Qt::WindowStaysOnTopHint); // on top ui->setupUi(this); - this->setWindowIcon(QIcon("://assets/images/gnome-calc.png")); + this->setWindowIcon(icons()->icon("gnome-calc.png")); - connect(AppContext::prices, &Prices::fiatPricesUpdated, this, &CalcWindow::initFiat); - connect(AppContext::prices, &Prices::cryptoPricesUpdated, this, &CalcWindow::initCrypto); + connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &CalcWindow::initFiat); + connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &CalcWindow::initCrypto); } void CalcWindow::initFiat() { diff --git a/src/coinswidget.cpp b/src/coinswidget.cpp index 042750f..507da8b 100644 --- a/src/coinswidget.cpp +++ b/src/coinswidget.cpp @@ -6,6 +6,7 @@ #include "dialog/outputinfodialog.h" #include "dialog/outputsweepdialog.h" #include "mainwindow.h" +#include "utils/Icons.h" #include #include @@ -26,7 +27,7 @@ CoinsWidget::CoinsWidget(QWidget *parent) connect(ui->coins->header(), &QHeaderView::customContextMenuRequested, this, &CoinsWidget::showHeaderMenu); // copy menu - m_copyMenu->setIcon(QIcon(":/assets/images/copy.png")); + m_copyMenu->setIcon(icons()->icon("copy.png")); m_copyMenu->addAction("Public key", this, [this]{copy(copyField::PubKey);}); m_copyMenu->addAction("Key Image", this, [this]{copy(copyField::KeyImage);}); m_copyMenu->addAction("Transaction ID", this, [this]{copy(copyField::TxID);}); @@ -44,7 +45,7 @@ CoinsWidget::CoinsWidget(QWidget *parent) m_freezeAllSelectedAction = new QAction("Freeze selected", this); m_thawAllSelectedAction = new QAction("Thaw selected", this); - m_viewOutputAction = new QAction(QIcon(":/assets/images/info.png"), "Details", this); + m_viewOutputAction = new QAction(icons()->icon("info2.svg"), "Details", this); m_sweepOutputAction = new QAction("Sweep output", this); connect(m_freezeOutputAction, &QAction::triggered, this, &CoinsWidget::freezeOutput); connect(m_thawOutputAction, &QAction::triggered, this, &CoinsWidget::thawOutput); @@ -129,7 +130,7 @@ void CoinsWidget::setShowSpent(bool show) void CoinsWidget::freezeOutput() { QModelIndex index = ui->coins->currentIndex(); QVector indexes = {m_proxyModel->mapToSource(index).row()}; - emit freeze(indexes); + this->freezeCoins(indexes); } void CoinsWidget::freezeAllSelected() { @@ -139,13 +140,13 @@ void CoinsWidget::freezeAllSelected() { for (QModelIndex index: list) { indexes.push_back(m_proxyModel->mapToSource(index).row()); // todo: will segfault if index get invalidated } - emit freeze(indexes); + this->freezeCoins(indexes); } void CoinsWidget::thawOutput() { QModelIndex index = ui->coins->currentIndex(); QVector indexes = {m_proxyModel->mapToSource(index).row()}; - emit thaw(indexes); + this->thawCoins(indexes); } void CoinsWidget::thawAllSelected() { @@ -155,7 +156,7 @@ void CoinsWidget::thawAllSelected() { for (QModelIndex index: list) { indexes.push_back(m_proxyModel->mapToSource(index).row()); } - emit thaw(indexes); + this->thawCoins(indexes); } void CoinsWidget::viewOutput() { @@ -181,7 +182,7 @@ void CoinsWidget::onSweepOutput() { int ret = dialog->exec(); if (!ret) return; - emit sweepOutput(keyImage, dialog->address(), dialog->churn(), dialog->outputs()); + m_ctx->onSweepOutput(keyImage, dialog->address(), dialog->churn(), dialog->outputs()); dialog->deleteLater(); } @@ -230,6 +231,22 @@ CoinsInfo* CoinsWidget::currentEntry() { } } +void CoinsWidget::freezeCoins(const QVector& indexes) { + for (int i : indexes) { + m_ctx->currentWallet->coins()->freeze(i); + } + m_ctx->currentWallet->coins()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); + m_ctx->updateBalance(); +} + +void CoinsWidget::thawCoins(const QVector &indexes) { + for (int i : indexes) { + m_ctx->currentWallet->coins()->thaw(i); + } + m_ctx->currentWallet->coins()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); + m_ctx->updateBalance(); +} + CoinsWidget::~CoinsWidget() { delete ui; } diff --git a/src/coinswidget.h b/src/coinswidget.h index 0a51eed..a2f3e5b 100644 --- a/src/coinswidget.h +++ b/src/coinswidget.h @@ -9,6 +9,7 @@ #include "model/CoinsProxyModel.h" #include "libwalletqt/Coins.h" +#include #include #include @@ -38,12 +39,10 @@ private slots: void viewOutput(); void onSweepOutput(); -signals: - void freeze(QVector indexes); - void thaw(QVector indexes); - void sweepOutput(const QString &keyImage, const QString &address, bool isChurn, int outputs); - private: + void freezeCoins(const QVector& indexes); + void thawCoins(const QVector& indexes); + enum copyField { PubKey = 0, KeyImage, diff --git a/src/contactswidget.cpp b/src/contactswidget.cpp index f3e048c..a7a7bc3 100644 --- a/src/contactswidget.cpp +++ b/src/contactswidget.cpp @@ -7,6 +7,7 @@ #include "model/ModelUtils.h" #include "mainwindow.h" #include "libwalletqt/AddressBook.h" +#include "utils/Icons.h" #include @@ -28,14 +29,14 @@ ContactsWidget::ContactsWidget(QWidget *parent) : // context menu ui->contacts->setContextMenuPolicy(Qt::CustomContextMenu); m_contextMenu = new QMenu(ui->contacts); - m_contextMenu->addAction(QIcon(":/assets/images/person.svg"), "New contact", [this]{ + m_contextMenu->addAction(icons()->icon("person.svg"), "New contact", [this]{ this->newContact(); }); // row context menu m_rowMenu = new QMenu(ui->contacts); - m_rowMenu->addAction(QIcon(":/assets/images/copy.png"), "Copy address", this, &ContactsWidget::copyAddress); - m_rowMenu->addAction(QIcon(":/assets/images/copy.png"), "Copy name", this, &ContactsWidget::copyName); + m_rowMenu->addAction(icons()->icon("copy.png"), "Copy address", this, &ContactsWidget::copyAddress); + m_rowMenu->addAction(icons()->icon("copy.png"), "Copy name", this, &ContactsWidget::copyName); m_rowMenu->addAction("Pay to", this, &ContactsWidget::payTo); m_rowMenu->addAction("Delete", this, &ContactsWidget::deleteContact); diff --git a/src/dialog/InfoDialog.cpp b/src/dialog/InfoDialog.cpp new file mode 100644 index 0000000..9b791a9 --- /dev/null +++ b/src/dialog/InfoDialog.cpp @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "InfoDialog.h" +#include "ui_InfoDialog.h" + +InfoDialog::InfoDialog(QWidget *parent, const QString &title, const QString &infoData) + : QDialog(parent) + , ui(new Ui::InfoDialog) +{ + ui->setupUi(this); + + this->setWindowTitle(title); + ui->info->setPlainText(infoData); +} + +InfoDialog::~InfoDialog() { + delete ui; +} \ No newline at end of file diff --git a/src/dialog/InfoDialog.h b/src/dialog/InfoDialog.h new file mode 100644 index 0000000..3858156 --- /dev/null +++ b/src/dialog/InfoDialog.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_INFODIALOG_H +#define FEATHER_INFODIALOG_H + +#include + +namespace Ui { + class InfoDialog; +} + +class InfoDialog : public QDialog +{ + Q_OBJECT + +public: + explicit InfoDialog(QWidget *parent, const QString &title, const QString &infoText); + ~InfoDialog() override; + +private: + Ui::InfoDialog *ui; +}; + + +#endif //FEATHER_INFODIALOG_H diff --git a/src/dialog/InfoDialog.ui b/src/dialog/InfoDialog.ui new file mode 100644 index 0000000..ad6930b --- /dev/null +++ b/src/dialog/InfoDialog.ui @@ -0,0 +1,71 @@ + + + InfoDialog + + + + 0 + 0 + 689 + 439 + + + + Dialog + + + + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + InfoDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + InfoDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/dialog/LocalMoneroInfoDialog.cpp b/src/dialog/LocalMoneroInfoDialog.cpp new file mode 100644 index 0000000..8cdfa03 --- /dev/null +++ b/src/dialog/LocalMoneroInfoDialog.cpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "LocalMoneroInfoDialog.h" +#include "ui_LocalMoneroInfoDialog.h" + +#include "utils/config.h" +#include "utils/utils.h" + +LocalMoneroInfoDialog::LocalMoneroInfoDialog(QWidget *parent, LocalMoneroModel *model, int row) + : QDialog(parent) + , ui(new Ui::LocalMoneroInfoDialog) + , m_model(model) + , m_row(row) +{ + ui->setupUi(this); + + setLabelText(ui->label_price, LocalMoneroModel::PriceXMR); + setLabelText(ui->label_seller, LocalMoneroModel::Seller); + setLabelText(ui->label_paymentMethod, LocalMoneroModel::PaymentMethod); + setLabelText(ui->label_paymentDetail, LocalMoneroModel::PaymentMethodDetail); + setLabelText(ui->label_tradeLimits, LocalMoneroModel::Limits); + + QJsonObject offerData = model->getOffer(row); + QString details = offerData["data"].toObject()["msg"].toString(); + details.remove("*"); + + if (details.isEmpty()) { + details = "No details."; + } + + ui->info->setPlainText(details); + + connect(ui->btn_goToOffer, &QPushButton::clicked, this, &LocalMoneroInfoDialog::onGoToOffer); +} + +void LocalMoneroInfoDialog::setLabelText(QLabel *label, LocalMoneroModel::Column column) { + QString data = m_model->data(m_model->index(m_row, column)).toString(); + label->setText(data); +} + +void LocalMoneroInfoDialog::onGoToOffer() { + QJsonObject offerData = m_model->getOffer(m_row); + QString frontend = config()->get(Config::localMoneroFrontend).toString(); + QString offerUrl = QString("%1/ad/%2").arg(frontend, offerData["data"].toObject()["ad_id"].toString()); + Utils::externalLinkWarning(this, offerUrl); +} + +LocalMoneroInfoDialog::~LocalMoneroInfoDialog() { + delete ui; +} \ No newline at end of file diff --git a/src/dialog/LocalMoneroInfoDialog.h b/src/dialog/LocalMoneroInfoDialog.h new file mode 100644 index 0000000..584f87a --- /dev/null +++ b/src/dialog/LocalMoneroInfoDialog.h @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_LOCALMONEROINFODIALOG_H +#define FEATHER_LOCALMONEROINFODIALOG_H + +#include +#include +#include "model/LocalMoneroModel.h" + +namespace Ui { + class LocalMoneroInfoDialog; +} + +class LocalMoneroInfoDialog : public QDialog +{ + Q_OBJECT + +public: + explicit LocalMoneroInfoDialog(QWidget *parent, LocalMoneroModel *model, int row); + ~LocalMoneroInfoDialog() override; + +private slots: + void onGoToOffer(); + +private: + void setLabelText(QLabel *label, LocalMoneroModel::Column column); + + Ui::LocalMoneroInfoDialog *ui; + LocalMoneroModel *m_model; + int m_row; +}; + + +#endif //FEATHER_INFODIALOG_H diff --git a/src/dialog/LocalMoneroInfoDialog.ui b/src/dialog/LocalMoneroInfoDialog.ui new file mode 100644 index 0000000..6a37b51 --- /dev/null +++ b/src/dialog/LocalMoneroInfoDialog.ui @@ -0,0 +1,195 @@ + + + LocalMoneroInfoDialog + + + + 0 + 0 + 758 + 557 + + + + Offer info + + + + + + Info + + + + + + Price: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Seller: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Payment method: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Trade limits: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Payment detail: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Go to offer + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + + + buttonBox + accepted() + LocalMoneroInfoDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + LocalMoneroInfoDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/dialog/TxProofDialog.cpp b/src/dialog/TxProofDialog.cpp index 5c1d0d9..811ddbc 100644 --- a/src/dialog/TxProofDialog.cpp +++ b/src/dialog/TxProofDialog.cpp @@ -211,7 +211,8 @@ TxProof TxProofDialog::getProof() { return m_wallet->getSpendProof(m_txid, message); } case Mode::OutProof: - case Mode::InProof: { // Todo: split this into separate functions + case Mode::InProof: + default: { // Todo: split this into separate functions return m_wallet->getTxProof(m_txid, address, message); } } diff --git a/src/dialog/UpdateDialog.cpp b/src/dialog/UpdateDialog.cpp new file mode 100644 index 0000000..f548370 --- /dev/null +++ b/src/dialog/UpdateDialog.cpp @@ -0,0 +1,228 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "UpdateDialog.h" +#include "ui_UpdateDialog.h" + +#include +#include + +#include "utils/networking.h" +#include "utils/NetworkManager.h" +#include "utils/AsyncTask.h" +#include "utils/Updater.h" +#include "utils/utils.h" + +#include "zip.h" + +UpdateDialog::UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer) + : QDialog(parent) + , ui(new Ui::UpdateDialog) + , m_version(std::move(version)) + , m_downloadUrl(std::move(downloadUrl)) + , m_hash(std::move(hash)) + , m_signer(std::move(signer)) +{ + ui->setupUi(this); + + ui->btn_installUpdate->hide(); + ui->btn_restart->hide(); + ui->progressBar->hide(); + + auto bigFont = Utils::relativeFont(4); + ui->label_header->setFont(bigFont); + ui->label_header->setText(QString("New Feather version %1 is available").arg(m_version)); + + connect(ui->btn_cancel, &QPushButton::clicked, [this]{ + if (m_reply) { + m_reply->abort(); + } + this->reject(); + }); + connect(ui->btn_download, &QPushButton::clicked, this, &UpdateDialog::onDownloadClicked); + connect(ui->btn_installUpdate, &QPushButton::clicked, this, &UpdateDialog::onInstallUpdate); + connect(ui->btn_restart, &QPushButton::clicked, this, &UpdateDialog::onRestartClicked); + + this->adjustSize(); +} + +void UpdateDialog::onDownloadClicked() { + ui->label_body->setText("Downloading update.."); + ui->btn_download->hide(); + ui->progressBar->show(); + + UtilsNetworking network{getNetworkTor()}; + + m_reply = network.get(m_downloadUrl); + connect(m_reply, &QNetworkReply::downloadProgress, this, &UpdateDialog::onDownloadProgress); + connect(m_reply, &QNetworkReply::finished, this, &UpdateDialog::onDownloadFinished); +} + +void UpdateDialog::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + ui->progressBar->setValue(bytesReceived); + ui->progressBar->setMaximum(bytesTotal); +} + +void UpdateDialog::onDownloadFinished() { + bool error = (m_reply->error() != QNetworkReply::NoError); + if (error) { + this->onDownloadError(QString("Network error: %1").arg(m_reply->errorString())); + return; + } + + QByteArray response = m_reply->readAll(); + if (response.isEmpty()) { + this->onDownloadError("Network error: Empty response"); + return; + } + + std::string responseStr = response.toStdString(); + + try { + const QByteArray calculatedHash = AsyncTask::runAndWaitForFuture([this, responseStr]{ + return Updater().getHash(&responseStr[0], responseStr.size()); + }); + + const QByteArray signedHash = QByteArray::fromHex(m_hash.toUtf8()); + + if (signedHash != calculatedHash) { + this->onDownloadError("Error: Hash sum mismatch."); + return; + } + } + catch (const std::exception &e) + { + this->onDownloadError(QString("Error: Unable to calculate sha256sum: %1").arg(e.what())); + return; + } + + this->setStatus("Download finished and verified.", true); + + ui->btn_installUpdate->show(); + ui->progressBar->hide(); + + m_updateZipArchive = responseStr; +} + +void UpdateDialog::onDownloadError(const QString &errMsg) { + // Clean up so download can be retried + this->setStatus(errMsg); + ui->progressBar->hide(); + ui->progressBar->setMaximum(100); + ui->progressBar->setValue(0); + ui->btn_download->show(); + ui->btn_download->setText("Retry download"); +} + +void UpdateDialog::onInstallUpdate() { + ui->btn_installUpdate->hide(); + this->setStatus("Unzipping archive..."); + + zip_error_t err; + zip_error_init(&err); + + zip_source_t *zip_source = zip_source_buffer_create(&m_updateZipArchive[0], m_updateZipArchive.size(), 0, &err); + if (!zip_source) { + this->onInstallError(QString("Error in libzip: Unable to create zip source from buffer: %1").arg(QString::fromStdString(err.str))); + return; + } + + zip_t *zip_archive = zip_open_from_source(zip_source, 0, &err); + if (!zip_archive) { + this->onInstallError(QString("Error in libzip: Unable to open archive from source: %1").arg(QString::fromStdString(err.str))); + return; + } + + auto num_entries = zip_get_num_entries(zip_archive, 0); + if (num_entries <= 0) { + this->onInstallError("Error in libzip: Archive has no entries"); + return; + } + + // We only expect the archive to contain 1 file + std::string fname = zip_get_name(zip_archive, 0, 0); + if (fname.empty()) { + this->onInstallError("Error in libzip: Invalid filename in archive"); + return; + } + + struct zip_stat sb; + if (zip_stat_index(zip_archive, 0, 0, &sb) != 0) { + this->onInstallError("Error in libzip: Entry index not found"); + return; + } + + QString name = QString::fromStdString(sb.name); + qDebug() << "File found in archive: " << name << ", with size: " << QString::number(sb.size); + + struct zip_file *zf; + zf = zip_fopen_index(zip_archive, 0, 0); + if (!zf) { + this->onInstallError("Error in libzip: Unable to open entry"); + return; + } + + std::unique_ptr contents{new char[sb.size]}; + + auto bytes_read = zip_fread(zf, contents.get(), sb.size); + if (bytes_read != sb.size){ + this->onInstallError("Error in libzip: File size inconsistent"); + return; + } + + zip_fclose(zf); + zip_close(zip_archive); + + QString applicationPath = qgetenv("APPIMAGE"); + if (!applicationPath.isEmpty()) { + applicationPath = QFileInfo(applicationPath).absoluteDir().path(); + } else { + applicationPath = QCoreApplication::applicationDirPath(); + } + + QDir applicationDir(applicationPath); + QString filePath = applicationDir.filePath(name); + m_updatePath = filePath; + + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) + { + this->onInstallError(QString("Error: Could not write to application path: %1").arg(filePath)); + return; + } + + if (static_cast(file.write(&contents[0], sb.size)) != sb.size) { + this->onInstallError("Error: Unable to write file"); + return; + } + + if (!file.setPermissions(QFile::ExeUser | QFile::ExeOwner | QFile::ExeGroup | QFile::ExeOther + | QFile::ReadUser | QFile::ReadOwner + | QFile::WriteUser | QFile::WriteOwner)) { + this->onInstallError("Error: Unable to set executable flags"); + return; + } + + this->setStatus("Installation successful. Do you want to restart Feather now?"); + ui->btn_restart->show(); +} + +void UpdateDialog::onInstallError(const QString &errMsg) { + this->setStatus(errMsg); +} + +void UpdateDialog::onRestartClicked() { + emit restartWallet(m_updatePath); +} + +void UpdateDialog::setStatus(const QString &msg, bool success) { + ui->label_body->setText(msg); + if (success) + ui->label_body->setStyleSheet("QLabel { color : #2EB358; }"); + else + ui->label_body->setStyleSheet(""); +} + +UpdateDialog::~UpdateDialog() { + delete ui; +} diff --git a/src/dialog/UpdateDialog.h b/src/dialog/UpdateDialog.h new file mode 100644 index 0000000..70fe313 --- /dev/null +++ b/src/dialog/UpdateDialog.h @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_UPDATEDIALOG_H +#define FEATHER_UPDATEDIALOG_H + +#include +#include + +namespace Ui { + class UpdateDialog; +} + +class UpdateDialog : public QDialog +{ +Q_OBJECT + +public: + explicit UpdateDialog(QWidget *parent, QString version, QString downloadUrl, QString hash, QString signer); + ~UpdateDialog() override; + +private slots: + void onDownloadClicked(); + void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void onDownloadFinished(); + void onDownloadError(const QString &errMsg); + void onInstallUpdate(); + void onInstallError(const QString &errMsg); + void onRestartClicked(); + +signals: + void restartWallet(const QString &binaryFilename); + +private: + void setStatus(const QString &msg, bool success = false); + + QString m_version; + QString m_downloadUrl; + QString m_hash; + QString m_signer; + + QString m_updatePath; + + std::string m_updateZipArchive; + + QNetworkReply *m_reply = nullptr; + + Ui::UpdateDialog *ui; +}; + +#endif //FEATHER_UPDATEDIALOG_H diff --git a/src/dialog/UpdateDialog.ui b/src/dialog/UpdateDialog.ui new file mode 100644 index 0000000..fba1689 --- /dev/null +++ b/src/dialog/UpdateDialog.ui @@ -0,0 +1,87 @@ + + + UpdateDialog + + + + 0 + 0 + 569 + 148 + + + + Update Available + + + + + + New Feather version is available. + + + + + + + Do you want to download and verify the new version? + + + + + + + 0 + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Download + + + + + + + Install Update + + + + + + + Restart Feather + + + + + + + + + + diff --git a/src/dialog/broadcasttxdialog.cpp b/src/dialog/broadcasttxdialog.cpp index 33c73b6..9d1ed71 100644 --- a/src/dialog/broadcasttxdialog.cpp +++ b/src/dialog/broadcasttxdialog.cpp @@ -3,6 +3,7 @@ #include "broadcasttxdialog.h" #include "ui_broadcasttxdialog.h" +#include "utils/NetworkManager.h" #include @@ -13,10 +14,8 @@ BroadcastTxDialog::BroadcastTxDialog(QWidget *parent, AppContext *ctx, const QSt { ui->setupUi(this); - m_network = new UtilsNetworking(m_ctx->network, this); - auto node = ctx->nodes->connection(); - m_rpc = new DaemonRpc(this, m_network, node.full); + m_rpc = new DaemonRpc(this, getNetworkTor(), node.toAddress()); connect(ui->btn_Broadcast, &QPushButton::clicked, this, &BroadcastTxDialog::broadcastTx); connect(ui->btn_Close, &QPushButton::clicked, this, &BroadcastTxDialog::reject); @@ -38,7 +37,7 @@ void BroadcastTxDialog::broadcastTx() { if (ui->radio_useCustom->isChecked()) node = ui->customNode->text(); else if (ui->radio_useDefault->isChecked()) - node = m_ctx->nodes->connection().full; + node = m_ctx->nodes->connection().toAddress(); if (!node.startsWith("http://")) node = QString("http://%1").arg(node); diff --git a/src/dialog/debuginfodialog.cpp b/src/dialog/debuginfodialog.cpp index 56bc0b1..28544df 100644 --- a/src/dialog/debuginfodialog.cpp +++ b/src/dialog/debuginfodialog.cpp @@ -4,6 +4,9 @@ #include "debuginfodialog.h" #include "ui_debuginfodialog.h" #include "config-feather.h" +#include "utils/WebsocketClient.h" +#include "utils/TorManager.h" +#include "utils/WebsocketNotifier.h" DebugInfoDialog::DebugInfoDialog(AppContext *ctx, QWidget *parent) : QDialog(parent) @@ -26,16 +29,16 @@ void DebugInfoDialog::updateInfo() { // Special case for Tails because we know the status of the daemon by polling tails-tor-has-bootstrapped.target if(m_ctx->isTails) { - if(m_ctx->tor->torConnected) + if(torManager()->torConnected) torStatus = "Connected"; else torStatus = "Disconnected"; } - else if(m_ctx->isTorSocks) + else if(Utils::isTorsocks()) torStatus = "Torsocks"; - else if(m_ctx->tor->localTor) + else if(torManager()->isLocalTor()) torStatus = "Local (assumed to be running)"; - else if(m_ctx->tor->torConnected) + else if(torManager()->torConnected) torStatus = "Running"; else torStatus = "Unknown"; @@ -50,13 +53,37 @@ void DebugInfoDialog::updateInfo() { ui->label_synchronized->setText(m_ctx->currentWallet->isSynchronized() ? "True" : "False"); auto node = m_ctx->nodes->connection(); - ui->label_remoteNode->setText(node.full); + ui->label_remoteNode->setText(node.toAddress()); ui->label_walletStatus->setText(this->statusToString(m_ctx->currentWallet->connectionStatus())); ui->label_torStatus->setText(torStatus); - ui->label_websocketStatus->setText(Utils::QtEnumToString(m_ctx->ws->webSocket.state()).remove("State")); + ui->label_websocketStatus->setText(Utils::QtEnumToString(websocketNotifier()->websocketClient.webSocket.state()).remove("State")); + + QString seedType = [this](){ + if (m_ctx->currentWallet->isHwBacked()) + return "Hardware"; + if (m_ctx->currentWallet->getCacheAttribute("feather.seed").isEmpty()) + return "25 word"; + else + return "14 word"; + }(); + + QString deviceType = [this](){ + if (m_ctx->currentWallet->isHwBacked()) { + if (m_ctx->currentWallet->isLedger()) + return "Ledger"; + else if (m_ctx->currentWallet->isTrezor()) + return "Trezor"; + else + return "Unknown"; + } + else { + return "Software"; + } + }(); ui->label_netType->setText(Utils::QtEnumToString(m_ctx->currentWallet->nettype())); - ui->label_seedType->setText(m_ctx->currentWallet->getCacheAttribute("feather.seed").isEmpty() ? "25 word" : "14 word"); + ui->label_seedType->setText(seedType); + ui->label_deviceType->setText(deviceType); ui->label_viewOnly->setText(m_ctx->currentWallet->viewOnly() ? "True" : "False"); ui->label_primaryOnly->setText(m_ctx->currentWallet->balance(0) == m_ctx->currentWallet->balanceAll() ? "True" : "False"); @@ -107,6 +134,7 @@ void DebugInfoDialog::copyToClipboad() { text += QString("Network type: %1 \n").arg(ui->label_netType->text()); text += QString("Seed type: %1 \n").arg(ui->label_seedType->text()); + text += QString("Device type: %1 \n").arg(ui->label_deviceType->text()); text += QString("View only: %1 \n").arg(ui->label_viewOnly->text()); text += QString("Primary only: %1 \n").arg(ui->label_primaryOnly->text()); diff --git a/src/dialog/debuginfodialog.ui b/src/dialog/debuginfodialog.ui index e650475..87c204e 100644 --- a/src/dialog/debuginfodialog.ui +++ b/src/dialog/debuginfodialog.ui @@ -7,7 +7,7 @@ 0 0 693 - 612 + 613 @@ -91,6 +91,23 @@ + + + + Target height: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + @@ -242,82 +259,14 @@ - + - View only: + Device type: - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Timestamp: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Operating system: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - - - Qt::Horizontal - - - - - - - Target height: - - - - - - - TextLabel - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - - - - - + TextLabel @@ -327,12 +276,80 @@ + + + View only: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + Primary only: + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Qt::Horizontal + + + + + + + Operating system: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + Timestamp: + + + + + + + TextLabel + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + diff --git a/src/dialog/keysdialog.cpp b/src/dialog/keysdialog.cpp index e49278d..7353349 100644 --- a/src/dialog/keysdialog.cpp +++ b/src/dialog/keysdialog.cpp @@ -10,12 +10,15 @@ KeysDialog::KeysDialog(AppContext *ctx, QWidget *parent) { ui->setupUi(this); - ui->label_restoreHeight->setText(QString::number(ctx->currentWallet->getWalletCreationHeight())); - ui->label_primaryAddress->setText(ctx->currentWallet->address(0, 0)); - ui->label_secretSpendKey->setText(ctx->currentWallet->getSecretSpendKey()); - ui->label_secretViewKey->setText(ctx->currentWallet->getSecretViewKey()); - ui->label_publicSpendKey->setText(ctx->currentWallet->getPublicSpendKey()); - ui->label_publicViewKey->setText(ctx->currentWallet->getPublicViewKey()); + auto w = ctx->currentWallet; + QString unavailable = "Unavailable: Key is stored on hardware device"; + + ui->label_restoreHeight->setText(QString::number(w->getWalletCreationHeight())); + ui->label_primaryAddress->setText(w->address(0, 0)); + ui->label_secretSpendKey->setText(w->isHwBacked() ? unavailable : w->getSecretSpendKey()); + ui->label_secretViewKey->setText(w->getSecretViewKey()); + ui->label_publicSpendKey->setText(w->getPublicSpendKey()); + ui->label_publicViewKey->setText(w->getPublicViewKey()); this->adjustSize(); } diff --git a/src/dialog/restoredialog.cpp b/src/dialog/restoredialog.cpp index af6f589..49edce0 100644 --- a/src/dialog/restoredialog.cpp +++ b/src/dialog/restoredialog.cpp @@ -18,7 +18,7 @@ RestoreDialog::RestoreDialog(AppContext *ctx, QWidget *parent) ui->restoreHeightWidget->hideSlider(); } else { // load restoreHeight lookup db - ui->restoreHeightWidget->initRestoreHeights(m_ctx->restoreHeights[m_ctx->networkType]); + ui->restoreHeightWidget->initRestoreHeights(appData()->restoreHeights[m_ctx->networkType]); } } diff --git a/src/dialog/splashdialog.cpp b/src/dialog/splashdialog.cpp new file mode 100644 index 0000000..ded389f --- /dev/null +++ b/src/dialog/splashdialog.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "splashdialog.h" +#include "ui_splashdialog.h" + +SplashDialog::SplashDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::SplashDialog) +{ + ui->setupUi(this); + + QPixmap pixmap = QPixmap(":/assets/images/key.png"); + ui->icon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation)); + + this->setWindowFlags(Qt::Dialog | Qt::FramelessWindowHint); + + this->adjustSize(); +} + +void SplashDialog::setMessage(const QString &message) { + ui->label_message->setText(message); +} + +void SplashDialog::setIcon(const QPixmap &icon) { + ui->icon->setPixmap(icon.scaledToWidth(32, Qt::SmoothTransformation)); +} + +SplashDialog::~SplashDialog() { + delete ui; +} diff --git a/src/dialog/splashdialog.h b/src/dialog/splashdialog.h new file mode 100644 index 0000000..c4acf90 --- /dev/null +++ b/src/dialog/splashdialog.h @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_SPLASHDIALOG_H +#define FEATHER_SPLASHDIALOG_H + +#include + +namespace Ui { + class SplashDialog; +} + +class SplashDialog : public QDialog +{ +Q_OBJECT + +public: + explicit SplashDialog(QWidget *parent = nullptr); + ~SplashDialog() override; + + void setMessage(const QString &message); + void setIcon(const QPixmap &icon); + +private: + Ui::SplashDialog *ui; +}; + +#endif //FEATHER_SPLASHDIALOG_H diff --git a/src/dialog/splashdialog.ui b/src/dialog/splashdialog.ui new file mode 100644 index 0000000..e3ceeb0 --- /dev/null +++ b/src/dialog/splashdialog.ui @@ -0,0 +1,60 @@ + + + SplashDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 751 + 101 + + + + Device Action Required + + + + + + + 0 + 0 + + + + icon + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 5 + 20 + + + + + + + + Action required on device: Export the view key to open the wallet. + + + + + + + + diff --git a/src/dialog/torinfodialog.cpp b/src/dialog/torinfodialog.cpp index 97cad90..4852876 100644 --- a/src/dialog/torinfodialog.cpp +++ b/src/dialog/torinfodialog.cpp @@ -4,32 +4,145 @@ #include "torinfodialog.h" #include "ui_torinfodialog.h" +#include #include +#include -TorInfoDialog::TorInfoDialog(AppContext *ctx, QWidget *parent) +#include "utils/TorManager.h" + +TorInfoDialog::TorInfoDialog(QWidget *parent, AppContext *ctx) : QDialog(parent) , ui(new Ui::TorInfoDialog) , m_ctx(ctx) { ui->setupUi(this); - if (!m_ctx->tor->torConnected && !m_ctx->tor->errorMsg.isEmpty()) { - ui->message->setText(m_ctx->tor->errorMsg); + if (!torManager()->torConnected && !torManager()->errorMsg.isEmpty()) { + ui->message->setText(torManager()->errorMsg); } else { - ui->message->setText(QString("Currently using Tor instance: %1:%2").arg(Tor::torHost).arg(Tor::torPort)); + ui->message->hide(); } - if (m_ctx->tor->localTor) { - ui->logs->setHidden(true); + if (torManager()->isLocalTor()) { + ui->frame_logs->setHidden(true); } else { - ui->logs->setPlainText(m_ctx->tor->torLogs); + ui->logs->setPlainText(torManager()->torLogs); } + initConnectionSettings(); + initPrivacyLevel(); + onConnectionStatusChanged(torManager()->torConnected); + + connect(torManager(), &TorManager::connectionStateChanged, this, &TorInfoDialog::onConnectionStatusChanged); + connect(torManager(), &TorManager::logsUpdated, this, &TorInfoDialog::onLogsUpdated); + + connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &TorInfoDialog::onApplySettings); + + connect(ui->line_host, &QLineEdit::textEdited, this, &TorInfoDialog::onSettingsChanged); + connect(ui->line_port, &QLineEdit::textEdited, this, &TorInfoDialog::onSettingsChanged); + connect(ui->check_useLocalTor, &QCheckBox::stateChanged, this, &TorInfoDialog::onSettingsChanged); + connect(ui->btnGroup_privacyLevel, &QButtonGroup::idToggled, this, &TorInfoDialog::onSettingsChanged); + + ui->label_changes->hide(); + +#ifndef HAS_TOR_BIN + ui->check_useLocalTor->setChecked(true); + ui->check_useLocalTor->setEnabled(false); + ui->check_useLocalTor->setToolTip("Feather was bundled without Tor"); +#endif + this->adjustSize(); } void TorInfoDialog::onLogsUpdated() { - ui->logs->setPlainText(m_ctx->tor->torLogs); + ui->logs->setPlainText(torManager()->torLogs); +} + +void TorInfoDialog::onConnectionStatusChanged(bool connected) { + if (connected) { + ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_connected.png").scaledToWidth(16, Qt::SmoothTransformation)); + ui->label_testConnectionStatus->setText("Connected"); + } else { + ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_disconnected.png").scaledToWidth(16, Qt::SmoothTransformation)); + ui->label_testConnectionStatus->setText("Disconnected"); + } +} + +void TorInfoDialog::onApplySettings() { + config()->set(Config::socks5Host, ui->line_host->text()); + config()->set(Config::socks5Port, ui->line_port->text()); + + int id = ui->btnGroup_privacyLevel->checkedId(); + config()->set(Config::torPrivacyLevel, id); + + ui->label_changes->hide(); + + bool useLocalTor = ui->check_useLocalTor->isChecked(); + if (config()->get(Config::useLocalTor).toBool() && useLocalTor && torManager()->isStarted()) { + QMessageBox::warning(this, "Warning", "Feather is running the bundled Tor daemon, " + "but the option to never start a bundled Tor daemon was selected. " + "A restart is required to apply the setting."); + } + config()->set(Config::useLocalTor, useLocalTor); + + ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_lagging.png").scaledToWidth(16, Qt::SmoothTransformation)); + ui->label_testConnectionStatus->setText("Connecting"); + + emit torSettingsChanged(); +} + +void TorInfoDialog::onSettingsChanged() { + ui->label_changes->show(); +} + +void TorInfoDialog::initConnectionSettings() { + bool localTor = torManager()->isLocalTor(); + ui->label_connectionSettingsMessage->setVisible(!localTor); + ui->frame_connectionSettings->setVisible(localTor); + + ui->line_host->setText(config()->get(Config::socks5Host).toString()); + ui->line_port->setText(config()->get(Config::socks5Port).toString()); + + ui->check_useLocalTor->setChecked(config()->get(Config::useLocalTor).toBool()); +} + +void TorInfoDialog::initPrivacyLevel() { + ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptNode, Config::allTorExceptNode); + ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptInitSync, Config::allTorExceptInitSync); + ui->btnGroup_privacyLevel->setId(ui->radio_allTor, Config::allTor); + + int privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); + auto button = ui->btnGroup_privacyLevel->button(privacyLevel); + if (button) { + button->setChecked(true); + } + + if (m_ctx->nodes->connection().isLocal()) { + ui->label_notice->setText("You are connected to a local node. Traffic is not routed over Tor."); + } + else if (Utils::isTorsocks()) { + ui->label_notice->setText("Feather was started with torsocks, all traffic is routed over Tor"); + } + else if (WhonixOS::detect()) { + ui->label_notice->setText("Feather is running on Whonix, all traffic is routed over Tor"); + } + else if (TailsOS::detect()) { + ui->label_notice->setText("Feather is running on Tails, all traffic is routed over Tor"); + } + else { + ui->frame_notice->hide(); + } + + QPixmap iconNoTor(":/assets/images/securityLevelStandardWhite.png"); + QPixmap iconNoSync(":/assets/images/securityLevelSaferWhite.png"); + QPixmap iconAllTor(":/assets/images/securityLevelSafestWhite.png"); + ui->icon_noTor->setPixmap(iconNoTor.scaledToHeight(16, Qt::SmoothTransformation)); + ui->icon_noSync->setPixmap(iconNoSync.scaledToHeight(16, Qt::SmoothTransformation)); + ui->icon_allTor->setPixmap(iconAllTor.scaledToHeight(16, Qt::SmoothTransformation)); +} + +void TorInfoDialog::onStopTor() { + torManager()->stop(); } TorInfoDialog::~TorInfoDialog() { diff --git a/src/dialog/torinfodialog.h b/src/dialog/torinfodialog.h index 64c7e61..60b5b86 100644 --- a/src/dialog/torinfodialog.h +++ b/src/dialog/torinfodialog.h @@ -5,6 +5,7 @@ #define FEATHER_TORINFODIALOG_H #include +#include #include "appcontext.h" @@ -17,13 +18,25 @@ class TorInfoDialog : public QDialog Q_OBJECT public: - explicit TorInfoDialog(AppContext *ctx, QWidget *parent = nullptr); + explicit TorInfoDialog(QWidget *parent, AppContext *ctx); ~TorInfoDialog() override; public slots: void onLogsUpdated(); +private slots: + void onConnectionStatusChanged(bool connected); + void onApplySettings(); + void onSettingsChanged(); + void onStopTor(); + +signals: + void torSettingsChanged(); + private: + void initConnectionSettings(); + void initPrivacyLevel(); + Ui::TorInfoDialog *ui; AppContext *m_ctx; }; diff --git a/src/dialog/torinfodialog.ui b/src/dialog/torinfodialog.ui index d31d1e7..a71c109 100644 --- a/src/dialog/torinfodialog.ui +++ b/src/dialog/torinfodialog.ui @@ -6,8 +6,8 @@ 0 0 - 618 - 386 + 703 + 804 @@ -15,24 +15,335 @@ - - - Message + + + Connection settings + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Host + + + + + + + 127.0.0.1 + + + 127.0.0.1 + + + + + + + Port + + + + + + + 9050 + + + 9050 + + + + + + + + + + + + Tor daemon is managed by Feather. + + + + + + + Never start bundled Tor (requires local Tor daemon) + + + + - - + + + Privacy Level + + + + + + + + + 0 + 0 + + + + icon + + + + + + + Route all traffic over Tor, except traffic to node + + + btnGroup_privacyLevel + + + + + + + + + + + + 0 + 0 + + + + icon + + + + + + + Route all traffic over Tor, except initial wallet synchronization + + + btnGroup_privacyLevel + + + + + + + + + + + + 0 + 0 + + + + icon + + + + + + + Route all traffic over Tor + + + btnGroup_privacyLevel + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + notice + + + true + + + + + + + + + + + + + Status + + + + + + + + + 0 + 0 + + + + icon + + + + + + + status + + + + + + + (changes not applied) + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Message + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Logs + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 600 + 0 + + + + true + + + + + + + + + + + + + Qt::Vertical + + - 600 + 20 0 - - true - - + @@ -40,7 +351,7 @@ Qt::Horizontal - QDialogButtonBox::Ok + QDialogButtonBox::Apply|QDialogButtonBox::Close @@ -81,4 +392,7 @@ + + + diff --git a/src/dialog/txconfadvdialog.cpp b/src/dialog/txconfadvdialog.cpp index 13dfb63..98fbc8d 100644 --- a/src/dialog/txconfadvdialog.cpp +++ b/src/dialog/txconfadvdialog.cpp @@ -8,7 +8,6 @@ #include "libwalletqt/Transfer.h" #include "libwalletqt/Input.h" #include "model/ModelUtils.h" -#include "utils/ColorScheme.h" #include #include diff --git a/src/dialog/txconfdialog.cpp b/src/dialog/txconfdialog.cpp index 9c5e3e5..47d959a 100644 --- a/src/dialog/txconfdialog.cpp +++ b/src/dialog/txconfdialog.cpp @@ -6,6 +6,7 @@ #include "model/ModelUtils.h" #include "txconfadvdialog.h" #include "globals.h" +#include "utils/AppData.h" #include "utils/ColorScheme.h" #include @@ -26,7 +27,7 @@ TxConfDialog::TxConfDialog(AppContext *ctx, PendingTransaction *tx, const QStrin QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); auto convert = [preferredCur](double amount){ - return QString::number(AppContext::prices->convert("XMR", preferredCur, amount), 'f', 2); + return QString::number(appData()->prices.convert("XMR", preferredCur, amount), 'f', 2); }; QString amount = WalletManager::displayAmount(tx->amount()); diff --git a/src/dialog/tximportdialog.cpp b/src/dialog/tximportdialog.cpp index 41897f6..2dcc5f9 100644 --- a/src/dialog/tximportdialog.cpp +++ b/src/dialog/tximportdialog.cpp @@ -3,6 +3,7 @@ #include "tximportdialog.h" #include "ui_tximportdialog.h" +#include "utils/NetworkManager.h" #include @@ -16,10 +17,8 @@ TxImportDialog::TxImportDialog(QWidget *parent, AppContext *ctx) ui->resp->hide(); ui->label_loading->hide(); - m_network = new UtilsNetworking(m_ctx->network, this); - auto node = ctx->nodes->connection(); - m_rpc = new DaemonRpc(this, m_network, node.full); + m_rpc = new DaemonRpc(this, getNetworkTor(), node.toAddress()); connect(ui->btn_load, &QPushButton::clicked, this, &TxImportDialog::loadTx); connect(ui->btn_import, &QPushButton::clicked, this, &TxImportDialog::onImport); @@ -35,7 +34,7 @@ TxImportDialog::TxImportDialog(QWidget *parent, AppContext *ctx) void TxImportDialog::loadTx() { QString txid = ui->line_txid->text(); - QString node = m_ctx->nodes->connection().full; + QString node = m_ctx->nodes->connection().toAddress(); if (!node.startsWith("http://")) node = QString("http://%1").arg(node); diff --git a/src/globals.h b/src/globals.h index cb9d031..fd9b67e 100644 --- a/src/globals.h +++ b/src/globals.h @@ -22,6 +22,9 @@ namespace globals // websocket constants const QUrl websocketUrl = QUrl(QStringLiteral("ws://7e6egbawekbkxzkv4244pqeqgoo4axko2imgjbedwnn6s5yb6b7oliqd.onion/ws")); + + // website constants + const QString websiteUrl = "https://featherwallet.org"; } #endif //FEATHER_GLOBALS_H diff --git a/src/historywidget.cpp b/src/historywidget.cpp index 9558ffa..c74f8bb 100644 --- a/src/historywidget.cpp +++ b/src/historywidget.cpp @@ -5,6 +5,10 @@ #include "ui_historywidget.h" #include "dialog/transactioninfodialog.h" #include "dialog/TxProofDialog.h" +#include "utils/Icons.h" +#include "utils/config.h" +#include "appcontext.h" + #include HistoryWidget::HistoryWidget(QWidget *parent) @@ -15,11 +19,11 @@ HistoryWidget::HistoryWidget(QWidget *parent) { ui->setupUi(this); m_contextMenu->addMenu(m_copyMenu); - m_contextMenu->addAction(QIcon(":/assets/images/info.png"), "Show details", this, &HistoryWidget::showTxDetails); - m_contextMenu->addAction(QIcon(":/assets/images/network.png"), "View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer); + m_contextMenu->addAction(icons()->icon("info2.svg"), "Show details", this, &HistoryWidget::showTxDetails); + m_contextMenu->addAction("View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer); // copy menu - m_copyMenu->setIcon(QIcon(":/assets/images/copy.png")); + m_copyMenu->setIcon(icons()->icon("copy.png")); m_copyMenu->addAction("Transaction ID", this, [this]{copy(copyField::TxID);}); m_copyMenu->addAction("Description", this, [this]{copy(copyField::Description);}); m_copyMenu->addAction("Date", this, [this]{copy(copyField::Date);}); @@ -56,12 +60,12 @@ void HistoryWidget::showContextMenu(const QPoint &point) { bool unconfirmed = tx->isFailed() || tx->isPending(); if (AppContext::txCache.contains(tx->hash()) && unconfirmed && tx->direction() != TransactionInfo::Direction_In) { - menu.addAction(QIcon(":/assets/images/info.png"), "Resend transaction", this, &HistoryWidget::onResendTransaction); + menu.addAction(icons()->icon("info2.svg"), "Resend transaction", this, &HistoryWidget::onResendTransaction); } menu.addMenu(m_copyMenu); - menu.addAction(QIcon(":/assets/images/info.png"), "Show details", this, &HistoryWidget::showTxDetails); - menu.addAction(QIcon(":/assets/images/network.png"), "View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer); + menu.addAction(icons()->icon("info2.svg"), "Show details", this, &HistoryWidget::showTxDetails); + menu.addAction(icons()->icon("network.png"), "View on block explorer", this, &HistoryWidget::onViewOnBlockExplorer); menu.addAction("Create tx proof", this, &HistoryWidget::createTxProof); menu.exec(ui->history->viewport()->mapToGlobal(point)); diff --git a/src/libwalletqt/Subaddress.cpp b/src/libwalletqt/Subaddress.cpp index 28f3125..308eb4a 100644 --- a/src/libwalletqt/Subaddress.cpp +++ b/src/libwalletqt/Subaddress.cpp @@ -10,6 +10,11 @@ Subaddress::Subaddress(Monero::Subaddress *subaddressImpl, QObject *parent) getAll(); } +QString Subaddress::errorString() const +{ + return QString::fromStdString(m_subaddressImpl->errorString()); +} + void Subaddress::getAll() const { emit refreshStarted(); @@ -46,17 +51,24 @@ bool Subaddress::getRow(int index, std::functionaddRow(accountIndex, label.toStdString()); - getAll(); + bool r = m_subaddressImpl->addRow(accountIndex, label.toStdString()); + + if (r) + getAll(); + + return r; } -void Subaddress::setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const +bool Subaddress::setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const { - m_subaddressImpl->setLabel(accountIndex, addressIndex, label.toStdString()); - getAll(); - emit labelChanged(); + bool r = m_subaddressImpl->setLabel(accountIndex, addressIndex, label.toStdString()); + if (r) { + getAll(); + emit labelChanged(); + } + return r; } void Subaddress::refresh(quint32 accountIndex) const diff --git a/src/libwalletqt/Subaddress.h b/src/libwalletqt/Subaddress.h index 735d370..f031e60 100644 --- a/src/libwalletqt/Subaddress.h +++ b/src/libwalletqt/Subaddress.h @@ -16,13 +16,14 @@ class Subaddress : public QObject { Q_OBJECT public: - Q_INVOKABLE void getAll() const; - Q_INVOKABLE bool getRow(int index, std::function callback) const; - Q_INVOKABLE void addRow(quint32 accountIndex, const QString &label) const; - Q_INVOKABLE void setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const; - Q_INVOKABLE void refresh(quint32 accountIndex) const; - Q_INVOKABLE quint64 unusedLookahead() const; + void getAll() const; + bool getRow(int index, std::function callback) const; + bool addRow(quint32 accountIndex, const QString &label) const; + bool setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const; + void refresh(quint32 accountIndex) const; + quint64 unusedLookahead() const; quint64 count() const; + QString errorString() const; Monero::SubaddressRow* row(int index) const; signals: diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index c1e272f..42d2349 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -4,8 +4,8 @@ #include "TransactionHistory.h" #include "TransactionInfo.h" #include "utils/utils.h" -#include "appcontext.h" - +#include "utils/AppData.h" +#include "utils/config.h" bool TransactionHistory::transaction(int index, std::function callback) { @@ -164,11 +164,11 @@ bool TransactionHistory::writeCSV(const QString &path) { // calc historical fiat price QString fiatAmount; QString preferredFiatSymbol = config()->get(Config::preferredFiatCurrency).toString(); - const double usd_price = AppContext::txFiatHistory->get(timeStamp.toString("yyyyMMdd")); + const double usd_price = appData()->txFiatHistory->get(timeStamp.toString("yyyyMMdd")); double fiat_price = usd_price * amount; if(preferredFiatSymbol != "USD") - fiat_price = AppContext::prices->convert("USD", preferredFiatSymbol, fiat_price); + fiat_price = appData()->prices.convert("USD", preferredFiatSymbol, fiat_price); double fiat_rounded = ceil(Utils::roundSignificant(fiat_price, 3) * 100.0) / 100.0; if(fiat_price != 0) fiatAmount = QString("%1 %2").arg(QString::number(fiat_rounded)).arg(preferredFiatSymbol); diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 45ed1fe..2079519 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -283,6 +283,11 @@ bool Wallet::isTrezor() const return m_walletImpl->getDeviceType() == Monero::Wallet::Device_Trezor; } +bool Wallet::reconnectDevice() +{ + return m_walletImpl->reconnectDevice(); +} + //! create a view only wallet bool Wallet::createViewOnly(const QString &path, const QString &password) const { @@ -600,15 +605,15 @@ PendingTransaction *Wallet::createTransaction(const QString &dst_addr, const QSt quint64 amount, quint32 mixin_count, PendingTransaction::Priority priority) { - pauseRefresh(); +// pauseRefresh(); std::set subaddr_indices; Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( dst_addr.toStdString(), payment_id.toStdString(), amount, mixin_count, static_cast(priority), currentSubaddressAccount(), subaddr_indices); - PendingTransaction * result = new PendingTransaction(ptImpl,0); + PendingTransaction * result = new PendingTransaction(ptImpl, nullptr); - startRefresh(); +// startRefresh(); return result; } @@ -626,7 +631,7 @@ void Wallet::createTransactionAsync(const QString &dst_addr, const QString &paym PendingTransaction* Wallet::createTransactionMultiDest(const QVector &dst_addr, const QVector &amount, PendingTransaction::Priority priority) { - pauseRefresh(); +// pauseRefresh(); std::vector dests; for (auto &addr : dst_addr) { @@ -642,7 +647,7 @@ PendingTransaction* Wallet::createTransactionMultiDest(const QVector &d Monero::PendingTransaction * ptImpl = m_walletImpl->createTransactionMultDest(dests, "", amounts, 11, static_cast(priority)); PendingTransaction * result = new PendingTransaction(ptImpl); - startRefresh(); +// startRefresh(); return result; } @@ -662,7 +667,7 @@ void Wallet::createTransactionMultiDestAsync(const QVector &dst_addr, c PendingTransaction *Wallet::createTransactionAll(const QString &dst_addr, const QString &payment_id, quint32 mixin_count, PendingTransaction::Priority priority) { - pauseRefresh(); +// pauseRefresh(); std::set subaddr_indices; Monero::PendingTransaction * ptImpl = m_walletImpl->createTransaction( @@ -670,7 +675,7 @@ PendingTransaction *Wallet::createTransactionAll(const QString &dst_addr, const static_cast(priority), currentSubaddressAccount(), subaddr_indices); PendingTransaction * result = new PendingTransaction(ptImpl, this); - startRefresh(); +// startRefresh(); return result; } @@ -688,13 +693,13 @@ void Wallet::createTransactionAllAsync(const QString &dst_addr, const QString &p PendingTransaction *Wallet::createTransactionSingle(const QString &key_image, const QString &dst_addr, const size_t outputs, PendingTransaction::Priority priority) { - pauseRefresh(); +// pauseRefresh(); Monero::PendingTransaction * ptImpl = m_walletImpl->createTransactionSingle(key_image.toStdString(), dst_addr.toStdString(), outputs, static_cast(priority)); PendingTransaction * result = new PendingTransaction(ptImpl, this); - startRefresh(); +// startRefresh(); return result; } @@ -710,12 +715,12 @@ void Wallet::createTransactionSingleAsync(const QString &key_image, const QStrin PendingTransaction *Wallet::createSweepUnmixableTransaction() { - pauseRefresh(); +// pauseRefresh(); Monero::PendingTransaction * ptImpl = m_walletImpl->createSweepUnmixableTransaction(); PendingTransaction * result = new PendingTransaction(ptImpl, this); - startRefresh(); +// startRefresh(); return result; } @@ -1224,13 +1229,23 @@ void Wallet::onWalletPassphraseNeeded(bool on_device) } quint64 Wallet::getBytesReceived() const { - return m_walletImpl->getBytesReceived(); + // TODO: this can segfault. Unclear why. + try { + return m_walletImpl->getBytesReceived(); + } + catch (...) { + return 0; + } } quint64 Wallet::getBytesSent() const { return m_walletImpl->getBytesSent(); } +bool Wallet::isDeviceConnected() const { + return m_walletImpl->isDeviceConnected(); +} + void Wallet::onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) { if (m_walletListener != nullptr) @@ -1303,14 +1318,14 @@ Wallet::~Wallet() delete m_coins; m_coins = NULL; //Monero::WalletManagerFactory::getWalletManager()->closeWallet(m_walletImpl); - if(status() == Status_Critical) + if(status() == Status_Critical || status() == Status_BadPassword) qDebug("Not storing wallet cache"); else if( m_walletImpl->store("")) qDebug("Wallet cache stored successfully"); else qDebug("Error storing wallet cache"); delete m_walletImpl; - m_walletImpl = NULL; + m_walletImpl = nullptr; delete m_walletListener; m_walletListener = NULL; qDebug("m_walletImpl deleted"); @@ -1325,7 +1340,7 @@ void Wallet::startRefreshThread() auto last = std::chrono::steady_clock::now(); while (!m_scheduler.stopping()) { - if (m_refreshEnabled) + if (m_refreshEnabled && (!isHwBacked() || isDeviceConnected())) { const auto now = std::chrono::steady_clock::now(); const auto elapsed = now - last; diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 8af1833..a3b9612 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -78,9 +78,10 @@ Q_OBJECT public: enum Status { - Status_Ok = Monero::Wallet::Status_Ok, - Status_Error = Monero::Wallet::Status_Error, - Status_Critical = Monero::Wallet::Status_Critical + Status_Ok = Monero::Wallet::Status_Ok, + Status_Error = Monero::Wallet::Status_Error, + Status_Critical = Monero::Wallet::Status_Critical, + Status_BadPassword = Monero::Wallet::Status_BadPassword }; Q_ENUM(Status) @@ -198,6 +199,9 @@ public: bool isLedger() const; bool isTrezor() const; + //! attempt to reconnect to hw-device + bool reconnectDevice(); + //! returns if view only wallet bool viewOnly() const; @@ -424,6 +428,8 @@ public: quint64 getBytesReceived() const; quint64 getBytesSent() const; + bool isDeviceConnected() const; + // TODO: setListenter() when it implemented in API signals: // emitted on every event happened with wallet @@ -432,7 +438,7 @@ signals: // emitted when refresh process finished (could take a long time) // signalling only after we - void refreshed(bool success); + void refreshed(bool success, const QString &message); void moneySpent(const QString &txId, quint64 amount); void moneyReceived(const QString &txId, quint64 amount); @@ -443,6 +449,7 @@ signals: void walletCreationHeightChanged(); void deviceButtonRequest(quint64 buttonCode); void deviceButtonPressed(); + void deviceError(const QString &message); void walletPassphraseNeeded(bool onDevice); void transactionCommitted(bool status, PendingTransaction *t, const QStringList& txid); void heightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) const; diff --git a/src/libwalletqt/WalletListenerImpl.cpp b/src/libwalletqt/WalletListenerImpl.cpp index a92f2db..0cd8307 100644 --- a/src/libwalletqt/WalletListenerImpl.cpp +++ b/src/libwalletqt/WalletListenerImpl.cpp @@ -45,8 +45,9 @@ void WalletListenerImpl::updated() void WalletListenerImpl::refreshed(bool success) { qDebug() << __FUNCTION__; + QString message = m_wallet->errorString(); m_wallet->onRefreshed(success); - emit m_wallet->refreshed(success); + emit m_wallet->refreshed(success, message); } void WalletListenerImpl::onDeviceButtonRequest(uint64_t code) @@ -61,6 +62,12 @@ void WalletListenerImpl::onDeviceButtonPressed() emit m_wallet->deviceButtonPressed(); } +void WalletListenerImpl::onDeviceError(const std::string &message) +{ + qDebug() << __FUNCTION__; + emit m_wallet->deviceError(QString::fromStdString(message)); +} + void WalletListenerImpl::onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) { qDebug() << __FUNCTION__; diff --git a/src/libwalletqt/WalletListenerImpl.h b/src/libwalletqt/WalletListenerImpl.h index 2b250e4..36fe50b 100644 --- a/src/libwalletqt/WalletListenerImpl.h +++ b/src/libwalletqt/WalletListenerImpl.h @@ -31,6 +31,8 @@ public: virtual void onDeviceButtonPressed() override; + virtual void onDeviceError(const std::string &message) override; + virtual void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort) override; virtual Monero::optional onDevicePassphraseRequest(bool & on_device) override; diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index 970589d..d1feb23 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -4,7 +4,6 @@ #include "libwalletqt/WalletManager.h" #include "Wallet.h" -//#include "qt/updater.h" #include "utils/ScopeGuard.h" class WalletPassphraseListenerImpl : public Monero::WalletListener, public PassphraseReceiver @@ -31,11 +30,11 @@ public: // return m_phelper.onDevicePassphraseRequest(on_device); // } // -// virtual void onDeviceButtonRequest(uint64_t code) override -// { -// qDebug() << __FUNCTION__; -// emit m_mgr->deviceButtonRequest(code); -// } + virtual void onDeviceButtonRequest(uint64_t code) override + { + qDebug() << __FUNCTION__; + emit m_mgr->deviceButtonRequest(code); + } // // virtual void onDeviceButtonPressed() override // { @@ -43,6 +42,12 @@ public: // emit m_mgr->deviceButtonPressed(); // } + virtual void onDeviceError(const std::string &message) override + { + qDebug() << __FUNCTION__; + emit m_mgr->deviceError(QString::fromStdString(message)); + } + private: WalletManager * m_mgr; PassphraseHelper m_phelper; @@ -125,17 +130,17 @@ Wallet *WalletManager::recoveryWallet(const QString &path, const QString &passwo return m_currentWallet; } -Wallet *WalletManager::createWalletFromKeys(const QString &path, const QString &language, NetworkType::Type nettype, - const QString &address, const QString &viewkey, const QString &spendkey, - quint64 restoreHeight, quint64 kdfRounds) +Wallet *WalletManager::createWalletFromKeys(const QString &path, const QString &password, const QString &language, + NetworkType::Type nettype, const QString &address, const QString &viewkey, + const QString &spendkey, quint64 restoreHeight, quint64 kdfRounds) { QMutexLocker locker(&m_mutex); if (m_currentWallet) { qDebug() << "Closing open m_currentWallet" << m_currentWallet; delete m_currentWallet; - m_currentWallet = NULL; + m_currentWallet = nullptr; } - Monero::Wallet * w = m_pimpl->createWalletFromKeys(path.toStdString(), "", language.toStdString(), static_cast(nettype), restoreHeight, + Monero::Wallet * w = m_pimpl->createWalletFromKeys(path.toStdString(), password.toStdString(), language.toStdString(), static_cast(nettype), restoreHeight, address.toStdString(), viewkey.toStdString(), spendkey.toStdString(), kdfRounds); m_currentWallet = new Wallet(w); return m_currentWallet; @@ -148,9 +153,9 @@ Wallet *WalletManager::createDeterministicWalletFromSpendKey(const QString &path if (m_currentWallet) { qDebug() << "Closing open m_currentWallet" << m_currentWallet; delete m_currentWallet; - m_currentWallet = NULL; + m_currentWallet = nullptr; } - Monero::Wallet * w = m_pimpl->createDeterministicWalletFromSpendKey(path.toStdString(), "", language.toStdString(), static_cast(nettype), restoreHeight, + Monero::Wallet * w = m_pimpl->createDeterministicWalletFromSpendKey(path.toStdString(), password.toStdString(), language.toStdString(), static_cast(nettype), restoreHeight, spendkey.toStdString(), kdfRounds, offset_passphrase.toStdString()); m_currentWallet = new Wallet(w); return m_currentWallet; @@ -172,7 +177,7 @@ Wallet *WalletManager::createWalletFromDevice(const QString &path, const QString if (m_currentWallet) { qDebug() << "Closing open m_currentWallet" << m_currentWallet; delete m_currentWallet; - m_currentWallet = NULL; + m_currentWallet = nullptr; } Monero::Wallet * w = m_pimpl->createWalletFromDevice(path.toStdString(), password.toStdString(), static_cast(nettype), deviceName.toStdString(), restoreHeight, subaddressLookahead.toStdString(), 1, &tmpListener); @@ -201,10 +206,12 @@ void WalletManager::createWalletFromDeviceAsync(const QString &path, const QStri QString WalletManager::closeWallet() { QMutexLocker locker(&m_mutex); + qDebug() << Q_FUNC_INFO ; QString result; if (m_currentWallet) { result = m_currentWallet->address(0, 0); delete m_currentWallet; + m_currentWallet = nullptr; } else { qCritical() << "Trying to close non existing wallet " << m_currentWallet; result = "0"; @@ -428,15 +435,6 @@ QUrl WalletManager::localPathToUrl(const QString &path) const return QUrl::fromLocalFile(path); } -QString WalletManager::checkUpdates(const QString &software, const QString &subdir) const -{ - qDebug() << "Checking for updates"; - const std::tuple result = Monero::WalletManager::checkUpdates(software.toStdString(), subdir.toStdString()); - if (!std::get<0>(result)) - return QString(""); - return QString::fromStdString(std::get<1>(result) + "|" + std::get<2>(result) + "|" + std::get<3>(result) + "|" + std::get<4>(result)); -} - bool WalletManager::clearWalletCache(const QString &wallet_path) const { @@ -459,6 +457,7 @@ WalletManager::WalletManager(QObject *parent) : QObject(parent) , m_passphraseReceiver(nullptr) , m_scheduler(this) + , m_currentWallet(nullptr) { m_pimpl = Monero::WalletManagerFactory::getWalletManager(); } diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index 42aa54e..0f4e58e 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -62,6 +62,7 @@ public: NetworkType::Type nettype = NetworkType::MAINNET, quint64 restoreHeight = 0, quint64 kdfRounds = 1); Q_INVOKABLE Wallet * createWalletFromKeys(const QString &path, + const QString &password, const QString &language, NetworkType::Type nettype, const QString &address, @@ -159,12 +160,6 @@ public: Q_INVOKABLE bool parse_uri(const QString &uri, QString &address, QString &payment_id, uint64_t &amount, QString &tx_description, QString &recipient_name, QVector &unknown_parameters, QString &error) const; Q_INVOKABLE QVariantMap parse_uri_to_object(const QString &uri) const; // Q_INVOKABLE bool saveQrCode(const QString &, const QString &) const; -// Q_INVOKABLE void checkUpdatesAsync( -// const QString &software, -// const QString &subdir, -// const QString &buildTag, -// const QString &version); - Q_INVOKABLE QString checkUpdates(const QString &software, const QString &subdir) const; // clear/rename wallet cache Q_INVOKABLE bool clearWalletCache(const QString &fileName) const; @@ -182,12 +177,7 @@ signals: void walletPassphraseNeeded(bool onDevice); void deviceButtonRequest(quint64 buttonCode); void deviceButtonPressed(); - void checkUpdatesComplete( - const QString &version, - const QString &downloadUrl, - const QString &hash, - const QString &firstSigner, - const QString &secondSigner) const; + void deviceError(const QString &message); void miningStatus(bool isMining) const; void proxyAddressChanged() const; diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 1fe5095..228c4bc 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,15 +1,14 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020-2021, The Monero Project. -#include #include -#include #include -#include #include -#include #include "mainwindow.h" +#include "ui_mainwindow.h" + +#include "config-feather.h" #include "dialog/txconfdialog.h" #include "dialog/txconfadvdialog.h" #include "dialog/debuginfodialog.h" @@ -21,88 +20,322 @@ #include "dialog/passworddialog.h" #include "dialog/balancedialog.h" #include "dialog/WalletCacheDebugDialog.h" -#include "ui_mainwindow.h" +#include "dialog/UpdateDialog.h" #include "globals.h" -#include "utils/ColorScheme.h" - -// libwalletqt #include "libwalletqt/AddressBook.h" +#include "utils/AsyncTask.h" +#include "utils/AppData.h" +#include "utils/ColorScheme.h" +#include "utils/SemanticVersion.h" +#include "utils/NetworkManager.h" +#include "utils/Icons.h" +#include "utils/WebsocketNotifier.h" +#include "utils/Updater.h" MainWindow * MainWindow::pMainWindow = nullptr; -MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : - QMainWindow(parent), - ui(new Ui::MainWindow), - m_ctx(ctx) +MainWindow::MainWindow(AppContext *ctx, QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) + , m_ctx(ctx) { pMainWindow = this; ui->setupUi(this); - // Preload icons for better performance - m_statusDisconnected = QIcon(":/assets/images/status_disconnected.svg"); - m_statusConnecting = QIcon(":/assets/images/status_lagging.svg"); - m_statusSynchronizing = QIcon(":/assets/images/status_waiting.svg"); - m_statusSynchronized = QIcon(":/assets/images/status_connected.svg"); - m_windowSettings = new Settings(this); - m_aboutDialog = new AboutDialog(this); m_windowCalc = new CalcWindow(this); - - // light/dark theme - m_skins.insert("Native", ""); - this->loadSkins(); - QString skin = config()->get(Config::skin).toString(); - qApp->setStyleSheet(m_skins[skin]); - - this->screenDpiRef = 128; - this->screenGeo = QApplication::primaryScreen()->availableGeometry(); - this->screenRect = QGuiApplication::primaryScreen()->geometry(); - this->screenDpi = QGuiApplication::primaryScreen()->logicalDotsPerInch(); - this->screenDpiPhysical = QGuiApplication::primaryScreen()->physicalDotsPerInch(); - this->screenRatio = this->screenDpiPhysical / this->screenDpiRef; - qInfo() << QString("%1x%2 (%3 DPI)").arg(this->screenRect.width()).arg(this->screenRect.height()).arg(this->screenDpi); + m_splashDialog = new SplashDialog(this); this->restoreGeo(); + this->startupWarning(); - this->create_status_bar(); + this->initSkins(); + this->initStatusBar(); + this->initWidgets(); + this->initMenu(); + this->initTray(); + this->initHome(); + this->initTouchBar(); + this->initWalletContext(); - // Bootstrap Tor/websockets - m_ctx->initTor(); - m_ctx->initWS(); + // Websocket notifier + connect(websocketNotifier(), &WebsocketNotifier::CCSReceived, ui->ccsWidget->model(), &CCSModel::updateEntries); + connect(websocketNotifier(), &WebsocketNotifier::RedditReceived, ui->redditWidget->model(), &RedditModel::updatePosts); + connect(websocketNotifier(), &WebsocketNotifier::UpdatesReceived, this, &MainWindow::onUpdatesAvailable); +#ifdef HAS_XMRIG + connect(websocketNotifier(), &WebsocketNotifier::XMRigDownloadsReceived, m_xmrig, &XMRigWidget::onDownloads); +#endif - // update statusbar - connect(m_ctx->tor, &Tor::connectionStateChanged, [this](bool connected){ - connected ? m_statusBtnTor->setIcon(QIcon(":/assets/images/tor_logo.png")) - : m_statusBtnTor->setIcon(QIcon(":/assets/images/tor_logo_disabled.png"));}); - connect(m_ctx->nodes, &Nodes::updateStatus, [=](const QString &msg){ - this->setStatusText(msg); + // Settings + for (auto tickerWidget: m_tickerWidgets) + connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, tickerWidget, &TickerWidget::init); + connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_balanceWidget, &TickerWidget::init); + connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_ctx, &AppContext::onPreferredFiatCurrencyChanged); + connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, ui->sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged)); + connect(m_windowSettings, &Settings::amountPrecisionChanged, m_ctx, &AppContext::onAmountPrecisionChanged); + connect(m_windowSettings, &Settings::skinChanged, this, &MainWindow::skinChanged); + + // Wizard + connect(this, &MainWindow::closed, [=]{ + if (m_wizard) + m_wizard->close(); }); - // menu connects - connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::menuQuitClicked); - connect(ui->actionSettings, &QAction::triggered, this, &MainWindow::menuSettingsClicked); - connect(ui->actionCalculator, &QAction::triggered, this, &MainWindow::showCalcWindow); - connect(ui->actionPay_to_many, &QAction::triggered, this, &MainWindow::payToMany); - connect(ui->actionWallet_cache_debug, &QAction::triggered, this, &MainWindow::showWalletCacheDebugDialog); + // History + // TODO: move this + connect(m_ctx, &AppContext::walletRefreshed, ui->historyWidget, &HistoryWidget::onWalletRefreshed); + connect(m_ctx, &AppContext::walletOpened, ui->historyWidget, &HistoryWidget::onWalletOpened); + if (!config()->get(Config::firstRun).toBool() || TailsOS::detect() || WhonixOS::detect()) { + this->onInitialNetworkConfigured(); + } + + this->setEnabled(true); + this->show(); + ColorScheme::updateFromWidget(this); + + if (!this->autoOpenWallet()) { + this->initWizard(); + } + + // Timers + connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateNetStats); + connect(&m_txTimer, &QTimer::timeout, [this]{ + m_statusLabelStatus->setText("Constructing transaction" + this->statusDots()); + }); +} + +void MainWindow::initSkins() { + m_skins.insert("Native", ""); + + QString qdarkstyle = this->loadStylesheet(":qdarkstyle/style.qss"); + if (!qdarkstyle.isEmpty()) + m_skins.insert("QDarkStyle", qdarkstyle); + + QString breeze_dark = this->loadStylesheet(":/dark.qss"); + if (!breeze_dark.isEmpty()) + m_skins.insert("Breeze/Dark", breeze_dark); + + QString breeze_light = this->loadStylesheet(":/light.qss"); + if (!breeze_light.isEmpty()) + m_skins.insert("Breeze/Light", breeze_light); + + QString skin = config()->get(Config::skin).toString(); + qApp->setStyleSheet(m_skins[skin]); + ColorScheme::updateFromWidget(this); +} + +void MainWindow::initStatusBar() { +#if defined(Q_OS_WIN) + // No seperators between statusbar widgets + this->statusBar()->setStyleSheet("QStatusBar::item {border: None;}"); +#endif + + this->statusBar()->setFixedHeight(30); + + m_statusLabelStatus = new QLabel("Idle", this); + m_statusLabelStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); + this->statusBar()->addWidget(m_statusLabelStatus); + + m_statusLabelNetStats = new QLabel("", this); + m_statusLabelNetStats->setTextInteractionFlags(Qt::TextSelectableByMouse); + this->statusBar()->addWidget(m_statusLabelNetStats); + + m_statusUpdateAvailable = new QPushButton(this); + m_statusUpdateAvailable->setFlat(true); + m_statusUpdateAvailable->setCursor(Qt::PointingHandCursor); + m_statusUpdateAvailable->setIcon(icons()->icon("tab_party.png")); + m_statusUpdateAvailable->hide(); + this->statusBar()->addPermanentWidget(m_statusUpdateAvailable); + + m_statusLabelBalance = new ClickableLabel(this); + m_statusLabelBalance->setText("Balance: 0 XMR"); + m_statusLabelBalance->setTextInteractionFlags(Qt::TextSelectableByMouse); + m_statusLabelBalance->setCursor(Qt::PointingHandCursor); + this->statusBar()->addPermanentWidget(m_statusLabelBalance); + connect(m_statusLabelBalance, &ClickableLabel::clicked, this, &MainWindow::showBalanceDialog); + + m_statusBtnConnectionStatusIndicator = new StatusBarButton(icons()->icon("status_disconnected.svg"), "Connection status", this); + connect(m_statusBtnConnectionStatusIndicator, &StatusBarButton::clicked, this, &MainWindow::showConnectionStatusDialog); + this->statusBar()->addPermanentWidget(m_statusBtnConnectionStatusIndicator); + + m_statusBtnPassword = new StatusBarButton(icons()->icon("lock.svg"), "Password", this); + connect(m_statusBtnPassword, &StatusBarButton::clicked, this, &MainWindow::showPasswordDialog); + this->statusBar()->addPermanentWidget(m_statusBtnPassword); + + m_statusBtnPreferences = new StatusBarButton(icons()->icon("preferences.svg"), "Settings", this); + connect(m_statusBtnPreferences, &StatusBarButton::clicked, this, &MainWindow::menuSettingsClicked); + this->statusBar()->addPermanentWidget(m_statusBtnPreferences); + + m_statusBtnSeed = new StatusBarButton(icons()->icon("seed.png"), "Seed", this); + connect(m_statusBtnSeed, &StatusBarButton::clicked, this, &MainWindow::showSeedDialog); + this->statusBar()->addPermanentWidget(m_statusBtnSeed); + + m_statusBtnTor = new StatusBarButton(icons()->icon("tor_logo_disabled.png"), "Tor", this); + connect(m_statusBtnTor, &StatusBarButton::clicked, this, &MainWindow::menuTorClicked); + this->statusBar()->addPermanentWidget(m_statusBtnTor); + + m_statusBtnHwDevice = new StatusBarButton(icons()->icon("ledger.png"), "Ledger", this); + connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked); + this->statusBar()->addPermanentWidget(m_statusBtnHwDevice); + m_statusBtnHwDevice->hide(); +} + +void MainWindow::initWidgets() { + int homeWidget = config()->get(Config::homeWidget).toInt(); + ui->tabHomeWidget->setCurrentIndex(TabsHome(homeWidget)); + connect(ui->tabHomeWidget, &QTabWidget::currentChanged, [](int index){ + config()->set(Config::homeWidget, TabsHome(index)); + }); + + // [History] + connect(ui->historyWidget, &HistoryWidget::viewOnBlockExplorer, this, &MainWindow::onViewOnBlockExplorer); + connect(ui->historyWidget, &HistoryWidget::resendTransaction, this, &MainWindow::onResendTransaction); + + // [Receive] + connect(ui->receiveWidget, &ReceiveWidget::showTransactions, [this](const QString &text) { + ui->historyWidget->setSearchText(text); + ui->tabWidget->setCurrentIndex(Tabs::HISTORY); + }); + connect(ui->contactWidget, &ContactsWidget::fillAddress, ui->sendWidget, &SendWidget::fillAddress); + + +#ifdef HAS_LOCALMONERO + m_localMoneroWidget = new LocalMoneroWidget(this, m_ctx); + ui->localMoneroLayout->addWidget(m_localMoneroWidget); +#else + ui->tabWidgetExchanges->setTabVisible(0, false); +#endif + +#ifdef HAS_XMRIG + m_xmrig = new XMRigWidget(m_ctx, this); + ui->xmrRigLayout->addWidget(m_xmrig); + + connect(m_xmrig, &XMRigWidget::miningStarted, [this]{ this->setTitle(true); }); + connect(m_xmrig, &XMRigWidget::miningEnded, [this]{ this->setTitle(false); }); +#else + ui->tabWidget->setTabVisible(Tabs::XMRIG, false); +#endif +} + +void MainWindow::initMenu() { + // TODO: Rename actions to follow style + // [File] + connect(ui->actionClose, &QAction::triggered, this, &MainWindow::menuWalletCloseClicked); // Close current wallet + connect(ui->actionQuit, &QAction::triggered, this, &MainWindow::menuQuitClicked); // Quit application + connect(ui->actionSettings, &QAction::triggered, this, &MainWindow::menuSettingsClicked); + + // [Wallet] + connect(ui->actionInformation, &QAction::triggered, this, &MainWindow::showWalletInfoDialog); + connect(ui->actionPassword, &QAction::triggered, this, &MainWindow::showPasswordDialog); + connect(ui->actionSeed, &QAction::triggered, this, &MainWindow::showSeedDialog); + connect(ui->actionKeys, &QAction::triggered, this, &MainWindow::showKeysDialog); + connect(ui->actionViewOnly, &QAction::triggered, this, &MainWindow::showViewOnlyDialog); + + // [Wallet] -> [Advanced] + connect(ui->actionStore_wallet, &QAction::triggered, [this]{m_ctx->currentWallet->store();}); + connect(ui->actionUpdate_balance, &QAction::triggered, [this]{m_ctx->updateBalance();}); + connect(ui->actionRefresh_tabs, &QAction::triggered, [this]{m_ctx->refreshModels();}); + connect(ui->actionRescan_spent, &QAction::triggered, this, &MainWindow::rescanSpent); + connect(ui->actionWallet_cache_debug, &QAction::triggered, this, &MainWindow::showWalletCacheDebugDialog); + connect(ui->actionChange_restore_height, &QAction::triggered, this, &MainWindow::showRestoreHeightDialog); + + // [Wallet] -> [Advanced] -> [Export] + connect(ui->actionExportOutputs, &QAction::triggered, this, &MainWindow::exportOutputs); + connect(ui->actionExportKeyImages, &QAction::triggered, this, &MainWindow::exportKeyImages); + + // [Wallet] -> [Advanced] -> [Import] + connect(ui->actionImportOutputs, &QAction::triggered, this, &MainWindow::importOutputs); + connect(ui->actionImportKeyImages, &QAction::triggered, this, &MainWindow::importKeyImages); + + // [Wallet] -> [History] + connect(ui->actionExport_CSV, &QAction::triggered, this, &MainWindow::onExportHistoryCSV); + + // [Wallet] -> [Contacts] + connect(ui->actionExportContactsCSV, &QAction::triggered, this, &MainWindow::onExportContactsCSV); + connect(ui->actionImportContactsCSV, &QAction::triggered, this, &MainWindow::importContacts); + + // [View] + m_tabShowHideSignalMapper = new QSignalMapper(this); + + // Show/Hide Home + connect(ui->actionShow_Home, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["Home"] = new ToggleTab(ui->tabHome, "Home", "Home", ui->actionShow_Home, Config::showTabHome); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_Home, "Home"); + + // Show/Hide Coins + connect(ui->actionShow_Coins, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["Coins"] = new ToggleTab(ui->tabCoins, "Coins", "Coins", ui->actionShow_Coins, Config::showTabCoins); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_Coins, "Coins"); + + // Show/Hide Calc + connect(ui->actionShow_calc, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["Calc"] = new ToggleTab(ui->tabCalc, "Calc", "Calc", ui->actionShow_calc, Config::showTabCalc); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_calc, "Calc"); + + // Show/Hide Exchange +#if defined(HAS_LOCALMONERO) + connect(ui->actionShow_Exchange, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["Exchange"] = new ToggleTab(ui->tabExchange, "Exchange", "Exchange", ui->actionShow_Exchange, Config::showTabExchange); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_Exchange, "Exchange"); +#else + ui->actionShow_Exchange->setVisible(false); + ui->tabWidget->setTabVisible(Tabs::EXCHANGES, false); +#endif + + // Show/Hide Mining +#if defined(HAS_XMRIG) + connect(ui->actionShow_XMRig, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); + m_tabShowHideMapper["Mining"] = new ToggleTab(ui->tabXmrRig, "Mining", "Mining", ui->actionShow_XMRig, Config::showTabXMRig); + m_tabShowHideSignalMapper->setMapping(ui->actionShow_XMRig, "Mining"); +#else + ui->actionShow_XMRig->setVisible(false); +#endif + + for (const auto &key: m_tabShowHideMapper.keys()) { + const auto toggleTab = m_tabShowHideMapper.value(key); + const bool show = config()->get(toggleTab->configKey).toBool(); + toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name); + ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show); + } + connect(m_tabShowHideSignalMapper, &QSignalMapper::mappedString, this, &MainWindow::menuToggleTabVisible); + + // [Tools] + connect(ui->actionSignVerify, &QAction::triggered, this, &MainWindow::menuSignVerifyClicked); + connect(ui->actionVerifyTxProof, &QAction::triggered, this, &MainWindow::menuVerifyTxProof); + connect(ui->actionLoadUnsignedTxFromFile, &QAction::triggered, this, &MainWindow::loadUnsignedTx); + connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard); + connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx); + connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText); + connect(ui->actionImport_transaction, &QAction::triggered, this, &MainWindow::importTransaction); + connect(ui->actionPay_to_many, &QAction::triggered, this, &MainWindow::payToMany); + connect(ui->actionCalculator, &QAction::triggered, this, &MainWindow::showCalcWindow); + connect(ui->actionCreateDesktopEntry, &QAction::triggered, this, &MainWindow::onCreateDesktopEntry); + + // TODO: Allow creating desktop entry on Windows and Mac #if defined(Q_OS_WIN) || defined(Q_OS_MACOS) ui->actionCreateDesktopEntry->setDisabled(true); #endif - connect(ui->actionCreateDesktopEntry, &QAction::triggered, [=]{ - auto msg = Utils::xdgDesktopEntryRegister() ? "Desktop entry created" : "Desktop entry not created due to an error."; - QMessageBox::information(this, "Desktop entry", msg); - }); - connect(ui->actionReport_bug, &QAction::triggered, [this](){ - QMessageBox::information(this, "Reporting Bugs", - "Please report any bugs as issues on our git repo:
\n" - "https://git.featherwallet.org/feather/feather/issues

" - "\n" - "Before reporting a bug, upgrade to the most recent version of Feather " - "(latest release or git HEAD), and include the version number in your report. " - "Try to explain not only what the bug is, but how it occurs."); - }); - connect(ui->actionShow_debug_info, &QAction::triggered, this, &MainWindow::showDebugInfo); - connect(ui->actionOfficialWebsite, &QAction::triggered, [=] { Utils::externalLinkWarning(this, "https://featherwallet.org"); }); + + // [Help] + connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked); + connect(ui->actionOfficialWebsite, &QAction::triggered, [this](){Utils::externalLinkWarning(this, "https://featherwallet.org");}); + connect(ui->actionDonate_to_Feather, &QAction::triggered, this, &MainWindow::donateButtonClicked); + connect(ui->actionReport_bug, &QAction::triggered, this, &MainWindow::onReportBug); + connect(ui->actionShow_debug_info, &QAction::triggered, this, &MainWindow::showDebugInfo); + + + // Setup shortcuts + ui->actionStore_wallet->setShortcut(QKeySequence("Ctrl+S")); + ui->actionRefresh_tabs->setShortcut(QKeySequence("Ctrl+R")); + ui->actionClose->setShortcut(QKeySequence("Ctrl+W")); + ui->actionShow_debug_info->setShortcut(QKeySequence("Ctrl+D")); + ui->actionSettings->setShortcut(QKeySequence("Ctrl+Alt+S")); + ui->actionUpdate_balance->setShortcut(QKeySequence("Ctrl+U")); +} + +void MainWindow::initTray() { + // TODO: Add tray support on Windows and Mac #if defined(Q_OS_LINUX) // system tray @@ -127,379 +360,108 @@ MainWindow::MainWindow(AppContext *ctx, QWidget *parent) : m_trayMenu.addAction(m_trayActionExit); m_trayIcon->setContextMenu(&m_trayMenu); - // @TODO: only init tray *after* boot - connect(m_trayActionCalc, &QAction::triggered, this, &MainWindow::showCalcWindow); - connect(m_trayActionSend, &QAction::triggered, this, &MainWindow::showSendTab); + connect(m_trayActionCalc, &QAction::triggered, this, &MainWindow::showCalcWindow); + connect(m_trayActionSend, &QAction::triggered, this, &MainWindow::showSendTab); connect(m_trayActionHistory, &QAction::triggered, this, &MainWindow::showHistoryTab); - connect(m_trayActionExit, &QAction::triggered, this, &QMainWindow::close); + connect(m_trayActionExit, &QAction::triggered, this, &QMainWindow::close); #endif +} - // ticker widgets - m_tickerWidgets.append(new TickerWidget(this, "XMR")); - m_tickerWidgets.append(new TickerWidget(this, "BTC")); - for(auto tickerWidget: m_tickerWidgets) { +void MainWindow::initHome() { + // Ticker widgets + m_tickerWidgets.append(new TickerWidget(this, m_ctx, "XMR")); + m_tickerWidgets.append(new TickerWidget(this, m_ctx, "BTC")); + for (auto tickerWidget: m_tickerWidgets) { ui->tickerLayout->addWidget(tickerWidget); } - - m_balanceWidget = new TickerWidget(this, "XMR", "Balance", true, true); + m_balanceWidget = new TickerWidget(this, m_ctx, "XMR", "Balance", true, true); ui->fiatTickerLayout->addWidget(m_balanceWidget); - // Send widget - connect(ui->sendWidget, &SendWidget::createTransaction, m_ctx, QOverload::of(&AppContext::onCreateTransaction)); - connect(ui->sendWidget, &SendWidget::createTransactionMultiDest, m_ctx, &AppContext::onCreateTransactionMultiDest); - - // Nodes - connect(m_ctx->nodes, &Nodes::nodeExhausted, this, &MainWindow::showNodeExhaustedMessage); - connect(m_ctx->nodes, &Nodes::WSNodeExhausted, this, &MainWindow::showWSNodeExhaustedMessage); - - // XMRig -#ifdef HAS_XMRIG - m_xmrig = new XMRigWidget(m_ctx, this); - ui->xmrRigLayout->addWidget(m_xmrig); - connect(m_ctx->XMRig, &XmRig::output, m_xmrig, &XMRigWidget::onProcessOutput); - connect(m_ctx->XMRig, &XmRig::error, m_xmrig, &XMRigWidget::onProcessError); - connect(m_ctx->XMRig, &XmRig::hashrate, m_xmrig, &XMRigWidget::onHashrate); - - connect(m_ctx, &AppContext::walletClosed, m_xmrig, &XMRigWidget::onWalletClosed); - connect(m_ctx, &AppContext::walletOpened, m_xmrig, &XMRigWidget::onWalletOpened); - connect(m_ctx, &AppContext::XMRigDownloads, m_xmrig, &XMRigWidget::onDownloads); - - connect(m_xmrig, &XMRigWidget::miningStarted, [=]{ m_ctx->setWindowTitle(true); }); - connect(m_xmrig, &XMRigWidget::miningEnded, [=]{ m_ctx->setWindowTitle(false); }); -#else - ui->tabWidget->setTabVisible(Tabs::XMRIG, false); -#endif - connect(ui->ccsWidget, &CCSWidget::selected, this, &MainWindow::showSendScreen); - connect(m_ctx, &AppContext::ccsUpdated, ui->ccsWidget->model(), &CCSModel::updateEntries); - connect(m_ctx, &AppContext::redditUpdated, ui->redditWidget->model(), &RedditModel::updatePosts); - connect(ui->redditWidget, &RedditWidget::setStatusText, this, &MainWindow::setStatusText); +} - connect(ui->tabHomeWidget, &QTabWidget::currentChanged, [](int index){ - config()->set(Config::homeWidget, TabsHome(index)); - }); - - connect(m_ctx, &AppContext::donationNag, [=]{ - auto msg = "Feather is a 100% community-sponsored endeavor. Please consider supporting " - "the project financially. Get rid of this message by donating any amount."; - int ret = QMessageBox::information(this, "Donate to Feather", msg, QMessageBox::Yes, QMessageBox::No); - switch (ret) { - case QMessageBox::Yes: - this->donateButtonClicked(); - case QMessageBox::No: - break; - default: - return; - } - }); - - // libwalletqt - connect(m_ctx, &AppContext::walletClosed, [this]{ - this->onWalletClosed(); - }); - connect(m_ctx, &AppContext::walletClosed, ui->sendWidget, &SendWidget::onWalletClosed); - connect(m_ctx, &AppContext::balanceUpdated, this, &MainWindow::onBalanceUpdated); - connect(m_ctx, &AppContext::walletOpened, this, &MainWindow::onWalletOpened); - connect(m_ctx, &AppContext::walletOpenedError, this, &MainWindow::onWalletOpenedError); - connect(m_ctx, &AppContext::walletCreatedError, this, &MainWindow::onWalletCreatedError); - connect(m_ctx, &AppContext::walletCreated, this, &MainWindow::onWalletCreated); - connect(m_ctx, &AppContext::synchronized, this, &MainWindow::onSynchronized); - connect(m_ctx, &AppContext::blockchainSync, this, &MainWindow::onBlockchainSync); - connect(m_ctx, &AppContext::refreshSync, this, &MainWindow::onRefreshSync); - connect(m_ctx, &AppContext::createTransactionError, this, &MainWindow::onCreateTransactionError); - connect(m_ctx, &AppContext::createTransactionSuccess, this, &MainWindow::onCreateTransactionSuccess); - connect(m_ctx, &AppContext::transactionCommitted, this, &MainWindow::onTransactionCommitted); - connect(m_ctx, &AppContext::walletOpenPasswordNeeded, this, &MainWindow::onWalletOpenPasswordRequired); - - // Send - connect(m_ctx, &AppContext::initiateTransaction, ui->sendWidget, &SendWidget::onInitiateTransaction); - connect(m_ctx, &AppContext::endTransaction, ui->sendWidget, &SendWidget::onEndTransaction); - - connect(m_ctx, &AppContext::initiateTransaction, [this]{ - m_statusDots = 0; - m_constructingTransaction = true; - m_txTimer.start(1000); - }); - connect(m_ctx, &AppContext::endTransaction, [this]{ - // Todo: endTransaction can fail to fire when the node is switched during tx creation - m_constructingTransaction = false; - m_txTimer.stop(); - this->setStatusText(m_statusText); - }); - connect(&m_txTimer, &QTimer::timeout, [this]{ - m_statusLabelStatus->setText("Constructing transaction" + this->statusDots()); - }); - - - // testnet/stagenet warning - auto worthlessWarning = QString("Feather wallet is currently running in %1 mode. This is meant " - "for developers only. Your coins are WORTHLESS."); - if(m_ctx->networkType == NetworkType::STAGENET) { - if (config()->get(Config::warnOnStagenet).toBool()) { - QMessageBox::warning(this, "Warning", worthlessWarning.arg("stagenet")); - config()->set(Config::warnOnStagenet, false); - } - } - else if(m_ctx->networkType == NetworkType::TESTNET){ - if (config()->get(Config::warnOnTestnet).toBool()) { - QMessageBox::warning(this, "Warning", worthlessWarning.arg("testnet")); - config()->set(Config::warnOnTestnet, false); - } - } - - if(config()->get(Config::warnOnAlpha).toBool()) { - QString warning = "Feather Wallet is currently in beta.\n\nPlease report any bugs " - "you encounter on our Git repository, IRC or on /r/FeatherWallet."; - QMessageBox warningMb(this); - warningMb.setWindowTitle("Beta Warning"); - warningMb.setText(warning); - this->centerWidget(warningMb); - warningMb.exec(); - config()->set(Config::warnOnAlpha, false); - } - - // settings connects - // Update ticker widget(s) on home tab when settings preferred fiat currency is changed - for(auto tickerWidget: m_tickerWidgets) - connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, tickerWidget, &TickerWidget::init); - connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_balanceWidget, &TickerWidget::init); - connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, m_ctx, &AppContext::onPreferredFiatCurrencyChanged); - connect(m_windowSettings, &Settings::preferredFiatCurrencyChanged, ui->sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged)); - connect(m_windowSettings, &Settings::amountPrecisionChanged, m_ctx, &AppContext::onAmountPrecisionChanged); - - // Skin - connect(m_windowSettings, &Settings::skinChanged, this, &MainWindow::skinChanged); - - // Wizard - connect(this, &MainWindow::closed, [=]{ - if(m_wizard != nullptr) - m_wizard->close(); - }); - - // Receive - connect(ui->receiveWidget, &ReceiveWidget::generateSubaddress, [=]() { - m_ctx->currentWallet->subaddress()->addRow( m_ctx->currentWallet->currentSubaddressAccount(), ""); - }); - connect(ui->receiveWidget, &ReceiveWidget::showTransactions, [this](const QString &text) { - ui->historyWidget->setSearchText(text); - ui->tabWidget->setCurrentIndex(Tabs::HISTORY); - }); - - // History - connect(ui->historyWidget, &HistoryWidget::viewOnBlockExplorer, this, &MainWindow::onViewOnBlockExplorer); - connect(ui->historyWidget, &HistoryWidget::resendTransaction, this, &MainWindow::onResendTransaction); - connect(m_ctx, &AppContext::walletRefreshed, ui->historyWidget, &HistoryWidget::onWalletRefreshed); - connect(m_ctx, &AppContext::walletOpened, ui->historyWidget, &HistoryWidget::onWalletOpened); - - // Contacts - connect(ui->contactWidget, &ContactsWidget::fillAddress, ui->sendWidget, &SendWidget::fillAddress); - - // Open alias - connect(ui->sendWidget, &SendWidget::resolveOpenAlias, m_ctx, &AppContext::onOpenAliasResolve); - connect(m_ctx, &AppContext::openAliasResolveError, ui->sendWidget, &SendWidget::onOpenAliasResolveError); - connect(m_ctx, &AppContext::openAliasResolved, ui->sendWidget, &SendWidget::onOpenAliasResolved); - - // Coins - connect(ui->coinsWidget, &CoinsWidget::freeze, [&](const QVector& indexes) { - for (int i : indexes) { - m_ctx->currentWallet->coins()->freeze(i); - } - m_ctx->currentWallet->coins()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); - m_ctx->updateBalance(); - }); - connect(ui->coinsWidget, &CoinsWidget::thaw, [&](const QVector& indexes) { - for (int i : indexes) { - m_ctx->currentWallet->coins()->thaw(i); - } - m_ctx->currentWallet->coins()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); - m_ctx->updateBalance(); - }); - connect(ui->coinsWidget, &CoinsWidget::sweepOutput, m_ctx, &AppContext::onSweepOutput); - - connect(m_ctx, &AppContext::walletAboutToClose, [=]{ - if (!config()->get(Config::showTabHome).toBool()) - ui->tabWidget->setCurrentIndex(Tabs::HISTORY); - else - ui->tabWidget->setCurrentIndex(Tabs::HOME); - - // Clear all tables when wallet is closed - ui->historyWidget->resetModel(); - ui->contactWidget->resetModel(); - ui->receiveWidget->resetModel(); - ui->coinsWidget->resetModel(); - }); - - // window title - connect(m_ctx, &AppContext::setTitle, this, &QMainWindow::setWindowTitle); - - // init touchbar +void MainWindow::initTouchBar() { #ifdef Q_OS_MAC m_touchbar = new KDMacTouchBar(this); m_touchbarActionWelcome = new QAction(QIcon(":/assets/images/feather.png"), "Welcome to Feather!"); m_touchbarWalletItems = {ui->actionSettings, ui->actionCalculator, ui->actionKeys, ui->actionDonate_to_Feather}; m_touchbarWizardItems = {m_touchbarActionWelcome}; #endif - - // setup some UI - this->initMain(); - this->initWidgets(); - this->initMenu(); - - connect(&m_updateBytes, &QTimer::timeout, this, &MainWindow::updateNetStats); - ColorScheme::updateFromWidget(this); } -void MainWindow::initMain() { - // show wizards or open wallet directly based on cmdargs - if(m_ctx->cmdargs->isSet("password")) - m_ctx->walletPassword = m_ctx->cmdargs->value("password"); +void MainWindow::initWalletContext() { + connect(m_ctx, &AppContext::walletClosed, [this](){this->onWalletClosed();}); + connect(m_ctx, &AppContext::balanceUpdated, this, &MainWindow::onBalanceUpdated); + connect(m_ctx, &AppContext::walletOpened, this, &MainWindow::onWalletOpened); + connect(m_ctx, &AppContext::walletOpenedError, this, &MainWindow::onWalletOpenedError); + connect(m_ctx, &AppContext::walletCreatedError, this, &MainWindow::onWalletCreatedError); + connect(m_ctx, &AppContext::synchronized, this, &MainWindow::onSynchronized); + connect(m_ctx, &AppContext::blockchainSync, this, &MainWindow::onBlockchainSync); + connect(m_ctx, &AppContext::refreshSync, this, &MainWindow::onRefreshSync); + connect(m_ctx, &AppContext::createTransactionError, this, &MainWindow::onCreateTransactionError); + connect(m_ctx, &AppContext::createTransactionSuccess, this, &MainWindow::onCreateTransactionSuccess); + connect(m_ctx, &AppContext::transactionCommitted, this, &MainWindow::onTransactionCommitted); + connect(m_ctx, &AppContext::walletOpenPasswordNeeded, this, &MainWindow::onWalletOpenPasswordRequired); + connect(m_ctx, &AppContext::deviceButtonRequest, this, &MainWindow::onDeviceButtonRequest); + connect(m_ctx, &AppContext::deviceError, this, &MainWindow::onDeviceError); + connect(m_ctx, &AppContext::donationNag, this, &MainWindow::onShowDonationNag); + connect(m_ctx, &AppContext::initiateTransaction, this, &MainWindow::onInitiateTransaction); + connect(m_ctx, &AppContext::endTransaction, this, &MainWindow::onEndTransaction); + connect(m_ctx, &AppContext::customRestoreHeightSet, this, &MainWindow::onCustomRestoreHeightSet); + connect(m_ctx, &AppContext::walletAboutToClose, this, &MainWindow::onWalletAboutToClose); - QString openPath = ""; - QString autoPath = config()->get(Config::autoOpenWalletPath).toString(); - if(m_ctx->cmdargs->isSet("wallet-file")) - openPath = m_ctx->cmdargs->value("wallet-file"); - else if(!autoPath.isEmpty()) - if (autoPath.startsWith(QString::number(m_ctx->networkType))) - openPath = autoPath.remove(0, 1); + // Nodes + connect(m_ctx->nodes, &Nodes::updateStatus, [=](const QString &msg){this->setStatusText(msg);}); + connect(m_ctx->nodes, &Nodes::nodeExhausted, this, &MainWindow::showNodeExhaustedMessage); + connect(m_ctx->nodes, &Nodes::WSNodeExhausted, this, &MainWindow::showWSNodeExhaustedMessage); +} - if(!openPath.isEmpty() && Utils::fileExists(openPath)) { - this->show(); - this->setEnabled(true); - - m_ctx->onOpenWallet(openPath, m_ctx->walletPassword); - return; +void MainWindow::initWizard() { + this->setEnabled(false); + auto startPage = WalletWizard::Page_Menu; + if (config()->get(Config::firstRun).toBool() && !(TailsOS::detect() || WhonixOS::detect())) { + startPage = WalletWizard::Page_Network; } - this->setEnabled(true); - this->show(); - m_wizard = this->createWizard(WalletWizard::Page_Menu); + m_wizard = this->createWizard(startPage); m_wizard->show(); - - // wizard won't spawn on top of MainWindow without this dumb pattern - this->setEnabled(false); m_wizard->setEnabled(true); this->touchbarShowWizard(); } -void MainWindow::initMenu() { - // setup shortcuts - ui->actionStore_wallet->setShortcut(QKeySequence("Ctrl+S")); - ui->actionRefresh_tabs->setShortcut(QKeySequence("Ctrl+R")); - ui->actionClose->setShortcut(QKeySequence("Ctrl+W")); - ui->actionShow_debug_info->setShortcut(QKeySequence("Ctrl+D")); - ui->actionSettings->setShortcut(QKeySequence("Ctrl+Alt+S")); - ui->actionUpdate_balance->setShortcut(QKeySequence("Ctrl+U")); - - // hide/show tabs - m_tabShowHideSignalMapper = new QSignalMapper(this); - - connect(ui->actionShow_Home, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); - m_tabShowHideMapper["Home"] = new ToggleTab(ui->tabHome, "Home", "Home", ui->actionShow_Home, Config::showTabHome); - m_tabShowHideSignalMapper->setMapping(ui->actionShow_Home, "Home"); - - connect(ui->actionShow_Coins, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); - m_tabShowHideMapper["Coins"] = new ToggleTab(ui->tabCoins, "Coins", "Coins", ui->actionShow_Coins, Config::showTabCoins); - m_tabShowHideSignalMapper->setMapping(ui->actionShow_Coins, "Coins"); - - connect(ui->actionShow_calc, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); - m_tabShowHideMapper["Calc"] = new ToggleTab(ui->tabCalc, "Calc", "Calc", ui->actionShow_calc, Config::showTabCalc); - m_tabShowHideSignalMapper->setMapping(ui->actionShow_calc, "Calc"); - - ui->actionShow_Exchange->setVisible(false); - ui->tabWidget->setTabVisible(Tabs::EXCHANGES, false); - -#if defined(HAS_XMRIG) - connect(ui->actionShow_XMRig, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); - m_tabShowHideMapper["Mining"] = new ToggleTab(ui->tabXmrRig, "Mining", "Mining", ui->actionShow_XMRig, Config::showTabXMRig); - m_tabShowHideSignalMapper->setMapping(ui->actionShow_XMRig, "Mining"); -#else - ui->actionShow_XMRig->setVisible(false); -#endif - - for (const auto &key: m_tabShowHideMapper.keys()) { - const auto toggleTab = m_tabShowHideMapper.value(key); - const bool show = config()->get(toggleTab->configKey).toBool(); - toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name); - ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show); +void MainWindow::startupWarning() { + // Stagenet / Testnet + auto worthlessWarning = QString("Feather wallet is currently running in %1 mode. This is meant " + "for developers only. Your coins are WORTHLESS."); + if (m_ctx->networkType == NetworkType::STAGENET && config()->get(Config::warnOnStagenet).toBool()) { + QMessageBox::warning(this, "Warning", worthlessWarning.arg("stagenet")); + config()->set(Config::warnOnStagenet, false); + } + else if (m_ctx->networkType == NetworkType::TESTNET && config()->get(Config::warnOnTestnet).toBool()){ + QMessageBox::warning(this, "Warning", worthlessWarning.arg("testnet")); + config()->set(Config::warnOnTestnet, false); } - connect(m_tabShowHideSignalMapper, &QSignalMapper::mappedString, this, &MainWindow::menuToggleTabVisible); - // Wallet - connect(ui->actionInformation, &QAction::triggered, this, &MainWindow::showWalletInfoDialog); - connect(ui->actionSeed, &QAction::triggered, this, &MainWindow::showSeedDialog); - connect(ui->actionPassword, &QAction::triggered, this, &MainWindow::showPasswordDialog); - connect(ui->actionKeys, &QAction::triggered, this, &MainWindow::showKeysDialog); - connect(ui->actionViewOnly, &QAction::triggered, this, &MainWindow::showViewOnlyDialog); - connect(ui->actionStore_wallet, &QAction::triggered, [this]{ - m_ctx->currentWallet->store(); - }); - connect(ui->actionRefresh_tabs, &QAction::triggered, [this]{ - m_ctx->refreshModels(); - }); - connect(ui->actionUpdate_balance, &QAction::triggered, [this]{ - m_ctx->updateBalance(); - }); - connect(ui->actionRescan_spent, &QAction::triggered, this, &MainWindow::rescanSpent); - connect(ui->actionExportKeyImages, &QAction::triggered, this, &MainWindow::exportKeyImages); - connect(ui->actionImportKeyImages, &QAction::triggered, this, &MainWindow::importKeyImages); - connect(ui->actionExportOutputs, &QAction::triggered, this, &MainWindow::exportOutputs); - connect(ui->actionImportOutputs, &QAction::triggered, this, &MainWindow::importOutputs); + // Beta + if (config()->get(Config::warnOnAlpha).toBool()) { + QString warning = "Feather Wallet is currently in beta.\n\nPlease report any bugs " + "you encounter on our Git repository, IRC or on /r/FeatherWallet."; + QMessageBox::warning(this, "Beta Warning", warning); + config()->set(Config::warnOnAlpha, false); + } +} - // set restore height - connect(ui->actionChange_restore_height, &QAction::triggered, this, &MainWindow::showRestoreHeightDialog); - connect(m_ctx, &AppContext::customRestoreHeightSet, [=](int height){ - auto msg = QString("The restore height for this wallet has been set to %1. " - "Please re-open the wallet. Feather will now quit.").arg(height); - QMessageBox::information(this, "Cannot set custom restore height", msg); - this->menuQuitClicked(); - }); - - // CSV tx export - connect(ui->actionExport_CSV, &QAction::triggered, [=]{ - if(m_ctx->currentWallet == nullptr) return; - QString fn = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)"); - if(fn.isEmpty()) return; - if(!fn.endsWith(".csv")) fn += ".csv"; - m_ctx->currentWallet->history()->writeCSV(fn); - QMessageBox::information(this, "CSV export", QString("Transaction history exported to %1").arg(fn)); - }); - - // Contact widget - connect(ui->actionExportContactsCSV, &QAction::triggered, [=]{ - if(m_ctx->currentWallet == nullptr) return; - auto *model = m_ctx->currentWallet->addressBookModel(); - if (model->rowCount() <= 0){ - QMessageBox::warning(this, "Error", "Addressbook empty"); - return; - } - - const QString targetDir = QFileDialog::getExistingDirectory(this, "Select CSV output directory ", QDir::homePath(), QFileDialog::ShowDirsOnly); - if(targetDir.isEmpty()) return; - - qint64 now = QDateTime::currentDateTime().currentMSecsSinceEpoch(); - QString fn = QString("%1/monero-contacts_%2.csv").arg(targetDir, QString::number(now / 1000)); - if(model->writeCSV(fn)) - QMessageBox::information(this, "Address book exported", QString("Address book exported to %1").arg(fn)); - }); - - connect(ui->actionImportContactsCSV, &QAction::triggered, this, &MainWindow::importContacts); - - // Tools - connect(ui->actionSignVerify, &QAction::triggered, this, &MainWindow::menuSignVerifyClicked); - connect(ui->actionVerifyTxProof, &QAction::triggered, this, &MainWindow::menuVerifyTxProof); - connect(ui->actionLoadUnsignedTxFromFile, &QAction::triggered, this, &MainWindow::loadUnsignedTx); - connect(ui->actionLoadUnsignedTxFromClipboard, &QAction::triggered, this, &MainWindow::loadUnsignedTxFromClipboard); - connect(ui->actionLoadSignedTxFromFile, &QAction::triggered, this, &MainWindow::loadSignedTx); - connect(ui->actionLoadSignedTxFromText, &QAction::triggered, this, &MainWindow::loadSignedTxFromText); - connect(ui->actionImport_transaction, &QAction::triggered, this, &MainWindow::importTransaction); - - // About screen - connect(ui->actionAbout, &QAction::triggered, this, &MainWindow::menuAboutClicked); - connect(ui->actionDonate_to_Feather, &QAction::triggered, this, &MainWindow::donateButtonClicked); - - // Close wallet - connect(ui->actionClose, &QAction::triggered, this, &MainWindow::menuWalletCloseClicked); +bool MainWindow::autoOpenWallet() { + QString autoPath = config()->get(Config::autoOpenWalletPath).toString(); + if (!autoPath.isEmpty() && autoPath.startsWith(QString::number(m_ctx->networkType))) { + autoPath.remove(0, 1); + } + if (!autoPath.isEmpty() && Utils::fileExists(autoPath)) { + m_ctx->onOpenWallet(autoPath, m_ctx->walletPassword); + return true; + } + return false; } void MainWindow::menuToggleTabVisible(const QString &key){ @@ -511,13 +473,9 @@ void MainWindow::menuToggleTabVisible(const QString &key){ toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name); } -void MainWindow::initWidgets() { - int homeWidget = config()->get(Config::homeWidget).toInt(); - ui->tabHomeWidget->setCurrentIndex(TabsHome(homeWidget)); -} - WalletWizard *MainWindow::createWizard(WalletWizard::Page startPage){ auto *wizard = new WalletWizard(m_ctx, startPage, this); + connect(wizard, &WalletWizard::initialNetworkConfigured, this, &MainWindow::onInitialNetworkConfigured); connect(wizard, &WalletWizard::skinChanged, this, &MainWindow::skinChanged); connect(wizard, &WalletWizard::openWallet, m_ctx, &AppContext::onOpenWallet); connect(wizard, &WalletWizard::defaultWalletDirChanged, m_windowSettings, &Settings::updatePaths); @@ -559,8 +517,9 @@ void MainWindow::touchbarShowWallet() { } void MainWindow::onWalletCreatedError(const QString &err) { - QMessageBox::warning(this, "Wallet creation error", err); - this->showWizard(WalletWizard::Page_WalletFile); + this->displayWalletErrorMsg(err); + m_splashDialog->hide(); + this->showWizard(WalletWizard::Page_Menu); } void MainWindow::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) { @@ -581,29 +540,64 @@ void MainWindow::onWalletOpenPasswordRequired(bool invalidPassword, const QStrin dialog->deleteLater(); } +void MainWindow::onDeviceButtonRequest(quint64 code) { + if (m_wizard) { + m_wizard->hide(); + } + + m_splashDialog->setMessage("Action required on device: Export the view key to open the wallet."); + m_splashDialog->setIcon(QPixmap(":/assets/images/key.png")); + m_splashDialog->show(); + m_splashDialog->setEnabled(true); +} + void MainWindow::onWalletOpenedError(const QString &err) { qDebug() << Q_FUNC_INFO << QString("Wallet open error: %1").arg(err); - QMessageBox::warning(this, "Wallet open error", err); + m_splashDialog->hide(); + this->displayWalletErrorMsg(err); this->setWindowTitle("Feather"); this->showWizard(WalletWizard::Page_OpenWallet); this->touchbarShowWizard(); } -void MainWindow::onWalletCreated(Wallet *wallet) { - qDebug() << Q_FUNC_INFO; - // emit signal on behalf of walletManager - m_ctx->walletManager->walletOpened(wallet); +void MainWindow::displayWalletErrorMsg(const QString &err) { + QString errMsg = err; + if (err.contains("No device found")) { + errMsg += "\n\nThis wallet is backed by a hardware device. Make sure the Monero app is opened on the device.\n" + "You may need to restart Feather before the device can get detected."; + } + if (errMsg.contains("Unable to open device")) { + errMsg += "\n\nThe device might be in use by a different application."; + } + + if (errMsg.contains("SW_CLIENT_NOT_SUPPORTED")) { + errMsg += "\n\nIncompatible version: you may need to upgrade the Monero app on the Ledger device to the latest version."; + } + else if (errMsg.contains("Wrong Device Status")) { + errMsg += "\n\nThe device may need to be unlocked."; + } + else if (errMsg.contains("Wrong Channel")) { + errMsg += "\n\nRestart the hardware device and try again."; + } + + QMessageBox::warning(this, "Wallet error", errMsg); } void MainWindow::onWalletOpened() { qDebug() << Q_FUNC_INFO; - if(m_wizard != nullptr) { + m_splashDialog->hide(); + + if (m_wizard) { m_wizard->hide(); } + if (m_ctx->currentWallet->isHwBacked()) { + m_statusBtnHwDevice->show(); + } + this->bringToFront(); this->setEnabled(true); - if(!m_ctx->tor->torConnected) + if(!torManager()->torConnected) this->setStatusText("Wallet opened - Starting Tor (may take a while)"); else this->setStatusText("Wallet opened - Searching for node"); @@ -633,16 +627,27 @@ void MainWindow::onWalletOpened() { this->touchbarShowWallet(); this->updatePasswordIcon(); - m_updateBytes.start(100); + this->setTitle(false); + + m_updateBytes.start(250); } void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) { qDebug() << Q_FUNC_INFO; bool hide = config()->get(Config::hideBalance).toBool(); - QString label_str = QString("Balance: %1 XMR").arg(Utils::balanceFormat(spendable)); - if (balance > spendable) + int amountPrecision = config()->get(Config::amountPrecision).toInt(); + + QString balance_str = WalletManager::displayAmount(spendable); + balance_str.remove(QRegExp("0+$")); + + QString label_str = QString("Balance: %1 XMR").arg(balance_str); + if (balance > spendable) { + QString unconfirmed_str = WalletManager::displayAmount(spendable); + unconfirmed_str.remove(QRegExp("0+$")); label_str += QString(" (+%1 XMR unconfirmed)").arg(Utils::balanceFormat(balance - spendable)); + } + if (hide) label_str = "Balance: HIDDEN"; @@ -693,32 +698,32 @@ void MainWindow::onConnectionStatusChanged(int status) // Update connection info in status bar. - QIcon *icon; + QIcon icon; switch(status){ case Wallet::ConnectionStatus_Disconnected: - icon = &m_statusDisconnected; + icon = icons()->icon("status_disconnected.svg"); this->setStatusText("Disconnected"); break; case Wallet::ConnectionStatus_Connecting: - icon = &m_statusConnecting; + icon = icons()->icon("status_lagging.svg"); this->setStatusText("Connecting to node"); break; case Wallet::ConnectionStatus_WrongVersion: - icon = &m_statusDisconnected; + icon = icons()->icon("status_disconnected.svg"); this->setStatusText("Incompatible node"); break; case Wallet::ConnectionStatus_Synchronizing: - icon = &m_statusSynchronizing; + icon = icons()->icon("status_waiting.svg"); break; case Wallet::ConnectionStatus_Synchronized: - icon = &m_statusSynchronized; + icon = icons()->icon("status_connected.svg"); break; default: - icon = &m_statusDisconnected; + icon = icons()->icon("status_disconnected.svg"); break; } - m_statusBtnConnectionStatusIndicator->setIcon(*icon); + m_statusBtnConnectionStatusIndicator->setIcon(icon); } void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address) { @@ -735,12 +740,12 @@ void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVecto err = QString("%1 %2").arg(err).arg(tx_err); qDebug() << Q_FUNC_INFO << err; - QMessageBox::warning(this, "Transactions error", err); + this->displayWalletErrorMsg(err); m_ctx->currentWallet->disposeTransaction(tx); } else if (tx->txCount() == 0) { err = QString("%1 %2").arg(err).arg("No unmixable outputs to sweep."); qDebug() << Q_FUNC_INFO << err; - QMessageBox::warning(this, "Transaction error", err); + this->displayWalletErrorMsg(err); m_ctx->currentWallet->disposeTransaction(tx); } else { const auto &description = m_ctx->tmpTxDescription; @@ -799,49 +804,6 @@ void MainWindow::onCreateTransactionError(const QString &message) { QMessageBox::warning(this, "Transaction failed", msg); } -void MainWindow::create_status_bar() { -#if defined(Q_OS_WIN) - // No seperators between statusbar widgets - this->statusBar()->setStyleSheet("QStatusBar::item {border: None;}"); -#endif - - this->statusBar()->setFixedHeight(30); - - m_statusLabelStatus = new QLabel("Idle", this); - m_statusLabelStatus->setTextInteractionFlags(Qt::TextSelectableByMouse); - this->statusBar()->addWidget(m_statusLabelStatus); - - m_statusLabelNetStats = new QLabel("", this); - m_statusLabelNetStats->setTextInteractionFlags(Qt::TextSelectableByMouse); - this->statusBar()->addWidget(m_statusLabelNetStats); - - m_statusLabelBalance = new ClickableLabel(this); - m_statusLabelBalance->setText("Balance: 0 XMR"); - m_statusLabelBalance->setTextInteractionFlags(Qt::TextSelectableByMouse); - this->statusBar()->addPermanentWidget(m_statusLabelBalance); - connect(m_statusLabelBalance, &ClickableLabel::clicked, this, &MainWindow::showBalanceDialog); - - m_statusBtnConnectionStatusIndicator = new StatusBarButton(QIcon(":/assets/images/status_disconnected.svg"), "Connection status"); - connect(m_statusBtnConnectionStatusIndicator, &StatusBarButton::clicked, this, &MainWindow::showConnectionStatusDialog); - this->statusBar()->addPermanentWidget(m_statusBtnConnectionStatusIndicator); - - m_statusBtnPassword = new StatusBarButton(QIcon(":/assets/images/lock.svg"), "Password"); - connect(m_statusBtnPassword, &StatusBarButton::clicked, this, &MainWindow::showPasswordDialog); - this->statusBar()->addPermanentWidget(m_statusBtnPassword); - - m_statusBtnPreferences = new StatusBarButton(QIcon(":/assets/images/preferences.svg"), "Settings"); - connect(m_statusBtnPreferences, &StatusBarButton::clicked, this, &MainWindow::menuSettingsClicked); - this->statusBar()->addPermanentWidget(m_statusBtnPreferences); - - m_statusBtnSeed = new StatusBarButton(QIcon(":/assets/images/seed.png"), "Seed"); - connect(m_statusBtnSeed, &StatusBarButton::clicked, this, &MainWindow::showSeedDialog); - this->statusBar()->addPermanentWidget(m_statusBtnSeed); - - m_statusBtnTor = new StatusBarButton(QIcon(":/assets/images/tor_logo_disabled.png"), "Tor"); - connect(m_statusBtnTor, &StatusBarButton::clicked, this, &MainWindow::menuTorClicked); - this->statusBar()->addPermanentWidget(m_statusBtnTor); -} - void MainWindow::showWalletInfoDialog() { auto *dialog = new WalletInfoDialog(m_ctx, this); dialog->exec(); @@ -849,6 +811,11 @@ void MainWindow::showWalletInfoDialog() { } void MainWindow::showSeedDialog() { + if (m_ctx->currentWallet->isHwBacked()) { + QMessageBox::information(this, "Information", "Seed unavailable: Wallet keys are stored on hardware device."); + return; + } + if (m_ctx->currentWallet->viewOnly()) { QMessageBox::information(this, "Information", "Wallet is view-only and has no seed.\n\nTo obtain wallet keys go to Wallet -> View-Only"); return; @@ -875,7 +842,7 @@ void MainWindow::showConnectionStatusDialog() { break; case Wallet::ConnectionStatus_Connecting: { auto node = m_ctx->nodes->connection(); - statusMsg = QString("Wallet is connecting to %1").arg(node.full); + statusMsg = QString("Wallet is connecting to %1").arg(node.toAddress()); break; } case Wallet::ConnectionStatus_WrongVersion: @@ -884,7 +851,7 @@ void MainWindow::showConnectionStatusDialog() { case Wallet::ConnectionStatus_Synchronizing: case Wallet::ConnectionStatus_Synchronized: { auto node = m_ctx->nodes->connection(); - statusMsg = QString("Wallet is connected to %1 ").arg(node.full); + statusMsg = QString("Wallet is connected to %1 ").arg(node.toAddress()); if (synchronized) { statusMsg += "and synchronized"; @@ -911,7 +878,7 @@ void MainWindow::showPasswordDialog() { } void MainWindow::updatePasswordIcon() { - QIcon icon = m_ctx->currentWallet->getPassword().isEmpty() ? QIcon(":/assets/images/unlock.svg") : QIcon(":/assets/images/lock.svg"); + QIcon icon = m_ctx->currentWallet->getPassword().isEmpty() ? icons()->icon("unlock.svg") : icons()->icon("lock.svg"); m_statusBtnPassword->setIcon(icon); } @@ -953,21 +920,22 @@ void MainWindow::showViewOnlyDialog() { } void MainWindow::menuTorClicked() { - - auto *dialog = new TorInfoDialog(m_ctx, this); - connect(m_ctx->tor, &Tor::logsUpdated, dialog, &TorInfoDialog::onLogsUpdated); + auto *dialog = new TorInfoDialog(this, m_ctx); + connect(dialog, &TorInfoDialog::torSettingsChanged, m_ctx, &AppContext::onTorSettingsChanged); dialog->exec(); - disconnect(m_ctx->tor, &Tor::logsUpdated, dialog, &TorInfoDialog::onLogsUpdated); dialog->deleteLater(); } +void MainWindow::menuHwDeviceClicked() { + QMessageBox::information(this, "Hardware Device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice())); +} + void MainWindow::menuNewRestoreClicked() { // TODO: implement later } void MainWindow::menuQuitClicked() { cleanupBeforeClose(); - QCoreApplication::quit(); } @@ -979,7 +947,8 @@ void MainWindow::menuWalletCloseClicked() { } void MainWindow::menuAboutClicked() { - m_aboutDialog->show(); + AboutDialog dialog{this}; + dialog.exec(); } void MainWindow::menuSettingsClicked() { @@ -989,19 +958,17 @@ void MainWindow::menuSettingsClicked() { } void MainWindow::menuSignVerifyClicked() { - auto *dialog = new SignVerifyDialog(m_ctx->currentWallet, this); - dialog->exec(); - dialog->deleteLater(); + SignVerifyDialog dialog{m_ctx->currentWallet, this}; + dialog.exec(); } void MainWindow::menuVerifyTxProof() { - auto *dialog = new VerifyProofDialog(m_ctx->currentWallet, this); - dialog->exec(); - dialog->deleteLater(); + VerifyProofDialog dialog{m_ctx->currentWallet, this}; + dialog.exec(); } void MainWindow::skinChanged(const QString &skinName) { - if(!m_skins.contains(skinName)) { + if (!m_skins.contains(skinName)) { qWarning() << QString("No such skin %1").arg(skinName); return; } @@ -1011,6 +978,10 @@ void MainWindow::skinChanged(const QString &skinName) { qDebug() << QString("Skin changed to %1").arg(skinName); ColorScheme::updateFromWidget(this); +#ifdef HAS_LOCALMONERO + m_localMoneroWidget->skinChanged(); +#endif + ui->conversionWidget->skinChanged(); } @@ -1021,7 +992,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { } void MainWindow::donateButtonClicked() { - double donation = AppContext::prices->convert("EUR", "XMR", globals::donationAmount); + double donation = appData()->prices.convert("EUR", "XMR", globals::donationAmount); if (donation <= 0) donation = 0.1337; @@ -1103,20 +1074,6 @@ AppContext *MainWindow::getContext(){ return pMainWindow->m_ctx; } -void MainWindow::loadSkins() { - QString qdarkstyle = this->loadStylesheet(":qdarkstyle/style.qss"); - if (!qdarkstyle.isEmpty()) - m_skins.insert("QDarkStyle", qdarkstyle); - - QString breeze_dark = this->loadStylesheet(":/dark.qss"); - if (!breeze_dark.isEmpty()) - m_skins.insert("Breeze/Dark", breeze_dark); - - QString breeze_light = this->loadStylesheet(":/light.qss"); - if (!breeze_light.isEmpty()) - m_skins.insert("Breeze/Light", breeze_light); -} - QString MainWindow::loadStylesheet(const QString &resource) { QFile f(resource); if (!f.exists()) { @@ -1226,8 +1183,7 @@ void MainWindow::importOutputs() { void MainWindow::cleanupBeforeClose() { m_ctx->closeWallet(false, true); - m_ctx->tor->stop(); - + torManager()->stop(); this->saveGeo(); } @@ -1291,15 +1247,45 @@ void MainWindow::createUnsignedTxDialog(UnsignedTransaction *tx) { void MainWindow::importTransaction() { - auto result = QMessageBox::warning(this, "Warning", "Using this feature may allow a remote node to associate the transaction with your IP address.\n" - "\n" - "Connect to a trusted node or run Feather over Tor if network level metadata leakage is included in your threat model.", - QMessageBox::Ok | QMessageBox::Cancel); - if (result == QMessageBox::Ok) { - auto *dialog = new TxImportDialog(this, m_ctx); - dialog->exec(); - dialog->deleteLater(); + if (config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptNode) { + // TODO: don't show if connected to local node + + auto result = QMessageBox::warning(this, "Warning", "Using this feature may allow a remote node to associate the transaction with your IP address.\n" + "\n" + "Connect to a trusted node or run Feather over Tor if network level metadata leakage is included in your threat model.", + QMessageBox::Ok | QMessageBox::Cancel); + if (result != QMessageBox::Ok) { + return; + } } + + auto *dialog = new TxImportDialog(this, m_ctx); + dialog->exec(); + dialog->deleteLater(); +} + +void MainWindow::onDeviceError(const QString &error) { + if (m_showDeviceError) { + return; + } + m_statusBtnHwDevice->setIcon(icons()->icon("ledger_unpaired.png")); + while (true) { + m_showDeviceError = true; + auto result = QMessageBox::question(this, "Hardware device", "Lost connection to hardware device. Attempt to reconnect?"); + if (result == QMessageBox::Yes) { + bool r = m_ctx->currentWallet->reconnectDevice(); + if (r) { + break; + } + } + if (result == QMessageBox::No){ + m_ctx->closeWallet(true); + return; + } + } + m_statusBtnHwDevice->setIcon(icons()->icon("ledger.png")); + m_ctx->currentWallet->startRefresh(); + m_showDeviceError = false; } void MainWindow::updateNetStats() { @@ -1318,6 +1304,7 @@ void MainWindow::updateNetStats() { return; } + m_statusLabelNetStats->setText(QString("(D: %1)").arg(Utils::formatBytes(m_ctx->currentWallet->getBytesReceived()))); } @@ -1352,14 +1339,239 @@ void MainWindow::bringToFront() { activateWindow(); } -void MainWindow::centerWidget(QWidget &w) { - QScreen *s = QGuiApplication::primaryScreen(); +void MainWindow::onInitialNetworkConfigured() { + m_ctx->onInitialNetworkConfigured(); - const QRect sr = s->geometry(); - const QRect wr({}, w.frameSize().boundedTo(sr.size())); + connect(torManager(), &TorManager::connectionStateChanged, [this](bool connected){ + connected ? m_statusBtnTor->setIcon(icons()->icon("tor_logo.png")) + : m_statusBtnTor->setIcon(icons()->icon("tor_logo_disabled.png"));}); +} - w.resize(wr.size()); - w.move(sr.center() - wr.center()); +void MainWindow::onCheckUpdatesComplete(const QString &version, const QString &binaryFilename, + const QString &hash, const QString &signer) { + QString versionDisplay{version}; + versionDisplay.replace("beta", "Beta"); + QString updateText = QString("Update to Feather %1 is available").arg(versionDisplay); + m_statusUpdateAvailable->setText(updateText); + m_statusUpdateAvailable->setToolTip("Click to Download update."); + m_statusUpdateAvailable->show(); + + m_statusUpdateAvailable->disconnect(); + connect(m_statusUpdateAvailable, &StatusBarButton::clicked, [this, version, binaryFilename, hash, signer] { + this->onShowUpdateCheck(version, binaryFilename, hash, signer); + }); +} + +void MainWindow::onShowUpdateCheck(const QString &version, const QString &binaryFilename, + const QString &hash, const QString &signer) { + QString downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(this->getPlatformTag(), binaryFilename); + + UpdateDialog updateDialog{this, version, downloadUrl, hash, signer}; + connect(&updateDialog, &UpdateDialog::restartWallet, this, &MainWindow::onRestartApplication); + updateDialog.exec(); +} + +void MainWindow::onUpdatesAvailable(const QJsonObject &updates) { + QString featherVersionStr{FEATHER_VERSION}; + + auto featherVersion = SemanticVersion::fromString(featherVersionStr); + + QString platformTag = getPlatformTag(); + if (platformTag.isEmpty()) { + qWarning() << "Unsupported platform, unable to fetch update"; + return; + } + + QJsonObject platformData = updates["platform"].toObject()[platformTag].toObject(); + if (platformData.isEmpty()) { + qWarning() << "Unable to find current platform in updates data"; + return; + } + + QString newVersion = platformData["version"].toString(); + if (SemanticVersion::fromString(newVersion) <= featherVersion) { + return; + } + + // Hooray! New update available + + QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(globals::websiteUrl, newVersion); + + UtilsNetworking network{getNetworkTor()}; + QNetworkReply *reply = network.get(hashesUrl); + + connect(reply, &QNetworkReply::finished, this, std::bind(&MainWindow::onSignedHashesReceived, this, reply, platformTag, newVersion)); +} + +void MainWindow::onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version) { + if (reply->error() != QNetworkReply::NoError) { + qWarning() << "Unable to fetch signed hashes: " << reply->errorString(); + return; + } + + QByteArray armoredSignedHashes = reply->readAll(); + reply->deleteLater(); + + const QString binaryFilename = QString("feather-%1-%2.zip").arg(version, platformTag); + QString signer; + QByteArray signedHash = AsyncTask::runAndWaitForFuture([armoredSignedHashes, binaryFilename, &signer]{ + try { + return Updater().verifyParseSignedHashes(armoredSignedHashes, binaryFilename, signer); + } + catch (const std::exception &e) { + qWarning() << "Failed to fetch and verify signed hash: " << e.what(); + return QByteArray{}; + } + }); + if (signedHash.isEmpty()) { + return; + } + + QString hash = signedHash.toHex(); + qInfo() << "Update found: " << binaryFilename << hash << "signed by:" << signer; + this->onCheckUpdatesComplete(version, binaryFilename, hash, signer); +} + +void MainWindow::onShowDonationNag() { + auto msg = "Feather is a 100% community-sponsored endeavor. Please consider supporting " + "the project financially. Get rid of this message by donating any amount."; + int ret = QMessageBox::information(this, "Donate to Feather", msg, QMessageBox::Yes, QMessageBox::No); + if (ret == QMessageBox::Yes) { + this->donateButtonClicked(); + } +} + +void MainWindow::onInitiateTransaction() { + m_statusDots = 0; + m_constructingTransaction = true; + m_txTimer.start(1000); + + if (m_ctx->currentWallet->isHwBacked()) { + QString message = "Constructing transaction: action may be required on device."; + m_splashDialog->setMessage(message); + m_splashDialog->setIcon(QPixmap(":/assets/images/unconfirmed.png")); + m_splashDialog->show(); + m_splashDialog->setEnabled(true); + } +} + +void MainWindow::onEndTransaction() { + // Todo: endTransaction can fail to fire when the node is switched during tx creation + m_constructingTransaction = false; + m_txTimer.stop(); + this->setStatusText(m_statusText); + + if (m_ctx->currentWallet->isHwBacked()) { + m_splashDialog->hide(); + } +} + +void MainWindow::onCustomRestoreHeightSet(int height) { + auto msg = QString("The restore height for this wallet has been set to %1. " + "Please re-open the wallet. Feather will now quit.").arg(height); + QMessageBox::information(this, "Cannot set custom restore height", msg); + this->menuQuitClicked(); +} + +void MainWindow::onWalletAboutToClose() { + if (!config()->get(Config::showTabHome).toBool()) + ui->tabWidget->setCurrentIndex(Tabs::HISTORY); + else + ui->tabWidget->setCurrentIndex(Tabs::HOME); + + // Clear all tables when wallet is closed + ui->historyWidget->resetModel(); + ui->contactWidget->resetModel(); + ui->receiveWidget->resetModel(); + ui->coinsWidget->resetModel(); +} + +void MainWindow::onExportHistoryCSV(bool checked) { + if (m_ctx->currentWallet == nullptr) + return; + QString fn = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)"); + if (fn.isEmpty()) + return; + if (!fn.endsWith(".csv")) + fn += ".csv"; + m_ctx->currentWallet->history()->writeCSV(fn); + QMessageBox::information(this, "CSV export", QString("Transaction history exported to %1").arg(fn)); +} + +void MainWindow::onExportContactsCSV(bool checked) { + if (m_ctx->currentWallet == nullptr) return; + auto *model = m_ctx->currentWallet->addressBookModel(); + if (model->rowCount() <= 0){ + QMessageBox::warning(this, "Error", "Addressbook empty"); + return; + } + + const QString targetDir = QFileDialog::getExistingDirectory(this, "Select CSV output directory ", QDir::homePath(), QFileDialog::ShowDirsOnly); + if(targetDir.isEmpty()) return; + + qint64 now = QDateTime::currentDateTime().currentMSecsSinceEpoch(); + QString fn = QString("%1/monero-contacts_%2.csv").arg(targetDir, QString::number(now / 1000)); + if(model->writeCSV(fn)) + QMessageBox::information(this, "Address book exported", QString("Address book exported to %1").arg(fn)); +} + +void MainWindow::onCreateDesktopEntry(bool checked) { + auto msg = Utils::xdgDesktopEntryRegister() ? "Desktop entry created" : "Desktop entry not created due to an error."; + QMessageBox::information(this, "Desktop entry", msg); +} + +void MainWindow::onReportBug(bool checked) { + QMessageBox::information(this, "Reporting Bugs", + "Please report any bugs as issues on our git repo:
\n" + "https://git.featherwallet.org/feather/feather/issues

" + "\n" + "Before reporting a bug, upgrade to the most recent version of Feather " + "(latest release or git HEAD), and include the version number in your report. " + "Try to explain not only what the bug is, but how it occurs."); +} + +void MainWindow::onRestartApplication(const QString &binaryFilename) { + QProcess::startDetached(binaryFilename, qApp->arguments()); + + this->cleanupBeforeClose(); + QCoreApplication::quit(); +} + +QString MainWindow::getPlatformTag() { +#ifdef Q_OS_MACOS + return "mac"; +#endif +#ifdef Q_OS_WIN + return "win"; +#endif +#ifdef Q_OS_LINUX + if (!qgetenv("APPIMAGE").isEmpty()) { + return "linux-appimage"; + } + return "linux"; +#endif + return ""; +} + +QString MainWindow::getHardwareDevice() { + if (!m_ctx->currentWallet->isHwBacked()) + return ""; + if (m_ctx->currentWallet->isTrezor()) + return "Trezor"; + if (m_ctx->currentWallet->isLedger()) + return "Ledger"; + return "Unknown"; +} + +void MainWindow::setTitle(bool mining) { + QFileInfo fileInfo(m_ctx->walletPath); + auto title = QString("Feather - [%1]").arg(fileInfo.fileName()); + if (m_ctx->currentWallet && m_ctx->currentWallet->viewOnly()) + title += " [view-only]"; + if (mining) + title += " [mining]"; + + this->setWindowTitle(title); } MainWindow::~MainWindow() { diff --git a/src/mainwindow.h b/src/mainwindow.h index f4c6146..9faa7d3 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -4,32 +4,16 @@ #ifndef MAINWINDOW_H #define MAINWINDOW_H -#ifdef Q_OS_MAC -#include "src/kdmactouchbar.h" -#endif - #include #include -#include -#include +#include #include -#include -#include -#include -#include -#include +#include "appcontext.h" #include "components.h" #include "calcwindow.h" -#include "widgets/ccswidget.h" -#include "widgets/redditwidget.h" -#include "widgets/tickerwidget.h" -#include "widgets/xmrigwidget.h" -#include "utils/networking.h" -#include "appcontext.h" -#include "utils/config.h" -#include "wizard/WalletWizard.h" #include "settings.h" + #include "dialog/aboutdialog.h" #include "dialog/signverifydialog.h" #include "dialog/verifyproofdialog.h" @@ -38,7 +22,31 @@ #include "dialog/keysdialog.h" #include "dialog/aboutdialog.h" #include "dialog/restoredialog.h" +#include "dialog/splashdialog.h" #include "libwalletqt/Wallet.h" +#include "model/SubaddressModel.h" +#include "model/SubaddressProxyModel.h" +#include "model/TransactionHistoryModel.h" +#include "model/CoinsModel.h" +#include "model/CoinsProxyModel.h" +#include "utils/networking.h" +#include "utils/config.h" +#include "widgets/ccswidget.h" +#include "widgets/redditwidget.h" +#include "widgets/tickerwidget.h" +#include "wizard/WalletWizard.h" + +#ifdef HAS_LOCALMONERO +#include "widgets/LocalMoneroWidget.h" +#endif + +#ifdef HAS_XMRIG +#include "widgets/xmrigwidget.h" +#endif + +#ifdef Q_OS_MAC +#include "src/kdmactouchbar.h" +#endif namespace Ui { class MainWindow; @@ -64,13 +72,6 @@ public: static AppContext *getContext(); ~MainWindow() override; - qreal screenDpiRef; - QRect screenGeo; - QRect screenRect; - qreal screenDpi; - qreal screenDpiPhysical; - qreal screenRatio; - enum Tabs { HOME = 0, HISTORY, @@ -88,8 +89,6 @@ public: }; public slots: - void initWidgets(); - void initMenu(); void showWizard(WalletWizard::Page startPage); void menuNewRestoreClicked(); void menuQuitClicked(); @@ -116,14 +115,17 @@ public slots: void onRefreshSync(int height, int target); void onWalletOpenedError(const QString &err); void onWalletCreatedError(const QString &err); - void onWalletCreated(Wallet *wallet); void menuWalletCloseClicked(); void onWalletOpenPasswordRequired(bool invalidPassword, const QString &path); + void onDeviceButtonRequest(quint64 code); void onViewOnBlockExplorer(const QString &txid); void onResendTransaction(const QString &txid); void importContacts(); void showRestoreHeightDialog(); void importTransaction(); + void onDeviceError(const QString &error); + void menuHwDeviceClicked(); + void onUpdatesAvailable(const QJsonObject &updates); // offline tx signing void exportKeyImages(); @@ -148,15 +150,42 @@ public slots: signals: void closed(); +private slots: + void onInitialNetworkConfigured(); + void onCheckUpdatesComplete(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer); + void onShowUpdateCheck(const QString &version, const QString &binaryFilename, const QString &hash, const QString &signer); + void onRestartApplication(const QString &binaryFilename); + void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version); + void onShowDonationNag(); + void onInitiateTransaction(); + void onEndTransaction(); + void onCustomRestoreHeightSet(int height); + void onWalletAboutToClose(); + + // Menu + void onExportHistoryCSV(bool checked); + void onExportContactsCSV(bool checked); + void onCreateDesktopEntry(bool checked); + void onReportBug(bool checked); + private: + void initSkins(); + void initStatusBar(); + void initWidgets(); + void initMenu(); + void initTray(); + void initHome(); + void initTouchBar(); + void initWalletContext(); + void initWizard(); + void startupWarning(); + bool autoOpenWallet(); + AppContext *m_ctx; static MainWindow * pMainWindow; void closeEvent(QCloseEvent *event) override; void cleanupBeforeClose(); - void create_status_bar(); - void initMain(); - void loadSkins(); QString loadStylesheet(const QString &resource); void saveGeo(); void restoreGeo(); @@ -173,7 +202,10 @@ private: void showBalanceDialog(); QString statusDots(); void bringToFront(); - void centerWidget(QWidget &w); + QString getPlatformTag(); + void displayWalletErrorMsg(const QString &err); + QString getHardwareDevice(); + void setTitle(bool mining); WalletWizard *createWizard(WalletWizard::Page startPage); @@ -181,8 +213,12 @@ private: Settings *m_windowSettings = nullptr; CalcWindow *m_windowCalc = nullptr; RestoreDialog *m_restoreDialog = nullptr; - AboutDialog *m_aboutDialog = nullptr; XMRigWidget *m_xmrig = nullptr; + SplashDialog *m_splashDialog = nullptr; + +#ifdef HAS_LOCALMONERO + LocalMoneroWidget *m_localMoneroWidget = nullptr; +#endif QSystemTrayIcon *m_trayIcon; QMenu m_trayMenu; @@ -195,6 +231,7 @@ private: TickerWidget *m_balanceWidget; // lower status bar + QPushButton *m_statusUpdateAvailable; ClickableLabel *m_statusLabelBalance; QLabel *m_statusLabelStatus; QLabel *m_statusLabelNetStats; @@ -203,6 +240,7 @@ private: StatusBarButton *m_statusBtnPreferences; StatusBarButton *m_statusBtnSeed; StatusBarButton *m_statusBtnTor; + StatusBarButton *m_statusBtnHwDevice; #ifdef Q_OS_MAC QAction *m_touchbarActionWelcome; @@ -210,6 +248,7 @@ private: QList m_touchbarWalletItems; QList m_touchbarWizardItems; #endif + QSignalMapper *m_tabShowHideSignalMapper; QMap m_tabShowHideMapper; WalletWizard *m_wizard = nullptr; @@ -222,13 +261,9 @@ private: int m_statusDots; bool m_constructingTransaction = false; bool m_statusOverrideActive = false; + bool m_showDeviceError = false; QTimer m_txTimer; - QIcon m_statusDisconnected; - QIcon m_statusConnecting; - QIcon m_statusSynchronizing; - QIcon m_statusSynchronized; - private slots: void menuToggleTabVisible(const QString &key); }; diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 8a6efc2..be1b381 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 1156 - 496 + 977 + 499 @@ -293,6 +293,39 @@ 0 + + + + 0 + + + + + :/assets/images/localMonero_logo.png:/assets/images/localMonero_logo.png + + + LocalMonero + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + +
@@ -319,8 +352,8 @@ 0 0 - 1156 - 30 + 977 + 28 diff --git a/src/model/AddressBookModel.cpp b/src/model/AddressBookModel.cpp index a232da1..92004b1 100644 --- a/src/model/AddressBookModel.cpp +++ b/src/model/AddressBookModel.cpp @@ -5,6 +5,7 @@ #include "AddressBook.h" #include "ModelUtils.h" #include "utils/utils.h" +#include "utils/Icons.h" AddressBookModel::AddressBookModel(QObject *parent, AddressBook *addressBook) : QAbstractTableModel(parent), @@ -13,7 +14,7 @@ AddressBookModel::AddressBookModel(QObject *parent, AddressBook *addressBook) { connect(m_addressBook, &AddressBook::refreshStarted, this, &AddressBookModel::startReset); connect(m_addressBook, &AddressBook::refreshFinished, this, &AddressBookModel::endReset); - m_contactIcon = QIcon(":/assets/images/person.svg"); + m_contactIcon = icons()->icon("person.svg"); } void AddressBookModel::startReset(){ diff --git a/src/model/CoinsModel.cpp b/src/model/CoinsModel.cpp index 85468b4..83c4471 100644 --- a/src/model/CoinsModel.cpp +++ b/src/model/CoinsModel.cpp @@ -7,9 +7,9 @@ #include "ModelUtils.h" #include "globals.h" #include "utils/ColorScheme.h" +#include "utils/Icons.h" #include -#include CoinsModel::CoinsModel(QObject *parent, Coins *coins) : QAbstractTableModel(parent), @@ -17,9 +17,6 @@ CoinsModel::CoinsModel(QObject *parent, Coins *coins) { connect(m_coins, &Coins::refreshStarted, this, &CoinsModel::startReset); connect(m_coins, &Coins::refreshFinished, this, &CoinsModel::endReset); - - m_eye = QIcon(":/assets/images/eye1.png"); - m_eyeBlind = QIcon(":/assets/images/eye_blind.png"); } void CoinsModel::startReset(){ @@ -88,10 +85,10 @@ QVariant CoinsModel::data(const QModelIndex &index, int role) const case KeyImageKnown: { if (cInfo.keyImageKnown()) { - result = QVariant(m_eye); + result = QVariant(icons()->icon("eye1.png")); } else { - result = QVariant(m_eyeBlind); + result = QVariant(icons()->icon("eye_blind.png")); } } } diff --git a/src/model/CoinsModel.h b/src/model/CoinsModel.h index 6db5e03..97dd19f 100644 --- a/src/model/CoinsModel.h +++ b/src/model/CoinsModel.h @@ -53,8 +53,6 @@ private: QVariant parseTransactionInfo(const CoinsInfo &cInfo, int column, int role) const; Coins *m_coins; - QIcon m_eye; - QIcon m_eyeBlind; }; #endif //FEATHER_COINSMODEL_H diff --git a/src/model/HistoryView.cpp b/src/model/HistoryView.cpp index ec0f411..bc3f89b 100644 --- a/src/model/HistoryView.cpp +++ b/src/model/HistoryView.cpp @@ -5,6 +5,7 @@ #include "TransactionHistoryProxyModel.h" #include "libwalletqt/TransactionInfo.h" +#include "utils/utils.h" #include #include diff --git a/src/model/HistoryView.h b/src/model/HistoryView.h index 69c200a..80a7095 100644 --- a/src/model/HistoryView.h +++ b/src/model/HistoryView.h @@ -9,6 +9,7 @@ #include #include "TransactionHistoryModel.h" +#include "TransactionHistoryProxyModel.h" class HistoryView : public QTreeView { diff --git a/src/model/LocalMoneroModel.cpp b/src/model/LocalMoneroModel.cpp new file mode 100644 index 0000000..065a7be --- /dev/null +++ b/src/model/LocalMoneroModel.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "LocalMoneroModel.h" +#include +#include "utils/utils.h" + +LocalMoneroModel::LocalMoneroModel(QObject *parent) + : QAbstractTableModel(parent) +{ +} + +int LocalMoneroModel::rowCount(const QModelIndex &parent) const { + return m_data.count(); +} + +int LocalMoneroModel::columnCount(const QModelIndex &parent) const { + return Column::COUNT; +} + +QVariant LocalMoneroModel::headerData(int section, Qt::Orientation orientation, int role) const { + if (role == Qt::DisplayRole && orientation == Qt::Horizontal) { + switch (section) { + case Seller: + return QString("Seller"); + case Country: + return QString("Country"); + case PaymentMethod: + return QString("Payment Method"); + case PaymentMethodDetail: + return QString("Detail"); + case PriceXMR: + return QString("Price/XMR"); + case Limits: + return QString("Limits"); + default: + return QVariant(); + } + } + return QVariant(); +} + +QVariant LocalMoneroModel::data(const QModelIndex &index, int role) const { + const int col = index.column(); + const auto row = m_data.at(index.row()).toObject()["data"].toObject(); + + if (row.isEmpty()) { + return QVariant(); + } + + if (role == Qt::DisplayRole) { + switch (col) { + case Column::Seller: { + auto seller = row["profile"].toObject(); + return seller["name"].toString(); + // TODO: online indicator + } + case Column::Country: { + return row["countrycode"].toString(); + } + case Column::PaymentMethod: { + auto paymentMethodCode = row["online_provider"].toString(); + if (paymentMethodCode == "NATIONAL_BANK") { + return QString("National bank transfer"); + } + return m_paymentMethodNames.value(paymentMethodCode, paymentMethodCode); + } + case Column::PaymentMethodDetail: { + auto paymentMethodDetailText = row["payment_method_detail"].toString(); + QString filteredString; // We can't display emojis in QTreeView + for (const auto &Char : paymentMethodDetailText) { + if (Char.unicode() < 256) + filteredString.append(Char); + else + filteredString.append(" "); + } + + return filteredString.trimmed(); + } + case Column::PriceXMR: { + return QString("%1 %2").arg(row["temp_price"].toString(), row["currency"].toString()); + } + case Column::Limits: { + auto minAmount = row["min_amount"].toString(); + auto maxAmount = row["max_amount"].toString(); + if (maxAmount.isEmpty()) { + maxAmount = row["max_amount_available"].toString(); + } + auto currency = row["currency"].toString(); + + if (minAmount.isEmpty() && maxAmount.isEmpty()) { + return QString("Up to any amount %1").arg(currency); + } + + if (!minAmount.isEmpty() && maxAmount.isEmpty()) { + return QString("%1 - any amount %2").arg(minAmount, currency); + } + + if (!minAmount.isEmpty() && !maxAmount.isEmpty()) { + return QString("%1 - %2 %3").arg(minAmount, maxAmount, currency); + } + + if (minAmount.isEmpty() && !maxAmount.isEmpty()) { + return QString("Up to %1 %2").arg(maxAmount, currency); + } + + return QVariant(); + } + } + } + else if (role == Qt::ForegroundRole) { + switch (col) { + case Column::PriceXMR: { + return QVariant(QColor("#388538")); + } + } + } + else if (role == Qt::FontRole) { + switch (col) { + case Column::PriceXMR: { + auto bigFont = Utils::relativeFont(2); + bigFont.setBold(true); + return bigFont; + } + } + } + + return QVariant(); +} + +void LocalMoneroModel::setData(const QJsonArray &data) { + beginResetModel(); + m_data = data; + endResetModel(); +} + +void LocalMoneroModel::addData(const QJsonArray &data) { + beginResetModel(); + + for (const auto &row : data) { + m_data.append(row); + } + + endResetModel(); +} + +void LocalMoneroModel::clearData() { + beginResetModel(); + m_data = {}; + endResetModel(); +} + +void LocalMoneroModel::setPaymentMethods(const QJsonObject &data) { + beginResetModel(); + + m_paymentMethods = data; + m_paymentMethodNames.clear(); + for (const auto &payment_method : data) { + auto code = payment_method["code"].toString(); + auto name = payment_method["name"].toString(); + + if (!code.isEmpty() && !name.isEmpty()) { + m_paymentMethodNames[code] = name; + } + } + + endResetModel(); +} + +QJsonObject LocalMoneroModel::getOffer(int index) const { + return m_data.at(index).toObject(); +} diff --git a/src/model/LocalMoneroModel.h b/src/model/LocalMoneroModel.h new file mode 100644 index 0000000..b09aca6 --- /dev/null +++ b/src/model/LocalMoneroModel.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_LOCALMONEROMODEL_H +#define FEATHER_LOCALMONEROMODEL_H + +#include +#include +#include +#include + +class LocalMoneroModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + enum Column + { + Seller = 0, + Country, + PaymentMethod, + PaymentMethodDetail, + PriceXMR, + Limits, + COUNT + }; + + LocalMoneroModel(QObject *parent = nullptr); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + void setData(const QJsonArray &data); + void setPaymentMethods(const QJsonObject &data); + void addData(const QJsonArray &data); + void clearData(); + + QJsonObject getOffer(int index) const; + +private: + QJsonArray m_data; + QJsonObject m_paymentMethods; + QHash m_paymentMethodNames; +}; + +#endif //FEATHER_LOCALMONEROMODEL_H diff --git a/src/model/NodeModel.cpp b/src/model/NodeModel.cpp index fabbbc1..6224d0b 100644 --- a/src/model/NodeModel.cpp +++ b/src/model/NodeModel.cpp @@ -4,12 +4,13 @@ #include "NodeModel.h" #include "utils/nodes.h" #include "utils/ColorScheme.h" +#include "utils/Icons.h" NodeModel::NodeModel(int nodeSource, QObject *parent) : QAbstractTableModel(parent) , m_nodeSource(nodeSource) - , m_offline(QIcon(":/assets/images/expired_icon.png")) - , m_online(QIcon(":/assets/images/confirmed_icon.png")) + , m_offline(icons()->icon("expired_icon.png")) + , m_online(icons()->icon("confirmed_icon.png")) { } @@ -47,7 +48,7 @@ QVariant NodeModel::data(const QModelIndex &index, int role) const { if(role == Qt::DisplayRole) { switch(index.column()) { case NodeModel::URL: - return node.full; + return node.toFullAddress(); case NodeModel::Height: if(node.online) return node.height == 0 ? QString("-") : QString::number(node.height); diff --git a/src/model/SubaddressModel.cpp b/src/model/SubaddressModel.cpp index a6ec715..7b170b3 100644 --- a/src/model/SubaddressModel.cpp +++ b/src/model/SubaddressModel.cpp @@ -9,7 +9,6 @@ #include #include #include -#include SubaddressModel::SubaddressModel(QObject *parent, Subaddress *subaddress) : QAbstractTableModel(parent), diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index 8f699c4..db0d799 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -5,21 +5,16 @@ #include "TransactionHistory.h" #include "TransactionInfo.h" #include "globals.h" +#include "utils/config.h" #include "utils/ColorScheme.h" +#include "utils/Icons.h" +#include "utils/AppData.h" #include "ModelUtils.h" TransactionHistoryModel::TransactionHistoryModel(QObject *parent) : QAbstractTableModel(parent), m_transactionHistory(nullptr) { - m_unconfirmedTx = QIcon(":/assets/images/unconfirmed.png"); - m_warning = QIcon(":/assets/images/warning.png"); - m_clock1 = QIcon(":/assets/images/clock1.png"); - m_clock2 = QIcon(":/assets/images/clock2.png"); - m_clock3 = QIcon(":/assets/images/clock3.png"); - m_clock4 = QIcon(":/assets/images/clock4.png"); - m_clock5 = QIcon(":/assets/images/clock5.png"); - m_confirmedTx = QIcon(":/assets/images/confirmed.png"); } void TransactionHistoryModel::setTransactionHistory(TransactionHistory *th) { @@ -86,21 +81,21 @@ QVariant TransactionHistoryModel::data(const QModelIndex &index, int role) const case Column::Date: { if (tInfo.isFailed()) - result = QVariant(m_warning); + result = QVariant(icons()->icon("warning.png")); else if (tInfo.isPending()) - result = QVariant(m_unconfirmedTx); + result = QVariant(icons()->icon("unconfirmed.png")); else if (tInfo.confirmations() <= (1.0/5.0 * tInfo.confirmationsRequired())) - result = QVariant(m_clock1); + result = QVariant(icons()->icon("clock1.png")); else if (tInfo.confirmations() <= (2.0/5.0 * tInfo.confirmationsRequired())) - result = QVariant(m_clock2); + result = QVariant(icons()->icon("clock2.png")); else if (tInfo.confirmations() <= (3.0/5.0 * tInfo.confirmationsRequired())) - result = QVariant(m_clock3); + result = QVariant(icons()->icon("clock3.png")); else if (tInfo.confirmations() <= (4.0/5.0 * tInfo.confirmationsRequired())) - result = QVariant(m_clock4); + result = QVariant(icons()->icon("clock4.png")); else if (tInfo.confirmations() < tInfo.confirmationsRequired()) - result = QVariant(m_clock5); + result = QVariant(icons()->icon("clock5.png")); else if (tInfo.confirmations()) - result = QVariant(m_confirmedTx); + result = QVariant(icons()->icon("confirmed.png")); } } } @@ -161,13 +156,13 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI } case Column::FiatAmount: { - double usd_price = AppContext::txFiatHistory->get(tInfo.timestamp().toString("yyyyMMdd")); + double usd_price = appData()->txFiatHistory->get(tInfo.timestamp().toString("yyyyMMdd")); if (usd_price == 0.0) return QVariant("?"); double usd_amount = usd_price * (tInfo.balanceDelta() / globals::cdiv); if(this->preferredFiatSymbol != "USD") - usd_amount = AppContext::prices->convert("USD", this->preferredFiatSymbol, usd_amount); + usd_amount = appData()->prices.convert("USD", this->preferredFiatSymbol, usd_amount); if (role == Qt::UserRole) { return usd_amount; } diff --git a/src/model/TransactionHistoryModel.h b/src/model/TransactionHistoryModel.h index b7a0427..4df7dbd 100644 --- a/src/model/TransactionHistoryModel.h +++ b/src/model/TransactionHistoryModel.h @@ -6,7 +6,6 @@ #include #include -#include "appcontext.h" class TransactionHistory; class TransactionInfo; @@ -54,14 +53,6 @@ private: QVariant parseTransactionInfo(const TransactionInfo &tInfo, int column, int role) const; TransactionHistory * m_transactionHistory; - QIcon m_unconfirmedTx; - QIcon m_warning; - QIcon m_clock1; - QIcon m_clock2; - QIcon m_clock3; - QIcon m_clock4; - QIcon m_clock5; - QIcon m_confirmedTx; }; #endif // TRANSACTIONHISTORYMODEL_H diff --git a/src/receivewidget.cpp b/src/receivewidget.cpp index 23078a1..b86517c 100644 --- a/src/receivewidget.cpp +++ b/src/receivewidget.cpp @@ -5,8 +5,10 @@ #include "receivewidget.h" #include "model/ModelUtils.h" #include "dialog/qrcodedialog.h" +#include "utils/Icons.h" #include +#include ReceiveWidget::ReceiveWidget(QWidget *parent) : QWidget(parent), @@ -29,9 +31,7 @@ ReceiveWidget::ReceiveWidget(QWidget *parent) : connect(m_showTransactionsAction, &QAction::triggered, this, &ReceiveWidget::onShowTransactions); connect(ui->addresses, &QTreeView::customContextMenuRequested, this, &ReceiveWidget::showContextMenu); - connect(ui->btn_generateSubaddress, &QPushButton::clicked, [=]() { - emit generateSubaddress(); - }); + connect(ui->btn_generateSubaddress, &QPushButton::clicked, this, &ReceiveWidget::generateSubaddress); connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog); connect(ui->label_addressSearch, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter); @@ -88,9 +88,9 @@ void ReceiveWidget::showContextMenu(const QPoint &point) { auto *menu = new QMenu(ui->addresses); - menu->addAction(QIcon(":/assets/images/copy.png"), "Copy address", this, &ReceiveWidget::copyAddress); - menu->addAction(QIcon(":/assets/images/copy.png"), "Copy label", this, &ReceiveWidget::copyLabel); - menu->addAction(QIcon(":/assets/images/edit.png"), "Edit label", this, &ReceiveWidget::editLabel); + menu->addAction(icons()->icon("copy.png"), "Copy address", this, &ReceiveWidget::copyAddress); + menu->addAction(icons()->icon("copy.png"), "Copy label", this, &ReceiveWidget::copyLabel); + menu->addAction(icons()->icon("edit.png"), "Edit label", this, &ReceiveWidget::editLabel); if (isUsed) { menu->addAction(m_showTransactionsAction); @@ -103,6 +103,10 @@ void ReceiveWidget::showContextMenu(const QPoint &point) { menu->addAction("Hide address", this, &ReceiveWidget::hideAddress); } + if (m_wallet->isHwBacked()) { + menu->addAction("Show on device", this, &ReceiveWidget::showOnDevice); + } + menu->popup(ui->addresses->viewport()->mapToGlobal(point)); } @@ -164,6 +168,21 @@ void ReceiveWidget::showAddress() m_proxyModel->setHiddenAddresses(this->getHiddenAddresses()); } +void ReceiveWidget::showOnDevice() { + Monero::SubaddressRow* row = this->currentEntry(); + if (!row) return; + m_wallet->deviceShowAddressAsync(m_wallet->currentSubaddressAccount(), row->getRowId(), ""); +} + +void ReceiveWidget::generateSubaddress() { + if (!m_wallet) return; + + bool r = m_wallet->subaddress()->addRow(m_wallet->currentSubaddressAccount(), ""); + if (!r) { + QMessageBox::warning(this, "Warning", QString("Failed to generate subaddress:\n\n%1").arg(m_wallet->subaddress()->errorString())); + } +} + void ReceiveWidget::updateQrCode(){ QModelIndex index = ui->addresses->currentIndex(); if (!index.isValid()) { diff --git a/src/receivewidget.h b/src/receivewidget.h index a522caa..da6285a 100644 --- a/src/receivewidget.h +++ b/src/receivewidget.h @@ -40,13 +40,14 @@ public slots: void resetModel(); signals: - void generateSubaddress(); void showTransactions(const QString& address); private slots: void showHeaderMenu(const QPoint& position); void hideAddress(); void showAddress(); + void showOnDevice(); + void generateSubaddress(); private: Ui::ReceiveWidget *ui; diff --git a/src/sendwidget.cpp b/src/sendwidget.cpp index 7196cab..cc09f9c 100644 --- a/src/sendwidget.cpp +++ b/src/sendwidget.cpp @@ -6,10 +6,11 @@ #include "mainwindow.h" #include "ui_sendwidget.h" #include "globals.h" +#include "utils/AppData.h" -SendWidget::SendWidget(QWidget *parent) : - QWidget(parent), - ui(new Ui::SendWidget) +SendWidget::SendWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::SendWidget) { ui->setupUi(this); m_ctx = MainWindow::getContext(); @@ -20,6 +21,12 @@ SendWidget::SendWidget(QWidget *parent) : QValidator *validator = new QRegExpValidator(rx, this); ui->lineAmount->setValidator(validator); + connect(m_ctx, &AppContext::initiateTransaction, this, &SendWidget::onInitiateTransaction); + connect(m_ctx, &AppContext::endTransaction, this, &SendWidget::onEndTransaction); + connect(m_ctx, &AppContext::openAliasResolved, this, &SendWidget::onOpenAliasResolved); + connect(m_ctx, &AppContext::openAliasResolveError, this, &SendWidget::onOpenAliasResolveError); + connect(m_ctx, &AppContext::walletClosed, this, &SendWidget::onWalletClosed); + connect(ui->btnSend, &QPushButton::clicked, this, &SendWidget::sendClicked); connect(ui->btnClear, &QPushButton::clicked, this, &SendWidget::clearClicked); connect(ui->btnMax, &QPushButton::clicked, this, &SendWidget::btnMaxClicked); @@ -140,7 +147,7 @@ void SendWidget::sendClicked() { amounts.push_back(output.amount); } - emit createTransactionMultiDest(addresses, amounts, description); + m_ctx->onCreateTransactionMultiDest(addresses, amounts, description); return; } @@ -152,20 +159,20 @@ void SendWidget::sendClicked() { QMessageBox::warning(this, "Amount error", "Invalid amount specified."); return; } - emit createTransaction(recipient, amount, description, sendAll); + m_ctx->onCreateTransaction(recipient, amount, description, sendAll); } else { amount = WalletManager::amountFromDouble(this->conversionAmount()); if (amount == 0) { QMessageBox::warning(this, "Fiat conversion error", "Could not create transaction."); return; } - emit createTransaction(recipient, amount, description, false); + m_ctx->onCreateTransaction(recipient, amount, description, false); } } void SendWidget::aliasClicked() { auto address = ui->lineAddress->text(); - emit resolveOpenAlias(address); + m_ctx->onOpenAliasResolve(address); } void SendWidget::clearClicked() { @@ -195,7 +202,7 @@ void SendWidget::updateConversionLabel() { } else { auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); - double conversionAmount = AppContext::prices->convert("XMR", preferredFiatCurrency, this->amountDouble()); + double conversionAmount = appData()->prices.convert("XMR", preferredFiatCurrency, this->amountDouble()); return QString("~%1 %2").arg(QString::number(conversionAmount, 'f', 2), preferredFiatCurrency); } }(); @@ -206,7 +213,7 @@ void SendWidget::updateConversionLabel() { double SendWidget::conversionAmount() { QString currency = ui->comboCurrencySelection->currentText(); - return AppContext::prices->convert(currency, "XMR", this->amountDouble()); + return appData()->prices.convert(currency, "XMR", this->amountDouble()); } quint64 SendWidget::amount() { diff --git a/src/sendwidget.h b/src/sendwidget.h index 0821f01..056fb40 100644 --- a/src/sendwidget.h +++ b/src/sendwidget.h @@ -43,11 +43,6 @@ public slots: void onInitiateTransaction(); void onEndTransaction(); -signals: - void resolveOpenAlias(const QString &address); - void createTransaction(const QString &address, quint64 amount, const QString &description, bool all); - void createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); - private: void setupComboBox(); double amountDouble(); diff --git a/src/settings.cpp b/src/settings.cpp index cf7d293..bf73847 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -17,7 +17,7 @@ Settings::Settings(QWidget *parent) : this->setWindowIcon(QIcon("://assets/images/appicons/64x64.png")); ui->tabWidget->setTabVisible(2, false); - ui->tabWidget->setTabVisible(4, false); + ui->tabWidget->setTabVisible(5, false); connect(ui->btnCopyToClipboard, &QPushButton::clicked, this, &Settings::copyToClipboard); connect(ui->checkBox_multiBroadcast, &QCheckBox::toggled, [](bool toggled){ @@ -70,8 +70,6 @@ Settings::Settings(QWidget *parent) : ui->comboBox_timeFormat->setCurrentIndex(m_timeFormats.indexOf(timeFormatSetting)); connect(ui->comboBox_skin, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_skinChanged); - connect(ui->comboBox_blockExplorer, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_blockExplorerChanged); - connect(ui->comboBox_redditFrontend, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_redditFrontendChanged); connect(ui->comboBox_amountPrecision, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_amountPrecisionChanged); connect(ui->comboBox_dateFormat, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_dateFormatChanged); connect(ui->comboBox_timeFormat, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_timeFormatChanged); @@ -98,6 +96,16 @@ Settings::Settings(QWidget *parent) : ui->lineEdit_defaultWalletDir->setText(m_ctx->defaultWalletDir); }); + // Links tab + connect(ui->combo_blockExplorer, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_blockExplorerChanged); + connect(ui->combo_redditFrontend, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_redditFrontendChanged); + connect(ui->combo_localMoneroFrontend, QOverload::of(&QComboBox::currentIndexChanged), this, &Settings::comboBox_localMoneroFrontendChanged); + + ui->combo_blockExplorer->setCurrentIndex(ui->combo_blockExplorer->findText(config()->get(Config::blockExplorer).toString())); + ui->combo_redditFrontend->setCurrentIndex(ui->combo_redditFrontend->findText(config()->get(Config::redditFrontend).toString())); + + this->setupLocalMoneroFrontendCombobox(); + this->adjustSize(); } @@ -118,16 +126,21 @@ void Settings::comboBox_skinChanged(int pos) { } void Settings::comboBox_blockExplorerChanged(int pos) { - QString blockExplorer = ui->comboBox_blockExplorer->currentText(); + QString blockExplorer = ui->combo_blockExplorer->currentText(); config()->set(Config::blockExplorer, blockExplorer); emit blockExplorerChanged(blockExplorer); } void Settings::comboBox_redditFrontendChanged(int pos) { - QString redditFrontend = ui->comboBox_redditFrontend->currentText(); + QString redditFrontend = ui->combo_redditFrontend->currentText(); config()->set(Config::redditFrontend, redditFrontend); } +void Settings::comboBox_localMoneroFrontendChanged(int pos) { + QString localMoneroFrontend = ui->combo_localMoneroFrontend->currentData().toString(); + config()->set(Config::localMoneroFrontend, localMoneroFrontend); +} + void Settings::comboBox_amountPrecisionChanged(int pos) { config()->set(Config::amountPrecision, pos); emit amountPrecisionChanged(pos); @@ -161,6 +174,15 @@ void Settings::setupSkinCombobox() { ui->comboBox_skin->insertItems(0, m_skins); } +void Settings::setupLocalMoneroFrontendCombobox() { + ui->combo_localMoneroFrontend->addItem("localmonero.co", "https://localmonero.co"); + ui->combo_localMoneroFrontend->addItem("localmonero.co/nojs", "https://localmonero.co/nojs"); + ui->combo_localMoneroFrontend->addItem("nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion", + "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion"); + + ui->combo_localMoneroFrontend->setCurrentIndex(ui->combo_localMoneroFrontend->findData(config()->get(Config::localMoneroFrontend).toString())); +} + Settings::~Settings() { delete ui; } diff --git a/src/settings.h b/src/settings.h index 437c680..65b9078 100644 --- a/src/settings.h +++ b/src/settings.h @@ -37,14 +37,17 @@ public slots: void checkboxExternalLinkWarn(); void fiatCurrencySelected(int index); void comboBox_skinChanged(int pos); - void comboBox_blockExplorerChanged(int pos); - void comboBox_redditFrontendChanged(int pos); void comboBox_amountPrecisionChanged(int pos); void comboBox_dateFormatChanged(int pos); void comboBox_timeFormatChanged(int pos); + void comboBox_blockExplorerChanged(int pos); + void comboBox_redditFrontendChanged(int pos); + void comboBox_localMoneroFrontendChanged(int pos); + private: void setupSkinCombobox(); + void setupLocalMoneroFrontendCombobox(); AppContext *m_ctx; Ui::Settings *ui; diff --git a/src/settings.ui b/src/settings.ui index 1892866..71787f3 100644 --- a/src/settings.ui +++ b/src/settings.ui @@ -6,8 +6,8 @@ 0 0 - 1123 - 555 + 915 + 519 @@ -133,90 +133,33 @@
- - - Block explorer: - - - - - - - - exploremonero.com - - - - - xmrchain.net - - - - - moneroblocks.info - - - - - blockchair.com - - - - - - - - Reddit frontend: - - - - - - - - old.reddit.com - - - - - reddit.com - - - - - teddit.net - - - - - Amount precision: - + - + Date format: - + - + Time format: - + @@ -420,6 +363,80 @@ + + + Links + + + + + + Block explorer: + + + + + + + + exploremonero.com + + + + + xmrchain.net + + + + + moneroblocks.info + + + + + blockchair.com + + + + + + + + Reddit frontend: + + + + + + + + old.reddit.com + + + + + reddit.com + + + + + teddit.net + + + + + + + + LocalMonero frontend: + + + + + + + + diff --git a/src/utils/AppData.cpp b/src/utils/AppData.cpp new file mode 100644 index 0000000..7db6e7b --- /dev/null +++ b/src/utils/AppData.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "AppData.h" +#include "config.h" +#include "WebsocketNotifier.h" + +AppData::AppData(QObject *parent) + : QObject(parent) +{ + this->initRestoreHeights(); + + auto genesis_timestamp = this->restoreHeights[NetworkType::Type::MAINNET]->data.firstKey(); + this->txFiatHistory = new TxFiatHistory(genesis_timestamp, Config::defaultConfigDir().path()); + + connect(&websocketNotifier()->websocketClient, &WebsocketClient::connectionEstablished, this->txFiatHistory, &TxFiatHistory::onUpdateDatabase); + connect(this->txFiatHistory, &TxFiatHistory::requestYear, [](int year){ + QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1}})").arg(year).toUtf8(); + websocketNotifier()->websocketClient.sendMsg(data); + }); + connect(this->txFiatHistory, &TxFiatHistory::requestYearMonth, [](int year, int month){ + QByteArray data = QString(R"({"cmd": "txFiatHistory", "data": {"year": %1, "month": %2}})").arg(year).arg(month).toUtf8(); + websocketNotifier()->websocketClient.sendMsg(data); + }); + + connect(websocketNotifier(), &WebsocketNotifier::CryptoRatesReceived, &this->prices, &Prices::cryptoPricesReceived); + connect(websocketNotifier(), &WebsocketNotifier::FiatRatesReceived, &this->prices, &Prices::fiatPricesReceived); + connect(websocketNotifier(), &WebsocketNotifier::TxFiatHistoryReceived, this->txFiatHistory, &TxFiatHistory::onWSData); + connect(websocketNotifier(), &WebsocketNotifier::BlockHeightsReceived, this, &AppData::onBlockHeightsReceived); +} + +QPointer AppData::m_instance(nullptr); + +void AppData::onBlockHeightsReceived(int mainnet, int stagenet) { + this->heights[NetworkType::MAINNET] = mainnet; + this->heights[NetworkType::STAGENET] = stagenet; +} + +void AppData::initRestoreHeights() { + restoreHeights[NetworkType::TESTNET] = new RestoreHeightLookup(NetworkType::TESTNET); + restoreHeights[NetworkType::STAGENET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_stagenet.txt", NetworkType::STAGENET); + restoreHeights[NetworkType::MAINNET] = RestoreHeightLookup::fromFile(":/assets/restore_heights_monero_mainnet.txt", NetworkType::MAINNET); +} + +AppData* AppData::instance() +{ + if (!m_instance) { + m_instance = new AppData(QCoreApplication::instance()); + } + + return m_instance; +} \ No newline at end of file diff --git a/src/utils/AppData.h b/src/utils/AppData.h new file mode 100644 index 0000000..a5415f3 --- /dev/null +++ b/src/utils/AppData.h @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_APPDATA_H +#define FEATHER_APPDATA_H + +#include +#include +#include + +#include "prices.h" +#include "txfiathistory.h" +#include "RestoreHeightLookup.h" + +class AppData : public QObject { +Q_OBJECT + +public: + explicit AppData(QObject *parent); + static AppData* instance(); + + Prices prices; + TxFiatHistory *txFiatHistory; + QMap heights; + QMap restoreHeights; + +private slots: + void onBlockHeightsReceived(int mainnet, int stagenet); + +private: + void initRestoreHeights(); + + static QPointer m_instance; +}; + +inline AppData* appData() +{ + return AppData::instance(); +} + +#endif //FEATHER_APPDATA_H diff --git a/src/utils/AsyncTask.h b/src/utils/AsyncTask.h new file mode 100644 index 0000000..1a289db --- /dev/null +++ b/src/utils/AsyncTask.h @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_ASYNCTASK_HPP +#define KEEPASSXC_ASYNCTASK_HPP + +#include +#include +#include + +/** + * Asynchronously run computations outside the GUI thread. + */ +namespace AsyncTask +{ + + /** + * Wait for the given future without blocking the event loop. + * + * @param future future to wait for + * @return async task result + */ + template + typename std::result_of::type + waitForFuture(QFuture::type> future) + { + QEventLoop loop; + QFutureWatcher::type> watcher; + QObject::connect(&watcher, SIGNAL(finished()), &loop, SLOT(quit())); + watcher.setFuture(future); + loop.exec(); + return future.result(); + } + + /** + * Run a given task and wait for it to finish without blocking the event loop. + * + * @param task std::function object to run + * @return async task result + */ + template + typename std::result_of::type runAndWaitForFuture(FunctionObject task) + { + return waitForFuture(QtConcurrent::run(task)); + } + + /** + * Run a given task then call the defined callback. Prevents event loop blocking and + * ensures the validity of the follow-on task through the context. If the context is + * deleted, the callback will not be processed preventing use after free errors. + * + * @param task std::function object to run + * @param context QObject responsible for calling this function + * @param callback std::function object to run after the task completess + */ + template + void runThenCallback(FunctionObject task, QObject* context, FunctionObject2 callback) + { + typedef QFutureWatcher::type> FutureWatcher; + auto future = QtConcurrent::run(task); + auto watcher = new FutureWatcher(context); + QObject::connect(watcher, &QFutureWatcherBase::finished, context, [=]() { + watcher->deleteLater(); + callback(future.result()); + }); + watcher->setFuture(future); + } + +}; // namespace AsyncTask + +#endif // KEEPASSXC_ASYNCTASK_HPP diff --git a/src/utils/ColorScheme.cpp b/src/utils/ColorScheme.cpp index 4862c11..6d420e9 100644 --- a/src/utils/ColorScheme.cpp +++ b/src/utils/ColorScheme.cpp @@ -3,6 +3,7 @@ // Copyright (c) 2012 thomasv@gitorious #include "ColorScheme.h" +#include bool ColorScheme::darkScheme = false; ColorSchemeItem ColorScheme::GREEN = ColorSchemeItem("#117c11", "#8af296"); diff --git a/src/utils/FeatherSeed.h b/src/utils/FeatherSeed.h index c7a9346..bf50101 100644 --- a/src/utils/FeatherSeed.h +++ b/src/utils/FeatherSeed.h @@ -3,6 +3,7 @@ #include "libwalletqt/WalletManager.h" #include "libwalletqt/Wallet.h" +#include "utils/AppData.h" #include #include "RestoreHeightLookup.h" @@ -13,6 +14,8 @@ enum SeedType { }; struct FeatherSeed { + // TODO: this is spaghetti, needs refactor + QString coin; QString language; SeedType seedType; @@ -21,17 +24,18 @@ struct FeatherSeed { QString spendKey; QString correction; + NetworkType::Type netType; + time_t time; int restoreHeight = 0; - RestoreHeightLookup *lookup = nullptr; QString errorString; - explicit FeatherSeed(RestoreHeightLookup *lookup, + explicit FeatherSeed(NetworkType::Type networkType = NetworkType::MAINNET, const QString &coin = "monero", const QString &language = "English", const QStringList &mnemonic = {}) - : lookup(lookup), coin(coin), language(language), mnemonic(mnemonic) + : netType(networkType), coin(coin), language(language), mnemonic(mnemonic) { // Generate a new mnemonic if none was given if (mnemonic.length() == 0) { @@ -85,21 +89,17 @@ struct FeatherSeed { } } - int setRestoreHeight() { - if (this->lookup == nullptr) - return 1; - + void setRestoreHeight() { if (this->time == 0) - return 1; + this->restoreHeight = 1; - this->restoreHeight = this->lookup->dateToRestoreHeight(this->time); - return this->restoreHeight; + this->restoreHeight = appData()->restoreHeights[netType]->dateToRestoreHeight(this->time); } int setRestoreHeight(int height) { auto now = std::time(nullptr); auto nowClearance = 3600 * 24; - auto currentBlockHeight = this->lookup->dateToRestoreHeight(now - nowClearance); + auto currentBlockHeight = appData()->restoreHeights[netType]->dateToRestoreHeight(now - nowClearance); if (height >= currentBlockHeight + nowClearance) { qCritical() << "unrealistic restore height detected, setting to current blockheight instead: " << currentBlockHeight; this->restoreHeight = currentBlockHeight; diff --git a/src/utils/Icons.cpp b/src/utils/Icons.cpp new file mode 100644 index 0000000..d0e18d7 --- /dev/null +++ b/src/utils/Icons.cpp @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "Icons.h" + +Icons* Icons::m_instance(nullptr); + +Icons::Icons() += default; + +QIcon Icons::icon(const QString& name) +{ + QIcon icon = m_iconCache.value(name); + + if (!icon.isNull()) { + return icon; + } + + icon = QIcon{":/assets/images/" + name}; + + m_iconCache.insert(name, icon); + return icon; +} + +Icons* Icons::instance() +{ + if (!m_instance) { + m_instance = new Icons(); + } + + return m_instance; +} diff --git a/src/utils/Icons.h b/src/utils/Icons.h new file mode 100644 index 0000000..dd35976 --- /dev/null +++ b/src/utils/Icons.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_ICONS_H +#define FEATHER_ICONS_H + +#include +#include +#include + +class Icons { +public: + QIcon icon(const QString& name); + + static Icons* instance(); + +private: + Icons(); + + static Icons* m_instance; + + QHash m_iconCache; + + Q_DISABLE_COPY(Icons) +}; + +inline Icons* icons() +{ + return Icons::instance(); +} + +#endif //FEATHER_ICONS_H diff --git a/src/utils/NetworkManager.cpp b/src/utils/NetworkManager.cpp new file mode 100644 index 0000000..1fa4e47 --- /dev/null +++ b/src/utils/NetworkManager.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "NetworkManager.h" + +#include +#include + +QNetworkAccessManager *g_networkManagerTor = nullptr; +QNetworkAccessManager *g_networkManagerClearnet = nullptr; + +QNetworkAccessManager* getNetworkTor() +{ + if (!g_networkManagerTor) { + g_networkManagerTor = new QNetworkAccessManager(QCoreApplication::instance()); + QNetworkProxy proxy; + proxy.setType(QNetworkProxy::Socks5Proxy); + proxy.setHostName("127.0.0.1"); + proxy.setPort(9050); + g_networkManagerTor->setProxy(proxy); + } + return g_networkManagerTor; +} + +QNetworkAccessManager* getNetworkClearnet() +{ + if (!g_networkManagerClearnet) { + g_networkManagerClearnet = new QNetworkAccessManager(QCoreApplication::instance()); + } + return g_networkManagerClearnet; +} + +//void setTorProxy(const QNetworkProxy &proxy) +//{ +// QNetworkAccessManager *network = getNetworkTor(); +// network->setProxy(proxy); +//} \ No newline at end of file diff --git a/src/utils/NetworkManager.h b/src/utils/NetworkManager.h new file mode 100644 index 0000000..a65f7cf --- /dev/null +++ b/src/utils/NetworkManager.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_NETWORKMANAGER_H +#define FEATHER_NETWORKMANAGER_H + +#include + +QNetworkAccessManager* getNetworkTor(); +QNetworkAccessManager* getNetworkClearnet(); + +//void setTorProxy(const QNetworkProxy &proxy); + +#endif //FEATHER_NETWORKMANAGER_H diff --git a/src/utils/SemanticVersion.h b/src/utils/SemanticVersion.h new file mode 100644 index 0000000..02ece73 --- /dev/null +++ b/src/utils/SemanticVersion.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_SEMANTICVERSION_H +#define FEATHER_SEMANTICVERSION_H + +#include + +struct SemanticVersion +{ + explicit SemanticVersion(int major=0, int minor=0, int patch=0, int release=0) + : patch(patch), release(release) + { + this->major = major; + this->minor = minor; + } + + friend bool operator== (const SemanticVersion &v1, const SemanticVersion &v2) { + return (v1.major == v2.major && + v1.minor == v2.minor && + v1.patch == v2.patch && + v1.release == v2.release); + } + + friend bool operator!= (const SemanticVersion &v1, const SemanticVersion &v2) { + return !(v1 == v2); + } + + friend bool operator> (const SemanticVersion &v1, const SemanticVersion &v2) { + if (v1.major != v2.major) + return v1.major > v2.major; + if (v1.minor != v2.minor) + return v1.minor > v2.minor; + if (v1.patch != v2.patch) + return v1.patch > v2.patch; + if (v1.release != v2.release) + return v1.release > v2.release; + return false; + } + + friend bool operator< (const SemanticVersion &v1, const SemanticVersion &v2) { + if (v1 == v2) + return false; + return !(v1 > v2); + } + + friend bool operator <= (const SemanticVersion &v1, const SemanticVersion &v2) { + if (v1 == v2) + return true; + return v1 < v2; + } + + friend bool operator >= (const SemanticVersion &v1, const SemanticVersion &v2) { + if (v1 == v2) + return true; + return v1 > v2; + } + + QString toString() const { + return QString("%1.%2.%3.%4").arg(QString::number(major), QString::number(minor), + QString::number(patch), QString::number(release)); + } + + static SemanticVersion fromString(const QString &ver) { + SemanticVersion version; + + if (ver.contains("Beta")) { + QRegularExpression verRe("Beta-(?\\d+)"); + QRegularExpressionMatch match = verRe.match(ver); + version.minor = match.captured("minor").toInt(); + return version; + } + + QRegularExpression re(R"((?\d+)\.(?\d+)\.(?\d+)(\.(?\d+))?)"); + QRegularExpressionMatch match = re.match(ver); + + version.major = match.captured("major").toInt(); + version.minor = match.captured("minor").toInt(); + version.patch = match.captured("patch").toInt(); + version.release = match.captured("release").toInt(); + return version; + } + + static bool isValid(const SemanticVersion &v) { + return v != SemanticVersion(); + } + + int major = 0; + int minor = 0; + int patch = 0; + int release = 0; +}; + +#endif //FEATHER_SEMANTICVERSION_H diff --git a/src/utils/tor.cpp b/src/utils/TorManager.cpp similarity index 53% rename from src/utils/tor.cpp rename to src/utils/TorManager.cpp index 3d1a0f4..252417b 100644 --- a/src/utils/tor.cpp +++ b/src/utils/TorManager.cpp @@ -1,87 +1,53 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020-2021, The Monero Project. +#include "utils/TorManager.h" + #include #include #include #include + #include "utils/utils.h" -#include "utils/tor.h" #include "appcontext.h" #include "config-feather.h" -QString Tor::torHost = "127.0.0.1"; -quint16 Tor::torPort = 9050; - -Tor::Tor(AppContext *ctx, QObject *parent) - : QObject(parent) - , m_ctx(ctx) - , m_checkConnectionTimer(new QTimer(this)) +TorManager::TorManager(QObject *parent) + : QObject(parent) + , m_checkConnectionTimer(new QTimer(this)) { - connect(m_checkConnectionTimer, &QTimer::timeout, this, &Tor::checkConnection); + connect(m_checkConnectionTimer, &QTimer::timeout, this, &TorManager::checkConnection); this->torDir = Config::defaultConfigDir().filePath("tor"); this->torDataPath = QDir(this->torDir).filePath("data"); - if (m_ctx->cmdargs->isSet("tor-port") || m_ctx->cmdargs->isSet("tor-host")) { - if (m_ctx->cmdargs->isSet("tor-host")) - Tor::torHost = m_ctx->cmdargs->value("tor-host"); - if (m_ctx->cmdargs->isSet("tor-port")) - Tor::torPort = m_ctx->cmdargs->value("tor-port").toUShort(); - this->localTor = true; - if (!Utils::portOpen(Tor::torHost, Tor::torPort)) { - this->errorMsg = QString("--tor-host || --tor-port were specified but no running Tor instance was found on %1:%2.").arg(Tor::torHost,QString::number(Tor::torPort)); - } - return; - } - - // Assume Tor is already running - this->localTor = m_ctx->cmdargs->isSet("use-local-tor"); - if (this->localTor && !Utils::portOpen(Tor::torHost, Tor::torPort)) { - this->errorMsg = "--use-local-tor was specified but no running Tor instance found."; - } - if (m_ctx->isTorSocks || m_ctx->isTails || m_ctx->isWhonix || Utils::portOpen(Tor::torHost, Tor::torPort)) - this->localTor = true; - if (this->localTor) { - return; - } - -#ifndef HAS_TOR_BIN - qCritical() << "Feather built without embedded Tor. Assuming --use-local-tor"; - this->localTor = true; - return; -#endif - - bool unpacked = this->unpackBins(); - if (!unpacked) { - qCritical() << "Error unpacking embedded Tor. Assuming --use-local-tor"; - this->localTor = true; - return; - } - - // Don't spawn Tor on default port to avoid conflicts - Tor::torPort = 19450; - if (Utils::portOpen(Tor::torHost, Tor::torPort)) { - this->localTor = true; - return; - } - - qDebug() << "Using embedded tor instance"; m_process.setProcessChannelMode(QProcess::MergedChannels); - connect(&m_process, &QProcess::readyReadStandardOutput, this, &Tor::handleProcessOutput); - connect(&m_process, &QProcess::errorOccurred, this, &Tor::handleProcessError); - connect(&m_process, &QProcess::stateChanged, this, &Tor::stateChanged); + connect(&m_process, &QProcess::readyReadStandardOutput, this, &TorManager::handleProcessOutput); + connect(&m_process, &QProcess::errorOccurred, this, &TorManager::handleProcessError); + connect(&m_process, &QProcess::stateChanged, this, &TorManager::stateChanged); } -void Tor::stop() { +QPointer TorManager::m_instance(nullptr); + +void TorManager::init() { + m_localTor = !shouldStartTorDaemon(); + + auto state = m_process.state(); + if (m_localTor && (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)) { + m_process.kill(); + } +} + +void TorManager::stop() { m_process.kill(); } -void Tor::start() { - if (this->localTor) { +void TorManager::start() { + m_checkConnectionTimer->start(5000); + + if (m_localTor) { this->checkConnection(); - m_checkConnectionTimer->start(5000); return; } @@ -91,8 +57,8 @@ void Tor::start() { return; } - if (Utils::portOpen(Tor::torHost, Tor::torPort)) { - this->errorMsg = QString("Unable to start Tor on %1:%2. Port already in use.").arg(Tor::torHost, Tor::torPort); + if (Utils::portOpen(featherTorHost, featherTorPort)) { + this->errorMsg = QString("Unable to start Tor on %1:%2. Port already in use.").arg(featherTorHost, QString::number(featherTorPort)); return; } @@ -107,7 +73,7 @@ void Tor::start() { QStringList arguments; arguments << "--ignore-missing-torrc"; - arguments << "--SocksPort" << QString("%1:%2").arg(Tor::torHost, QString::number(Tor::torPort)); + arguments << "--SocksPort" << QString("%1:%2").arg(featherTorHost, QString::number(featherTorPort)); arguments << "--TruncateLogFile" << "1"; arguments << "--DataDirectory" << this->torDataPath; arguments << "--Log" << "notice"; @@ -116,38 +82,46 @@ void Tor::start() { qDebug() << QString("%1 %2").arg(this->torPath, arguments.join(" ")); m_process.start(this->torPath, arguments); + m_started = true; } -void Tor::checkConnection() { +void TorManager::checkConnection() { // We might not be able to connect to localhost if torsocks is used to start feather - if (m_ctx->isTorSocks) + if (Utils::isTorsocks()) { this->setConnectionState(true); + } - else if (m_ctx->isWhonix) + else if (WhonixOS::detect()) { this->setConnectionState(true); + } - else if (m_ctx->isTails) { + else if (TailsOS::detect()) { QStringList args = QStringList() << "--quiet" << "is-active" << "tails-tor-has-bootstrapped.target"; int code = QProcess::execute("/bin/systemctl", args); this->setConnectionState(code == 0); } - else if (Utils::portOpen(Tor::torHost, Tor::torPort)) - this->setConnectionState(true); + else if (m_localTor) { + QString host = config()->get(Config::socks5Host).toString(); + quint16 port = config()->get(Config::socks5Port).toString().toUShort(); + this->setConnectionState(Utils::portOpen(host, port)); + } - else - this->setConnectionState(false); + else { + this->setConnectionState(Utils::portOpen(featherTorHost, featherTorPort)); + } } -void Tor::setConnectionState(bool connected) { +void TorManager::setConnectionState(bool connected) { this->torConnected = connected; emit connectionStateChanged(connected); } -void Tor::stateChanged(QProcess::ProcessState state) { - if(state == QProcess::ProcessState::Running) +void TorManager::stateChanged(QProcess::ProcessState state) { + if (state == QProcess::ProcessState::Running) { qWarning() << "Tor started, awaiting bootstrap"; + } else if (state == QProcess::ProcessState::NotRunning) { this->setConnectionState(false); @@ -160,7 +134,7 @@ void Tor::stateChanged(QProcess::ProcessState state) { } } -void Tor::handleProcessOutput() { +void TorManager::handleProcessOutput() { QByteArray output = m_process.readAllStandardOutput(); this->torLogs.append(Utils::barrayToString(output)); emit logsUpdated(); @@ -172,7 +146,7 @@ void Tor::handleProcessOutput() { qDebug() << output; } -void Tor::handleProcessError(QProcess::ProcessError error) { +void TorManager::handleProcessError(QProcess::ProcessError error) { if (error == QProcess::ProcessError::Crashed) qWarning() << "Tor crashed or killed"; else if (error == QProcess::ProcessError::FailedToStart) { @@ -181,7 +155,7 @@ void Tor::handleProcessError(QProcess::ProcessError error) { } } -bool Tor::unpackBins() { +bool TorManager::unpackBins() { QString torFile; // On MacOS write libevent to disk @@ -211,10 +185,10 @@ bool Tor::unpackBins() { this->torPath += ".exe"; #endif - TorVersion embeddedVersion = this->stringToVersion(QString(TOR_VERSION)); - TorVersion filesystemVersion = this->getVersion(torPath); + SemanticVersion embeddedVersion = SemanticVersion::fromString(QString(TOR_VERSION)); + SemanticVersion filesystemVersion = this->getVersion(torPath); qDebug() << QString("Tor versions: embedded %1, filesystem %2").arg(embeddedVersion.toString(), filesystemVersion.toString()); - if (TorVersion::isValid(filesystemVersion) && (embeddedVersion > filesystemVersion)) { + if (SemanticVersion::isValid(filesystemVersion) && (embeddedVersion > filesystemVersion)) { qInfo() << "Embedded version is newer, overwriting."; QFile::setPermissions(torPath, QFile::ReadOther | QFile::WriteOther); if (!QFile::remove(torPath)) { @@ -228,12 +202,71 @@ bool Tor::unpackBins() { #if defined(Q_OS_UNIX) QFile torBin(this->torPath); - torBin.setPermissions(QFile::ExeGroup | QFile::ExeOther | QFile::ExeOther | QFile::ExeUser); + torBin.setPermissions(QFile::ExeUser | QFile::ExeGroup | QFile::ExeOther + | QFile::ReadOwner | QFile::ReadGroup | QFile::ReadOther); #endif return true; } -TorVersion Tor::getVersion(const QString &fileName) { +bool TorManager::isLocalTor() { + return m_localTor; +} + +bool TorManager::isStarted() { + return m_started; +} + +bool TorManager::shouldStartTorDaemon() { + QString torHost = config()->get(Config::socks5Host).toString(); + quint16 torPort = config()->get(Config::socks5Port).toString().toUShort(); + QString torHostPort = QString("%1:%2").arg(torHost, QString::number(torPort)); + + // Don't start a Tor daemon if Feather is run with Torsocks + if (Utils::isTorsocks()) { + return false; + } + + // Don't start a Tor daemon on privacy OSes + if (TailsOS::detect() || WhonixOS::detect()) { + return false; + } + + // Don't start a Tor daemon if we don't have one +#ifndef HAS_TOR_BIN + qWarning() << "Feather built without embedded Tor. Assuming --use-local-tor"; + return false; +#endif + + // Don't start a Tor daemon if --use-local-tor is specified + if (config()->get(Config::useLocalTor).toBool()) { + return false; + } + + // Don't start a Tor daemon if one is already running + if (Utils::portOpen(torHost, torPort)) { + return false; + } + + bool unpacked = this->unpackBins(); + if (!unpacked) { + // Don't try to start a Tor daemon if unpacking failed + qWarning() << "Error unpacking embedded Tor. Assuming --use-local-tor"; + this->errorMsg = "Error unpacking embedded Tor. Assuming --use-local-tor"; + return false; + } + + // Tor daemon (or other service) is already running on our port (19450) + if (Utils::portOpen(featherTorHost, featherTorPort)) { + // TODO: this is a hack, fix it later + config()->set(Config::socks5Host, featherTorHost); + config()->set(Config::socks5Port, featherTorPort); + return false; + } + + return true; +} + +SemanticVersion TorManager::getVersion(const QString &fileName) { QProcess process; process.setProcessChannelMode(QProcess::MergedChannels); process.start(this->torPath, QStringList() << "--version"); @@ -242,23 +275,17 @@ TorVersion Tor::getVersion(const QString &fileName) { if(output.isEmpty()) { qWarning() << "Could not grab Tor version"; - return TorVersion(); + return SemanticVersion(); } - return this->stringToVersion(output); + return SemanticVersion::fromString(output); } -TorVersion Tor::stringToVersion(const QString &version) { - QRegularExpression re("(?\\d)\\.(?\\d)\\.(?\\d)\\.(?\\d)"); - QRegularExpressionMatch match = re.match(version); - - if (!match.hasMatch()) { - qWarning() << "Could not parse Tor version"; - return TorVersion(); +TorManager* TorManager::instance() +{ + if (!m_instance) { + m_instance = new TorManager(QCoreApplication::instance()); } - return TorVersion(match.captured("major").toInt(), - match.captured("minor").toInt(), - match.captured("patch").toInt(), - match.captured("release").toInt()); + return m_instance; } \ No newline at end of file diff --git a/src/utils/TorManager.h b/src/utils/TorManager.h new file mode 100644 index 0000000..2edf8e9 --- /dev/null +++ b/src/utils/TorManager.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_TOR_H +#define FEATHER_TOR_H + +#include +#include +#include +#include +#include +#include "utils/childproc.h" +#include "utils/SemanticVersion.h" + +class TorManager : public QObject +{ +Q_OBJECT + +public: + explicit TorManager(QObject *parent = nullptr); + + void init(); + void start(); + void stop(); + bool unpackBins(); + bool isLocalTor(); + bool isStarted(); + SemanticVersion getVersion(const QString &fileName); + + static TorManager* instance(); + + bool torConnected = false; + + QString featherTorHost = "127.0.0.1"; + quint16 featherTorPort = 19450; + + QString torDir; + QString torPath; + QString torDataPath; + + QString torLogs; + QString errorMsg = ""; + +signals: + void connectionStateChanged(bool connected); + void startupFailure(QString reason); + void logsUpdated(); + +private slots: + void stateChanged(QProcess::ProcessState); + void handleProcessOutput(); + void handleProcessError(QProcess::ProcessError error); + void checkConnection(); + +private: + bool shouldStartTorDaemon(); + void setConnectionState(bool connected); + + static QPointer m_instance; + + ChildProcess m_process; + int m_restarts = 0; + bool m_stopRetries = false; + bool m_localTor; + bool m_started = false; + QTimer *m_checkConnectionTimer; +}; + +inline TorManager* torManager() +{ + return TorManager::instance(); +} + +#endif //FEATHER_TOR_H \ No newline at end of file diff --git a/src/utils/Updater.cpp b/src/utils/Updater.cpp new file mode 100644 index 0000000..6cd339e --- /dev/null +++ b/src/utils/Updater.cpp @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "Updater.h" + +#include +#include + +#include "utils.h" + +Updater::Updater() { + std::string featherWallet = Utils::fileGetContents(":/assets/gpg_keys/featherwallet.asc").toStdString(); + m_maintainers.emplace_back(featherWallet); +} + +QByteArray Updater::verifyParseSignedHashes( + const QByteArray &armoredSignedHashes, + const QString &binaryFilename, + QString &signers) const +{ + const QString signedMessage = verifySignature(armoredSignedHashes, signers); + + return parseShasumOutput(signedMessage, binaryFilename); +} + +QByteArray Updater::getHash(const void *data, size_t size) const +{ + QByteArray hash(sizeof(crypto::hash), 0); + tools::sha256sum(static_cast(data), size, *reinterpret_cast(hash.data())); + return hash; +} + +QByteArray Updater::parseShasumOutput(const QString &message, const QString &filename) const +{ + for (const auto &line : message.splitRef("\n")) + { + const auto trimmed = line.trimmed(); + if (trimmed.endsWith(filename)) + { + const int pos = trimmed.indexOf(' '); + if (pos != -1) + { + return QByteArray::fromHex(trimmed.left(pos).toUtf8()); + } + } + else if (trimmed.startsWith(filename)) + { + const int pos = trimmed.lastIndexOf(' '); + if (pos != -1) + { + return QByteArray::fromHex(trimmed.right(trimmed.size() - pos).toUtf8()); + } + } + } + + throw std::runtime_error("hash not found"); +} + +QString Updater::verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const +{ + const std::string messageString = armoredSignedMessage.toStdString(); + + const openpgp::message_armored signedMessage(messageString); + signer = verifySignature(signedMessage, openpgp::signature_rsa::from_armored(messageString)); + + const epee::span message = signedMessage; + return QString(QByteArray(reinterpret_cast(&message[0]), message.size())); +} + +QString Updater::verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const +{ + for (const auto &maintainer : m_maintainers) + { + for (const auto &public_key : maintainer) + { + try { + if (signature.verify(data, public_key)) + { + return QString::fromStdString(maintainer.user_id()); + } + } + catch (const std::exception &e) { + qWarning() << e.what(); + } + } + } + + throw std::runtime_error("not signed by a maintainer"); +} diff --git a/src/utils/Updater.h b/src/utils/Updater.h new file mode 100644 index 0000000..c6a3924 --- /dev/null +++ b/src/utils/Updater.h @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#pragma once + +#include +#include + +#include + +class Updater +{ +public: + explicit Updater(); + + QByteArray verifyParseSignedHashes(const QByteArray &armoredSignedHashes, const QString &binaryFilename, QString &signers) const; + + QByteArray getHash(const void *data, size_t size) const; + QString verifySignature(const QByteArray &armoredSignedMessage, QString &signer) const; + QByteArray parseShasumOutput(const QString &message, const QString &filename) const; + +private: + QString verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const; + +private: + std::vector m_maintainers; +}; diff --git a/src/utils/WebsocketClient.cpp b/src/utils/WebsocketClient.cpp new file mode 100644 index 0000000..00bf16b --- /dev/null +++ b/src/utils/WebsocketClient.cpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "WebsocketClient.h" + +#include +#include "utils/utils.h" + +WebsocketClient::WebsocketClient(QObject *parent) + : QObject(parent) +{ + connect(&webSocket, &QWebSocket::binaryMessageReceived, this, &WebsocketClient::onbinaryMessageReceived); + connect(&webSocket, &QWebSocket::connected, this, &WebsocketClient::onConnected); + connect(&webSocket, &QWebSocket::disconnected, this, &WebsocketClient::closed); + connect(&webSocket, QOverload::of(&QWebSocket::error), this, &WebsocketClient::onError); + connect(&m_connectionTimer, &QTimer::timeout, this, &WebsocketClient::checkConnection); + + // Keep websocket connection alive + connect(&m_pingTimer, &QTimer::timeout, [this]{ + if (webSocket.state() == QAbstractSocket::ConnectedState) + webSocket.ping(); + }); + m_pingTimer.setInterval(30 * 1000); + m_pingTimer.start(); +} + +void WebsocketClient::sendMsg(const QByteArray &data) { + if (webSocket.state() == QAbstractSocket::ConnectedState) + webSocket.sendBinaryMessage(data); +} + +void WebsocketClient::onToggleConnect(bool connect) { + m_connect = connect; + if (m_connect) + checkConnection(); +} + +void WebsocketClient::start() { + // connect & reconnect on errors/close +#ifdef QT_DEBUG + qDebug() << "WebSocket connect:" << m_url.url(); +#endif + + if (m_connect) + webSocket.open(m_url); + + if (!m_connectionTimer.isActive()) { + m_connectionTimer.start(2000); + } +} + +void WebsocketClient::checkConnection() { + if (!m_connect) + return; + + if (webSocket.state() == QAbstractSocket::UnconnectedState) { +#ifdef QT_DEBUG + qDebug() << "WebSocket reconnect"; +#endif + this->start(); + } +} + +void WebsocketClient::onConnected() { +#ifdef QT_DEBUG + qDebug() << "WebSocket connected"; +#endif + emit connectionEstablished(); +} + +void WebsocketClient::onError(QAbstractSocket::SocketError error) { + qCritical() << "WebSocket error: " << error; + auto state = webSocket.state(); + if (state == QAbstractSocket::ConnectedState || state == QAbstractSocket::ConnectingState) + webSocket.abort(); +} + +void WebsocketClient::onbinaryMessageReceived(const QByteArray &message) { +#ifdef QT_DEBUG + qDebug() << "WebSocket received:" << message; +#endif + if (!Utils::validateJSON(message)) { + qCritical() << "Could not interpret WebSocket message as JSON"; + return; + } + + QJsonDocument doc = QJsonDocument::fromJson(message); + QJsonObject object = doc.object(); + if(!object.contains("cmd") || !object.contains("data")) { + qCritical() << "Invalid WebSocket message received"; + return; + } + + emit WSMessage(object); +} diff --git a/src/utils/WebsocketClient.h b/src/utils/WebsocketClient.h new file mode 100644 index 0000000..805e652 --- /dev/null +++ b/src/utils/WebsocketClient.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_WEBSOCKETCLIENT_H +#define FEATHER_WEBSOCKETCLIENT_H + +#include +#include +#include +#include +#include "globals.h" + +class WebsocketClient : public QObject { + Q_OBJECT + +public: + explicit WebsocketClient(QObject *parent = nullptr); + void start(); + void sendMsg(const QByteArray &data); + + QWebSocket webSocket; + +public slots: + void onToggleConnect(bool connect); + +signals: + void closed(); + void connectionEstablished(); + void WSMessage(QJsonObject message); + +private slots: + void onConnected(); + void onbinaryMessageReceived(const QByteArray &message); + void checkConnection(); + void onError(QAbstractSocket::SocketError error); + +private: + bool m_connect = false; + QUrl m_url = globals::websocketUrl; + QTimer m_connectionTimer; + QTimer m_pingTimer; +}; + +#endif //FEATHER_WEBSOCKETCLIENT_H diff --git a/src/utils/WebsocketNotifier.cpp b/src/utils/WebsocketNotifier.cpp new file mode 100644 index 0000000..1d509a6 --- /dev/null +++ b/src/utils/WebsocketNotifier.cpp @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "WebsocketNotifier.h" +#include "utils.h" +#include "tails.h" +#include "whonix.h" + +#include + +WebsocketNotifier::WebsocketNotifier(QObject *parent) + : QObject(parent) + , websocketClient(new WebsocketClient(this)) +{ + connect(&websocketClient, &WebsocketClient::WSMessage, this, &WebsocketNotifier::onWSMessage); +} + +QPointer WebsocketNotifier::m_instance(nullptr); + +void WebsocketNotifier::onWSMessage(const QJsonObject &msg) { + QString cmd = msg.value("cmd").toString(); + + if (cmd == "blockheights") { + QJsonObject data = msg.value("data").toObject(); + int mainnet = data.value("mainnet").toInt(); + int stagenet = data.value("stagenet").toInt(); + + emit BlockHeightsReceived(mainnet, stagenet); + } + + else if(cmd == "nodes") { + this->onWSNodes(msg.value("data").toArray()); + } + + else if(cmd == "crypto_rates") { + QJsonArray crypto_rates = msg.value("data").toArray(); + emit CryptoRatesReceived(crypto_rates); + } + + else if(cmd == "fiat_rates") { + QJsonObject fiat_rates = msg.value("data").toObject(); + emit FiatRatesReceived(fiat_rates); + } + + else if(cmd == "reddit") { + QJsonArray reddit_data = msg.value("data").toArray(); + this->onWSReddit(reddit_data); + } + + else if(cmd == "ccs") { + auto ccs_data = msg.value("data").toArray(); + this->onWSCCS(ccs_data); + } + + else if(cmd == "txFiatHistory") { + auto txFiatHistory_data = msg.value("data").toObject(); + emit TxFiatHistoryReceived(txFiatHistory_data); + } + +#if defined(CHECK_UPDATES) + else if (cmd == "updates") { + this->onWSUpdates(msg.value("data").toObject()); + } +#endif + +#if defined(HAS_XMRIG) + else if(cmd == "xmrig") { + this->onWSXMRigDownloads(msg.value("data").toObject()); + } +#endif + +#if defined(HAS_LOCALMONERO) + else if (cmd == "localmonero_countries") { + emit LocalMoneroCountriesReceived(msg.value("data").toArray()); + } + + else if (cmd == "localmonero_currencies") { + emit LocalMoneroCurrenciesReceived(msg.value("data").toArray()); + } + + else if (cmd == "localmonero_payment_methods") { + emit LocalMoneroPaymentMethodsReceived(msg.value("data").toObject()); + } +#endif +} + +void WebsocketNotifier::onWSNodes(const QJsonArray &nodes) { + // TODO: Refactor, should be filtered client side + + QList l; + for (auto &&entry: nodes) { + auto obj = entry.toObject(); + auto nettype = obj.value("nettype"); + auto type = obj.value("type"); + + auto networkType = config()->get(Config::networkType).toInt(); + + // filter remote node network types + if(nettype == "mainnet" && networkType != NetworkType::MAINNET) + continue; + if(nettype == "stagenet" && networkType != NetworkType::STAGENET) + continue; + if(nettype == "testnet" && networkType != NetworkType::TESTNET) + continue; + + if(type == "clearnet" && (TailsOS::detect() || WhonixOS::detect() || Utils::isTorsocks())) + continue; + + FeatherNode node{obj.value("address").toString(), + obj.value("height").toInt(), + obj.value("target_height").toInt(), + obj.value("online").toBool()}; + l.append(node); + } + + emit NodesReceived(l); +} + +void WebsocketNotifier::onWSReddit(const QJsonArray& reddit_data) { + QList> l; + + for (auto &&entry: reddit_data) { + auto obj = entry.toObject(); + auto redditPost = new RedditPost( + obj.value("title").toString(), + obj.value("author").toString(), + obj.value("permalink").toString(), + obj.value("comments").toInt()); + QSharedPointer r = QSharedPointer(redditPost); + l.append(r); + } + + emit RedditReceived(l); +} + +void WebsocketNotifier::onWSCCS(const QJsonArray &ccs_data) { + QList> l; + + QStringList fonts = {"state", "address", "author", "date", + "title", "target_amount", "raised_amount", + "percentage_funded", "contributions"}; + + for (auto &&entry: ccs_data) { + auto obj = entry.toObject(); + auto c = QSharedPointer(new CCSEntry()); + + if (obj.value("state").toString() != "FUNDING-REQUIRED") + continue; + + c->state = obj.value("state").toString(); + c->address = obj.value("address").toString(); + c->author = obj.value("author").toString(); + c->date = obj.value("date").toString(); + c->title = obj.value("title").toString(); + c->url = obj.value("url").toString(); + c->target_amount = obj.value("target_amount").toDouble(); + c->raised_amount = obj.value("raised_amount").toDouble(); + c->percentage_funded = obj.value("percentage_funded").toDouble(); + c->contributions = obj.value("contributions").toInt(); + l.append(c); + } + + emit CCSReceived(l); +} + +void WebsocketNotifier::onWSUpdates(const QJsonObject &updates) { + emit UpdatesReceived(updates); +} + +void WebsocketNotifier::onWSXMRigDownloads(const QJsonObject &downloads) { + emit XMRigDownloadsReceived(downloads); +} + +WebsocketNotifier* WebsocketNotifier::instance() +{ + if (!m_instance) { + m_instance = new WebsocketNotifier(QCoreApplication::instance()); + } + + return m_instance; +} \ No newline at end of file diff --git a/src/utils/WebsocketNotifier.h b/src/utils/WebsocketNotifier.h new file mode 100644 index 0000000..3fa17c5 --- /dev/null +++ b/src/utils/WebsocketNotifier.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_WEBSOCKETNOTIFIER_H +#define FEATHER_WEBSOCKETNOTIFIER_H + +#include +#include + +#include "WebsocketClient.h" +#include "networktype.h" +#include "nodes.h" +#include "prices.h" +#include "widgets/RedditPost.h" +#include "widgets/CCSEntry.h" +#include "txfiathistory.h" + +class WebsocketNotifier : public QObject { + Q_OBJECT + +public: + explicit WebsocketNotifier(QObject *parent); + + QMap heights; + + WebsocketClient websocketClient; + + static WebsocketNotifier* instance(); + +signals: + void BlockHeightsReceived(int mainnet, int stagenet); + void NodesReceived(QList &L); + void CryptoRatesReceived(const QJsonArray &data); + void FiatRatesReceived(const QJsonObject &fiat_rates); + void RedditReceived(QList> L); + void CCSReceived(QList> L); + void TxFiatHistoryReceived(const QJsonObject &data); + void UpdatesReceived(const QJsonObject &updates); + void XMRigDownloadsReceived(const QJsonObject &downloads); + void LocalMoneroCountriesReceived(const QJsonArray &countries); + void LocalMoneroCurrenciesReceived(const QJsonArray ¤cies); + void LocalMoneroPaymentMethodsReceived(const QJsonObject &payment_methods); + +private slots: + void onWSMessage(const QJsonObject &msg); + + void onWSNodes(const QJsonArray &nodes); + void onWSReddit(const QJsonArray &reddit_data); + void onWSCCS(const QJsonArray &ccs_data); + void onWSUpdates(const QJsonObject &updates); + void onWSXMRigDownloads(const QJsonObject &downloads); + +private: + static QPointer m_instance; +}; + +inline WebsocketNotifier* websocketNotifier() +{ + return WebsocketNotifier::instance(); +} + + +#endif //FEATHER_WEBSOCKETNOTIFIER_H \ No newline at end of file diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 1234719..883a871 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -18,7 +18,7 @@ struct ConfigDirective static const QHash configStrings = { // General {Config::warnOnExternalLink,{QS("warnOnExternalLink"), true}}, - {Config::checkForAppUpdates,{QS("checkForAppUpdates"), true}}, + {Config::checkForUpdates,{QS("checkForUpdates"), true}}, {Config::warnOnStagenet,{QS("warnOnStagenet"), true}}, {Config::warnOnTestnet,{QS("warnOnTestnet"), true}}, {Config::warnOnAlpha,{QS("warnOnAlpha"), true}}, @@ -38,12 +38,12 @@ static const QHash configStrings = { {Config::useOnionNodes,{QS("useOnionNodes"), false}}, {Config::showTabHome,{QS("showTabHome"), true}}, {Config::showTabCoins,{QS("showTabCoins"), false}}, - {Config::showTabExchange, {QS("showTabExchange"), true}}, + {Config::showTabExchange, {QS("showTabExchange"), false}}, {Config::showTabXMRig,{QS("showTabXMRig"), false}}, {Config::showTabCalc,{QS("showTabCalc"), true}}, {Config::geometry, {QS("geometry"), {}}}, {Config::windowState, {QS("windowState"), {}}}, - {Config::firstRun,{QS("firstRun"), false}}, + {Config::firstRun, {QS("firstRun"), true}}, {Config::hideBalance, {QS("hideBalance"), false}}, {Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}}, {Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}}, @@ -51,7 +51,15 @@ static const QHash configStrings = { {Config::amountPrecision, {QS("amountPrecision"), 12}}, {Config::dateFormat, {QS("dateFormat"), "yyyy-MM-dd"}}, {Config::timeFormat, {QS("timeFormat"), "HH:mm"}}, - {Config::multiBroadcast, {QS("multiBroadcast"), true}} + {Config::multiBroadcast, {QS("multiBroadcast"), true}}, + {Config::torPrivacyLevel, {QS("torPrivacyLevel"), 1}}, + {Config::socks5Host, {QS("socks5Host"), "127.0.0.1"}}, + {Config::socks5Port, {QS("socks5Port"), "9050"}}, + {Config::socks5User, {QS("socks5User"), ""}}, + {Config::socks5Pass, {QS("socks5Pass"), ""}}, + {Config::useLocalTor, {QS("useLocalTor"), false}}, + {Config::networkType, {QS("networkType"), NetworkType::Type::MAINNET}}, + {Config::localMoneroFrontend, {QS("localMoneroFrontend"), "https://localmonero.co"}} }; diff --git a/src/utils/config.h b/src/utils/config.h index 1b0b11e..9126897 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -21,7 +21,7 @@ public: enum ConfigKey { warnOnExternalLink, - checkForAppUpdates, + checkForUpdates, warnOnStagenet, warnOnTestnet, warnOnAlpha, @@ -55,7 +55,21 @@ public: portableMode, dateFormat, timeFormat, - multiBroadcast + multiBroadcast, + torPrivacyLevel, + socks5Host, + socks5Port, + socks5User, + socks5Pass, + useLocalTor, // Prevents Feather from starting bundled Tor daemon + networkType, + localMoneroFrontend + }; + + enum PrivacyLevel { + allTorExceptNode = 0, + allTorExceptInitSync, + allTor }; ~Config() override; @@ -71,7 +85,7 @@ public: static Config* instance(); signals: - void changed(ConfigKey key); + void changed(Config::ConfigKey key); private: Config(const QString& fileName, QObject* parent = nullptr); diff --git a/src/utils/daemonrpc.cpp b/src/utils/daemonrpc.cpp index 69c8bfc..58c57d9 100644 --- a/src/utils/daemonrpc.cpp +++ b/src/utils/daemonrpc.cpp @@ -3,9 +3,9 @@ #include "daemonrpc.h" -DaemonRpc::DaemonRpc(QObject *parent, UtilsNetworking *network, QString daemonAddress) +DaemonRpc::DaemonRpc(QObject *parent, QNetworkAccessManager *network, QString daemonAddress) : QObject(parent) - , m_network(network) + , m_network(new UtilsNetworking(network, this)) , m_daemonAddress(std::move(daemonAddress)) { } @@ -37,6 +37,7 @@ void DaemonRpc::onResponse(QNetworkReply *reply, Endpoint endpoint) { const auto err = reply->errorString(); QByteArray data = reply->readAll(); + reply->deleteLater(); QJsonObject obj; if (!data.isEmpty() && Utils::validateJSON(data)) { auto doc = QJsonDocument::fromJson(data); @@ -65,8 +66,8 @@ void DaemonRpc::onResponse(QNetworkReply *reply, Endpoint endpoint) { return; } - reply->deleteLater(); - emit ApiResponse(DaemonResponse(true, endpoint, "", obj)); + DaemonResponse resp{true, endpoint, "", obj}; + emit ApiResponse(resp); } QString DaemonRpc::onSendRawTransactionFailed(const QJsonObject &obj) { @@ -92,7 +93,3 @@ QString DaemonRpc::onSendRawTransactionFailed(const QJsonObject &obj) { void DaemonRpc::setDaemonAddress(const QString &daemonAddress) { m_daemonAddress = daemonAddress; } - -void DaemonRpc::setNetwork(UtilsNetworking *network) { - m_network = network; -} \ No newline at end of file diff --git a/src/utils/daemonrpc.h b/src/utils/daemonrpc.h index c2d6692..403e893 100644 --- a/src/utils/daemonrpc.h +++ b/src/utils/daemonrpc.h @@ -27,13 +27,12 @@ public: QJsonObject obj; }; - explicit DaemonRpc(QObject *parent, UtilsNetworking *network, QString daemonAddress); + explicit DaemonRpc(QObject *parent, QNetworkAccessManager *network, QString daemonAddress); void sendRawTransaction(const QString &tx_as_hex, bool do_not_relay = false, bool do_sanity_checks = true); void getTransactions(const QStringList &txs_hashes, bool decode_as_json = false, bool prune = false); void setDaemonAddress(const QString &daemonAddress); - void setNetwork(UtilsNetworking *network); signals: void ApiResponse(DaemonResponse resp); diff --git a/src/utils/networking.cpp b/src/utils/networking.cpp index be7ce31..9b7e578 100644 --- a/src/utils/networking.cpp +++ b/src/utils/networking.cpp @@ -15,14 +15,12 @@ void UtilsNetworking::setUserAgent(const QString &userAgent) { this->m_userAgent = userAgent; } -void UtilsNetworking::get(const QString &url) { +QNetworkReply* UtilsNetworking::get(const QString &url) { QNetworkRequest request; request.setUrl(QUrl(url)); request.setRawHeader("User-Agent", m_userAgent.toUtf8()); - QNetworkReply *reply; - reply = this->m_networkAccessManager->get(request); - connect(reply, &QNetworkReply::finished, std::bind(&UtilsNetworking::webResponse, this, reply)); + return this->m_networkAccessManager->get(request); } QNetworkReply* UtilsNetworking::getJson(const QString &url) { @@ -44,36 +42,3 @@ QNetworkReply* UtilsNetworking::postJson(const QString &url, const QJsonObject & QByteArray bytes = doc.toJson(); return this->m_networkAccessManager->post(request, bytes); } - -void UtilsNetworking::webResponse(QNetworkReply *reply) { - QByteArray data = reply->readAll(); - QString err; - if (reply->error()) { - err = reply->errorString(); - qCritical() << err; - qCritical() << data; - if (!data.isEmpty()) - err += QString("%1 %2").arg(err).arg(Utils::barrayToString(data)); - } - reply->deleteLater(); - - if(!err.isEmpty()) - emit webErrorReceived(err); - else - emit webReceived(data); -} - -QString UtilsNetworking::validateJSON(QNetworkReply *reply){ - QList headerList = reply->rawHeaderList(); - QByteArray headerJson = reply->rawHeader("Content-Type"); - if(headerJson.length() <= 15) - return "Bad Content-Type"; - QString headerJsonStr = QTextCodec::codecForMib(106)->toUnicode(headerJson); - int _contentType = headerList.indexOf("Content-Type"); - if (_contentType < 0 || !headerJsonStr.startsWith("application/json")) - return "Bad Content-Type"; - QByteArray data = reply->readAll(); - if(!Utils::validateJSON(data)) - return "Bad or empty JSON"; - return "OK"; -} diff --git a/src/utils/networking.h b/src/utils/networking.h index 2d163e3..9452c8a 100644 --- a/src/utils/networking.h +++ b/src/utils/networking.h @@ -11,8 +11,6 @@ #include "utils/utils.h" -class CCSEntry; - class UtilsNetworking : public QObject { Q_OBJECT @@ -20,18 +18,10 @@ Q_OBJECT public: explicit UtilsNetworking(QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr); - void get(const QString &url); + QNetworkReply* get(const QString &url); QNetworkReply* getJson(const QString &url); QNetworkReply* postJson(const QString &url, const QJsonObject &data); void setUserAgent(const QString &userAgent); - static QString validateJSON(QNetworkReply *reply); - -private slots: - void webResponse(QNetworkReply *reply); - -signals: - void webErrorReceived(QString msg); - void webReceived(QByteArray data); private: QString m_userAgent = "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0"; diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index 45f8281..2d7b08c 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -2,20 +2,22 @@ // Copyright (c) 2020-2021, The Monero Project. #include -#include #include "nodes.h" #include "utils/utils.h" +#include "utils/WebsocketClient.h" #include "appcontext.h" -Nodes::Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObject *parent) : - QObject(parent), - m_ctx(ctx), - m_networkAccessManager(networkAccessManager), - m_connection(FeatherNode()), - modelWebsocket(new NodeModel(NodeSource::websocket, this)), - modelCustom(new NodeModel(NodeSource::custom, this)) { + +Nodes::Nodes(AppContext *ctx, QObject *parent) + : QObject(parent) + , m_ctx(ctx) + , m_connection(FeatherNode()) + , modelWebsocket(new NodeModel(NodeSource::websocket, this)) + , modelCustom(new NodeModel(NodeSource::custom, this)) +{ this->loadConfig(); + connect(m_ctx, &AppContext::walletRefreshed, this, &Nodes::onWalletRefreshed); } void Nodes::loadConfig() { @@ -71,10 +73,6 @@ void Nodes::loadConfig() { m_websocketNodes.append(wsNode); } - if (!obj.contains("source")) - obj["source"] = NodeSource::websocket; - m_source = static_cast(obj.value("source").toInt()); - if (m_websocketNodes.count() > 0) { qDebug() << QString("Loaded %1 cached websocket nodes from config").arg(m_websocketNodes.count()); } @@ -98,11 +96,8 @@ void Nodes::loadConfig() { if (nodes_obj.contains(netKey)) { QJsonArray nodes_list; - if (m_ctx->isTails || m_ctx->isWhonix || m_ctx->isTorSocks) { - nodes_list = nodes_json[netKey].toObject()["tor"].toArray(); - } else { - nodes_list = nodes_json[netKey].toObject()["clearnet"].toArray(); - } + nodes_list = nodes_json[netKey].toObject()["tor"].toArray(); + nodes_list.append(nodes_list = nodes_json[netKey].toObject()["clearnet"].toArray()); for (auto node: nodes_list) { auto wsNode = FeatherNode(node.toString()); wsNode.custom = false; @@ -135,19 +130,29 @@ void Nodes::connectToNode() { } void Nodes::connectToNode(const FeatherNode &node) { - if (node.address.isEmpty()) + if (!node.isValid()) return; - emit updateStatus(QString("Connecting to %1").arg(node.address)); - qInfo() << QString("Attempting to connect to %1 (%2)").arg(node.address).arg(node.custom ? "custom" : "ws"); + emit updateStatus(QString("Connecting to %1").arg(node.toAddress())); + qInfo() << QString("Attempting to connect to %1 (%2)").arg(node.toAddress()).arg(node.custom ? "custom" : "ws"); - if (!node.username.isEmpty() && !node.password.isEmpty()) - m_ctx->currentWallet->setDaemonLogin(node.username, node.password); + if (!node.url.userName().isEmpty() && !node.url.password().isEmpty()) + m_ctx->currentWallet->setDaemonLogin(node.url.userName(), node.url.password()); // Don't use SSL over Tor - m_ctx->currentWallet->setUseSSL(!node.tor); + m_ctx->currentWallet->setUseSSL(!node.isOnion()); - m_ctx->currentWallet->initAsync(node.address, true, 0, false, false, 0); + QString proxyAddress; + if (useTorProxy(node)) { + if (!torManager()->isLocalTor()) { + proxyAddress = QString("%1:%2").arg(torManager()->featherTorHost, QString::number(torManager()->featherTorPort)); + } else { + proxyAddress = QString("%1:%2").arg(config()->get(Config::socks5Host).toString(), + config()->get(Config::socks5Port).toString()); + } + } + + m_ctx->currentWallet->initAsync(node.toAddress(), true, 0, false, false, 0, proxyAddress); m_connection = node; m_connection.isActive = false; @@ -165,17 +170,16 @@ void Nodes::autoConnect(bool forceReconnect) { Wallet::ConnectionStatus status = m_ctx->currentWallet->connectionStatus(); bool wsMode = (this->source() == NodeSource::websocket); - auto nodes = wsMode ? m_customNodes : m_websocketNodes; - if (wsMode && !m_wsNodesReceived && m_websocketNodes.count() == 0) { + if (wsMode && !m_wsNodesReceived && websocketNodes().count() == 0) { // this situation should rarely occur due to the usage of the websocket node cache on startup. qInfo() << "Feather is in websocket connection mode but was not able to receive any nodes (yet)."; return; } if (status == Wallet::ConnectionStatus_Disconnected || forceReconnect) { - if (!m_connection.address.isEmpty() && !forceReconnect) { - m_recentFailures << m_connection.address; + if (m_connection.isValid() && !forceReconnect) { + m_recentFailures << m_connection.toAddress(); } // try a connect @@ -184,7 +188,7 @@ void Nodes::autoConnect(bool forceReconnect) { return; } else if ((status == Wallet::ConnectionStatus_Synchronizing || status == Wallet::ConnectionStatus_Synchronized) && m_connection.isConnecting) { - qInfo() << QString("Node connected to %1").arg(m_connection.address); + qInfo() << QString("Node connected to %1").arg(m_connection.toAddress()); // set current connection object m_connection.isConnecting = false; @@ -204,7 +208,7 @@ FeatherNode Nodes::pickEligibleNode() { // Pick a node at random to connect to auto rtn = FeatherNode(); auto wsMode = (this->source() == NodeSource::websocket); - auto nodes = wsMode ? m_websocketNodes : m_customNodes; + auto nodes = wsMode ? websocketNodes() : m_customNodes; if (nodes.count() == 0) { if (wsMode) @@ -243,7 +247,7 @@ FeatherNode Nodes::pickEligibleNode() { } // Don't connect to nodes that failed to connect recently - if (m_recentFailures.contains(node.address)) { + if (m_recentFailures.contains(node.toAddress())) { continue; } @@ -258,18 +262,19 @@ FeatherNode Nodes::pickEligibleNode() { return rtn; } -void Nodes::onWSNodesReceived(const QList> &nodes) { +void Nodes::onWSNodesReceived(QList &nodes) { m_websocketNodes.clear(); + m_wsNodesReceived = true; for (auto &node: nodes) { - if (m_connection == *node) { + if (m_connection == node) { if (m_connection.isActive) - node->isActive = true; + node.isActive = true; else if (m_connection.isConnecting) - node->isConnecting = true; + node.isConnecting = true; } - m_websocketNodes.push_back(*node); + m_websocketNodes.push_back(node); } // cache into config @@ -277,7 +282,7 @@ void Nodes::onWSNodesReceived(const QList> &nodes) { auto obj = m_configJson.value(key).toObject(); auto ws = QJsonArray(); for (auto const &node: m_websocketNodes) - ws.push_back(node.address); + ws.push_back(node.toAddress()); obj["ws"] = ws; m_configJson[key] = obj; @@ -287,20 +292,9 @@ void Nodes::onWSNodesReceived(const QList> &nodes) { } void Nodes::onNodeSourceChanged(NodeSource nodeSource) { - if (nodeSource == this->source()) - return; - m_source = nodeSource; - - auto key = QString::number(m_ctx->networkType); - auto obj = m_configJson.value(key).toObject(); - obj["source"] = nodeSource; - - m_configJson[key] = obj; - this->writeConfig(); this->resetLocalState(); this->updateModels(); - - this->autoConnect(true); + this->connectToNode(); } void Nodes::setCustomNodes(const QList &nodes) { @@ -310,8 +304,8 @@ void Nodes::setCustomNodes(const QList &nodes) { QStringList nodesList; for (auto const &node: nodes) { - if (nodesList.contains(node.full)) continue; - nodesList.append(node.full); + if (nodesList.contains(node.toAddress())) continue; + nodesList.append(node.toAddress()); m_customNodes.append(node); } @@ -323,14 +317,48 @@ void Nodes::setCustomNodes(const QList &nodes) { this->updateModels(); } +void Nodes::onWalletRefreshed() { + if (config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptInitSync) { + if (m_connection.isLocal()) + return; + if (TailsOS::detect() || WhonixOS::detect()) + return; + this->autoConnect(true); + } +} + +bool Nodes::useOnionNodes() { + if (TailsOS::detect() || WhonixOS::detect()) { + return true; + } + auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); + if (privacyLevel == Config::allTor || (privacyLevel == Config::allTorExceptInitSync && m_ctx->refreshed)) { + return true; + } + return false; +} + +bool Nodes::useTorProxy(const FeatherNode &node) { + if (node.isLocal()) { + return false; + } + + if (Utils::isTorsocks()) { + return false; + } + + return this->useOnionNodes(); +} + void Nodes::updateModels() { this->modelCustom->updateNodes(m_customNodes); - this->modelWebsocket->updateNodes(m_websocketNodes); + + this->modelWebsocket->updateNodes(this->websocketNodes()); } void Nodes::resetLocalState() { - auto resetState = [this](QList *model){ - for (auto&& node: *model) { + auto resetState = [this](QList &model){ + for (auto &node: model) { node.isConnecting = false; node.isActive = false; @@ -341,14 +369,12 @@ void Nodes::resetLocalState() { } }; - resetState(&m_customNodes); - resetState(&m_websocketNodes); + resetState(m_customNodes); + resetState(m_websocketNodes); } void Nodes::exhausted() { - bool wsMode = (this->source() == NodeSource::websocket); - - if (wsMode) + if (this->source() == NodeSource::websocket) this->WSNodeExhaustedWarning(); else this->nodeExhaustedWarning(); @@ -377,7 +403,26 @@ QList Nodes::customNodes() { } QList Nodes::websocketNodes() { - return m_websocketNodes; + bool onionNode = this->useOnionNodes(); + + QList nodes; + for (const auto &node : m_websocketNodes) { + if (onionNode && !node.isOnion()) { + continue; + } + + if (!onionNode && node.isOnion()) { + continue; + } + + nodes.push_back(node); + } + + return nodes; +} + +void Nodes::onTorSettingsChanged() { + this->autoConnect(true); } FeatherNode Nodes::connection() { @@ -385,7 +430,7 @@ FeatherNode Nodes::connection() { } NodeSource Nodes::source() { - return m_source; + return static_cast(config()->get(Config::nodeSource).toInt()); } int Nodes::modeHeight(const QList &nodes) { diff --git a/src/utils/nodes.h b/src/utils/nodes.h index cc87b97..a70881d 100644 --- a/src/utils/nodes.h +++ b/src/utils/nodes.h @@ -21,60 +21,68 @@ enum NodeSource { }; struct FeatherNode { - explicit FeatherNode(QString _address = "", int height = 0, int target_height = 0, bool online = false) - : height(height), target_height(target_height), online(online){ - // wonky ipv4/host parsing, should be fine(tm)(c). - if(_address.isEmpty()) return; - if(_address.contains("https://")) { - this->isHttps = true; - } - _address = _address.replace("https://", ""); - _address = _address.replace("http://", ""); - if(_address.contains("@")){ // authentication, user/pass - const auto spl = _address.split("@"); - const auto &creds = spl.at(0); - if(creds.contains(":")) { - const auto _spl = creds.split(":"); - this->username = _spl.at(0).trimmed().replace(" ", ""); - this->password = _spl.at(1).trimmed().replace(" ", ""); - } - _address = spl.at(1); - } - if(!_address.contains(":")) - _address += ":18081"; - this->address = _address; - if(this->address.contains(".onion")) - tor = true; - this->full = this->generateFull(); + explicit FeatherNode(QString address = "", int height = 0, int target_height = 0, bool online = false) + : height(height) + , target_height(target_height) + , online(online) + { + if (address.isEmpty()) + return; + + address.remove("https://"); // todo: regex + if (!address.startsWith("http://")) + address.prepend("http://"); + + url = QUrl(address); + + if (!url.isValid()) + return; + + if (url.port() == -1) + url.setPort(18081); }; - QString address; - QString full; int height; int target_height; bool online = false; - QString username; - QString password; bool cached = false; bool custom = false; - bool tor = false; bool isConnecting = false; bool isActive = false; - bool isHttps = false; + QUrl url; - QString generateFull() { - QString auth; - if(!this->username.isEmpty() && !this->password.isEmpty()) - auth = QString("%1:%2@").arg(this->username).arg(this->password); - return QString("%1%2").arg(auth).arg(this->address); + bool isValid() const { + return url.isValid(); } - QString as_url() const { - return QString("%1://%2").arg(this->isHttps ? "https": "http",this->full); + bool isLocal() const { + return (url.host() == "127.0.0.1" || url.host() == "localhost"); + } + + bool isOnion() const { + return url.host().endsWith(".onion"); + } + + QString toAddress() const { + return QString("%1:%2").arg(url.host(), QString::number(url.port())); + } + + QString toFullAddress() const { + if (!url.userName().isEmpty() && !url.password().isEmpty()) + return QString("%1:%2@%3:%4").arg(url.userName(), url.password(), url.host(), QString::number(url.port())); + + return toAddress(); + } + + QString toURL() const { + QUrl withScheme(url); + withScheme.setScheme("http"); + + return withScheme.toString(QUrl::RemoveUserInfo | QUrl::RemovePath); } bool operator == (const FeatherNode &other) const { - return this->full == other.full; + return this->url == other.url; } }; @@ -82,7 +90,7 @@ class Nodes : public QObject { Q_OBJECT public: - explicit Nodes(AppContext *ctx, QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr); + explicit Nodes(AppContext *ctx, QObject *parent = nullptr); void loadConfig(); void writeConfig(); @@ -98,20 +106,23 @@ public: public slots: void connectToNode(); void connectToNode(const FeatherNode &node); - void onWSNodesReceived(const QList>& nodes); + void onWSNodesReceived(QList& nodes); void onNodeSourceChanged(NodeSource nodeSource); void setCustomNodes(const QList& nodes); void autoConnect(bool forceReconnect = false); + void onTorSettingsChanged(); + signals: void WSNodeExhausted(); void nodeExhausted(); void updateStatus(const QString &msg); +private slots: + void onWalletRefreshed(); + private: AppContext *m_ctx = nullptr; - NodeSource m_source = NodeSource::websocket; - QNetworkAccessManager *m_networkAccessManager = nullptr; QJsonObject m_configJson; QStringList m_recentFailures; @@ -128,6 +139,9 @@ private: FeatherNode pickEligibleNode(); + bool useOnionNodes(); + bool useTorProxy(const FeatherNode &node); + void updateModels(); void resetLocalState(); void exhausted(); diff --git a/src/utils/tails.cpp b/src/utils/tails.cpp index 4f8849b..80c5b37 100644 --- a/src/utils/tails.cpp +++ b/src/utils/tails.cpp @@ -7,10 +7,16 @@ #include "tails.h" #include "utils.h" +bool TailsOS::detected = false; +bool TailsOS::isTails = false; const QString TailsOS::tailsPathData = QString("/live/persistence/TailsData_unlocked/"); bool TailsOS::detect() { + if (detected) { + return TailsOS::isTails; + } + if (!Utils::fileExists("/etc/os-release")) return false; @@ -22,6 +28,9 @@ bool TailsOS::detect() if (matched) qDebug() << "Tails OS detected"; + TailsOS::detected = true; + TailsOS::isTails = matched; + return matched; } diff --git a/src/utils/tails.h b/src/utils/tails.h index 701d660..eaa932f 100644 --- a/src/utils/tails.h +++ b/src/utils/tails.h @@ -21,6 +21,9 @@ public: static bool usePersistence; static bool rememberChoice; static const QString tailsPathData; + + static bool isTails; + static bool detected; }; #endif // TAILSOS_H diff --git a/src/utils/tor.h b/src/utils/tor.h deleted file mode 100644 index 22ab243..0000000 --- a/src/utils/tor.h +++ /dev/null @@ -1,115 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// Copyright (c) 2020-2021, The Monero Project. - -#ifndef FEATHER_TOR_H -#define FEATHER_TOR_H - -#include -#include -#include -#include -#include -#include "utils/childproc.h" - -struct TorVersion -{ - explicit TorVersion(int major=0, int minor=0, int patch=0, int release=0) - : patch(patch), release(release) - { - this->major = major; - this->minor = minor; - } - - friend bool operator== (const TorVersion &v1, const TorVersion &v2) { - return (v1.major == v2.major && - v1.minor == v2.minor && - v1.patch == v2.patch && - v1.release == v2.release); - } - - friend bool operator!= (const TorVersion &v1, const TorVersion &v2) { - return !(v1 == v2); - } - - friend bool operator> (const TorVersion &v1, const TorVersion &v2) { - if (v1.major != v2.major) - return v1.major > v2.major; - if (v1.minor != v2.minor) - return v1.minor > v2.minor; - if (v1.patch != v2.patch) - return v1.patch > v2.patch; - if (v1.release != v2.release) - return v1.release > v2.release; - return false; - } - - friend bool operator< (const TorVersion &v1, const TorVersion &v2) { - if (v1 == v2) - return false; - return !(v1 > v2); - } - - QString toString() { - return QString("%1.%2.%3.%4").arg(QString::number(major), QString::number(minor), - QString::number(patch), QString::number(release)); - } - - static bool isValid(const TorVersion &v) { - return v != TorVersion(); - } - - int major; - int minor; - int patch; - int release; -}; - -class Tor : public QObject -{ -Q_OBJECT - -public: - explicit Tor(AppContext *ctx, QObject *parent = nullptr); - - void start(); - void stop(); - bool unpackBins(); - TorVersion getVersion(const QString &fileName); - TorVersion stringToVersion(const QString &version); - - bool torConnected = false; - bool localTor = false; - QString torDir; - QString torPath; - QString torDataPath; - - static QString torHost; - static quint16 torPort; - - QString torLogs; - QString errorMsg = ""; - -signals: - void connectionStateChanged(bool connected); - void startupFailure(QString reason); - void logsUpdated(); - -private slots: - void stateChanged(QProcess::ProcessState); - void handleProcessOutput(); - void handleProcessError(QProcess::ProcessError error); - void checkConnection(); - -private: - void setConnectionState(bool connected); - - ChildProcess m_process; - AppContext *m_ctx; - int m_restarts = 0; - bool m_stopRetries = false; - QTimer *m_checkConnectionTimer; -}; - -class AppContext; // forward declaration - -#endif //FEATHER_TOR_H \ No newline at end of file diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index 6464d70..97e38c9 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -17,9 +17,23 @@ #include "utils/ColorScheme.h" #include "globals.h" -// Application log for current session -QVector applicationLog = QVector(); // todo: replace with ring buffer -QMutex logMutex; +QByteArray Utils::fileGetContents(const QString &path) +{ + QFile file(path); + if (!file.open(QFile::ReadOnly)) + { + throw std::runtime_error(QString("failed to open %1").arg(path).toStdString()); + } + + QByteArray data; + data.resize(file.size()); + if (file.read(data.data(), data.size()) != data.size()) + { + throw std::runtime_error(QString("failed to read %1").arg(path).toStdString()); + } + + return data; +} bool Utils::fileExists(const QString &path) { QFileInfo check_file(path); @@ -110,12 +124,6 @@ void Utils::applicationLogHandler(QtMsgType type, const QMessageLogContext &cont auto message = logMessage(type, line, fn); - { - QMutexLocker locker(&logMutex); - applicationLog.append(message); - } - - //emit applicationLogUpdated(message); } diff --git a/src/utils/utils.h b/src/utils/utils.h index 05ca74a..e72a8e7 100644 --- a/src/utils/utils.h +++ b/src/utils/utils.h @@ -42,6 +42,7 @@ class Utils { public: + static QByteArray fileGetContents(const QString &path); static bool portOpen(const QString &hostname, quint16 port); static bool fileExists(const QString &path); static QByteArray fileOpen(const QString &path); diff --git a/src/utils/wsclient.cpp b/src/utils/wsclient.cpp index 478baa7..7ef3aa9 100644 --- a/src/utils/wsclient.cpp +++ b/src/utils/wsclient.cpp @@ -1,59 +1,60 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020-2021, The Monero Project. -#include #include -#include -#include +#include #include "wsclient.h" -#include "appcontext.h" +#include "utils/utils.h" -WSClient::WSClient(AppContext *ctx, const QUrl &url, QObject *parent) : - QObject(parent), - url(url), - m_ctx(ctx) { - connect(&this->webSocket, &QWebSocket::binaryMessageReceived, this, &WSClient::onbinaryMessageReceived); - connect(&this->webSocket, &QWebSocket::connected, this, &WSClient::onConnected); - connect(&this->webSocket, &QWebSocket::disconnected, this, &WSClient::closed); - connect(&this->webSocket, QOverload::of(&QWebSocket::error), this, &WSClient::onError); - - m_tor = url.host().endsWith(".onion"); +WSClient::WSClient(QUrl url, QObject *parent) + : QObject(parent) + , m_url(std::move(url)) +{ + connect(&webSocket, &QWebSocket::binaryMessageReceived, this, &WSClient::onbinaryMessageReceived); + connect(&webSocket, &QWebSocket::connected, this, &WSClient::onConnected); + connect(&webSocket, &QWebSocket::disconnected, this, &WSClient::closed); + connect(&webSocket, QOverload::of(&QWebSocket::error), this, &WSClient::onError); + connect(&m_connectionTimer, &QTimer::timeout, this, &WSClient::checkConnection); // Keep websocket connection alive connect(&m_pingTimer, &QTimer::timeout, [this]{ - if (this->webSocket.state() == QAbstractSocket::ConnectedState) - this->webSocket.ping(); + if (webSocket.state() == QAbstractSocket::ConnectedState) + webSocket.ping(); }); m_pingTimer.setInterval(30 * 1000); m_pingTimer.start(); } void WSClient::sendMsg(const QByteArray &data) { - auto state = this->webSocket.state(); - if(state == QAbstractSocket::ConnectedState) - this->webSocket.sendBinaryMessage(data); + if (webSocket.state() == QAbstractSocket::ConnectedState) + webSocket.sendBinaryMessage(data); +} + +void WSClient::onToggleConnect(bool connect) { + m_connect = connect; + if (m_connect) + checkConnection(); } void WSClient::start() { // connect & reconnect on errors/close #ifdef QT_DEBUG - qDebug() << "WebSocket connect:" << url.url(); + qDebug() << "WebSocket connect:" << m_url.url(); #endif - if((m_tor && this->m_ctx->tor->torConnected) || !m_tor) - this->webSocket.open(QUrl(this->url)); - if(!this->m_connectionTimer.isActive()) { - connect(&this->m_connectionTimer, &QTimer::timeout, this, &WSClient::checkConnection); - this->m_connectionTimer.start(2000); + if (m_connect) + webSocket.open(m_url); + + if (!m_connectionTimer.isActive()) { + m_connectionTimer.start(2000); } } void WSClient::checkConnection() { - if(m_tor && !this->m_ctx->tor->torConnected) + if (!m_connect) return; - auto state = this->webSocket.state(); - if(state == QAbstractSocket::UnconnectedState) { + if (webSocket.state() == QAbstractSocket::UnconnectedState) { #ifdef QT_DEBUG qDebug() << "WebSocket reconnect"; #endif @@ -70,9 +71,9 @@ void WSClient::onConnected() { void WSClient::onError(QAbstractSocket::SocketError error) { qCritical() << "WebSocket error: " << error; - auto state = this->webSocket.state(); - if(state == QAbstractSocket::ConnectedState || state == QAbstractSocket::ConnectingState) - this->webSocket.abort(); + auto state = webSocket.state(); + if (state == QAbstractSocket::ConnectedState || state == QAbstractSocket::ConnectingState) + webSocket.abort(); } void WSClient::onbinaryMessageReceived(const QByteArray &message) { diff --git a/src/utils/wsclient.h b/src/utils/wsclient.h index 0192b53..033679e 100644 --- a/src/utils/wsclient.h +++ b/src/utils/wsclient.h @@ -8,17 +8,18 @@ #include #include -class AppContext; class WSClient : public QObject { Q_OBJECT public: - explicit WSClient(AppContext *ctx, const QUrl &url, QObject *parent = nullptr); + explicit WSClient(QUrl url, QObject *parent = nullptr); void start(); void sendMsg(const QByteArray &data); QWebSocket webSocket; - QUrl url; + +public slots: + void onToggleConnect(bool connect); signals: void closed(); @@ -32,10 +33,10 @@ private slots: void onError(QAbstractSocket::SocketError error); private: + bool m_connect = false; + QUrl m_url; QTimer m_connectionTimer; QTimer m_pingTimer; - AppContext *m_ctx; - bool m_tor = true; }; #endif // ECHOCLIENT_H \ No newline at end of file diff --git a/src/utils/xmrig.cpp b/src/utils/xmrig.cpp index f6eb5e4..6d0d9c4 100644 --- a/src/utils/xmrig.cpp +++ b/src/utils/xmrig.cpp @@ -9,11 +9,11 @@ #include "utils/xmrig.h" #include "appcontext.h" -XmRig::XmRig(const QString &configDir, QObject *parent) : QObject(parent) { +XmRig::XmRig(const QString &configDir, QObject *parent) + : QObject(parent) +{ this->rigDir = QDir(configDir).filePath("xmrig"); -} -void XmRig::prepare() { m_process.setProcessChannelMode(QProcess::MergedChannels); connect(&m_process, &QProcess::readyReadStandardOutput, this, &XmRig::handleProcessOutput); connect(&m_process, &QProcess::errorOccurred, this, &XmRig::handleProcessError); @@ -30,12 +30,9 @@ void XmRig::stop() { } } -void XmRig::start(const QString &path, - int threads, - const QString &address, - const QString &username, - const QString &password, - bool tor, bool tls) { +void XmRig::start(const QString &path, int threads, const QString &address, const QString &username, + const QString &password, bool tor, bool tls) +{ auto state = m_process.state(); if (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting) { emit error("Can't start XMRig, already running or starting"); @@ -60,8 +57,16 @@ void XmRig::start(const QString &path, arguments << "-p" << password; arguments << "--no-color"; arguments << "-t" << QString::number(threads); - if(tor) - arguments << "-x" << QString("%1:%2").arg(Tor::torHost).arg(Tor::torPort); + if (tor) { + QString host = config()->get(Config::socks5Host).toString(); + QString port = config()->get(Config::socks5Port).toString(); + if (!torManager()->isLocalTor()) { + host = torManager()->featherTorHost; + port = QString::number(torManager()->featherTorPort); + } + arguments << "-x" << QString("%1:%2").arg(host, port); + } + if(tls) arguments << "--tls"; arguments << "--donate-level" << "1"; diff --git a/src/utils/xmrig.h b/src/utils/xmrig.h index 7d6afc2..2d89abc 100644 --- a/src/utils/xmrig.h +++ b/src/utils/xmrig.h @@ -20,7 +20,6 @@ Q_OBJECT public: explicit XmRig(const QString &configDir, QObject *parent = nullptr); - void prepare(); void start(const QString &path, int threads, const QString &address, const QString &username, const QString &password, bool tor = false, bool tls = true); void stop(); diff --git a/src/widgets/LocalMoneroWidget.cpp b/src/widgets/LocalMoneroWidget.cpp new file mode 100644 index 0000000..e8565c0 --- /dev/null +++ b/src/widgets/LocalMoneroWidget.cpp @@ -0,0 +1,209 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "LocalMoneroWidget.h" +#include "ui_LocalMoneroWidget.h" +#include "utils/ColorScheme.h" +#include "utils/Icons.h" +#include "utils/NetworkManager.h" +#include "utils/WebsocketNotifier.h" +#include "dialog/LocalMoneroInfoDialog.h" + +#include +#include + +LocalMoneroWidget::LocalMoneroWidget(QWidget *parent, AppContext *ctx) + : QWidget(parent) + , ui(new Ui::LocalMoneroWidget) + , m_ctx(ctx) +{ + ui->setupUi(this); + +// this->adjustSize(); + + QPixmap logo(":/assets/images/localMonero_logo.png"); + ui->logo->setPixmap(logo.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + + ui->combo_currency->addItem(config()->get(Config::preferredFiatCurrency).toString()); + + m_network = new UtilsNetworking(getNetworkTor(), this); + m_api = new LocalMoneroApi(this, m_network); + + m_model = new LocalMoneroModel(this); + ui->treeView->setModel(m_model); + + ui->treeView->header()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->treeView->header()->setSectionResizeMode(LocalMoneroModel::PaymentMethodDetail, QHeaderView::Stretch); + ui->treeView->header()->setStretchLastSection(false); + + connect(ui->treeView, &QTreeView::doubleClicked, this, &LocalMoneroWidget::viewOfferDetails); + connect(ui->treeView, &QTreeView::customContextMenuRequested, this, &LocalMoneroWidget::showContextMenu); + + connect(ui->btn_search, &QPushButton::clicked, this, &LocalMoneroWidget::onSearchClicked); + connect(ui->btn_signUp, &QPushButton::clicked, this, &LocalMoneroWidget::onSignUpClicked); + connect(m_api, &LocalMoneroApi::ApiResponse, this, &LocalMoneroWidget::onApiResponse); + connect(ui->btn_loadMore, &QPushButton::clicked, this, &LocalMoneroWidget::onLoadMore); + + connect(websocketNotifier(), &WebsocketNotifier::LocalMoneroCountriesReceived, this, &LocalMoneroWidget::onWsCountriesReceived); + connect(websocketNotifier(), &WebsocketNotifier::LocalMoneroCurrenciesReceived, this, &LocalMoneroWidget::onWsCurrenciesReceived); + connect(websocketNotifier(), &WebsocketNotifier::LocalMoneroPaymentMethodsReceived, this, &LocalMoneroWidget::onWsPaymentMethodsReceived); + + connect(ui->combo_currency, QOverload::of(&QComboBox::currentIndexChanged), this, &LocalMoneroWidget::updatePaymentMethods); + + ui->frame_loadMore->hide(); + + this->skinChanged(); +} + +void LocalMoneroWidget::skinChanged() { + if (ColorScheme::hasDarkBackground(this)) { + ui->radio_buy->setIcon(icons()->icon("localMonero_buy_white.svg")); + ui->radio_sell->setIcon(icons()->icon("localMonero_sell_white.svg")); + } else { + ui->radio_buy->setIcon(icons()->icon("localMonero_buy.svg")); + ui->radio_sell->setIcon(icons()->icon("localMonero_sell.svg")); + } +} + +void LocalMoneroWidget::onSearchClicked() { + m_model->clearData(); + m_currentPage = 0; + + this->searchOffers(); +} + +void LocalMoneroWidget::onSignUpClicked() { + QString signupUrl = QString("%1/signup").arg(config()->get(Config::localMoneroFrontend).toString()); + Utils::externalLinkWarning(this, signupUrl); +} + +void LocalMoneroWidget::onApiResponse(const LocalMoneroApi::LocalMoneroResponse &resp) { + qDebug() << "We got a response"; + + if (!resp.ok) { + QMessageBox::warning(this, "LocalMonero error", QString("Request failed:\n\n%1").arg(resp.message)); + return; + } + + if (resp.endpoint == LocalMoneroApi::BUY_MONERO_ONLINE + || resp.endpoint == LocalMoneroApi::SELL_MONERO_ONLINE) + { + bool hasNextPage = resp.obj["pagination"].toObject().contains("next"); + ui->frame_loadMore->setVisible(hasNextPage); + + m_model->addData(resp.obj["data"].toObject()["ad_list"].toArray()); + } + else if (resp.endpoint == LocalMoneroApi::PAYMENT_METHODS) { + m_model->setPaymentMethods(resp.obj["data"].toObject()["methods"].toObject()); + } +} + +void LocalMoneroWidget::onLoadMore() { + m_currentPage += 1; + this->searchOffers(m_currentPage); +} + +void LocalMoneroWidget::onWsCountriesReceived(const QJsonArray &countries) { + ui->combo_country->clear(); + ui->combo_country->addItem("Any country"); + for (const auto country : countries) { + ui->combo_country->addItem(country[0].toString(), country[1].toString()); + } +} + +void LocalMoneroWidget::onWsCurrenciesReceived(const QJsonArray ¤cies) { + QString currentText = ui->combo_currency->currentText(); + + ui->combo_currency->clear(); + for (const auto currency : currencies) { + ui->combo_currency->addItem(currency.toString()); + } + + // restore previous selection + int index = ui->combo_currency->findText(currentText); + ui->combo_currency->setCurrentIndex(index); +} + +void LocalMoneroWidget::onWsPaymentMethodsReceived(const QJsonObject &payment_methods) { + m_paymentMethods = payment_methods; + m_model->setPaymentMethods(payment_methods); + this->updatePaymentMethods(); +} + +void LocalMoneroWidget::searchOffers(int page) { + QString amount = ui->line_amount->text(); + QString currencyCode = ui->combo_currency->currentText(); + QString countryCode = ui->combo_country->currentData().toString(); + QString paymentMethod = ui->combo_paymentMethod->currentData().toString(); + + if (ui->radio_buy->isChecked()) + m_api->buyMoneroOnline(currencyCode, countryCode, paymentMethod, amount, page); + else if (ui->radio_sell->isChecked()) + m_api->sellMoneroOnline(currencyCode, countryCode, paymentMethod, amount, page); +} + +LocalMoneroWidget::~LocalMoneroWidget() { + delete ui; +} + +void LocalMoneroWidget::showContextMenu(const QPoint &point) { + QModelIndex index = ui->treeView->indexAt(point); + if (!index.isValid()) { + return; + } + + QMenu menu(this); + menu.addAction("Go to offer", this, &LocalMoneroWidget::openOfferUrl); + menu.addAction("View offer details", this, &LocalMoneroWidget::viewOfferDetails); + menu.exec(ui->treeView->viewport()->mapToGlobal(point)); +} + +void LocalMoneroWidget::openOfferUrl() { + QModelIndex index = ui->treeView->currentIndex(); + if (!index.isValid()) { + return; + } + + QJsonObject offerData = m_model->getOffer(index.row()); + QString frontend = config()->get(Config::localMoneroFrontend).toString(); + + QString offerUrl = QString("%1/ad/%2").arg(frontend, offerData["data"].toObject()["ad_id"].toString()); + + Utils::externalLinkWarning(this, offerUrl); +} + +void LocalMoneroWidget::viewOfferDetails() { + QModelIndex index = ui->treeView->currentIndex(); + if (!index.isValid()) { + return; + } + + QJsonObject offerData = m_model->getOffer(index.row()); + QString details = offerData["data"].toObject()["msg"].toString(); + details.remove("*"); + + if (details.isEmpty()) { + details = "No details."; + } + + LocalMoneroInfoDialog dialog(this, m_model, index.row()); + dialog.exec(); +} + +void LocalMoneroWidget::updatePaymentMethods() { + QString currency = ui->combo_currency->currentText().toUpper(); + + ui->combo_paymentMethod->clear(); + ui->combo_paymentMethod->addItem("Any payment method"); + + for (const auto &payment_method : m_paymentMethods.keys()) { + auto pm = m_paymentMethods[payment_method].toObject(); + + if (pm["currencies"].toArray().contains(currency)) { + QString name = pm["name"].toString(); + if (name.isEmpty()) + name = payment_method; + ui->combo_paymentMethod->addItem(name, payment_method); + } + } +} \ No newline at end of file diff --git a/src/widgets/LocalMoneroWidget.h b/src/widgets/LocalMoneroWidget.h new file mode 100644 index 0000000..1bace38 --- /dev/null +++ b/src/widgets/LocalMoneroWidget.h @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_LOCALMONEROWIDGET_H +#define FEATHER_LOCALMONEROWIDGET_H + +#include + +#include "appcontext.h" +#include "api/LocalMoneroApi.h" +#include "model/LocalMoneroModel.h" + +namespace Ui { + class LocalMoneroWidget; +} + +class LocalMoneroWidget : public QWidget +{ +Q_OBJECT + +public: + explicit LocalMoneroWidget(QWidget *parent, AppContext *ctx); + ~LocalMoneroWidget() override; + +public slots: + void skinChanged(); + +private slots: + void onSearchClicked(); + void onSignUpClicked(); + void onApiResponse(const LocalMoneroApi::LocalMoneroResponse &resp); + void onLoadMore(); + void onWsCountriesReceived(const QJsonArray &countries); + void onWsCurrenciesReceived(const QJsonArray ¤cies); + void onWsPaymentMethodsReceived(const QJsonObject &payment_methods); + +private: + void searchOffers(int page = 0); + void showContextMenu(const QPoint &point); + void openOfferUrl(); + void viewOfferDetails(); + void updatePaymentMethods(); + + int m_currentPage = 0; + + Ui::LocalMoneroWidget *ui; + + AppContext *m_ctx; + LocalMoneroApi *m_api; + LocalMoneroModel *m_model; + UtilsNetworking *m_network; + QJsonObject m_paymentMethods; +}; + + +#endif //FEATHER_LOCALMONEROWIDGET_H diff --git a/src/widgets/LocalMoneroWidget.ui b/src/widgets/LocalMoneroWidget.ui new file mode 100644 index 0000000..25a8cc7 --- /dev/null +++ b/src/widgets/LocalMoneroWidget.ui @@ -0,0 +1,385 @@ + + + LocalMoneroWidget + + + + 0 + 0 + 1003 + 607 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + logo + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 75 + true + + + + LocalMonero.co + + + Qt::AlignBottom|Qt::AlignHCenter + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 80 + 20 + + + + + + + + + + + 0 + 0 + + + + true + + + + Any payment method + + + + + + + + + 0 + 0 + + + + Amount + + + + + + + + 0 + 0 + + + + true + + + + + + + + 0 + 0 + + + + Sell XMR + + + + :/assets/images/localMonero_sell_white.svg:/assets/images/localMonero_sell_white.svg + + + + + + + Search + + + + :/assets/images/localMonero_search.svg:/assets/images/localMonero_search.svg + + + + + + + + 0 + 0 + + + + Buy XMR + + + + :/assets/images/localMonero_buy_white.svg:/assets/images/localMonero_buy_white.svg + + + true + + + + + + + + 0 + 0 + + + + true + + + + Any country + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Maximum + + + + 80 + 20 + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + + + Buy Monero. + + + Qt::AlignCenter + + + + + + + Sell Monero. + + + Qt::AlignCenter + + + + + + + Cash or online. + + + Qt::AlignCenter + + + + + + + Anywhere. + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Sign up free + + + + :/assets/images/localMonero_register.svg:/assets/images/localMonero_register.svg + + + + + + + + + + + + Qt::CustomContextMenu + + + false + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Load more + + + + + + + + + + radio_buy + radio_sell + line_amount + combo_currency + combo_country + combo_paymentMethod + btn_search + btn_signUp + btn_loadMore + treeView + + + + + + diff --git a/src/widgets/nodewidget.cpp b/src/widgets/nodewidget.cpp index 1f18e89..d082f93 100644 --- a/src/widgets/nodewidget.cpp +++ b/src/widgets/nodewidget.cpp @@ -10,6 +10,7 @@ #include "nodewidget.h" #include "ui_nodewidget.h" #include "mainwindow.h" +#include "utils/Icons.h" NodeWidget::NodeWidget(QWidget *parent) : QWidget(parent) @@ -19,19 +20,18 @@ NodeWidget::NodeWidget(QWidget *parent) connect(ui->btn_add_custom, &QPushButton::clicked, this, &NodeWidget::onCustomAddClicked); - connect(ui->nodeBtnGroup, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *button) { - auto name = button->objectName(); - if (name == "radioButton_websocket") { - emit nodeSourceChanged(NodeSource::websocket); - } else if (name == "radioButton_custom") { - emit nodeSourceChanged(NodeSource::custom); - } + ui->nodeBtnGroup->setId(ui->radioButton_websocket, NodeSource::websocket); + ui->nodeBtnGroup->setId(ui->radioButton_custom, NodeSource::custom); + + connect(ui->nodeBtnGroup, &QButtonGroup::idClicked, [this](int id){ + config()->set(Config::nodeSource, id); + emit nodeSourceChanged(static_cast(id)); }); m_contextActionRemove = new QAction("Remove", this); - m_contextActionConnect = new QAction(QIcon(":/assets/images/connect.svg"), "Connect to node", this); - m_contextActionOpenStatusURL = new QAction(QIcon(":/assets/images/network.png"), "Visit status page", this); - m_contextActionCopy = new QAction(QIcon(":/assets/images/copy.png"), "Copy", this); + m_contextActionConnect = new QAction(icons()->icon("connect.svg"), "Connect to node", this); + m_contextActionOpenStatusURL = new QAction(icons()->icon("network.png"), "Visit status page", this); + m_contextActionCopy = new QAction(icons()->icon("copy.png"), "Copy", this); connect(m_contextActionConnect, &QAction::triggered, this, &NodeWidget::onContextConnect); connect(m_contextActionRemove, &QAction::triggered, this, &NodeWidget::onContextCustomNodeRemove); connect(m_contextActionOpenStatusURL, &QAction::triggered, this, &NodeWidget::onContextStatusURL); @@ -50,7 +50,7 @@ NodeWidget::NodeWidget(QWidget *parent) void NodeWidget::onShowWSContextMenu(const QPoint &pos) { m_activeView = ui->wsView; FeatherNode node = this->selectedNode(); - if (node.full.isEmpty()) return; + if (node.toAddress().isEmpty()) return; this->showContextMenu(pos, node); } @@ -58,7 +58,7 @@ void NodeWidget::onShowWSContextMenu(const QPoint &pos) { void NodeWidget::onShowCustomContextMenu(const QPoint &pos) { m_activeView = ui->customView; FeatherNode node = this->selectedNode(); - if (node.full.isEmpty()) return; + if (node.toAddress().isEmpty()) return; this->showContextMenu(pos, node); } @@ -87,19 +87,19 @@ void NodeWidget::onContextConnect() { m_activeView = ui->wsView; FeatherNode node = this->selectedNode(); - if (!node.full.isEmpty()) + if (!node.toAddress().isEmpty()) emit connectToNode(node); } void NodeWidget::onContextStatusURL() { FeatherNode node = this->selectedNode(); - if (!node.full.isEmpty()) - Utils::externalLinkWarning(this, QString("%1/get_info").arg(node.as_url())); + if (!node.toAddress().isEmpty()) + Utils::externalLinkWarning(this, QString("%1/get_info").arg(node.toURL())); } void NodeWidget::onContextNodeCopy() { FeatherNode node = this->selectedNode(); - Utils::copyToClipboard(node.full); + Utils::copyToClipboard(node.toAddress()); } FeatherNode NodeWidget::selectedNode() { @@ -131,10 +131,10 @@ void NodeWidget::onContextCustomNodeRemove() { void NodeWidget::onCustomAddClicked(){ auto currentNodes = m_ctx->nodes->customNodes(); - auto currentNodesText = QString(""); + QString currentNodesText; - for(auto &entry: currentNodes) - currentNodesText += QString("%1\n").arg(entry.full); + for (auto &entry: currentNodes) + currentNodesText += QString("%1\n").arg(entry.url.toString()); bool ok; QString text = QInputDialog::getMultiLineText(this, "Add custom node(s).", "E.g: user:password@127.0.0.1:18081", currentNodesText, &ok); @@ -160,7 +160,6 @@ void NodeWidget::setupUI(AppContext *ctx) { m_ctx = ctx; auto nodeSource = m_ctx->nodes->source(); - qCritical() << nodeSource; if(nodeSource == NodeSource::websocket){ ui->radioButton_websocket->setChecked(true); diff --git a/src/widgets/tickerwidget.cpp b/src/widgets/tickerwidget.cpp index cb33704..9fa98ca 100644 --- a/src/widgets/tickerwidget.cpp +++ b/src/widgets/tickerwidget.cpp @@ -4,17 +4,18 @@ #include "tickerwidget.h" #include "ui_tickerwidget.h" -#include "mainwindow.h" +#include "globals.h" +#include "utils/AppData.h" -TickerWidget::TickerWidget(QWidget *parent, QString symbol, QString title, bool convertBalance, bool hidePercent) : +TickerWidget::TickerWidget(QWidget *parent, AppContext *ctx, QString symbol, QString title, bool convertBalance, bool hidePercent) : QWidget(parent), ui(new Ui::TickerWidget), + m_ctx(ctx), m_symbol(std::move(symbol)), m_convertBalance(convertBalance), m_hidePercent(hidePercent) { ui->setupUi(this); - m_ctx = MainWindow::getContext(); // default values before API data if (title == "") title = m_symbol; @@ -28,28 +29,30 @@ TickerWidget::TickerWidget(QWidget *parent, QString symbol, QString title, bool ui->tickerPct->setHidden(hidePercent); - connect(AppContext::prices, &Prices::fiatPricesUpdated, this, &TickerWidget::init); - connect(AppContext::prices, &Prices::cryptoPricesUpdated, this, &TickerWidget::init); + connect(&appData()->prices, &Prices::fiatPricesUpdated, this, &TickerWidget::init); + connect(&appData()->prices, &Prices::cryptoPricesUpdated, this, &TickerWidget::init); if (convertBalance) connect(m_ctx, &AppContext::balanceUpdated, this, &TickerWidget::init); } void TickerWidget::init() { - if(!AppContext::prices->markets.count() || !AppContext::prices->rates.count()) + if(!appData()->prices.markets.count() || !appData()->prices.rates.count()) return; QString fiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); - if(!AppContext::prices->rates.contains(fiatCurrency)){ + if(!appData()->prices.rates.contains(fiatCurrency)){ config()->set(Config::preferredFiatCurrency, "USD"); return; } - double amount = m_convertBalance ? AppContext::balance : 1.0; - double conversion = AppContext::prices->convert(m_symbol, fiatCurrency, amount); + double walletBalance = m_ctx->currentWallet ? (m_ctx->currentWallet->balance() / globals::cdiv) : 0; + + double amount = m_convertBalance ? walletBalance : 1.0; + double conversion = appData()->prices.convert(m_symbol, fiatCurrency, amount); if (conversion < 0) return; - auto markets = AppContext::prices->markets; + auto markets = appData()->prices.markets; if(!markets.contains(m_symbol)) return; bool hidePercent = (conversion == 0 || m_hidePercent); diff --git a/src/widgets/tickerwidget.h b/src/widgets/tickerwidget.h index f2a7487..e6464eb 100644 --- a/src/widgets/tickerwidget.h +++ b/src/widgets/tickerwidget.h @@ -17,7 +17,7 @@ class TickerWidget : public QWidget Q_OBJECT public: - explicit TickerWidget(QWidget *parent, QString symbol, QString title = "", bool convertBalance = false, bool hidePercent = false); + explicit TickerWidget(QWidget *parent, AppContext *ctx, QString symbol, QString title = "", bool convertBalance = false, bool hidePercent = false); void setFiatText(QString &fiatCurrency, double amount); void setPctText(QString &text, bool positive); void setFontSizes(); diff --git a/src/widgets/xmrigwidget.cpp b/src/widgets/xmrigwidget.cpp index 7753794..77f53a9 100644 --- a/src/widgets/xmrigwidget.cpp +++ b/src/widgets/xmrigwidget.cpp @@ -10,22 +10,31 @@ #include "xmrigwidget.h" #include "ui_xmrigwidget.h" +#include "utils/Icons.h" -XMRigWidget::XMRigWidget(AppContext *ctx, QWidget *parent) : - QWidget(parent), - ui(new Ui::XMRigWidget), - m_ctx(ctx), - m_model(new QStandardItemModel(this)), - m_contextMenu(new QMenu(this)) +XMRigWidget::XMRigWidget(AppContext *ctx, QWidget *parent) + : QWidget(parent) + , ui(new Ui::XMRigWidget) + , m_ctx(ctx) + , m_XMRig(new XmRig(Config::defaultConfigDir().path())) + , m_model(new QStandardItemModel(this)) + , m_contextMenu(new QMenu(this)) { ui->setupUi(this); QPixmap p(":assets/images/xmrig.svg"); ui->lbl_logo->setPixmap(p.scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation)); + connect(m_XMRig, &XmRig::output, this, &XMRigWidget::onProcessOutput); + connect(m_XMRig, &XmRig::error, this, &XMRigWidget::onProcessError); + connect(m_XMRig, &XmRig::hashrate, this, &XMRigWidget::onHashrate); + + connect(m_ctx, &AppContext::walletClosed, this, &XMRigWidget::onWalletClosed); + connect(m_ctx, &AppContext::walletOpened, this, &XMRigWidget::onWalletOpened); + // table ui->tableView->setModel(this->m_model); - m_contextMenu->addAction(QIcon(":/assets/images/network.png"), "Download file", this, &XMRigWidget::linkClicked); + m_contextMenu->addAction(icons()->icon("network.png"), "Download file", this, &XMRigWidget::linkClicked); connect(ui->tableView, &QHeaderView::customContextMenuRequested, this, &XMRigWidget::showContextMenu); connect(ui->tableView, &QTableView::doubleClicked, this, &XMRigWidget::linkClicked); @@ -166,14 +175,14 @@ void XMRigWidget::onStartClicked() { username = QString("%1.%2").arg(username, m_ctx->currentWallet->address(0, 0).mid(0, 6)); } - m_ctx->XMRig->start(xmrigPath, m_threads, address, username, password, ui->relayTor->isChecked(), ui->check_tls->isChecked()); + m_XMRig->start(xmrigPath, m_threads, address, username, password, ui->relayTor->isChecked(), ui->check_tls->isChecked()); ui->btn_start->setEnabled(false); ui->btn_stop->setEnabled(true); emit miningStarted(); } void XMRigWidget::onStopClicked() { - m_ctx->XMRig->stop(); + m_XMRig->stop(); ui->btn_start->setEnabled(true); ui->btn_stop->setEnabled(false); ui->label_status->hide(); diff --git a/src/widgets/xmrigwidget.h b/src/widgets/xmrigwidget.h index ef79b5c..aa5d7e7 100644 --- a/src/widgets/xmrigwidget.h +++ b/src/widgets/xmrigwidget.h @@ -54,6 +54,8 @@ private: Ui::XMRigWidget *ui; QStandardItemModel *m_model; QMenu *m_contextMenu; + XmRig * m_XMRig; + int m_threads; QStringList m_urls; QStringList m_pools{"pool.xmr.pt:9000", "pool.supportxmr.com:9000", "mine.xmrpool.net:443", "xmrpool.eu:9999", "xmr-eu1.nanopool.org:14433", "pool.minexmr.com:6666", "us-west.minexmr.com:6666", "monerohash.com:9999", "cryptonote.social:5555", "cryptonote.social:5556"}; diff --git a/src/wizard/PageHardwareDevice.cpp b/src/wizard/PageHardwareDevice.cpp new file mode 100644 index 0000000..03e0a41 --- /dev/null +++ b/src/wizard/PageHardwareDevice.cpp @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "PageHardwareDevice.h" +#include "ui_PageHardwareDevice.h" +#include "WalletWizard.h" + +#include + +PageHardwareDevice::PageHardwareDevice(AppContext *ctx, WizardFields *fields, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageHardwareDevice) + , m_ctx(ctx) + , m_fields(fields) +{ + ui->setupUi(this); +} + +void PageHardwareDevice::initializePage() { + ui->radioNewWallet->setChecked(true); +} + +int PageHardwareDevice::nextId() const { + if (ui->radioNewWallet->isChecked()) + return WalletWizard::Page_WalletFile; + if (ui->radioRestoreWallet->isChecked()) + return WalletWizard::Page_SetRestoreHeight; + return 0; +} + +bool PageHardwareDevice::validatePage() { + return true; +} + +bool PageHardwareDevice::isComplete() const { + return true; +} \ No newline at end of file diff --git a/src/wizard/PageHardwareDevice.h b/src/wizard/PageHardwareDevice.h new file mode 100644 index 0000000..9267c35 --- /dev/null +++ b/src/wizard/PageHardwareDevice.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_PAGEHARDWAREDEVICE_H +#define FEATHER_PAGEHARDWAREDEVICE_H + +#include +#include +#include +#include + +#include "appcontext.h" +#include "WalletWizard.h" + +namespace Ui { + class PageHardwareDevice; +} + +class PageHardwareDevice : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageHardwareDevice(AppContext *ctx, WizardFields *fields, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + bool isComplete() const override; + +private: + AppContext *m_ctx; + Ui::PageHardwareDevice *ui; + WizardFields *m_fields; +}; + + +#endif //FEATHER_PAGEHARDWAREDEVICE_H diff --git a/src/wizard/PageHardwareDevice.ui b/src/wizard/PageHardwareDevice.ui new file mode 100644 index 0000000..736edea --- /dev/null +++ b/src/wizard/PageHardwareDevice.ui @@ -0,0 +1,131 @@ + + + PageHardwareDevice + + + + 0 + 0 + 558 + 431 + + + + WizardPage + + + + + + Select device type: + + + + + + + + Ledger Nano S/X + + + + + + + + Qt::Horizontal + + + + + + + Create a new wallet file from device + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Use this option if the keys on the device hold no funds. (i.e. this wallet was never used before) + + + true + + + + + + + + + Restore a wallet from device + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + If this option is selected, you will be asked specify a wallet creation date or restore height next. + + + true + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/wizard/PageMenu.cpp b/src/wizard/PageMenu.cpp index c0f97f9..2ca886b 100644 --- a/src/wizard/PageMenu.cpp +++ b/src/wizard/PageMenu.cpp @@ -44,6 +44,8 @@ int PageMenu::nextId() const { return WalletWizard::Page_WalletRestoreSeed; if (ui->radioViewOnly->isChecked()) return WalletWizard::Page_WalletRestoreKeys; + if (ui->radioCreateFromDevice->isChecked()) + return WalletWizard::Page_HardwareDevice; return 0; } @@ -64,6 +66,10 @@ bool PageMenu::validatePage() { m_fields->mode = WizardMode::RestoreFromKeys; m_fields->modeText = "Restore wallet"; } + if (ui->radioCreateFromDevice->isChecked()) { + m_fields->mode = WizardMode::CreateWalletFromDevice; + m_fields->modeText = "Create from hardware device"; + } return true; } \ No newline at end of file diff --git a/src/wizard/PageMenu.ui b/src/wizard/PageMenu.ui index 65f4f37..9f7b74f 100644 --- a/src/wizard/PageMenu.ui +++ b/src/wizard/PageMenu.ui @@ -67,6 +67,13 @@ + + + + Create wallet from hardware device + + + diff --git a/src/wizard/PageNetwork.cpp b/src/wizard/PageNetwork.cpp index ce433c0..f6d39ec 100644 --- a/src/wizard/PageNetwork.cpp +++ b/src/wizard/PageNetwork.cpp @@ -3,65 +3,52 @@ #include "PageNetwork.h" #include "ui_PageNetwork.h" +#include "WalletWizard.h" -#include - -// Unused for now -PageNetwork::PageNetwork(AppContext *ctx, QWidget *parent) : - QWizardPage(parent), - ui(new Ui::PageNetwork), - m_ctx(ctx) { +PageNetwork::PageNetwork(AppContext *ctx, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageNetwork) + , m_ctx(ctx) +{ ui->setupUi(this); - this->setTitle("Welcome to Feather!"); + this->setTitle("Welcome to Feather"); - ui->customFrame->hide(); + ui->frame_customNode->hide(); - ui->label_eg->setText("Examples:\n- http://127.0.0.1:18089\n- my.node.com\n- my.node.com:18089\n- user:pass@my.node.com:18089"); + ui->btnGroup_network->setId(ui->radio_autoConnect, 0); + ui->btnGroup_network->setId(ui->radio_custom, 1); - auto nodeSourceUInt = config()->get(Config::nodeSource).toUInt(); - auto nodeSource = static_cast(nodeSourceUInt); - if(nodeSource == NodeSource::websocket){ - ui->radioRemote->setChecked(true); - ui->customFrame->hide(); - ui->remoteFrame->show(); - } else if(nodeSource == NodeSource::custom) { - ui->radioCustom->setChecked(true); - ui->remoteFrame->hide(); - ui->customFrame->show(); - } - - connect(ui->networkBtnGroup, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *button) { - auto name = button->objectName(); - if(name == "radioRemote") { - ui->customFrame->hide(); - ui->remoteFrame->show(); - } else if(name == "radioCustom") { - ui->remoteFrame->hide(); - ui->customFrame->show(); - } + connect(ui->btnGroup_network, &QButtonGroup::idClicked, [this](int id) { + ui->frame_customNode->setVisible(id == 1); + }); + connect(ui->line_customNode, &QLineEdit::textEdited, [this]{ + this->completeChanged(); }); } int PageNetwork::nextId() const { - return 0; + return WalletWizard::Page_NetworkTor; } bool PageNetwork::validatePage() { - auto cfg = config()->get(Config::nodeSource); - if(ui->radioRemote->isChecked()) { - if(cfg != NodeSource::websocket) - m_ctx->nodeSourceChanged(NodeSource::websocket); - } else if (ui->radioCustom->isChecked()) { - if(cfg != NodeSource::custom) - m_ctx->nodeSourceChanged(NodeSource::custom); - auto nodeText = ui->lineEdit_customNode->text().trimmed(); - if(!nodeText.isEmpty()) { - auto customNodes = m_ctx->nodes->customNodes(); - auto node = FeatherNode(nodeText); - customNodes.append(node); - m_ctx->setCustomNodes(customNodes); - } + int id = ui->btnGroup_network->checkedId(); + config()->set(Config::nodeSource, id); + if (id == 1) { + QList nodes; + FeatherNode node{ui->line_customNode->text()}; + nodes.append(node); + m_ctx->nodes->setCustomNodes(nodes); } + return true; +} + +bool PageNetwork::isComplete() const { + if (ui->btnGroup_network->checkedId() == 0) { + return true; + } + + FeatherNode node{ui->line_customNode->text()}; + return node.isValid(); } \ No newline at end of file diff --git a/src/wizard/PageNetwork.h b/src/wizard/PageNetwork.h index f0a9158..502f349 100644 --- a/src/wizard/PageNetwork.h +++ b/src/wizard/PageNetwork.h @@ -23,6 +23,7 @@ public: explicit PageNetwork(AppContext *ctx, QWidget *parent = nullptr); bool validatePage() override; int nextId() const override; + bool isComplete() const override; private: AppContext *m_ctx; diff --git a/src/wizard/PageNetwork.ui b/src/wizard/PageNetwork.ui index 481e0f0..6b54e7e 100644 --- a/src/wizard/PageNetwork.ui +++ b/src/wizard/PageNetwork.ui @@ -6,120 +6,106 @@ 0 0 - 521 - 639 + 693 + 512 + + + 0 + 0 + + WizardPage - - - How would you like to connect to the Monero network? - - - true - - - - - + - - - - - - 128 - 128 - - - - - 128 - 128 - - - - logo - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - + + + + 75 + true + + + + How do you want to connect to the Monero network? + + + false + + - - - - - - - - - - - - Remote node (Automatic) - - - true - - - networkBtnGroup - - - - - - - Manually connect - - - networkBtnGroup - - - - - - - Qt::Vertical - - - - 0 - 0 - - - - - + + + Feather needs to connect to a node to obtain the data required to scan for your transactions. + + + true + + + + + + + In most cases you want to let Feather pick one at random. Nodes are hosted by the Feather team and trusted members of the Monero community. + + + true + + + + + + + However if you run you own node or prefer to configure manually feel free to do so. + + + true + + - + + + + + Auto connect + + + true + + + btnGroup_network + + + + + + + Select node manually + + + btnGroup_network + + + + + + + QFrame::NoFrame - QFrame::Plain + QFrame::Raised - + 0 @@ -133,105 +119,44 @@ 0 - - - - - - - 127.0.0.1:18089 - - - - - - - Custom node - - - - + + + + + Custom node: + + - - - - Qt::Horizontal + + + + 127.0.0.1:18081 - - - 40 - 20 - + + 127.0.0.1:18081 - + - - - e.g: + + + false - - true + + You can configure more nodes later in Settings -> Node - - - - QFrame::NoFrame - - - QFrame::Plain - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Automatically connect to a remote node, hosted by the feather team and various trusted Monero community members. These nodes are provided as "best effort". - - - true - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - + diff --git a/src/wizard/PageNetworkTor.cpp b/src/wizard/PageNetworkTor.cpp new file mode 100644 index 0000000..cade2ee --- /dev/null +++ b/src/wizard/PageNetworkTor.cpp @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#include "PageNetworkTor.h" +#include "ui_PageNetworkTor.h" +#include "WalletWizard.h" + +PageNetworkTor::PageNetworkTor(AppContext *ctx, QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageNetworkTor) + , m_ctx(ctx) +{ + ui->setupUi(this); + + this->setCommitPage(true); + this->setButtonText(QWizard::CommitButton, "Next"); + + QPixmap iconAllTorExceptNode(":/assets/images/securityLevelStandard.png"); + QPixmap iconAllTorExceptInitSync(":/assets/images/securityLevelSafer.png"); + QPixmap iconAllTor(":/assets/images/securityLevelSafest.png"); + ui->icon_allTorExceptNode->setPixmap(iconAllTorExceptNode.scaledToHeight(16, Qt::SmoothTransformation)); + ui->icon_allTorExceptInitSync->setPixmap(iconAllTorExceptInitSync.scaledToHeight(16, Qt::SmoothTransformation)); + ui->icon_allTor->setPixmap(iconAllTor.scaledToHeight(16, Qt::SmoothTransformation)); + + connect(ui->radio_configureManually, &QRadioButton::toggled, [this](bool checked){ + ui->frame_privacyLevel->setVisible(checked); + this->adjustSize(); + this->updateGeometry(); + }); + + ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptNode, Config::allTorExceptNode); + ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptInitSync, Config::allTorExceptInitSync); + ui->btnGroup_privacyLevel->setId(ui->radio_allTor, Config::allTor); + + int privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); + auto button = ui->btnGroup_privacyLevel->button(privacyLevel); + if (button) { + button->setChecked(true); + } +} + +void PageNetworkTor::initializePage() { + // Fuck you Qt. No squish. + QTimer::singleShot(1, [this]{ + ui->frame_privacyLevel->setVisible(false); + }); +} + +int PageNetworkTor::nextId() const { + return WalletWizard::Page_Menu; +} + +bool PageNetworkTor::validatePage() { + int id = ui->btnGroup_privacyLevel->checkedId(); + config()->set(Config::torPrivacyLevel, id); + + emit initialNetworkConfigured(); + + return true; +} \ No newline at end of file diff --git a/src/wizard/PageNetworkTor.h b/src/wizard/PageNetworkTor.h new file mode 100644 index 0000000..9863b58 --- /dev/null +++ b/src/wizard/PageNetworkTor.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2020-2021, The Monero Project. + +#ifndef FEATHER_PAGENETWORKTOR_H +#define FEATHER_PAGENETWORKTOR_H + +#include + +#include "appcontext.h" + +namespace Ui { + class PageNetworkTor; +} + +class PageNetworkTor : public QWizardPage +{ +Q_OBJECT + +public: + explicit PageNetworkTor(AppContext *ctx, QWidget *parent = nullptr); + void initializePage() override; + bool validatePage() override; + int nextId() const override; + +signals: + void initialNetworkConfigured(); + +private: + AppContext *m_ctx; + Ui::PageNetworkTor *ui; +}; + +#endif //FEATHER_PAGENETWORKTOR_H diff --git a/src/wizard/PageNetworkTor.ui b/src/wizard/PageNetworkTor.ui new file mode 100644 index 0000000..8cabfc8 --- /dev/null +++ b/src/wizard/PageNetworkTor.ui @@ -0,0 +1,208 @@ + + + PageNetworkTor + + + + 0 + 0 + 618 + 438 + + + + WizardPage + + + + + + + 75 + true + + + + How should Feather route its network traffic? + + + + + + + By default, Feather routes most traffic over Tor. + + + true + + + + + + + An exception is made for the initial wallet synchronization after opening a wallet. Synchronization requires a lot of data transfer and is therefore very slow over Tor. + + + true + + + + + + + On Tails, Whonix, or when Feather is started with Torsocks, all traffic is routed through Tor regardless of application configuration. + + + true + + + + + + + Connections to local nodes are never routed over Tor. + + + + + + + Use default settings (recommended) + + + true + + + + + + + Configure manually + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + 0 + 0 + + + + icon + + + + + + + + 0 + 0 + + + + Route all traffic over Tor, except traffic to node + + + btnGroup_privacyLevel + + + + + + + + + + + + 0 + 0 + + + + icon + + + + + + + + 0 + 0 + + + + Route all traffic over Tor, except initial wallet sync + + + btnGroup_privacyLevel + + + + + + + + + + + + 0 + 0 + + + + icon + + + + + + + + 0 + 0 + + + + Route all traffic over Tor + + + btnGroup_privacyLevel + + + + + + + + + + + + + + + + diff --git a/src/wizard/PageSetRestoreHeight.cpp b/src/wizard/PageSetRestoreHeight.cpp index 046feb9..12e00a0 100644 --- a/src/wizard/PageSetRestoreHeight.cpp +++ b/src/wizard/PageSetRestoreHeight.cpp @@ -25,7 +25,7 @@ PageSetRestoreHeight::PageSetRestoreHeight(AppContext *ctx, WizardFields *fields QPixmap pixmap = QPixmap(":/assets/images/unpaid.png"); ui->icon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation)); - QPixmap pixmap2 = QPixmap(":/assets/images/info.png"); + QPixmap pixmap2 = QPixmap(":/assets/images/info2.svg"); ui->warningIcon->setPixmap(pixmap2.scaledToWidth(32, Qt::SmoothTransformation)); ui->infoIcon->setPixmap(pixmap2.scaledToWidth(32, Qt::SmoothTransformation)); @@ -61,7 +61,7 @@ void PageSetRestoreHeight::onCreationDateEdited() { QDateTime restoreDate = date > curDate ? curDate : date; int timestamp = restoreDate.toSecsSinceEpoch(); - QString restoreHeight = QString::number(m_ctx->restoreHeights[m_ctx->networkType]->dateToRestoreHeight(timestamp)); + QString restoreHeight = QString::number(appData()->restoreHeights[m_ctx->networkType]->dateToRestoreHeight(timestamp)); ui->line_restoreHeight->setText(restoreHeight); this->showScanWarning(restoreDate); @@ -77,7 +77,7 @@ void PageSetRestoreHeight::onRestoreHeightEdited() { return; } - int timestamp = m_ctx->restoreHeights[m_ctx->networkType]->restoreHeightToDate(restoreHeight); + int timestamp = appData()->restoreHeights[m_ctx->networkType]->restoreHeightToDate(restoreHeight); auto date = QDateTime::fromSecsSinceEpoch(timestamp); ui->line_creationDate->setText(date.toString("yyyy-MM-dd")); diff --git a/src/wizard/PageWalletFile.cpp b/src/wizard/PageWalletFile.cpp index 02d4169..cfde928 100644 --- a/src/wizard/PageWalletFile.cpp +++ b/src/wizard/PageWalletFile.cpp @@ -22,9 +22,6 @@ PageWalletFile::PageWalletFile(AppContext *ctx, WizardFields *fields, QWidget *p QPixmap pixmap = QPixmap(":/assets/images/file.png"); ui->lockIcon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation)); - this->registerField("walletName", ui->line_walletName); - this->registerField("walletDirectory", ui->line_walletDir); - connect(ui->btnChange, &QPushButton::clicked, [=] { QString walletDir = QFileDialog::getExistingDirectory(this, "Select wallet directory ", m_ctx->defaultWalletDir, QFileDialog::ShowDirsOnly); if(walletDir.isEmpty()) return; @@ -100,7 +97,11 @@ QString PageWalletFile::defaultWalletName() { int count = 1; QString walletName; do { - walletName = QString("wallet_%1").arg(count); + QString walletStr = QString("wallet_%1"); + if (m_fields->mode == WizardMode::CreateWalletFromDevice) { + walletStr = QString("ledger_%1"); + } + walletName = walletStr.arg(count); count++; } while (this->walletPathExists(walletName)); diff --git a/src/wizard/PageWalletRestoreSeed.cpp b/src/wizard/PageWalletRestoreSeed.cpp index 6f58478..a7d4421 100644 --- a/src/wizard/PageWalletRestoreSeed.cpp +++ b/src/wizard/PageWalletRestoreSeed.cpp @@ -110,7 +110,7 @@ bool PageWalletRestoreSeed::validatePage() { } } - auto _seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], QString::fromStdString(globals::coinName), m_ctx->seedLanguage, seedSplit); + auto _seed = FeatherSeed(m_ctx->networkType, QString::fromStdString(globals::coinName), m_ctx->seedLanguage, seedSplit); if (!_seed.errorString.isEmpty()) { QMessageBox::warning(this, "Invalid seed", QString("Invalid seed:\n\n%1").arg(_seed.errorString)); ui->seedEdit->setStyleSheet(errStyle); diff --git a/src/wizard/PageWalletSeed.cpp b/src/wizard/PageWalletSeed.cpp index 87b5489..241c8c9 100644 --- a/src/wizard/PageWalletSeed.cpp +++ b/src/wizard/PageWalletSeed.cpp @@ -50,8 +50,7 @@ void PageWalletSeed::seedRoulette(int count) { void PageWalletSeed::generateSeed() { do { - FeatherSeed seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], - QString::fromStdString(globals::coinName), m_ctx->seedLanguage); + FeatherSeed seed = FeatherSeed(m_ctx->networkType, QString::fromStdString(globals::coinName), m_ctx->seedLanguage); m_mnemonic = seed.mnemonic.join(" "); m_restoreHeight = seed.restoreHeight; } while (m_mnemonic.split(" ").length() != 14); // https://github.com/tevador/monero-seed/issues/2 diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index 08f8f2a..78198c5 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -13,6 +13,8 @@ #include "PageWalletRestoreKeys.h" #include "PageSetPassword.h" #include "PageSetRestoreHeight.h" +#include "PageHardwareDevice.h" +#include "PageNetworkTor.h" #include "globals.h" #include @@ -29,6 +31,8 @@ WalletWizard::WalletWizard(AppContext *ctx, WalletWizard::Page startPage, QWidge m_walletKeysFilesModel = new WalletKeysFilesModel(m_ctx, this); m_walletKeysFilesModel->refresh(); + auto networkPage = new PageNetwork(m_ctx, this); + auto networkTorPage = new PageNetworkTor(m_ctx, this); auto menuPage = new PageMenu(m_ctx, &m_wizardFields, m_walletKeysFilesModel, this); auto openWalletPage = new PageOpenWallet(m_ctx, m_walletKeysFilesModel, this); auto createWallet = new PageWalletFile(m_ctx, &m_wizardFields , this); @@ -39,13 +43,14 @@ WalletWizard::WalletWizard(AppContext *ctx, WalletWizard::Page startPage, QWidge setPage(Page_OpenWallet, openWalletPage); setPage(Page_CreateWalletSeed, createWalletSeed); setPage(Page_SetPasswordPage, walletSetPasswordPage); - setPage(Page_Network, new PageNetwork(m_ctx, this)); + setPage(Page_Network, networkPage); + setPage(Page_NetworkTor, networkTorPage); setPage(Page_WalletRestoreSeed, new PageWalletRestoreSeed(m_ctx, &m_wizardFields, this)); setPage(Page_WalletRestoreKeys, new PageWalletRestoreKeys(m_ctx, &m_wizardFields, this)); setPage(Page_SetRestoreHeight, new PageSetRestoreHeight(m_ctx, &m_wizardFields, this)); + setPage(Page_HardwareDevice, new PageHardwareDevice(m_ctx, &m_wizardFields, this)); - - setStartId(Page_Menu); + setStartId(startPage); setButtonText(QWizard::CancelButton, "Close"); setPixmap(QWizard::WatermarkPixmap, QPixmap(":/assets/images/banners/3.png")); @@ -56,6 +61,10 @@ WalletWizard::WalletWizard(AppContext *ctx, WalletWizard::Page startPage, QWidge return QApplication::exit(1); }); + connect(networkTorPage, &PageNetworkTor::initialNetworkConfigured, [this](){ + emit initialNetworkConfigured(); + }); + connect(menuPage, &PageMenu::enableDarkMode, [this](bool enable){ if (enable) emit skinChanged("QDarkStyle"); @@ -76,6 +85,21 @@ WalletWizard::WalletWizard(AppContext *ctx, WalletWizard::Page startPage, QWidge void WalletWizard::createWallet() { auto walletPath = QString("%1/%2").arg(m_wizardFields.walletDir, m_wizardFields.walletName); + int currentBlockHeight = 0; + if (appData()->heights.contains(m_ctx->networkType)) { + currentBlockHeight = appData()->heights[m_ctx->networkType]; + } + + if (m_wizardFields.mode == WizardMode::CreateWalletFromDevice) { + int restoreHeight = currentBlockHeight; + if (m_wizardFields.restoreHeight > 0) { + restoreHeight = m_wizardFields.restoreHeight; + } + + m_ctx->createWalletFromDevice(walletPath, m_wizardFields.password, restoreHeight); + return; + } + if (m_wizardFields.mode == WizardMode::RestoreFromKeys) { m_ctx->createWalletFromKeys(walletPath, m_wizardFields.password, @@ -86,12 +110,11 @@ void WalletWizard::createWallet() { return; } - auto seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], QString::fromStdString(globals::coinName), m_ctx->seedLanguage, m_wizardFields.seed.split(" ")); + auto seed = FeatherSeed(m_ctx->networkType, QString::fromStdString(globals::coinName), m_ctx->seedLanguage, m_wizardFields.seed.split(" ")); - if (m_wizardFields.mode == WizardMode::CreateWallet && m_ctx->heights.contains(m_ctx->networkType)) { - int restoreHeight = m_ctx->heights[m_ctx->networkType]; - qInfo() << "New wallet, setting restore height to latest blockheight: " << restoreHeight; - seed.setRestoreHeight(restoreHeight); + if (m_wizardFields.mode == WizardMode::CreateWallet) { + qInfo() << "New wallet, setting restore height to latest blockheight: " << currentBlockHeight; + seed.setRestoreHeight(currentBlockHeight); } if (m_wizardFields.mode == WizardMode::RestoreFromSeed && m_wizardFields.seedType == SeedType::MONERO) diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 076a362..19d0178 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -16,7 +16,8 @@ enum WizardMode { CreateWallet = 0, OpenWallet, RestoreFromSeed, - RestoreFromKeys + RestoreFromKeys, + CreateWalletFromDevice }; struct WizardFields { @@ -30,7 +31,7 @@ struct WizardFields { QString secretViewKey; QString secretSpendKey; WizardMode mode; - int restoreHeight; + int restoreHeight = 0; SeedType seedType; }; @@ -48,12 +49,15 @@ public: Page_Network, Page_WalletRestoreSeed, Page_WalletRestoreKeys, - Page_SetRestoreHeight + Page_SetRestoreHeight, + Page_HardwareDevice, + Page_NetworkTor }; explicit WalletWizard(AppContext *ctx, WalletWizard::Page startPage = WalletWizard::Page::Page_Menu, QWidget *parent = nullptr); signals: + void initialNetworkConfigured(); void skinChanged(const QString &skin); void openWallet(QString path, QString password); void defaultWalletDirChanged(QString walletDir);