From d4cfa051572c71c9b2397b64ba085b6cadba2a39 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Sun, 2 May 2021 20:22:38 +0200 Subject: [PATCH] Beta-6 --- .gitignore | 1 + BUILDING.md | 6 +- CMakeLists.txt | 11 +- HACKING.md | 2 + Makefile | 6 +- PKGBUILD | 11 +- cmake/FindHIDAPI.cmake | 60 + monero | 2 +- src/CMakeLists.txt | 17 + src/api/LocalMoneroApi.cpp | 121 ++ src/api/LocalMoneroApi.h | 53 + src/appcontext.cpp | 348 ++--- src/appcontext.h | 50 +- src/assets.qrc | 15 + src/assets/gpg_keys/featherwallet.asc | 52 + src/assets/images/info2.svg | 5 + src/assets/images/localMonero_buy.svg | 1 + src/assets/images/localMonero_buy_white.svg | 1 + src/assets/images/localMonero_logo.png | Bin 0 -> 10540 bytes src/assets/images/localMonero_register.svg | 1 + src/assets/images/localMonero_search.svg | 1 + src/assets/images/localMonero_sell.svg | 1 + src/assets/images/localMonero_sell_white.svg | 1 + src/assets/images/securityLevelSafer.png | Bin 0 -> 1652 bytes src/assets/images/securityLevelSafer.svg | 13 + src/assets/images/securityLevelSaferWhite.png | Bin 0 -> 2108 bytes src/assets/images/securityLevelSafest.png | Bin 0 -> 1371 bytes src/assets/images/securityLevelSafest.svg | 13 + .../images/securityLevelSafestWhite.png | Bin 0 -> 1712 bytes src/assets/images/securityLevelStandard.png | Bin 0 -> 1918 bytes src/assets/images/securityLevelStandard.svg | 13 + .../images/securityLevelStandardWhite.png | Bin 0 -> 2212 bytes src/calcwidget.cpp | 21 +- src/calcwidget.h | 5 +- src/calcwindow.cpp | 8 +- src/coinswidget.cpp | 31 +- src/coinswidget.h | 9 +- src/contactswidget.cpp | 7 +- src/dialog/InfoDialog.cpp | 19 + src/dialog/InfoDialog.h | 26 + src/dialog/InfoDialog.ui | 71 + src/dialog/LocalMoneroInfoDialog.cpp | 51 + src/dialog/LocalMoneroInfoDialog.h | 35 + src/dialog/LocalMoneroInfoDialog.ui | 195 +++ src/dialog/TxProofDialog.cpp | 3 +- src/dialog/UpdateDialog.cpp | 223 +++ src/dialog/UpdateDialog.h | 51 + src/dialog/UpdateDialog.ui | 87 ++ src/dialog/broadcasttxdialog.cpp | 7 +- src/dialog/debuginfodialog.cpp | 42 +- src/dialog/debuginfodialog.ui | 130 +- src/dialog/keysdialog.cpp | 15 +- src/dialog/restoredialog.cpp | 2 +- src/dialog/splashdialog.cpp | 24 + src/dialog/splashdialog.h | 25 + src/dialog/splashdialog.ui | 60 + src/dialog/torinfodialog.cpp | 130 +- src/dialog/torinfodialog.h | 15 +- src/dialog/torinfodialog.ui | 340 ++++- src/dialog/txconfadvdialog.cpp | 1 - src/dialog/txconfdialog.cpp | 3 +- src/dialog/tximportdialog.cpp | 7 +- src/globals.h | 3 + src/historywidget.cpp | 16 +- src/libwalletqt/Subaddress.cpp | 26 +- src/libwalletqt/Subaddress.h | 13 +- src/libwalletqt/TransactionHistory.cpp | 8 +- src/libwalletqt/Wallet.cpp | 23 +- src/libwalletqt/Wallet.h | 15 +- src/libwalletqt/WalletListenerImpl.cpp | 9 +- src/libwalletqt/WalletListenerImpl.h | 2 + src/libwalletqt/WalletManager.cpp | 45 +- src/libwalletqt/WalletManager.h | 14 +- src/mainwindow.cpp | 1202 ++++++++++------- src/mainwindow.h | 113 +- src/mainwindow.ui | 35 +- src/model/AddressBookModel.cpp | 3 +- src/model/CoinsModel.cpp | 9 +- src/model/CoinsModel.h | 2 - src/model/HistoryView.cpp | 1 + src/model/HistoryView.h | 1 + src/model/LocalMoneroModel.cpp | 172 +++ src/model/LocalMoneroModel.h | 47 + src/model/NodeModel.cpp | 7 +- src/model/SubaddressModel.cpp | 1 - src/model/TransactionHistoryModel.cpp | 31 +- src/model/TransactionHistoryModel.h | 9 - src/receivewidget.cpp | 31 +- src/receivewidget.h | 3 +- src/sendwidget.cpp | 25 +- src/sendwidget.h | 5 - src/settings.cpp | 32 +- src/settings.h | 7 +- src/settings.ui | 145 +- src/utils/AppData.cpp | 52 + src/utils/AppData.h | 41 + src/utils/AsyncTask.h | 85 ++ src/utils/ColorScheme.cpp | 1 + src/utils/FeatherSeed.h | 22 +- src/utils/Icons.cpp | 32 + src/utils/Icons.h | 32 + src/utils/NetworkManager.cpp | 37 + src/utils/NetworkManager.h | 14 + src/utils/SemanticVersion.h | 94 ++ src/utils/{tor.cpp => TorManager.cpp} | 211 +-- src/utils/TorManager.h | 74 + src/utils/Updater.cpp | 89 ++ src/utils/Updater.h | 27 + src/utils/WebsocketClient.cpp | 95 ++ src/utils/WebsocketClient.h | 44 + src/utils/WebsocketNotifier.cpp | 181 +++ src/utils/WebsocketNotifier.h | 63 + src/utils/config.cpp | 14 +- src/utils/config.h | 20 +- src/utils/daemonrpc.cpp | 8 +- src/utils/daemonrpc.h | 3 +- src/utils/networking.cpp | 39 +- src/utils/networking.h | 12 +- src/utils/nodes.cpp | 160 ++- src/utils/nodes.h | 100 +- src/utils/tor.h | 115 -- src/utils/utils.cpp | 18 + src/utils/utils.h | 1 + src/utils/wsclient.cpp | 63 +- src/utils/wsclient.h | 11 +- src/utils/xmrig.cpp | 27 +- src/utils/xmrig.h | 1 - src/widgets/LocalMoneroWidget.cpp | 209 +++ src/widgets/LocalMoneroWidget.h | 56 + src/widgets/LocalMoneroWidget.ui | 385 ++++++ src/widgets/nodewidget.cpp | 39 +- src/widgets/tickerwidget.cpp | 23 +- src/widgets/tickerwidget.h | 2 +- src/widgets/xmrigwidget.cpp | 27 +- src/widgets/xmrigwidget.h | 2 + src/wizard/PageHardwareDevice.cpp | 37 + src/wizard/PageHardwareDevice.h | 37 + src/wizard/PageHardwareDevice.ui | 136 ++ src/wizard/PageMenu.cpp | 6 + src/wizard/PageMenu.ui | 7 + src/wizard/PageNetwork.cpp | 79 +- src/wizard/PageNetwork.h | 1 + src/wizard/PageNetwork.ui | 277 ++-- src/wizard/PageNetworkTor.cpp | 49 + src/wizard/PageNetworkTor.h | 32 + src/wizard/PageNetworkTor.ui | 184 +++ src/wizard/PageSetRestoreHeight.cpp | 6 +- src/wizard/PageWalletFile.cpp | 3 - src/wizard/PageWalletRestoreSeed.cpp | 2 +- src/wizard/PageWalletSeed.cpp | 3 +- src/wizard/WalletWizard.cpp | 39 +- src/wizard/WalletWizard.h | 10 +- 152 files changed, 6060 insertions(+), 1880 deletions(-) create mode 100644 src/api/LocalMoneroApi.cpp create mode 100644 src/api/LocalMoneroApi.h create mode 100644 src/assets/gpg_keys/featherwallet.asc create mode 100644 src/assets/images/info2.svg create mode 100644 src/assets/images/localMonero_buy.svg create mode 100644 src/assets/images/localMonero_buy_white.svg create mode 100644 src/assets/images/localMonero_logo.png create mode 100644 src/assets/images/localMonero_register.svg create mode 100644 src/assets/images/localMonero_search.svg create mode 100644 src/assets/images/localMonero_sell.svg create mode 100644 src/assets/images/localMonero_sell_white.svg create mode 100644 src/assets/images/securityLevelSafer.png create mode 100644 src/assets/images/securityLevelSafer.svg create mode 100644 src/assets/images/securityLevelSaferWhite.png create mode 100644 src/assets/images/securityLevelSafest.png create mode 100644 src/assets/images/securityLevelSafest.svg create mode 100644 src/assets/images/securityLevelSafestWhite.png create mode 100644 src/assets/images/securityLevelStandard.png create mode 100644 src/assets/images/securityLevelStandard.svg create mode 100644 src/assets/images/securityLevelStandardWhite.png create mode 100644 src/dialog/InfoDialog.cpp create mode 100644 src/dialog/InfoDialog.h create mode 100644 src/dialog/InfoDialog.ui create mode 100644 src/dialog/LocalMoneroInfoDialog.cpp create mode 100644 src/dialog/LocalMoneroInfoDialog.h create mode 100644 src/dialog/LocalMoneroInfoDialog.ui create mode 100644 src/dialog/UpdateDialog.cpp create mode 100644 src/dialog/UpdateDialog.h create mode 100644 src/dialog/UpdateDialog.ui create mode 100644 src/dialog/splashdialog.cpp create mode 100644 src/dialog/splashdialog.h create mode 100644 src/dialog/splashdialog.ui create mode 100644 src/model/LocalMoneroModel.cpp create mode 100644 src/model/LocalMoneroModel.h create mode 100644 src/utils/AppData.cpp create mode 100644 src/utils/AppData.h create mode 100644 src/utils/AsyncTask.h create mode 100644 src/utils/Icons.cpp create mode 100644 src/utils/Icons.h create mode 100644 src/utils/NetworkManager.cpp create mode 100644 src/utils/NetworkManager.h create mode 100644 src/utils/SemanticVersion.h rename src/utils/{tor.cpp => TorManager.cpp} (54%) create mode 100644 src/utils/TorManager.h create mode 100644 src/utils/Updater.cpp create mode 100644 src/utils/Updater.h create mode 100644 src/utils/WebsocketClient.cpp create mode 100644 src/utils/WebsocketClient.h create mode 100644 src/utils/WebsocketNotifier.cpp create mode 100644 src/utils/WebsocketNotifier.h delete mode 100644 src/utils/tor.h create mode 100644 src/widgets/LocalMoneroWidget.cpp create mode 100644 src/widgets/LocalMoneroWidget.h create mode 100644 src/widgets/LocalMoneroWidget.ui create mode 100644 src/wizard/PageHardwareDevice.cpp create mode 100644 src/wizard/PageHardwareDevice.h create mode 100644 src/wizard/PageHardwareDevice.ui create mode 100644 src/wizard/PageNetworkTor.cpp create mode 100644 src/wizard/PageNetworkTor.h create mode 100644 src/wizard/PageNetworkTor.ui 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..b58da06 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 "626f1b4ba1c1a490ca97fe265230aea84eac0ed2") 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/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..626f1b4 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 41327974116dedccc2f9709d8ad3a8a1f591faed +Subproject commit 626f1b4ba1c1a490ca97fe265230aea84eac0ed2 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..9e332b7 --- /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()) + 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..c0eefdb 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,32 @@ 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); } 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) { @@ -249,6 +228,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,8 +266,7 @@ void AppContext::commitTransaction(PendingTransaction *tx) { } void AppContext::onMultiBroadcast(PendingTransaction *tx) { - UtilsNetworking *net = new UtilsNetworking(this->network, this); - DaemonRpc *rpc = new DaemonRpc(this, net, ""); + DaemonRpc rpc{this, getNetworkTor(), ""}; int count = tx->txCount(); for (int i = 0; i < count; i++) { @@ -286,40 +275,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); + rpc.setDaemonAddress(address); + 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 +361,7 @@ 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); emit walletOpened(); @@ -346,142 +377,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 +423,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 +445,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 +466,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 +536,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 +581,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; @@ -778,11 +677,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..53c8b58 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,6 +78,7 @@ 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); @@ -109,18 +89,17 @@ public slots: 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,13 @@ 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: 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----- + +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 +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 0000000000000000000000000000000000000000..1751ce722a1398912caf5b8e1a560e910b1a1fdf GIT binary patch literal 10540 zcmbtagC|}dPp~;zxn$Y zzU$g`y=U*cJ?}Z^sr$Ll6QQZD@D%qME&u>{s-!6U763p)4bcENSg4Ef$DcL;03$$2 zR!YZb@gUPXfOe+2BRh3L*G11W#jVl0Sy`Zs6$&{sQpW#oX_dCR0;Az}MMtB-hsFV( z3~p^FI!a4RdrGqdwb7}%FgHNnl`*B9gL+3MM%!0mp%^%;21-jdD+n_?Z`(0n_>u3p zFWj8~Cz$d7_z>xl*#(blzcbvPShLQ-7(k<)e6eMv>!K-`M<@KjC6?Ll> z2l6QsO#hB>gd(Vlr4`yt-&i_c@ib=<259FQb8)bt5?~~TJXLs%?=yD&jD(ikq2Y=- z>zB&{F4s-n0)JOQ{aJujb30%{B$aFAJsyNoXl298DH*48edN~sn2}X zQ)0kN(urn(kc^`#5@<`+@LU^!7xoa*>1AFs-+Iilk8R@%tDuhnlntt_-*wILE>sam z062wiV$r* zwX5Mo!hgSc(uoaWy?lv}9=J*tyHu|Zg0S~1E{idb@!&H(JB}lt3QqQaZi4u@%3N(= z@m+YZiYpZ;>$a2KCE51qQ?36TL+5psM(1lgsZu!rmb*b#|#Yb`UB2})|o}l z7ZXW~P%GlmC*v?Ge|O&)#yuu=3w(~B4*Qi2eIS_l+ez7!?j&}WFQTrN`iUaj_CSHYo{X15 zQ^8scXgjWLDA^$+ZyfbOkl$}L4F!PnPd6jPpS)6}@rU)YnFMjKmL{2?uJ>f}0e+BBP?0}O#Pko;+uB-Hy5iv z2MU#urC|mlKOT}H9_`gfG&a)l`d_&l(o_G9h^G@K8yHk(WqX-Qx{av)RUVd^J$=%6 zp&Q`QWFoRRJow7-jx_@C;Car2F$DFgP8|Qdoi@~qr{JbM=yTftRZ=Ur)z4_olR&@m z_;;W3Lrr_AX^^DI$=NyD8Ol7ki2)%Cg6(BK=r<0X!j=r^`Mq|=Zo59};6NbLYs<6! zspMJbmD{G^q6bqY{|+;ei|k)(I!W%0_cxDn0b^w3+5i_xFrkpqJ}`lfLqAI(t5!Wt zQQpu}wPQFunweXu2*Rf>_SIWLY#{MSe{w4^93Zq*b^WFx77$$rr!Ws)E?>`W_BHmn z^!Qj)?mOrvW5qV!(Hw?ooQ}ZgbjIA47vZ8hv8s2EObH-tnjA~kj7uCCdWmoyv(r{o zZ#X1T{P>pkEJ|MM^{dS8BrblB+kM!9u%wmD_3lMO3;(#lR@4<50o%4ak-K83X0AH0+)k=i}z93FETV9&3(7lh9!A`&5C9rmWOG#&CACU;G^3m^O*1_`-y@UGisRzk6H&8 zItJ%bi-Y#$xVha@+>AGG5=m`h@?GueQ<*~9k1$HiM7S*#-E~#>Qw8?9oS<$ECQSnC)iLR!v-D<%<7! zt&gK7@s0d;evVy8`nL5~Ma$oHoDIIab_ca;m=+WhE1K;$OvCa3%!2p6_^<=psHDdT z>Jymzy|nyxA;IohOU1^5QNPvu)&&DNo%WFgpJ0T82_O6SG{GHwX{S%v-j7~wnXfTM z@nKw44a&t;@&MspmB1$%)?rz+nfsgVG!(Bnc$_q|UF;6lVtE|5C@T+(B(P-GZn$|| zoGX5mR>ur(@L}sB;u2^zPU;+IT}pp+(>nkoI>SRxXLyXbfheq!j#t)?W!F)L5rggW zZU9-$g!N`O#Rc5OlLeQ{l;hYsO`wH&Xgxo(jiJ`z*ECq}A`Gm}Dl|1x#s1YPjY_(` zuZcTHXohs?vanbU+OF1hzTBn|I{&V0oEzqQzvR1FfA3-Np=a+D%g5HA&QF1Iip^%j z6~or+uU*6lRpL;qk;A?#Fdj>F^rgY6^?l(?3kwFL|3d;Egn>w=k(f{*!?3w2?z%Qu z6y}jX?<|<26l?HobF510$Z(2 z|9MFQ^}w}J;M&oycK^u%Um+IV_91wBAQJ9mu3=TV#dWLb58i8_FJK3Y?|^cv^Y|u2 zL>CN~?JjKILJ~68Svp+U&FCgEb6jC~$KvL>GyePEHYS^QGP-gygKiI>IlH-Rekl6% zaYaG24Ys~AaJl^EH+5;r@v>a4kVQUMMb!5XNcH{yOu!0aTaCnIFnIFpWu*y`htp#y zmu{KQN19IpVuo`x69%p2$uAb3L{ld@-?$TmDgWN&TrqBCs4O&SOI7|DnIg8J+1|pW z#{}_*c$8jEj>5T^1qWorh8p#2Pg4(koB}4=1#|ul9ar3#e~xc$6}sPU)$Z6IH29Gg z&`6N^{|A|Xt6G+;xTil{ANGxn;TiNgI~{w8hJnbU@{ffQ=%NI0xK@A0NwHMK&dwjE zow5pBBRLv%hwK7g(ITHgnx|W(v7gt6W!5v~1iyXqYs@aNs>_hYR-quzE!ocOsA>7) zpEYm47ymd6f}%#s@9VFP>U4G2hE!ArrtdwBL`f!4={q~}DcbR#F)+j4vztrYMOpLZ zLb;Q;*m2QE!<)^|FrLXt1Of_9np&K-y5Tc0cyTwERQuQYsJ;a!!yc(~jt7aqY=!S) z8HureAw?k48Mb}}!h%-19+D+aE-TH0R2UAyF2DEQ@Wd5m@Fz_bT%k_USN&U8mz+Byg|WMX|h<9wd^`MeA-?%Y&rHrC8=z$R{j9oO#= zL%JqkKlP!CD!Kk4<(mdyK~0_Gkh}8#^7JI z=tktq0M4OI?do+cQe)JqBXvsK+Yc=S<89Sprp1@k{B41mh?V(}1wIWto9ONQ2st4hF@I^IF3~ng2yoQb_s%uYL3OR@%yezShEssDFCM z%|phBtaDq*XJMv9u7yAm*9`+wA;fvH6rAIHY{iCyW3=AZ&kv+BhCEeG#gtpM>BeTF zKobMx&r1O)$YKOss`fajMoj2XkO}6yEL~{JW;k}8k}Z~eHqMP>5AU&MD)FAx{hrOp zgWQEoTY};&k><#MS!kcxyqYcpJX4$|u~FGvpH3+-M{v{ldXlvH_U4P~c+D6wt_nr; zdcqOe)1hnd)|b(G{&Wx63vKy$>dFN<3<{cdDhV`pPA+#qta(TsL+65=Ff&Wq(#Mwf zh(FIM@2zcF-4So_1~j6gt{YlEICHD;WW>nO7*_p#y5qRuJ3al#%F^rhFlg0&8N|s9 ze0ChZmbgX?_>{9+gptl`>sL|yh=<{^Vhk2rYEalN{CISMN1aI0>c31mI%0FZ-Yi7d z&8PmW`2hzlab3h^wz|lXf&@zPDNfEu3J;aA4MYT#ZqZQKmdf@p;4!Vmt6>@+4oyPg zTetByESs#tmKDomX(GXU+^Gryuw(JtDjwcl7g-ru<~A^*JT%@?+^ZSa&1d9a@i}rXqpQ*TvWHDx786BG*xlIJi#vy}8dY3(^D}bVmw=!+vi);d9f0gxa^_dH z$-wOQ)?b@X#BhM#h@5jHV^`j3X)^zeTFj!GIaLyBNP?6B-X^6&LvBaW%#gRN# z5Mw>2tag?n8m3;&Ai+P<3g zb!mQMFX6FY1Yn8DO#D%m;%%%motC7@Z&!`s->YQ!`^cGYSv%Ea82R1I*p?)pbY{)+ct_vcLSggsZMUqsQs*5_F@ZIyt(% z!)Vc9XweOM09AS`?);1EyZsWvjvpDuuc1q0*%ZXYie5iqjREwDtkz%d8@=bQR?kvf zB|i1#M?b&U!;k9x(Cv-Q1CUo4=8&-xbx-b ziw1y%#E~bGMhZba*}A%9|G;~x1|8+=qFboLGd7;@MC6~W8b+tyhBbm-VaYN6Io`^_ zG{+a{-(PtAZTy~GnP1o9q?xbz4!LGl+x8bWnBjT4`SUMLty9HkM%`y2AYPj)2f!r- zV8;0DJl!~+0Gl9eh#WD#-FkK!dLZf|<^Y6CSQ+YE#4HF1E`3+o zRBjpn+?u9AyAL{PG$(fx!c3w5I2>B)Xn-S{45Wtxmb65&c{RJF)pfrtcRK}c?JQ8BR@%-%ooLo)?Y4IN@Dn-)~^mg(NlcKC7GBx zMRPD^_qUcF^z-pVN2`6qQP+Lbt1mCOGt!4qbM0CSA2Y~Z5I8!#3|&#rHNr4`6X2_f zs+s`HUgVH5?n)&Ecan&;RoK`Zd1qKY3(oK873 zPR^m1U)!t1w7*443ikiRx-&#OZZf%rm@Vi6T3SX6(nv_xC%W?ityeSE?-g8!`39BT zV0Crh6RP_q7Iw~+sPC5EpGDxUv-4#0bdvHozxiZ57l$dy@WI{v(>$ese}ptKGHIJ5L9=Gm*!6W1PmJdzL5i^~Clz2(nVOs7e;msnzXGodEEGRp_nKi{f@ZFqsrS zjQ^eZ{+*to0kM0D`R=-BHb;(;>kV|Ng8K*im04*kB1U~}b_-861st?{7i3VISTq;r z9f10W%T4~8IQC+fv!E6TctzDN%8v%nTy--~T*k;LXQBEV| zt(mLWQK@P04uNC%Ev3gvKP8U7N6Qon8B=jN+`eMa`O=2(*Edh59hC@$sLMIo$~v-j zfvN_N4MODrC{D|CwIk+&a%p1XATMtU#=qM1$Ic?C-PsJ16a*Vt33)2@i5peO>W1Q| z4yvw(4XX@{DB=!XoB8?^cK9}eK>my)4W9n~gbGV-#I@Nt5fFmA#rDL)e7 zUV8%%!lUzfOpNi0aLSOMgu_bHWG-nK%D_Du!CEa7E=k|T=Vg`cw z0{HoX=EuAOi)>Y6R?a%Qx?f1~YME}z)Y>GR-Cgh_aLVAugP(#fWI2bfzo8zf!C&89 z#k|paukxG%$`tBC^7fx$JN*u&t{zz_q&GAi9rd$OAB{ntJ;-2`7@=HW(5cjBW;c`A z`@wv%sku3dPC+zpEDOb$dp~QZHvDG}cIJ6>7@af|#bTCYsjM0UtfIe($w5`S)oQfG z)P4j3two{!*~?HK++SeP-@iAXZ0DABCp{$=E_x*ms`_)R)CB^x!nhgGy)kYM7=f*q zsp(ify>?9$(UFn16^Hw_TP}k5N*I^>rMA7KMt>s`&IpY086kCv? zfl=A_A2zY#I1ZX;=0e_V8&0uJkWtEwKiSo#3!#LzVbi=jL2mzaLoH)=@8axkap-4b zsRaoXfp5lKhT21!oT*))=F&ky*JjsZ&mAX4v|0=YanlhYjam^O8b?W*!3EN0@g|S0 z(}-6fDdKv2OhCxzXV&iJ!}qH4=aG^{Yat;villn6OS)Q`hL<>1le?fQPj5PHR{Glp z16|#BH<{F&^9;u7f%^j!bw=6^O$<$^`%D-0Tvja@fgI4F5S&`jj&gh~T6h zej#-@mHlerZuc?SN%xg*&Ek|qmBot}OgsFiOej(xWQm$JQ5D%hV+}y8;}8cf>>%=l zmEyWkZOFnfPG37L4aHIPlR=r@IKeL7?PLT=0?69kly?Xy5B{Ewye|e@51EA7|#!^|ol2(oA><_b)z(wj{&?E1$0`KOr z8fN+)s$*LST*{ZNs6}s|5&JA2zsXew;OhbC^e7+*f$7N!>zENlavFETte3&M$?o0~ z@;*I`>%D^+2uK$iOPuu2HA|>^LO;LjD%$(?@F1Zgi>4-S= zL@9x9H7@Dt;X=ph?y{I@@c;m6UD@{{g1{n_Ow;9=0l~}JSME28?oPQcnAwDz zZwWnh%N zeFat_bvd^zgyQ1cQRuk!OluNzst?lJ~b+)rXQ+{wqq^Yx&JT#*s zy{=;TGpprCz(qWY*|_dBF1=K>YK+C78IOLkO$bc=mK~ATm{Oi}uzr$las6UL=+THz zGq}lOt01NEIK`8qqgGz`!LV_=B#x^r>}I>a6`)d?e2?Nk33O0Yp1Kwpw}cBT<>)YJ zr6BU~F5Wm`Wh5sf2>xIgcnPC-5R8_N-PLp<+|u~)^*8y^Edq8of3?3~4x+8C*&$ zqDS-o>U#uYm%}t#Y5d>a4~^v<=Eg=%HC?Xz8aKGsIb}Yg5KA-aTcO4^6sr4Jr00_V zYjM4+m|a1)h~hNu8P_y;LbH7_UHjK2P8C&It7+}t2R72l%*|Lji}sT@Nd{AAiZh=a zMW*dvA|4xER9DyK_(GmtqpjTD(g)ToT3ugi(e4J#CUcoEoU()-msuTyj6pJ5QzOh-?!`A*OAhzeKS`ugg$*5ID2M(Suni z8+VwKi5C2}!>&F{`_@A-{Rf zcJMM)3)rpM#?7;9pUfRGQCa^a$2DZI563-sGg<0%Juby)s%gXZ~mQ~GQYSuYC_mP zI%c>BS1$TFbXM<>{N>=Xyy|NoN`nGaT9Ua*qSfugDqBC~DMo0?r-77`0U05_dzr`AXu9)BUu*AYX^8^#q9pShTVoXOYZmgQJl^^7zZM5RB9vF3V zPd_I3rng^|JuR!>?u#%|)PEj6L3e>?UIIjnj%;J6jBjlw1gvjhH!kP5wS{3nB1~o- zox12(Ds}AK7T?qPo8mjT$;c*W;wcZ0VVm}_ev4LHOMcH{9YeKv@H>n^uzLF*y=>tR zkta>04kiWpR@F|O5^V~Y-^s;scN1z>_WpqFQYnTbt)T1Nbni-c}A2%zE`z&X#iB6(3Hb>K&UBIb?H5>u9~#&MKhexjuFA+`DS5 zGk7J~?ZpT&8dfR*fP?tg3y_-7dmgc%N|mkjcK!Ho!#Kir)M3eYHPyriT|m@9O*gB= z|Dxn2lfXrI6<`zC3RM-oXr`YKb~gsi@t7!j=?xtdzGzW5tBh^wBltS&H8DTgySAuq zh&rruIGcnW|6{^F6 zkl_fySqo7z5%MB|+6KqJ7Dl*`=RiTU*!l9K3y8X(8ABsacK@9(sVW1Pz~-M>2FBI7 zCp%Ccd|8f?!qthcw{+@^Ioj>b!3?c}uJgOCH|@A;%|*_mx}~5}DdNJaI!@Ctu1 zNJ%3Z>=XuY!b|QKtfHbb6i3VR^S={=R(eQ}^z+QM$qL@a%;7qi{;HXp1)9sEML|-2 z2VUl2do0q34*XdKCzH9|6i+ECR&BR@aH&kWdU$veE72N~#?2rgub`bWTW)Zvlh44+ zA+Q~7>blSl89V8;ZZ!zbW$$6@`!hK+4i}((@9&G5S3@!|sz$g}$GAB~U{w(9annJw6cJ(ezk6&z1QSn86CFiXW2UgAp`&1OKX$F8dIvG<< z*tzU<&RvZkBl9DQIZgRPuHD(gljYSg zp1lXgq3;7f%3?@>6~>f2ssjEnQ;cMHetsgXB`A)A;laW2Y+lp!_l5((#KZ=v?W4ZS zo=&Erl-j~12Qj3Q0(yiNySSjS*f z83vo&46NT5Vzk}QbDCCW7jCPUl%&nq7bWY1e{O3!Frc0I@nv}_y4%Ys3@3X<0BA*V zkrc;R^Zv{1L^u#g>HrROL$1=PY4enu)b5Y}xitRz!I4u<>tTED zLLVyUCGbpFS7})74Hqzwj-cV(-PPo2St)fmGd}rf_|$-sXI$1M<4SRS8rfCzc!T%V z)X~M%>_Og-ZUu*qM}}dj`FC^g+b04fM7g-x>zW#6 zpWZL@{iL!FNUkc~G9_8YLQSC@k@{@8HgUOI^uv#> z0iETRs2cZ`zzy8=Y%-}01n8E`DEWo=i&2jSL`0LuXpI>S=FhCK*tYb5~|2V##~ls9wK&#R9+rquKWi zwNFH(y`m!`4)+jSE)<$>wHorW{A+MUBy!+q1D41n^-AAt9viofmSC4SJ^ocZ;uP9K92H(Wc35ZV!`OHVWUBLWA1`5NveRHt7? zJn-%_%6S7&?KmI`KgT->@WBNgMgW>!^y-d+He#;apkazc>xZr%sx+a)q9?xnrpQ=F zpVw5rewR!F2*HLbSHC5O67>#WlxVm0zxA^zNI^mD%ueNM9b#x4vZG|h=xlP5l^F8j zkoijygs|6G4kJ|f9KNgiv4!i(P|yE|NrxLIwco{i^IipBZ*6Y17HIUuBm6oN2j5M7A+J}|xp9A_uY|`YA2W!9LQIhu zrSKV944tskiFgDLoCiphen;|?D24trj?kO8?gw1J$P!vN=KjZ#p`#CfSKdqpH=?&R zc^nHlasv%Byo_dQQ&7k6J?CU!5&!Ms#E*^}H+!2Q`{CO#I+5C(x-zPF*g`!8*kN#o zeY)c%^CL(w54Z5cW(k7Z$i??ZZO93#ol31!d%aJ_h*Yswi8-fNIIBA340Y^|Dtw&Q zhd@9xQKW;izwt*{W<%=k{FWTbT#2y+@SdQaMDVR#oA*wo=tKeDB`QGf*ZWX6ck$H^ zBm?Y&%NxEeDgR^Q2+{4Nq-0m|s=+-hA};X9{-_o?K-pavE)US5_e+=p##?u7&FOg! zR^fG82kqAhj^0b(EET%BP=S72(s1I1vzSM~y~N_4=mdJTflI-MNWm9O*PlZ)xVRga zAs-Y%E}BfX@L{hx@yBI7#ByhYA#xL`5;I+e1BsDa<#pvaE^Mk0mz9i~5{s~lC}QyWc;eN1l}LO2ni1%U zn}KbNPAfS^Se1{%UNX5I?p>xlsA}N4THGsI5awuzee6k=oGk1iM9q%&F^;Q8rm(V9 z