diff --git a/.gitignore b/.gitignore index 2cf37b67d..846f050b9 100755 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,13 @@ generated_proto/ terminal.*/ BlockSettleHW/ledger/hidapi/* GUI/QtWidgets/ui/* + +*.autosave + +# dvajdual test environment +GUI/QtQuick/main.cpp +GUI/QtQuick/qml/main_test.qml +GUI/QtQuick/qml.qrc +GUI/QtQuick/qml_test.pro +GUI/QtQuick/qml_test.pro.user + diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..27ef0bb60 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,75 @@ +{ + "files.associations": { + "map": "cpp", + "iosfwd": "cpp", + "xlocbuf": "cpp", + "ios": "cpp", + "type_traits": "cpp", + "xstring": "cpp", + "xutility": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "*.tcc": "cpp", + "bitset": "cpp", + "cctype": "cpp", + "chrono": "cpp", + "cinttypes": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "compare": "cpp", + "concepts": "cpp", + "condition_variable": "cpp", + "csignal": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "ctime": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "list": "cpp", + "set": "cpp", + "string": "cpp", + "unordered_map": "cpp", + "unordered_set": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "numeric": "cpp", + "optional": "cpp", + "random": "cpp", + "ratio": "cpp", + "regex": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "utility": "cpp", + "fstream": "cpp", + "future": "cpp", + "initializer_list": "cpp", + "iomanip": "cpp", + "iostream": "cpp", + "istream": "cpp", + "limits": "cpp", + "mutex": "cpp", + "new": "cpp", + "numbers": "cpp", + "ostream": "cpp", + "semaphore": "cpp", + "sstream": "cpp", + "stdexcept": "cpp", + "stop_token": "cpp", + "streambuf": "cpp", + "thread": "cpp", + "typeinfo": "cpp", + "variant": "cpp" + } +} \ No newline at end of file diff --git a/BlockSettleApp/CMakeLists.txt b/BlockSettleApp/CMakeLists.txt index 315ab5303..d92582688 100644 --- a/BlockSettleApp/CMakeLists.txt +++ b/BlockSettleApp/CMakeLists.txt @@ -19,12 +19,16 @@ INCLUDE_DIRECTORIES( ${BLOCKSETTLE_UI_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${BS_NETWORK_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${BS_COMMON_ENUMS_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${BS_COMMUNICATION_INCLUDE_DIR} ) +INCLUDE_DIRECTORIES( ${BS_HW_LIB_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${WALLET_LIB_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${CRYPTO_LIB_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${BOTAN_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${Qt5Svg_INCLUDE_DIRS} ) IF ( APPLE ) + include(CodeSign) + include(Notarize) + SET( BUNDLE_NAME "BlockSettle Terminal" ) SET( MACOSX_BUNDLE_BUNDLE_NAME ${BUNDLE_NAME} ) @@ -40,18 +44,51 @@ IF ( APPLE ) SET_SOURCE_FILES_PROPERTIES( ${BS_TERMINAL_ICNS_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION Resources) ADD_EXECUTABLE( ${BLOCKSETTLE_APP_NAME} MACOSX_BUNDLE ${SOURCES} ${HEADERS} ${BS_TERMINAL_ICNS_FILE}) SET_TARGET_PROPERTIES( ${BLOCKSETTLE_APP_NAME} PROPERTIES OUTPUT_NAME "${BUNDLE_NAME}" ) + + code_sign_is_enabled(can_sign) + if (can_sign) + set(FILES_TO_SIGN "${EXECUTABLE_OUTPUT_PATH}/${BUNDLE_NAME}.app") + configure_file(../common/build_scripts/CMakeModules/CodeSignScript.cmake.in sign_app.cmake) + list(APPEND postpkg + COMMAND "${CMAKE_COMMAND}" -E echo "Code signing ${BUNDLE_NAME}.app" + COMMAND "${CMAKE_COMMAND}" -P "sign_app.cmake" + ) + add_custom_target(sign + ${postpkg} + WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}" + VERBATIM + USES_TERMINAL + ) + endif () + +ELSEIF(UNIX AND NOT APPLE) + ADD_EXECUTABLE( ${BLOCKSETTLE_APP_NAME} ${SOURCES} ${HEADERS} ) + IF(BUILD_APPIMAGE) + configure_file(../common/build_scripts/CMakeModules/AppImageScript.cmake.in appimage.cmake) + ADD_CUSTOM_COMMAND(TARGET ${BLOCKSETTLE_APP_NAME} POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E echo "Building ${BUNDLE_NAME}.AppImage" + COMMAND "${CMAKE_COMMAND}" -P "appimage.cmake" + ) + ENDIF() + ELSEIF ( WIN32 ) ADD_EXECUTABLE( ${BLOCKSETTLE_APP_NAME} WIN32 ${SOURCES} ${HEADERS} blocksettle.rc ) + ELSE () ADD_EXECUTABLE( ${BLOCKSETTLE_APP_NAME} ${SOURCES} ${HEADERS} ) + ENDIF () +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath='$ORIGIN'") + TARGET_LINK_LIBRARIES( ${BLOCKSETTLE_APP_NAME} ${TERMINAL_CORE_NAME} + ${BLOCKSETTLE_HW_LIBRARY_NAME} ${TERMINAL_GUI_QT_NAME} ${TERMINAL_GUI_QTQUICK_NAME} ${BLOCKSETTLE_UI_LIBRARY_NAME} ${BS_NETWORK_LIB_NAME} + ${CURL_LIB} ${CPP_WALLET_LIB_NAME} ${CRYPTO_LIB_NAME} ${BOTAN_LIB} diff --git a/BlockSettleApp/blocksettle.ico b/BlockSettleApp/blocksettle.ico index 6790f065c..3b9b2b731 100644 Binary files a/BlockSettleApp/blocksettle.ico and b/BlockSettleApp/blocksettle.ico differ diff --git a/BlockSettleApp/macosapp.cpp b/BlockSettleApp/macosapp.cpp new file mode 100644 index 000000000..728f06af7 --- /dev/null +++ b/BlockSettleApp/macosapp.cpp @@ -0,0 +1,26 @@ +#include +#include +#include "macosapp.h" + +MacOsApp::MacOsApp(int &argc, char **argv) : QApplication(argc, argv) +{ + // EMPTY CONSTRUCTOR +} +bool MacOsApp::event(QEvent* ev) +{ + if (ev->type() == QEvent::ApplicationStateChange) { + auto appStateEvent = static_cast(ev); + + if (appStateEvent->applicationState() == Qt::ApplicationActive) { + if (activationRequired_) { + emit reactivateTerminal(); + } else { + activationRequired_ = true; + } + } else { + activationRequired_ = false; + } + } + + return QApplication::event(ev); + } diff --git a/BlockSettleApp/macosapp.h b/BlockSettleApp/macosapp.h new file mode 100644 index 000000000..cecaa550e --- /dev/null +++ b/BlockSettleApp/macosapp.h @@ -0,0 +1,26 @@ +#ifndef __MACOSAPP_H__ +#define __MACOSAPP_H__ + +#include +#include +#include + +class MacOsApp : public QApplication +{ + Q_OBJECT +public: + using QApplication::QApplication; + MacOsApp(int &argc, char **argv); // : QApplication(argc, argv) {} +// ~MacOsApp() override = default; + +signals: + void reactivateTerminal(); + +protected: + bool event(QEvent* ev) override; + +private: + bool activationRequired_ = false; +}; + +#endif diff --git a/BlockSettleApp/main.cpp b/BlockSettleApp/main.cpp index ebc4e6579..b26271030 100644 --- a/BlockSettleApp/main.cpp +++ b/BlockSettleApp/main.cpp @@ -29,6 +29,7 @@ #include "ApiJson.h" #include "AssetsAdapter.h" #include "BsServerAdapter.h" +#include "hwdevicemanager.h" #include "QtGuiAdapter.h" #include "QtQuickAdapter.h" #include "SettingsAdapter.h" @@ -36,6 +37,8 @@ #include #include +#include "macosapp.h" + //#include "AppNap.h" #ifdef USE_QWindowsIntegrationPlugin @@ -61,16 +64,19 @@ Q_IMPORT_PLUGIN(QICOPlugin) Q_IMPORT_PLUGIN(QtQuick2PrivateWidgetsPlugin) #endif +Q_IMPORT_PLUGIN(QtQmlPlugin) Q_IMPORT_PLUGIN(QtQuick2Plugin) Q_IMPORT_PLUGIN(QtQuick2WindowPlugin) Q_IMPORT_PLUGIN(QtQuickControls2Plugin) +Q_IMPORT_PLUGIN(QtQuick2DialogsPlugin) +Q_IMPORT_PLUGIN(QtQuick2DialogsPrivatePlugin) Q_IMPORT_PLUGIN(QtQuickTemplates2Plugin) -//Q_IMPORT_PLUGIN(QtQuickControls1Plugin) +Q_IMPORT_PLUGIN(QtQuickControls1Plugin) Q_IMPORT_PLUGIN(QtQuickLayoutsPlugin) Q_IMPORT_PLUGIN(QtQmlModelsPlugin) Q_IMPORT_PLUGIN(QmlFolderListModelPlugin) Q_IMPORT_PLUGIN(QmlSettingsPlugin) -//Q_IMPORT_PLUGIN(QtLabsPlatformPlugin) +Q_IMPORT_PLUGIN(QtLabsPlatformPlugin) #endif // STATIC_BUILD Q_DECLARE_METATYPE(ArmorySettings) @@ -86,7 +92,7 @@ Q_DECLARE_METATYPE(UTXO) #include #include - +/* class MacOsApp : public QApplication { Q_OBJECT @@ -120,6 +126,7 @@ class MacOsApp : public QApplication private: bool activationRequired_ = false; }; +*/ static void checkStyleSheet(QApplication &app) { @@ -204,6 +211,8 @@ int main(int argc, char** argv) //inprocBus.addAdapter(std::make_shared(logMgr->logger())); inprocBus.addAdapterWithQueue(std::make_shared(logMgr->logger() , userWallets, signAdapter->createClient(), userBlockchain), "wallets"); + inprocBus.addAdapterWithQueue(std::make_shared( + logMgr->logger()), "wallets"); inprocBus.addAdapter(std::make_shared(logMgr->logger("bscon"))); //inprocBus.addAdapter(std::make_shared(logMgr->logger("match"))); //inprocBus.addAdapter(std::make_shared(logMgr->logger("settl"))); @@ -211,7 +220,7 @@ int main(int argc, char** argv) //inprocBus.addAdapter(std::make_shared(logMgr->logger("mdh"))); //inprocBus.addAdapter(std::make_shared(logMgr->logger("chat"))); inprocBus.addAdapterWithQueue(std::make_shared(logMgr->logger() - , userBlockchain), /*"blkchain_conn"*/"signer"); + , userBlockchain), "signer"); if (!inprocBus.run(argc, argv)) { logMgr->logger()->error("No runnable adapter found on main inproc bus"); diff --git a/BlockSettleHW/CMakeLists.txt b/BlockSettleHW/CMakeLists.txt index f6efd92cb..bc6515cf5 100644 --- a/BlockSettleHW/CMakeLists.txt +++ b/BlockSettleHW/CMakeLists.txt @@ -11,6 +11,7 @@ PROJECT( ${BLOCKSETTLE_HW_LIBRARY_NAME} ) SET(BLOCKSETTLE_HW_ROOT ${CMAKE_CURRENT_SOURCE_DIR}) +ADD_DEFINITIONS(-DBUILD_HW_WALLETS) file(COPY ${THIRD_PARTY_COMMON_DIR}/hidapi/hidapi.h DESTINATION ${BLOCKSETTLE_HW_ROOT}/ledger/hidapi) @@ -22,6 +23,7 @@ FILE(GLOB SOURCES trezor/*.cpp ledger/*.cpp ledger/hidapi/*.c + jade/*.cpp ) FILE(GLOB HEADERS @@ -29,11 +31,14 @@ FILE(GLOB HEADERS trezor/*.h ledger/*.h ledger/hidapi/*.h + jade/*.h ) INCLUDE_DIRECTORIES( ${WALLET_LIB_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${BS_NETWORK_INCLUDE_DIR} ) INCLUDE_DIRECTORIES( ${COMMON_LIB_INCLUDE_DIR} ) +INCLUDE_DIRECTORIES(${BLOCK_SETTLE_ROOT}/Core) +INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR}) if (UNIX) INCLUDE_DIRECTORIES( ${LIBUSB_INCLUDE_DIR} ) @@ -57,19 +62,13 @@ FUNCTION(GENERATE_PROTO IN_DIR OUT_DIR) SET(PROTO_SOURCE_FILES ${PROTO_SOURCE_FILES} ${PROTO_SOURCE_FILE}) SET(PROTO_HEADER_FILES ${PROTO_HEADER_FILES} ${PROTO_HEADER_FILE}) - ADD_CUSTOM_COMMAND(OUTPUT ${PROTO_SOURCE_FILE} - OUTPUT ${PROTO_HEADER_FILE} - DEPENDS ${PROTO_FILE} - COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ${PROTO_FILE} --cpp_out=${OUT_DIR} --proto_path=${IN_DIR} - WORKING_DIRECTORY ${BLOCKSETTLE_HW_ROOT} - ) + EXECUTE_PROCESS(COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} ${PROTO_FILE} --cpp_out ${OUT_DIR} -I ${IN_DIR} --proto_path ${IN_DIR}) ENDFOREACH(PROTO_FILE) SET(GENERATED_SOURCE_FILES ${GENERATED_SOURCE_FILES} ${PROTO_SOURCE_FILES} PARENT_SCOPE) SET(GENERATED_HEADER_FILES ${GENERATED_HEADER_FILES} ${PROTO_HEADER_FILES} PARENT_SCOPE) SET(GENERATED_INCLUDE_DIRS ${GENERATED_INCLUDE_DIRS} ${OUT_DIR} PARENT_SCOPE) - ENDFUNCTION(GENERATE_PROTO) GENERATE_PROTO("${THIRD_PARTY_COMMON_DIR}/trezorCommon" "${BLOCKSETTLE_HW_ROOT}/trezor/generated_proto") @@ -84,7 +83,6 @@ TARGET_LINK_LIBRARIES(${BLOCKSETTLE_HW_LIBRARY_NAME} ${PROTO_LIB} Qt5::Core Qt5::Network + Qt5::SerialPort ${OS_SPECIFIC_LIBS} ) - - diff --git a/BlockSettleHW/hwcommonstructure.cpp b/BlockSettleHW/hwcommonstructure.cpp deleted file mode 100644 index 5243a6dfd..000000000 --- a/BlockSettleHW/hwcommonstructure.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - -*********************************************************************************** -* Copyright (C) 2020 - 2021, BlockSettle AB -* Distributed under the GNU Affero General Public License (AGPL v3) -* See LICENSE or http://www.gnu.org/licenses/agpl.html -* -********************************************************************************** - -*/ - -#include "hwcommonstructure.h" -using namespace bs::hd; - -Path getDerivationPath(bool testNet, Purpose element) -{ - Path path; - path.append(hardFlag | element); - path.append(testNet ? CoinType::Bitcoin_test : CoinType::Bitcoin_main); - path.append(hardFlag); - return path; -} - -bool isNestedSegwit(const bs::hd::Path& path) -{ - return path.get(0) == (bs::hd::Purpose::Nested | bs::hd::hardFlag); -} - -bool isNativeSegwit(const bs::hd::Path& path) -{ - return path.get(0) == (bs::hd::Purpose::Native | bs::hd::hardFlag); -} - -bool isNonSegwit(const bs::hd::Path& path) -{ - return path.get(0) == (bs::hd::Purpose::NonSegWit | bs::hd::hardFlag); -} diff --git a/BlockSettleHW/hwcommonstructure.h b/BlockSettleHW/hwcommonstructure.h deleted file mode 100644 index 6448d7ed4..000000000 --- a/BlockSettleHW/hwcommonstructure.h +++ /dev/null @@ -1,96 +0,0 @@ -/* - -*********************************************************************************** -* Copyright (C) 2020 - 2021, BlockSettle AB -* Distributed under the GNU Affero General Public License (AGPL v3) -* See LICENSE or http://www.gnu.org/licenses/agpl.html -* -********************************************************************************** - -*/ -#ifndef HWCOMMONSTRUCTURE_H -#define HWCOMMONSTRUCTURE_H - -#include -#include -#include -#include -#include -#include "CoreWallet.h" -#include "HDPath.h" - -using AsyncCallBack = std::function; -using AsyncCallBackCall = std::function; - -// There is no way to determinate difference between ledger devices -// so we use vendor name for identification -const std::string kDeviceLedgerId = "Ledger"; - -struct DeviceData -{ - QByteArray path_ = {}; - QByteArray vendor_ = {}; - QByteArray product_ = {}; - QByteArray sessionId_ = {}; - QByteArray debug_ = {}; - QByteArray debugSession_ = {}; -}; - -enum class DeviceType { - None = 0, - HWLedger, - HWTrezor -}; - -struct DeviceKey -{ - QString deviceLabel_; - QString deviceId_; - QString vendor_; - QString walletId_; - QString status_; - - DeviceType type_ = DeviceType::None; -}; - -class HwWalletWrapper { - Q_GADGET -public: - bs::core::wallet::HwWalletInfo info_; - Q_INVOKABLE QString walletName() { - return QString::fromStdString(info_.label); - } - Q_INVOKABLE QString walletDesc() { - return QString::fromStdString(info_.vendor); - } - bool isValid() { - return !info_.xpubRoot.empty() && - !info_.xpubNestedSegwit.empty() && - !info_.xpubNativeSegwit.empty() && - !info_.xpubLegacy.empty(); - } - - bool isFirmwareSupported_{true}; - std::string firmwareSupportedMsg_; -}; -Q_DECLARE_METATYPE(HwWalletWrapper) - -struct HWSignedTx { - std::string signedTx; -}; -Q_DECLARE_METATYPE(HWSignedTx) - -bs::hd::Path getDerivationPath(bool testNet, bs::hd::Purpose element); -bool isNestedSegwit(const bs::hd::Path& path); -bool isNativeSegwit(const bs::hd::Path& path); -bool isNonSegwit(const bs::hd::Path& path); - -namespace HWInfoStatus { - const QString kPressButton = QObject::tr("Confirm transaction output(s) on your device"); - const QString kTransaction = QObject::tr("Loading transaction to your device...."); - const QString kReceiveSignedTx = QObject::tr("Receiving signed transaction from device...."); - const QString kTransactionFinished = QObject::tr("Transaction signing finished with success"); - const QString kCancelledByUser = QObject::tr("Cancelled by user"); -} - -#endif // HWCOMMONSTRUCTURE_H diff --git a/BlockSettleHW/hwdeviceinterface.h b/BlockSettleHW/hwdeviceinterface.h index 5191b9c09..99144634e 100644 --- a/BlockSettleHW/hwdeviceinterface.h +++ b/BlockSettleHW/hwdeviceinterface.h @@ -11,10 +11,9 @@ #ifndef HWDEVICEABSTRACT_H #define HWDEVICEABSTRACT_H -#include "hwcommonstructure.h" -#include -#include -#include +#include +#include "CoreWallet.h" +#include "SecureBinaryData.h" namespace bs { namespace core { @@ -24,56 +23,70 @@ namespace bs { } } -class HwDeviceInterface : public QObject -{ - Q_OBJECT - -public: - HwDeviceInterface(QObject* parent = nullptr) - : QObject(parent) {} - ~HwDeviceInterface() override = default; - - virtual DeviceKey key() const = 0; - virtual DeviceType type() const = 0; - - // lifecycle - virtual void init(AsyncCallBack&& cb = nullptr) = 0; - virtual void cancel() = 0; - virtual void clearSession(AsyncCallBack&& cb = nullptr) = 0; - - // operation - virtual void getPublicKey(AsyncCallBackCall&& cb = nullptr) = 0; - virtual void signTX(const bs::core::wallet::TXSignRequest& reqTX, AsyncCallBackCall&& cb = nullptr) = 0; - virtual void retrieveXPubRoot(AsyncCallBack&& cb) = 0; - - // Management - virtual void setMatrixPin(const std::string& pin) {}; - virtual void setPassword(const std::string& password, bool enterOnDevice) {}; - - // State - virtual bool isBlocked() = 0; - virtual QString lastError() { return {}; }; - - // xpub root - bool inited() { - return !xpubRoot_.empty(); - } - -signals: - // operation result informing - void publicKeyReady(); - void deviceTxStatusChanged(QString status); - void operationFailed(QString reason); - void requestForRescan(); - - // Management - void requestPinMatrix(); - void requestHWPass(bool allowedOnDevice); - void cancelledOnDevice(); - void invalidPin(); - -protected: - std::string xpubRoot_; -}; +namespace bs { + namespace hww { + enum class DeviceType { + Unknown, + HWLedger, + HWTrezor, + HWJade + }; + + struct DeviceKey + { + std::string label; + std::string id; + std::string vendor; + std::string walletId; + DeviceType type{ DeviceType::Unknown }; + }; + + class DeviceInterface + { + public: + virtual DeviceKey key() const = 0; + virtual DeviceType type() const = 0; + + // lifecycle + virtual void init() = 0; + virtual void cancel() = 0; + virtual void clearSession() = 0; + + // operation + virtual void getPublicKeys() = 0; + virtual void signTX(const bs::core::wallet::TXSignRequest& reqTX) = 0; + virtual void retrieveXPubRoot() = 0; + + // Management + virtual void setMatrixPin(const SecureBinaryData& pin) {} + virtual void setPassword(const SecureBinaryData& password, bool enterOnDevice) {} + + virtual void setSupportingTXs(const std::vector&) {} + + // State + virtual bool isBlocked() const = 0; + virtual std::string lastError() const { return {}; }; + + virtual bool inited() + { + return !xpubRoot_.empty(); + } + + // operation result informing + virtual void publicKeyReady() = 0; + virtual void deviceTxStatusChanged(const std::string& status) = 0; + virtual void operationFailed(const std::string& reason) = 0; + virtual void requestForRescan() = 0; + + // Management + virtual void cancelledOnDevice() = 0; + virtual void invalidPin() = 0; + + protected: + BinaryData xpubRoot_; + }; + + } //hw +} //bs #endif // HWDEVICEABSTRACT_H diff --git a/BlockSettleHW/hwdevicemanager.cpp b/BlockSettleHW/hwdevicemanager.cpp index 88396ca11..3dbb20dd9 100644 --- a/BlockSettleHW/hwdevicemanager.cpp +++ b/BlockSettleHW/hwdevicemanager.cpp @@ -9,387 +9,812 @@ */ #include "hwdevicemanager.h" -#include "trezor/trezorClient.h" -#include "trezor/trezorDevice.h" -#include "ledger/ledgerClient.h" +#include +#include "jade/jadeDevice.h" #include "ledger/ledgerDevice.h" -#include "ConnectionManager.h" +#include "trezor/trezorDevice.h" +#include "TerminalMessage.h" #include "Wallets/SyncWalletsManager.h" #include "Wallets/SyncHDWallet.h" #include "Wallets/ProtobufHeadlessUtils.h" -using namespace Armory::Signer; - -HwDeviceManager::HwDeviceManager(const std::shared_ptr& connectionManager, std::shared_ptr walletManager, - bool testNet, QObject* parent /*= nullptr*/) - : QObject(parent) - , logger_(connectionManager->GetLogger()) - , testNet_(testNet) +#include "common.pb.h" +#include "hardware_wallet.pb.h" +#include "terminal.pb.h" + +//using namespace Armory::Signer; +using namespace bs::hww; +using namespace BlockSettle::Common; +using namespace BlockSettle::Terminal; +using namespace BlockSettle; + +DeviceManager::DeviceManager(const std::shared_ptr& logger) + : logger_(logger) + , user_(std::make_shared(bs::message::TerminalUsers::HWWallets)) + , userWallets_(std::make_shared(bs::message::TerminalUsers::Wallets)) + , userSigner_(std::make_shared(bs::message::TerminalUsers::Signer)) + , userBlockchain_(std::make_shared(bs::message::TerminalUsers::Blockchain)) +{} + +DeviceManager::~DeviceManager() { - walletManager_ = walletManager; - trezorClient_ = std::make_unique(connectionManager, walletManager, testNet, this); - ledgerClient_ = std::make_unique(logger_, walletManager, testNet); - - model_ = new HwDeviceModel(this); + releaseConnection(); } -HwDeviceManager::~HwDeviceManager() -{ - releaseConnection(nullptr); -}; - -void HwDeviceManager::scanDevices() +bs::message::ProcessingResult DeviceManager::process(const bs::message::Envelope& env) { - if (isScanning_) { - return; + if (env.isRequest()) { + return processOwnRequest(env); } - - setScanningFlag(true); - - auto doneScanning = [this, expectedClients = 2, finished = std::make_shared(0)]() { - ++(*finished); - - if (*finished == expectedClients) { - scanningDone(); + else { + switch (env.sender->value()) { + case bs::message::TerminalUsers::Settings: + return processSettings(env); + case bs::message::TerminalUsers::Wallets: + return processWallet(env); + case bs::message::TerminalUsers::Signer: + return processSigner(env); + case bs::message::TerminalUsers::Blockchain: + return processBlockchain(env); + default: break; } - }; + } + return bs::message::ProcessingResult::Ignored; +} - ledgerClient_->scanDevices(doneScanning); - releaseConnection([this, doneScanning] { - trezorClient_->initConnection(true, [this, doneScanning]() { - doneScanning(); - }); - }); +bool DeviceManager::processBroadcast(const bs::message::Envelope& env) +{ + if (env.sender->isSystem()) { + AdministrativeMessage msg; + if (msg.ParseFromString(env.message)) { + if (msg.data_case() == AdministrativeMessage::kStart) { + start(); + return true; + } + } + } + switch (env.sender->value()) { + case bs::message::TerminalUsers::Wallets: + return (processWallet(env) != bs::message::ProcessingResult::Ignored); + case bs::message::TerminalUsers::Signer: + return (processSigner(env) != bs::message::ProcessingResult::Ignored); + default: break; + } + return false; } -void HwDeviceManager::requestPublicKey(int deviceIndex) +void DeviceManager::scanDevices(const bs::message::Envelope& env) { - auto device = getDevice(model_->getDevice(deviceIndex)); - if (!device) { + if (nbScanning_ > 0) { + logger_->warn("[{}] device scan is already in progress"); + HW::DeviceMgrMessage msg; + msg.mutable_available_devices(); + pushResponse(user_, env, msg.SerializeAsString()); return; } - - device->getPublicKey([this](QVariant&& data) { - emit publicKeyReady(data); - }); - - connectDevice(device); + envReqScan_ = env; + devices_.clear(); + nbScanning_ = 3; // # of callbacks to receive + ledgerClient_->scanDevices(); + trezorClient_->listDevices(); + jadeClient_->scanDevices(); } -void HwDeviceManager::setMatrixPin(int deviceIndex, QString pin) +void DeviceManager::setMatrixPin(const DeviceKey& key, const std::string& pin) { - auto device = getDevice(model_->getDevice(deviceIndex)); + auto device = getDevice(key); if (!device) { return; } - - device->setMatrixPin(pin.toStdString()); + device->setMatrixPin(SecureBinaryData::fromString(pin)); } -void HwDeviceManager::setPassphrase(int deviceIndex, QString passphrase, bool enterOnDevice) +void DeviceManager::setPassphrase(const DeviceKey& key, const std::string& passphrase + , bool enterOnDevice) { - auto device = getDevice(model_->getDevice(deviceIndex)); + auto device = getDevice(key); if (!device) { return; } - - device->setPassword(passphrase.toStdString(), enterOnDevice); + device->setPassword(SecureBinaryData::fromString(passphrase), enterOnDevice); } -void HwDeviceManager::cancel(int deviceIndex) +void DeviceManager::cancel(const DeviceKey& key) { - auto device = getDevice(model_->getDevice(deviceIndex)); + auto device = getDevice(key); if (!device) { return; } - device->cancel(); } -void HwDeviceManager::prepareHwDeviceForSign(QString walletId) +void DeviceManager::start() { - auto hdWallet = walletManager_->getHDWalletById(walletId.toStdString()); - assert(hdWallet->isHardwareWallet()); - auto encKeys = hdWallet->encryptionKeys(); - bs::wallet::HardwareEncKey hwEncType(encKeys[0]); - - if (bs::wallet::HardwareEncKey::WalletType::Ledger == hwEncType.deviceType()) { - ledgerClient_->scanDevices([caller = QPointer(this), walletId]() { - if (!caller) { - return; - } - - auto devices = caller->ledgerClient_->deviceKeys(); - if (devices.empty()) { - caller->lastOperationError_ = caller->ledgerClient_->lastScanError(); - caller->deviceNotFound(QString::fromStdString(kDeviceLedgerId)); - return; - } + logger_->debug("[hww::DeviceManager::start]"); + SettingsMessage msg; + auto msgReq = msg.mutable_get_request(); + auto setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_NetType); + setReq->set_type(SettingType_Int); + + pushRequest(user_, std::make_shared(bs::message::TerminalUsers::Settings) + , msg.SerializeAsString()); +} - bool found = false; - DeviceKey deviceKey; - for (auto Key : devices) { - if (Key.walletId_ == walletId) { - deviceKey = Key; - found = true; - break; - } - } +bs::message::ProcessingResult DeviceManager::processPrepareDeviceForSign(const bs::message::Envelope& env + , const std::string& walletId) +{ + logger_->debug("[{}] {}", __func__, walletId); + WalletsMessage msg; + msg.set_hd_wallet_get(walletId); + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + prepareDeviceReq_[msgId] = {walletId, env}; + return bs::message::ProcessingResult::Success; +} - if (!found) { - if (!devices.isEmpty()) { - caller->lastOperationError_ = caller->getDevice(devices.front())->lastError(); - } +bs::message::ProcessingResult DeviceManager::processOwnRequest(const bs::message::Envelope& env) +{ + if (!trezorClient_ || !ledgerClient_) { + return bs::message::ProcessingResult::Retry; + } + HW::DeviceMgrMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[hww::DeviceManager::processOwnRequest] failed to parse #{}" + , env.foreignId()); + return bs::message::ProcessingResult::Error; + } + switch (msg.data_case()) { + case HW::DeviceMgrMessage::kStartScan: + scanDevices(env); + return bs::message::ProcessingResult::Success; + case HW::DeviceMgrMessage::kImportDevice: + return processImport(env, msg.import_device()); + case HW::DeviceMgrMessage::kSignTx: + return processSignTX(env, msg.sign_tx()); + case HW::DeviceMgrMessage::kSetPin: + return processSetPIN(msg.set_pin()); + case HW::DeviceMgrMessage::kSetPassword: + return processSetPassword(msg.set_password()); + case HW::DeviceMgrMessage::kPrepareWalletForTxSign: + return processPrepareDeviceForSign(env, msg.prepare_wallet_for_tx_sign()); + default: break; + } + return bs::message::ProcessingResult::Ignored; +} - caller->deviceNotFound(QString::fromStdString(kDeviceLedgerId)); - } - else { - caller->model_->resetModel({ std::move(deviceKey) }); - caller->deviceReady(QString::fromStdString(kDeviceLedgerId)); - } - }); +bs::message::ProcessingResult DeviceManager::processWallet(const bs::message::Envelope& env) +{ + WalletsMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[hww::DeviceManager::processWallet] failed to parse #{}" + , env.foreignId()); + return bs::message::ProcessingResult::Error; } - else if (bs::wallet::HardwareEncKey::WalletType::Trezor == hwEncType.deviceType()) { - auto deviceId = hwEncType.deviceId(); - const bool cleanPrevSession = (lastUsedTrezorWallet_ != walletId); - trezorClient_->initConnection(QString::fromStdString(deviceId), cleanPrevSession, [this](QVariant&& deviceId) { - DeviceKey deviceKey; - - const auto id = deviceId.toString(); - - bool found = false; - for (auto key : trezorClient_->deviceKeys()) { - if (key.deviceId_ == id) { - found = true; - deviceKey = key; - break; - } - } + switch (msg.data_case()) { + case WalletsMessage::kHdWallet: + return prepareDeviceForSign(env.responseId(), msg.hd_wallet()); + } + return bs::message::ProcessingResult::Ignored; +} - if (!found) { - emit deviceNotFound(id); - } - else { - model_->resetModel({ std::move(deviceKey) }); - emit deviceReady(id); +bs::message::ProcessingResult DeviceManager::processSettings(const bs::message::Envelope& env) +{ + SettingsMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[hww::DeviceManager::processSettings] failed to parse #{}" + , env.foreignId()); + return bs::message::ProcessingResult::Error; + } + if (msg.data_case() == SettingsMessage::kGetResponse) { + for (const auto& setting : msg.get_response().responses()) { + if (setting.request().index() == SetIdx_NetType) { + testNet_ = (static_cast(setting.i()) == NetworkType::TestNet); + logger_->debug("[hww::DeviceManager::processSettings] testnet={}", testNet_); + trezorClient_ = std::make_unique(logger_, testNet_, this); + ledgerClient_ = std::make_unique(logger_, testNet_, this); + jadeClient_ = std::make_unique(logger_, testNet_, this); + return bs::message::ProcessingResult::Success; } - }); - lastUsedTrezorWallet_ = walletId; + } } + return bs::message::ProcessingResult::Ignored; } -void HwDeviceManager::signTX(QVariant reqTX) +bs::message::ProcessingResult DeviceManager::processSigner(const bs::message::Envelope& env) { - auto device = getDevice(model_->getDevice(0)); - if (!device) { - return; + SignerMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[hww::DeviceManager::processSigner] failed to parse #{}" + , env.foreignId()); + return bs::message::ProcessingResult::Error; + } + switch (msg.data_case()) { + case SignerMessage::kSignTxResponse: + return processSignTxResponse(msg.sign_tx_response()); + default: break; } + return bs::message::ProcessingResult::Ignored; +} - Blocksettle::Communication::headless::SignTxRequest pbSignReq; - bool rc = pbSignReq.ParseFromString(reqTX.toByteArray().toStdString()); - if (!rc) { - SPDLOG_LOGGER_ERROR(logger_, "parse TX failed"); - emit operationFailed(tr("Invalid sign request")); - return; +bs::message::ProcessingResult bs::hww::DeviceManager::processBlockchain(const bs::message::Envelope& env) +{ + ArmoryMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[hww::DeviceManager::processBlockchain] failed to parse #{}" + , env.foreignId()); + return bs::message::ProcessingResult::Error; + } + switch (msg.data_case()) { + case ArmoryMessage::kTransactions: + return processTransactions(env.responseId(), msg.transactions()); + default: break; } + return bs::message::ProcessingResult::Ignored; +} - auto signReq = bs::signer::pbTxRequestToCore(pbSignReq); - auto cbSigned = [this, signReq, device](QVariant&& data) { - assert(data.canConvert()); - auto tx = data.value(); - - if (device->key().type_ == DeviceType::HWTrezor) { - // According to architecture, Trezor allow us to sign tx with incorrect - // passphrase, so let's check that the final tx is correct. In Ledger case - // this situation is impossible, since the wallets with different passphrase will be treated - // as different devices, which will be verified in sign part. - try { - std::map> utxoMap; - for (unsigned i=0; igetUtxo(); - auto& idMap = utxoMap[utxo.getTxHash()]; - idMap.emplace(utxo.getTxOutIndex(), utxo); - } - unsigned flags = SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_SEGWIT | SCRIPT_VERIFY_P2SH_SHA256; - bool validSign = Signer::verify(SecureBinaryData::fromString(tx.signedTx) - , utxoMap, flags, true).isValid(); - if (!validSign) { - SPDLOG_LOGGER_ERROR(logger_, "sign verification failed"); - releaseConnection(); - emit operationFailed(tr("Signing failed. Please ensure you type the correct passphrase.")); - return; - } - } - catch (const std::exception &e) { - SPDLOG_LOGGER_ERROR(logger_, "sign verification failed: {}", e.what()); - releaseConnection(); - emit operationFailed(tr("Signing failed. Please ensure you type the correct passphrase.")); - return; +bs::message::ProcessingResult DeviceManager::processTransactions(const bs::message::SeqId msgId + , const ArmoryMessage_Transactions& transactions) +{ + const auto& itReq = supportingTxReq_.find(msgId); + if (itReq == supportingTxReq_.end()) { + logger_->warn("[{}] unknown response #{}", __func__, msgId); + return bs::message::ProcessingResult::Error; + } + const auto& device = getDevice(itReq->second); + supportingTxReq_.erase(itReq); + if (!device) { + logger_->warn("[{}] device not found", __func__); + return bs::message::ProcessingResult::Error; + } + logger_->debug("[DeviceManager::processTransactions] received {} txs", transactions.transactions_size()); + std::vector txs; + for (const auto& txData : transactions.transactions()) { + try { + Tx tx(BinaryData::fromString(txData.tx())); + if (!tx.isInitialized()) { + logger_->warn("[{}] invalid TX at {}", __func__, txData.height()); + continue; } + tx.setTxHeight(txData.height()); + txs.push_back(tx); } - txSigned({ BinaryData::fromString(tx.signedTx) }); - }; - - device->signTX(signReq, std::move(cbSigned)); - - connectDevice(qobject_cast(device)); + catch (const std::exception& e) { + logger_->error("[{}] invalid TX: {}", __func__, e.what()); + } + } + device->setSupportingTXs(txs); + return bs::message::ProcessingResult::Success; +} - // tx specific connections - connect(device, &HwDeviceInterface::deviceTxStatusChanged, - this, &HwDeviceManager::deviceTxStatusChanged, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::cancelledOnDevice, - this, &HwDeviceManager::cancelledOnDevice, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::operationFailed, - this, &HwDeviceManager::deviceTxStatusChanged, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::invalidPin, - this, &HwDeviceManager::invalidPin, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::requestForRescan, - this, [this]() { - auto deviceInfo = model_->getDevice(0); - lastOperationError_ = getDevice(deviceInfo)->lastError(); - emit deviceNotFound(deviceInfo.deviceId_); - }, Qt::UniqueConnection); +bs::message::ProcessingResult DeviceManager::processImport(const bs::message::Envelope& env + , const HW::DeviceKey& key) +{ + DeviceKey devKey{ fromMsg(key) }; + if (devKey.type == DeviceType::Unknown) { + devKey.type = DeviceType::HWTrezor; + } + const auto& device = getDevice(devKey); + if (!device) { + logger_->error("[hww::DeviceManager::processImport] no device found for id {}" + , devKey.id); + return bs::message::ProcessingResult::Error; + } + device->getPublicKeys(); + return bs::message::ProcessingResult::Success; } -void HwDeviceManager::releaseDevices() +bs::message::ProcessingResult bs::hww::DeviceManager::processSignTX(const bs::message::Envelope& env + , const Blocksettle::Communication::headless::SignTxRequest& request) { - releaseConnection(); + logger_->debug("[{}] [{}] {}", __func__, request.unsigned_state().size(), request.DebugString()); + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_signed_tx(); + const auto& txSignReq = bs::signer::pbTxRequestToCore(request); +#if 0 + if (/*!txSignReq.isValid() ||*/ (txSignReq.walletIds.size() != 1)) { + logger_->error("[{}] invalid TX sign request (nb wallets: {})", __func__ + , txSignReq.walletIds.size()); + msgResp->set_error_msg("invalid TX sign request"); + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Error; + } +#endif + if (!txSignReq.isValid()) { + logger_->error("[{}] invalid TX sign request: {} inputs, {} outputs" + , __func__, txSignReq.armorySigner_.getTxInCount(), txSignReq.armorySigner_.getTxOutCount()); + } + envReqSign_ = env; + bool newSign = false; + if (!txSignReq_.isValid() && txSignReq.isValid()) { + txSignReq_ = txSignReq; + newSign = true; + } + if (txSignReq_.walletIds.empty() && !txSignReq.walletIds.empty()) { + newSign = true; + txSignReq_.walletIds = txSignReq.walletIds; + } + if (txSignReq_.walletIds.empty()) { + logger_->debug("[{}] skipping - waiting for wallet[s] ready", __func__); + return bs::message::ProcessingResult::Ignored; + } + if (!newSign) { + return bs::message::ProcessingResult::Success; + } + DeviceKey foundDevice; + for (const auto& device : devices_) { + if (device.walletId == txSignReq_.walletIds.at(0)) { + foundDevice = device; + break; + } + } + if (foundDevice.id.empty()) { + logger_->info("[{}] device for {} is not ready", __func__, txSignReq.walletIds.at(0)); + operationFailed({}, "not ready for TX signing"); + return bs::message::ProcessingResult::Error; + } + signTxWithDevice(foundDevice); + return bs::message::ProcessingResult::Success; } -void HwDeviceManager::hwOperationDone() +void DeviceManager::signTxWithDevice(const DeviceKey& key) { - model_->resetModel({}); + const auto& device = getDevice(key); + if (!device) { + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_signed_tx(); + msgResp->set_error_msg("failed to get device for key " + key.id); + pushResponse(user_, envReqSign_, msg.SerializeAsString()); + envReqSign_ = {}; + txSignReq_ = {}; + return; + } + device->signTX(txSignReq_); } -bool HwDeviceManager::awaitingUserAction(int deviceIndex) +bs::message::ProcessingResult DeviceManager::processSignTxResponse(const SignerMessage_SignTxResponse& response) { - if (model_->rowCount() <= deviceIndex) { - return false; + if (response.signed_tx().empty() && !response.error_text().empty()) { + operationFailed({}, response.error_text()); } + else { + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_signed_tx(); + msgResp->set_signed_tx(response.signed_tx()); + pushResponse(user_, envReqSign_, msg.SerializeAsString()); + envReqSign_ = {}; + txSignReq_ = {}; + } + return bs::message::ProcessingResult::Success; +} - auto device = getDevice(model_->getDevice(deviceIndex)); - return device && device->isBlocked(); +bs::message::ProcessingResult DeviceManager::processSetPIN(const HW::DeviceMgrMessage_SetPIN& request) +{ + const auto& key = fromMsg(request.key()); + const auto& device = getDevice(key); + if (!device) { + logger_->error("[{}] unknown device {}", __func__, key.id); + return bs::message::ProcessingResult::Error; + } + device->setMatrixPin(SecureBinaryData::fromString(request.pin())); + return bs::message::ProcessingResult::Success; } -QString HwDeviceManager::lastDeviceError() +bs::message::ProcessingResult DeviceManager::processSetPassword(const HW::DeviceMgrMessage_SetPassword& request) { - return lastOperationError_; + const auto& key = fromMsg(request.key()); + const auto& device = getDevice(key); + if (!device) { + logger_->error("[{}] unknown device {}", __func__, key.id); + return bs::message::ProcessingResult::Error; + } + device->setPassword(SecureBinaryData::fromString(request.password()) + , request.set_on_device()); + return bs::message::ProcessingResult::Success; } -void HwDeviceManager::releaseConnection(AsyncCallBack&& cb/*= nullptr*/) +bs::message::ProcessingResult DeviceManager::prepareDeviceForSign(bs::message::SeqId msgId + , const HDWalletData& hdWallet) { - for (int i = 0; i < model_->rowCount(); ++i) { - auto device = getDevice(model_->getDevice(i)); - if (device) { - trezorClient_->initConnection(true, [this, cbCopy = std::move(cb)] { - trezorClient_->releaseConnection([this, cb = std::move(cbCopy)]() { - if (cb) { - cb(); - } - }); - }); - model_->resetModel({}); - return; - } + const auto& itWallet = prepareDeviceReq_.find(msgId); + if (itWallet == prepareDeviceReq_.end()) { + logger_->warn("[{}] unknown response #{}", __func__, msgId); + return bs::message::ProcessingResult::Error; + } + prepareDeviceReq_.erase(itWallet); + if (!hdWallet.is_hardware() || !hdWallet.encryption_keys_size()) { + logger_->error("[{}] wallet {} is not suitable", __func__, hdWallet.wallet_id()); + return bs::message::ProcessingResult::Error; } - if (cb) { - cb(); + bool isDeviceReady = false; + DeviceKey deviceKey{}; + for (const auto& device : devices_) { + logger_->debug("[{}] device {}/{} vs {}", __func__, device.id, device.walletId, hdWallet.wallet_id()); + if (device.walletId == hdWallet.wallet_id()) { + isDeviceReady = true; + deviceKey = device; + break; + } } + const auto& walletReady = [this](const std::string& walletId) + { + HW::DeviceMgrMessage msg; + msg.set_device_ready(walletId); + pushBroadcast(user_, msg.SerializeAsString()); + }; + if (isDeviceReady) { + if ((deviceKey.type == bs::hww::DeviceType::HWJade) && !jadeClient_->isConnected(deviceKey.id)) { + operationFailed(deviceKey.id, "device not connected"); + return bs::message::ProcessingResult::Error; + } + logger_->debug("[{}] device {}/{} was ready", __func__, deviceKey.id, deviceKey.walletId); + walletReady(deviceKey.walletId); + return bs::message::ProcessingResult::Success; + } + bs::wallet::HardwareEncKey hwEncType(BinaryData::fromString(hdWallet.encryption_keys(0))); + logger_->debug("[{}] [re]scanning devices of type {}", __func__, (int)hwEncType.deviceType()); + if (bs::wallet::HardwareEncKey::WalletType::Ledger == hwEncType.deviceType()) { + bool found = false; + for (const auto& key : ledgerClient_->deviceKeys()) { + logger_->debug("[{}] ledger device {} for wallet {}", __func__, key.id, key.walletId); + if (key.walletId == hdWallet.wallet_id()) { + deviceKey = key; + found = true; + break; + } + } + if (!found) { + logger_->debug("[{}] ledger device for wallet {} not found", __func__, hdWallet.wallet_id()); + nbScanning_ = 1; + ledgerClient_->scanDevices(); + } + else { + logger_->debug("[{}] found ledger device {} for wallet {}", __func__ + , deviceKey.id, deviceKey.walletId); + deviceReady(deviceKey.id); + walletReady(deviceKey.walletId); + } + } + else if (bs::wallet::HardwareEncKey::WalletType::Trezor == hwEncType.deviceType()) { + nbScanning_ = 1; + trezorClient_->listDevices(); + } + else if (bs::wallet::HardwareEncKey::WalletType::Jade == hwEncType.deviceType()) { + nbScanning_ = 1; + jadeClient_->scanDevices(); + } + return bs::message::ProcessingResult::Success; } -void HwDeviceManager::scanningDone(bool initDevices /* = true */) +void DeviceManager::signTX(const DeviceKey& key, const bs::core::wallet::TXSignRequest& signReq) { - setScanningFlag(false); - auto allDevices = ledgerClient_->deviceKeys(); - allDevices.append(trezorClient_->deviceKeys()); - model_->resetModel(std::move(allDevices)); - emit devicesChanged(); - - if (!initDevices) { + auto device = getDevice(key); + if (!device) { + deviceNotFound(key.id); return; } + device->signTX(signReq); +} - for (const auto& key : trezorClient_->deviceKeys()) { - auto device = trezorClient_->getTrezorDevice(key.deviceId_); - if (!device->inited()) { - connectDevice(qobject_cast(device)); - device->retrieveXPubRoot([caller = QPointer(this)]() { - if (!caller) { - return; - } +void DeviceManager::releaseDevices() +{ + releaseConnection(); +} - caller->scanningDone(false); - }); +bool DeviceManager::awaitingUserAction(const DeviceKey& key) +{ + const auto& device = getDevice(key); + return device && device->isBlocked(); +} + +void DeviceManager::releaseConnection() +{ + if (trezorClient_) { + trezorClient_->releaseConnection(); + } +} + +void DeviceManager::devicesResponse() +{ + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_available_devices(); + for (const auto& key : devices_) { + if (key.walletId.empty()) { + continue; } + deviceKeyToMsg(key, msgResp->add_device_keys()); } + logger_->debug("[{}] {}", __func__, msg.DebugString()); + pushResponse(user_, envReqScan_, msg.SerializeAsString()); + envReqScan_ = {}; } -void HwDeviceManager::connectDevice(QPointer device) +void DeviceManager::scanningDone(bool initDevices) { - connect(device, &HwDeviceInterface::requestPinMatrix, - this, &HwDeviceManager::onRequestPinMatrix, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::requestHWPass, - this, &HwDeviceManager::onRequestHWPass, Qt::UniqueConnection); - connect(device, &HwDeviceInterface::operationFailed, - this, &HwDeviceManager::operationFailed, Qt::UniqueConnection); + const auto ledgerKeys = ledgerClient_->deviceKeys(); + devices_ = ledgerKeys; + const auto trezorKeys = trezorClient_->deviceKeys(); + devices_.insert(devices_.end(), trezorKeys.cbegin(), trezorKeys.cend()); + const auto& jadeKeys = jadeClient_->deviceKeys(); + devices_.insert(devices_.end(), jadeKeys.cbegin(), jadeKeys.cend()); + + for (const auto& device : devices_) { + logger_->debug("[{}] found: {} {} {}", __func__, device.id, device.label, device.vendor); + } + if (!initDevices || devices_.empty()) { + if (devices_.empty()) { + logger_->info("[{}] no devices scanned", __func__); + } + if (envReqScan_.sender) { + devicesResponse(); + } + return; + } + for (const auto& key : ledgerKeys) { + auto device = ledgerClient_->getDevice(key.id); + if (!device->inited()) { + device->init(); + } + } + for (const auto& key : trezorKeys) { + auto device = trezorClient_->getDevice(key.id); + if (!device->inited()) { + device->init(); + } + } + for (const auto& key : jadeKeys) { + auto device = jadeClient_->getDevice(key.id); + if (!device->inited()) { + device->init(); + } + } } -QPointer HwDeviceManager::getDevice(DeviceKey key) +std::shared_ptr DeviceManager::getDevice(const DeviceKey& key) const { - switch (key.type_) + switch (key.type) { case DeviceType::HWTrezor: - return static_cast>(trezorClient_->getTrezorDevice(key.deviceId_)); - break; + return trezorClient_->getDevice(key.id); case DeviceType::HWLedger: - return static_cast>(ledgerClient_->getDevice(key.deviceId_)); - break; + return ledgerClient_->getDevice(key.id); + case DeviceType::HWJade: + return jadeClient_->getDevice(key.id); default: + logger_->error("[{}] unknown device type {}", __func__, (int)key.type); // Add new device type assert(false); break; } - return nullptr; } -void HwDeviceManager::onRequestPinMatrix() +void DeviceManager::publicKeyReady(const DeviceKey& devKey) { - auto sender = qobject_cast(QObject::sender()); - int index = model_->getDeviceIndex(sender->key()); + logger_->debug("[{}] walletId = {} for {}", __func__, devKey.walletId, devKey.id); + if (!devKey.walletId.empty()) { + HW::DeviceMgrMessage msg; + msg.set_device_ready(devKey.walletId); + pushBroadcast(user_, msg.SerializeAsString()); + } + size_t nbCompleted = 0; + bool foundDevice = false; + bool delDevice = false; + for (auto& device : devices_) { + if (device.id == devKey.id) { + if (devKey.walletId.empty()) { + delDevice = true; + } + else { + device.walletId = devKey.walletId; + nbCompleted++; + foundDevice = true; + logger_->debug("[{}] device {}/{} found", __func__, devKey.id, devKey.walletId); + } + } + else if (!device.walletId.empty()) { + nbCompleted++; + } + } + if (!foundDevice) { + std::lock_guard lock{ devMtx_ }; + if (delDevice) { + const auto& it = std::find_if(devices_.cbegin(), devices_.cend() + , [devKey](const DeviceKey& key) {return (key.id == devKey.id); }); + if (it == devices_.end()) { + logger_->warn("[{}] device {} not found at delete", __func__, devKey.id); + } + else { + devices_.erase(it); + logger_->debug("[{}] # devices: {}", __func__, devices_.size()); + } + } + else { + devices_.push_back(devKey); + nbCompleted++; + } + } + logger_->debug("[{}] nbCompleted: {}, nbDevices: {}", __func__, nbCompleted, devices_.size()); + if (nbCompleted >= devices_.size()) { + logger_->debug("[{}] all public keys retrieved", __func__); + if (envReqScan_.sender) { + devicesResponse(); + } + else if (envReqScan_.sender && txSignReq_.isValid()) { + for (const auto& device : devices_) { + if (device.walletId == txSignReq_.walletIds.at(0)) { + signTxWithDevice(device); + break; + } + } + } + } +} - if (index >= 0) { - emit requestPinMatrix(index); +void bs::hww::DeviceManager::walletInfoReady(const DeviceKey& key + , const bs::core::HwWalletInfo& walletInfo) +{ + if (walletInfo.xpubRoot.empty()) { + logger_->error("[{}] failed to obtain wallet public keys for {}", key.id); + return; } + logger_->debug("[hww::DeviceManager::walletInfoReady] importing device {}", key.id); + + SignerMessage msg; + auto msgReq = msg.mutable_import_hw_wallet(); + msgReq->set_type((int)walletInfo.type); + msgReq->set_vendor(walletInfo.vendor); + msgReq->set_label(walletInfo.label); + msgReq->set_device_id(walletInfo.deviceId); + msgReq->set_xpub_root(walletInfo.xpubRoot); + msgReq->set_xpub_nested_segwit(walletInfo.xpubNestedSegwit); + msgReq->set_xpub_native_segwit(walletInfo.xpubNativeSegwit); + msgReq->set_xpub_legacy(walletInfo.xpubLegacy); + pushRequest(user_, userSigner_, msg.SerializeAsString()); +} + +void DeviceManager::requestPinMatrix(const DeviceKey& key) +{ + logger_->debug("[{}] {}", __func__, key.id); + HW::DeviceMgrMessage msg; + deviceKeyToMsg(key, msg.mutable_request_pin()); + pushBroadcast(user_, msg.SerializeAsString()); +} + +void DeviceManager::requestHWPass(const DeviceKey& key, bool allowedOnDevice) +{ + logger_->debug("[{}] {}", __func__, key.id); + HW::DeviceMgrMessage msg; + auto msgReq = msg.mutable_password_request(); + deviceKeyToMsg(key, msgReq->mutable_key()); + msgReq->set_allowed_on_device(allowedOnDevice); + pushBroadcast(user_, msg.SerializeAsString()); +} + +void DeviceManager::deviceNotFound(const std::string& deviceId) +{ + logger_->debug("[{}] {}", __func__, deviceId); +} + +void DeviceManager::deviceReady(const std::string& deviceId) +{ + logger_->debug("[{}] {}", __func__, deviceId); } -void HwDeviceManager::onRequestHWPass(bool allowedOnDevice) +void DeviceManager::deviceTxStatusChanged(const std::string& status) { - auto sender = qobject_cast(QObject::sender()); - int index = model_->getDeviceIndex(sender->key()); + logger_->debug("[{}] {}", __func__, status); +} - if (index >= 0) { - emit requestHWPass(index, allowedOnDevice); +void bs::hww::DeviceManager::needSupportingTXs(const DeviceKey& key + , const std::vector& txHashes) +{ + if (txHashes.empty()) { + logger_->warn("[{}] no TX hashes from {}", __func__, key.label); + return; + } + ArmoryMessage msg; + auto msgReq = msg.mutable_get_txs_by_hash(); + for (const auto& txHash : txHashes) { + msgReq->add_tx_hashes(txHash.toBinStr()); } + msgReq->set_disable_cache(true); + const auto msgId = pushRequest(user_, userBlockchain_, msg.SerializeAsString()); + supportingTxReq_[msgId] = key; } -void HwDeviceManager::setScanningFlag(bool isScanning) +void DeviceManager::txSigned(const DeviceKey& device, const SecureBinaryData& signData) { - if (isScanning_ == isScanning) { + if (device.type == DeviceType::HWTrezor) { + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_signed_tx(); + msgResp->set_signed_tx(signData.toBinStr()); + pushResponse(user_, envReqSign_, msg.SerializeAsString()); + envReqSign_ = {}; + txSignReq_ = {}; + } + else { + SignerMessage msg; + auto msgReq = msg.mutable_sign_tx_request(); + *msgReq->mutable_tx_request() = bs::signer::coreTxRequestToPb(txSignReq_); + msgReq->set_passphrase(signData.toBinStr()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); + } +} + +void DeviceManager::scanningDone() +{ + if (nbScanning_ == 0) { + logger_->error("[DeviceManager::scanningDone] more scanning done events than expected"); return; } + if (--nbScanning_ <= 0) { + logger_->debug("[DeviceManager::scanningDone] all devices scanned"); + scanningDone(true); + } +} - isScanning_ = isScanning; - emit isScanningChanged(); +void DeviceManager::operationFailed(const std::string& deviceId, const std::string& reason) +{ + if (envReqSign_.sender) { + HW::DeviceMgrMessage msg; + auto msgResp = msg.mutable_signed_tx(); + msgResp->set_error_msg(reason); + pushResponse(user_, envReqSign_, msg.SerializeAsString()); + envReqSign_ = {}; + txSignReq_ = {}; + } } -HwDeviceModel* HwDeviceManager::devices() +void DeviceManager::cancelledOnDevice() { - return model_; } -bool HwDeviceManager::isScanning() const +void DeviceManager::invalidPin() { - return isScanning_; } + +using namespace bs::hd; + +namespace bs { + namespace hww { + void deviceKeyToMsg(const DeviceKey& key, HW::DeviceKey* msgKey) + { + msgKey->set_label(key.label); + msgKey->set_id(key.id); + msgKey->set_vendor(key.vendor); + msgKey->set_wallet_id(key.walletId); + msgKey->set_type((int)key.type); + } + bs::hww::DeviceKey fromMsg(const BlockSettle::HW::DeviceKey& msg) + { + return { msg.label(), msg.id(), msg.vendor(), msg.wallet_id() + , static_cast(msg.type()) }; + } + + Path getDerivationPath(bool testNet, Purpose element) + { + Path path; + path.append(hardFlag | element); + path.append(testNet ? CoinType::Bitcoin_test : CoinType::Bitcoin_main); + path.append(hardFlag); + return path; + } + + bool isNestedSegwit(const bs::hd::Path& path) + { + return path.get(0) == (bs::hd::Purpose::Nested | bs::hd::hardFlag); + } + + bool isNativeSegwit(const bs::hd::Path& path) + { + return path.get(0) == (bs::hd::Purpose::Native | bs::hd::hardFlag); + } + + bool isNonSegwit(const bs::hd::Path& path) + { + return path.get(0) == (bs::hd::Purpose::NonSegWit | bs::hd::hardFlag); + } + } //hww +} //bs diff --git a/BlockSettleHW/hwdevicemanager.h b/BlockSettleHW/hwdevicemanager.h index dc20a23a0..49f450d4d 100644 --- a/BlockSettleHW/hwdevicemanager.h +++ b/BlockSettleHW/hwdevicemanager.h @@ -11,97 +11,165 @@ #ifndef HWDEVICESCANNER_H #define HWDEVICESCANNER_H -#include "hwcommonstructure.h" -#include "hwdevicemodel.h" -#include "SecureBinaryData.h" #include +#include "HDPath.h" +#include "hwdeviceinterface.h" +#include "Message/Adapter.h" +#include "ledger/ledgerClient.h" +#include "trezor/trezorClient.h" +#include "jade/jadeClient.h" +#include "SecureBinaryData.h" -#include -#include -#include -class HwDeviceInterface; -class TrezorClient; -class LedgerClient; -class ConnectionManager; -namespace bs { - namespace sync { - class WalletsManager; +namespace spdlog { + class logger; +} +namespace BlockSettle { + namespace Common { + class ArmoryMessage_Transactions; + class HDWalletData; + class SignerMessage_SignTxResponse; + } + namespace HW { + class DeviceKey; + class DeviceMgrMessage_SetPassword; + class DeviceMgrMessage_SetPIN; + } +} +namespace Blocksettle { + namespace Communication { + namespace headless { + class SignTxRequest; + } } } - -class HwDeviceManager : public QObject -{ - Q_OBJECT - Q_PROPERTY(HwDeviceModel* devices READ devices NOTIFY devicesChanged) - Q_PROPERTY(bool isScanning READ isScanning NOTIFY isScanningChanged) - -public: - HwDeviceManager(const std::shared_ptr& connectionManager, - std::shared_ptr walletManager, bool testNet, QObject* parent = nullptr); - ~HwDeviceManager() override; - - /// Property - HwDeviceModel* devices(); - bool isScanning() const; - - // Actions from UI - Q_INVOKABLE void scanDevices(); - Q_INVOKABLE void requestPublicKey(int deviceIndex); - Q_INVOKABLE void setMatrixPin(int deviceIndex, QString pin); - Q_INVOKABLE void setPassphrase(int deviceIndex, QString passphrase, bool enterOnDevice); - Q_INVOKABLE void cancel(int deviceIndex); - Q_INVOKABLE void prepareHwDeviceForSign(QString walletId); - Q_INVOKABLE void signTX(QVariant reqTX); - Q_INVOKABLE void releaseDevices(); - Q_INVOKABLE void hwOperationDone(); - - // Info asked from UI - Q_INVOKABLE bool awaitingUserAction(int deviceIndex); - Q_INVOKABLE QString lastDeviceError(); - -signals: - void devicesChanged(); - void publicKeyReady(QVariant walletInfo); - void requestPinMatrix(int deviceIndex); - void requestHWPass(int deviceIndex, bool allowedOnDevice); - - void deviceNotFound(QString deviceId); - void deviceReady(QString deviceId); - void deviceTxStatusChanged(QString status); - - void txSigned(SecureBinaryData signData); - void isScanningChanged(); - void operationFailed(QString reason); - void cancelledOnDevice(); - void invalidPin(); - -protected slots: - void onRequestPinMatrix(); - void onRequestHWPass(bool allowedOnDevice); - -private: - void setScanningFlag(bool isScanning); - void releaseConnection(AsyncCallBack&& cb = nullptr); - void scanningDone(bool initDevices = true); - void connectDevice(QPointer device); - - QPointer getDevice(DeviceKey key); - - std::shared_ptr logger_; - -public: - std::unique_ptr trezorClient_; - std::unique_ptr ledgerClient_; - std::shared_ptr walletManager_; - - HwDeviceModel* model_; - bool testNet_{}; - bool isScanning_{}; - bool isSigning_{}; - QString lastOperationError_; - QString lastUsedTrezorWallet_; -}; +namespace bs { + namespace hww { + + // There is no way to determinate difference between ledger devices + // so we use vendor name for identification + const std::string kDeviceLedgerId = "Ledger"; + + struct DeviceCallbacks + { + virtual void publicKeyReady(const DeviceKey&) = 0; + virtual void walletInfoReady(const DeviceKey&, const bs::core::HwWalletInfo&) = 0; + virtual void requestPinMatrix(const DeviceKey&) = 0; + virtual void requestHWPass(const DeviceKey&, bool allowedOnDevice) = 0; + + virtual void deviceNotFound(const std::string& deviceId) = 0; + virtual void deviceReady(const std::string& deviceId) = 0; + virtual void deviceTxStatusChanged(const std::string& status) = 0; + + virtual void needSupportingTXs(const DeviceKey&, const std::vector& txHashes) = 0; + + virtual void txSigned(const DeviceKey&, const SecureBinaryData& signData) = 0; + virtual void scanningDone() = 0; + virtual void operationFailed(const std::string& deviceId, const std::string& reason) = 0; + virtual void cancelledOnDevice() = 0; + virtual void invalidPin() = 0; + }; + + class DeviceManager : public bs::message::Adapter, public DeviceCallbacks + { + public: + DeviceManager(const std::shared_ptr&); + ~DeviceManager() override; + + bs::message::ProcessingResult process(const bs::message::Envelope&) override; + bool processBroadcast(const bs::message::Envelope&) override; + + Users supportedReceivers() const override { return { user_ }; } + std::string name() const override { return "HWWallets"; } + + private: // signals + void publicKeyReady(const DeviceKey&) override; + void walletInfoReady(const DeviceKey&, const bs::core::HwWalletInfo&) override; + void requestPinMatrix(const DeviceKey&) override; + void requestHWPass(const DeviceKey&, bool allowedOnDevice) override; + + void deviceNotFound(const std::string& deviceId) override; + void deviceReady(const std::string& deviceId) override; + void deviceTxStatusChanged(const std::string& status) override; + void needSupportingTXs(const DeviceKey&, const std::vector& txHashes) override; + void txSigned(const DeviceKey&, const SecureBinaryData& signData) override; + void scanningDone() override; + void operationFailed(const std::string& deviceId, const std::string& reason) override; + void cancelledOnDevice() override; + void invalidPin() override; + + //former invokables: + void scanDevices(const bs::message::Envelope&); + void setMatrixPin(const DeviceKey&, const std::string& pin); + void setPassphrase(const DeviceKey&, const std::string& passphrase + , bool enterOnDevice); + void cancel(const DeviceKey&); + bs::message::ProcessingResult prepareDeviceForSign(bs::message::SeqId + , const BlockSettle::Common::HDWalletData&); + void signTX(const DeviceKey&, const bs::core::wallet::TXSignRequest& reqTX); + void releaseDevices(); + //void hwOperationDone(); + bool awaitingUserAction(const DeviceKey&); + + void releaseConnection(); + void scanningDone(bool initDevices = true); + + std::shared_ptr getDevice(const DeviceKey& key) const; + + void start(); + bs::message::ProcessingResult processPrepareDeviceForSign(const bs::message::Envelope& + , const std::string& walletId); + bs::message::ProcessingResult processOwnRequest(const bs::message::Envelope&); + bs::message::ProcessingResult processWallet(const bs::message::Envelope&); + bs::message::ProcessingResult processSettings(const bs::message::Envelope&); + bs::message::ProcessingResult processSigner(const bs::message::Envelope&); + bs::message::ProcessingResult processBlockchain(const bs::message::Envelope&); + + bs::message::ProcessingResult processTransactions(const bs::message::SeqId + , const BlockSettle::Common::ArmoryMessage_Transactions&); + + void devicesResponse(); + bs::message::ProcessingResult processImport(const bs::message::Envelope& + , const BlockSettle::HW::DeviceKey&); + bs::message::ProcessingResult processSignTX(const bs::message::Envelope& + , const Blocksettle::Communication::headless::SignTxRequest&); + void signTxWithDevice(const DeviceKey&); + bs::message::ProcessingResult processSignTxResponse(const BlockSettle::Common::SignerMessage_SignTxResponse&); + bs::message::ProcessingResult processSetPIN(const BlockSettle::HW::DeviceMgrMessage_SetPIN&); + bs::message::ProcessingResult processSetPassword(const BlockSettle::HW::DeviceMgrMessage_SetPassword&); + + private: + std::shared_ptr logger_; + std::unique_ptr trezorClient_; + std::unique_ptr ledgerClient_; + std::unique_ptr jadeClient_; + std::shared_ptr user_, userWallets_, userSigner_, userBlockchain_; + std::vector devices_; + mutable std::mutex devMtx_; + + bool testNet_{false}; + int nbScanning_{0}; + bool isSigning_{}; + std::string lastOperationError_; + std::string lastUsedTrezorWallet_; + unsigned nbWaitScanReplies_{ 0 }; + bs::message::Envelope envReqScan_, envReqSign_; + bs::core::wallet::TXSignRequest txSignReq_; + + std::map> prepareDeviceReq_; //value: walletId + std::map supportingTxReq_; + }; + + void deviceKeyToMsg(const DeviceKey&, BlockSettle::HW::DeviceKey*); + DeviceKey fromMsg(const BlockSettle::HW::DeviceKey&); + + bs::hd::Path getDerivationPath(bool testNet, bs::hd::Purpose element); + bool isNestedSegwit(const bs::hd::Path& path); + bool isNativeSegwit(const bs::hd::Path& path); + bool isNonSegwit(const bs::hd::Path& path); + + } //hw +} //bs #endif // HWDEVICESCANNER_H diff --git a/BlockSettleHW/jade/jadeClient.cpp b/BlockSettleHW/jade/jadeClient.cpp new file mode 100644 index 000000000..c03febcd3 --- /dev/null +++ b/BlockSettleHW/jade/jadeClient.cpp @@ -0,0 +1,87 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "jadeClient.h" +#include "hwdevicemanager.h" +#include "jadeDevice.h" +#include "SystemFileUtils.h" +#include "Wallets/SyncWalletsManager.h" +#include "Wallets/SyncHDWallet.h" + + +using namespace bs::hww; +using json = nlohmann::json; + +JadeClient::JadeClient(const std::shared_ptr& logger + , bool testNet, DeviceCallbacks* cb) + : logger_(logger), cb_(cb), testNet_(testNet) +{} + +void JadeClient::initConnection() +{ + logger_->info("[JadeClient::initConnection]"); +} + +std::vector JadeClient::deviceKeys() const +{ + std::vector result; + for (const auto& device : devices_) { + result.push_back(device->key()); + } + return result; +} + +std::shared_ptr JadeClient::getDevice(const std::string& deviceId) +{ + for (const auto& device : devices_) { + if (device->key().id == deviceId) { + return device; + } + } + return nullptr; +} + +void JadeClient::scanDevices() +{ + logger_->info("[JadeClient::scanDevices]"); + QMetaObject::invokeMethod(qApp, [this] { + for (const auto& serial : QSerialPortInfo::availablePorts()) { + const auto& it = std::find_if(devices_.cbegin(), devices_.cend() + , [serial](const std::shared_ptr& dev) { + return JadeDevice::idFromSerial(serial) == dev->key().id; }); + if (it != devices_.end()) { + continue; + } + logger_->debug("[JadeClient::scanDevices] probing {}/{}", serial.portName().toStdString() + , serial.manufacturer().toStdString()); + try { + const auto& device = std::make_shared(logger_, testNet_ + , cb_, serial); + devices_.push_back(device); + } + catch (const std::exception& e) { + logger_->error("[JadeClient::scanDevices] {}", e.what()); + } + } + logger_->debug("[JadeClient::scanDevices] {} devices scanned", devices_.size()); + cb_->scanningDone(); + }); +} + +bool bs::hww::JadeClient::isConnected(const std::string& reqId) const +{ + for (const auto& serial : QSerialPortInfo::availablePorts()) { + const auto& id = JadeDevice::idFromSerial(serial); + if (id == reqId) { + return true; + } + } + return false; +} diff --git a/BlockSettleHW/jade/jadeClient.h b/BlockSettleHW/jade/jadeClient.h new file mode 100644 index 000000000..c7c3fd06e --- /dev/null +++ b/BlockSettleHW/jade/jadeClient.h @@ -0,0 +1,54 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef JADE_CLIENT_H +#define JADE_CLIENT_H + +#include +#include +#include "hwdeviceinterface.h" +#include "Message/Worker.h" + +namespace spdlog { + class logger; +} +struct curl_slist; + +namespace bs { + namespace hww { + class JadeDevice; + class DeviceCallbacks; + + class JadeClient + { + //friend class JadeDevice; + public: + JadeClient(const std::shared_ptr& + , bool testNet, DeviceCallbacks*); + ~JadeClient() = default; + + void initConnection(); + void scanDevices(); + bool isConnected(const std::string& id) const; + + std::vector deviceKeys() const; + std::shared_ptr getDevice(const std::string& deviceId); + + private: + std::shared_ptr logger_; + DeviceCallbacks* cb_{ nullptr }; + const bool testNet_; + std::vector> devices_; + }; + + } //hw +} //bs + +#endif // JADE_CLIENT_H diff --git a/BlockSettleHW/jade/jadeDevice.cpp b/BlockSettleHW/jade/jadeDevice.cpp new file mode 100644 index 000000000..4e0f09498 --- /dev/null +++ b/BlockSettleHW/jade/jadeDevice.cpp @@ -0,0 +1,887 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include +#include +#include +#include +#include +#include +#include "hwdevicemanager.h" +#include "jadeDevice.h" +#include "jadeClient.h" +#include "CoreWallet.h" +#include "StringUtils.h" + + +using namespace bs::hww; + +// Helpers to build basic jade cbor request object +static inline QCborMap getRequest(const int id, const QString& method) +{ + QCborMap req; + req.insert(QCborValue(QLatin1Literal("id")), QString::number(id)); + req.insert(QCborValue(QLatin1Literal("method")), method); + return req; +} + +static inline QCborMap getRequest(const int id, const QString& method + , const QCborValue& params) +{ + QCborMap req(getRequest(id, method)); + req.insert(QCborValue(QLatin1Literal("params")), params); + return req; +} + + +JadeDevice::JadeDevice(const std::shared_ptr &logger + , bool testNet, DeviceCallbacks* cb, const QSerialPortInfo& endpoint) + : bs::WorkerPool(1, 1) + , logger_(logger), testNet_(testNet), cb_(cb), endpoint_(endpoint) + , handlers_{ std::make_shared(logger_, endpoint_) + , std::make_shared(logger_) } +{} + +JadeDevice::~JadeDevice() = default; + +std::shared_ptr JadeDevice::worker(const std::shared_ptr&) +{ + return std::make_shared(handlers_); +} + +void JadeDevice::operationFailed(const std::string& reason) +{ + releaseConnection(); + cb_->operationFailed(key().id, reason); +} + +static std::string dump(const QCborMap&); +static std::string dump(const QCborValueRef&); +static std::string dump(const QCborArray& ary) +{ + std::string result = "["; + for (const auto& it : ary) { + result += dump(it) + ", "; + } + if (result.size() > 3) { + result.pop_back(), result.pop_back(); + } + result += "]"; + return result; +} + +static std::string dump(const QCborMap& map) +{ + std::string result = "{"; + for (const auto& it : map) { + result += it.first.toString().toStdString() + "=" + dump(it.second) + ", "; + } + if (result.size() > 3) { + result.pop_back(), result.pop_back(); + } + result += "}"; + return result; +} + +static std::string dump(const QCborValueRef& val) +{ + if (val.isInvalid()) { + return ""; + } + if (val.isMap()) { + return dump(val.toMap()); + } + if (val.isArray()) { + return dump(val.toArray()); + } + if (val.isString()) { + return "\"" + val.toString().toStdString() + "\""; + } + if (val.isBool()) { + return val.toBool() ? "true" : "false"; + } + if (val.isDouble()) { + return std::to_string(val.toDouble()); + } + if (val.isInteger()) { + return std::to_string(val.toInteger()); + } + if (val.isByteArray()) { + return bs::toHex(val.toByteArray().toStdString()); + } + if (val.isDateTime()) { + return val.toDateTime().toString().toStdString(); + } + if (val.isUrl()) { + return val.toUrl().toString().toStdString(); + } + if (val.isUuid()) { + return val.toUuid().toString().toStdString(); + } + if (val.isRegularExpression()) { + return val.toRegularExpression().pattern().toStdString(); + } + if (val.isNull()) { + return ""; + } + if (val.isUndefined()) { + return ""; + } + return ""; +} + +static std::string addrType(AddressEntryType aet) +{ + switch (aet) { + case AddressEntryType_P2PKH: return "pkh(k)"; + case AddressEntryType_P2WPKH: return "wpkh(k)"; + case AddressEntryType_P2SH: return "sh(k)"; + case AddressEntryType_P2WSH: return "sh(wpkh(k))"; + default: break; + } + return "unknown"; +} + +static QCborArray convertPath(const bs::hd::Path& path) +{ + QCborArray pathArray; + for (const auto val : path) { + pathArray.append((quint32)val); + } + return pathArray; +} + +void JadeDevice::setSupportingTXs(const std::vector& txs) +{ + if (!awaitingTXreq_.isValid()) { + logger_->error("[JadeDevice::setSupportingTXs] awaiting TX request is invalid"); + return; + } + if (awaitingTXreq_.armorySigner_.getTxInCount() != txs.size()) { + logger_->error("[JadeDevice::setSupportingTXs] awaiting TX request inputs" + " count mismatch: {} vs {}", awaitingTXreq_.armorySigner_.getTxInCount(), txs.size()); + return; + } + logger_->debug("[JadeDevice::setSupportingTXs] {}, lock time: {}", txs.size(), awaitingTXreq_.armorySigner_.getLockTime()); + for (const auto& tx : txs) { + awaitingTXreq_.armorySigner_.addSupportingTx(tx); + } + const auto& tx = awaitingTXreq_.armorySigner_.serializeUnsignedTx(); + QCborArray change; + for (int i = 0; i < awaitingTXreq_.armorySigner_.getTxOutCount() - 1; ++i) { + change.push_back(nullptr); + } + if (awaitingTXreq_.change.address.empty()) { + change.push_back(nullptr); + } + else { + const auto changePath = bs::hd::Path::fromString(awaitingTXreq_.change.index); + bs::hd::Path path; + path.append(bs::hd::Purpose::Native + bs::hd::hardFlag); + path.append(testNet_ ? bs::hd::CoinType::Bitcoin_test : bs::hd::CoinType::Bitcoin_main); + path.append(bs::hd::hardFlag); + path.append(changePath.get(-2)); + path.append(changePath.get(-1)); + + change.push_back(QCborMap{ {QLatin1Literal("variant"), QString::fromStdString(addrType(awaitingTXreq_.change.address.getType()))} + , {QLatin1Literal("path"), convertPath(path)} }); + } + const QCborMap params = { {QLatin1Literal("network"), network()}, {QLatin1Literal("txn") + , QByteArray::fromStdString(tx.toBinStr())}, {QLatin1Literal("use_ae_signatures"), false} + , {QLatin1Literal("num_inputs"), awaitingTXreq_.armorySigner_.getTxInCount()} + , {QLatin1Literal("change"), change} }; + if (!awaitingTXreq_.change.address.empty()) { + } + auto inReq = std::make_shared(); + inReq->request = getRequest(++seqId_, QLatin1Literal("sign_tx"), params); + + const auto& cbSign = [this, txs](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::signTX] invalid data"); + cb_->operationFailed(key().id, "Invalid data"); + return; + } + if (data->futResponse.wait_for(std::chrono::seconds{ 15 }) != std::future_status::ready) { + logger_->error("[JadeDevice::signTX] data timeout"); + cb_->operationFailed(key().id, "Device timeout"); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::signTX] response: {}", dump(msg)); + if (msg.contains(QLatin1Literal("error"))) { + cb_->operationFailed(key().id, msg[QLatin1Literal("error")][QLatin1Literal("message")].toString().toStdString()); + return; + } + if (msg[QLatin1Literal("result")].isFalse()) { + cb_->operationFailed(key().id, "Device refused to sign"); + return; + } + + auto bw = std::make_shared(); + bw->put_var_int(awaitingTXreq_.armorySigner_.getTxInCount()); + const auto& addSignedInput = [this, bw, txs](uint32_t i, const BinaryData& signedData) + { + bw->put_uint32_t(i); + bw->put_var_int(signedData.getSize()); + bw->put_BinaryData(signedData); + + logger_->debug("[JadeDevice::setSupportingTXs::addSignedInput] {} of {}", i + 1, awaitingTXreq_.armorySigner_.getTxInCount()); + if ((i + 1) >= awaitingTXreq_.armorySigner_.getTxInCount()) { + cb_->txSigned(key(), bw->getData()); + logger_->debug("[JadeDevice::setSupportingTXs::addSignedInput] done"); + } + }; + + for (uint32_t i = 0; i < awaitingTXreq_.armorySigner_.getTxInCount(); ++i) { + const auto& spender = awaitingTXreq_.armorySigner_.getSpender(i); + if (!spender) { + logger_->warn("[JadeDevice::signTX] no spender at {}", __func__, i); + continue; + } + auto bip32Paths = spender->getBip32Paths(); + if (bip32Paths.size() != 1) { + logger_->error("[TrezorDevice::handleTxRequest] TXINPUT {} BIP32 paths", bip32Paths.size()); + throw std::logic_error("unexpected pubkey count for spender"); + } + const auto& path = bip32Paths.begin()->second.getDerivationPathFromSeed(); + QCborArray paramPath; + for (unsigned i = 0; i < path.size(); i++) { + //skip first index, it's the wallet root fingerprint + paramPath.append(path.at(i)); + } + QCborMap params = { {QLatin1Literal("script"), QByteArray::fromStdString(spender->getOutputScript().toBinStr())} + , {QLatin1Literal("input_tx"), QByteArray::fromStdString(txs.at(i).serialize().toBinStr())} + //, {QLatin1Literal("input_tx"), nullptr } + , {QLatin1Literal("satoshi"), (qint64)spender->getValue()} + , {QLatin1Literal("is_witness"), true}, {QLatin1Literal("path"), paramPath} }; + auto inInput = std::make_shared(); + inInput->request = getRequest(++seqId_, QLatin1Literal("tx_input"), params); + + const auto& cbInput = [this, i, addSignedInput](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + return; + } + if (data->futResponse.wait_for(std::chrono::milliseconds{ 15000 }) != std::future_status::ready) { + return; //FIXME^ + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::tx_input] #{} response1: {}", i, dump(msg)); + if (msg.contains(QLatin1Literal("error"))) { + cb_->operationFailed(key().id, msg[QLatin1Literal("error")][QLatin1Literal("message")].toString().toStdString()); + return; + } + const auto binSignedData = BinaryData::fromString(msg[QLatin1Literal("result")].toByteArray().toStdString()); + addSignedInput(i, binSignedData); + }; + processQueued(inInput, cbInput); + } +#if 0 //temporarily + for (uint32_t i = 0; i < awaitingTXreq_.armorySigner_.getTxInCount(); ++i) { + const auto& cbResponse = [this, i, addSignedInput](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->warn("[JadeDevice::tx_input] invalid data"); + //cb_->operationFailed(key().id, "tx_input invalid data"); + return; + } + if (data->futResponse.wait_for(std::chrono::seconds{ 15 }) != std::future_status::ready) { + logger_->error("[JadeDevice::tx_input] data timeout"); + //cb_->operationFailed(key().id, "tx_input data timeout"); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::tx_input] #{} response2: {}", i, dump(msg)); + if (msg.contains(QLatin1Literal("error"))) { + cb_->operationFailed(key().id, msg[QLatin1Literal("error")][QLatin1Literal("message")].toString().toStdString()); + return; + } + const auto binSignedData = BinaryData::fromString(msg[QLatin1Literal("result")].toByteArray().toStdString()); + addSignedInput(i, binSignedData); + }; + auto inResponse = std::make_shared(); + processQueued(inResponse, cbResponse); + } +#endif + }; + processQueued(inReq, cbSign); +} + +void JadeDevice::releaseConnection() +{ + WorkerPool::cancel(); +} + +std::string bs::hww::JadeDevice::idFromSerial(const QSerialPortInfo& serial) +{ + return serial.hasProductIdentifier() ? std::to_string(serial.productIdentifier()) + : serial.portName().toStdString(); +} + +DeviceKey JadeDevice::key() const +{ + std::string status; + if (walletId_.empty()) { + if (!xpubRoot_.empty()) { + try { + /*const auto& seed = bs::core::wallet::Seed::fromXpub(xpubRoot_ + , testNet_ ? NetworkType::TestNet : NetworkType::MainNet);*/ + walletId_ = bs::core::wallet::computeID(xpubRoot_).toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to get walletId from {}: {}", __func__, xpubRoot_.toBinStr(), e.what()); + } + } + else { + status = "not inited"; + } + } + return { "Jade @" + endpoint_.portName().toStdString() + , idFromSerial(endpoint_) + , endpoint_.hasVendorIdentifier() ? std::to_string(endpoint_.vendorIdentifier()) : endpoint_.manufacturer().toStdString() + , walletId_, type() }; + return {}; +} + +DeviceType JadeDevice::type() const +{ + return DeviceType::HWJade; +} + +static uint32_t epoch() +{ + return std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); +} + +void JadeDevice::init() +{ + if (inited()) { + logger_->debug("[JadeDevice::init] already inited"); + cb_->publicKeyReady(key()); + return; + } + logger_->debug("[JadeDevice::init] start"); + auto in = std::make_shared(); + in->request = getRequest(++seqId_, QLatin1Literal("get_version_info")); + + const auto& cbXPub = [this](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::init::xpub] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::milliseconds{ 1500 }) != std::future_status::ready) { + logger_->error("[JadeDevice::init::xpub] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] xpub response: {}", dump(msg)); + + xpubRoot_ = BinaryData::fromString(msg[QLatin1Literal("result")].toString().toStdString()); + cb_->publicKeyReady(key()); + }; + + const auto& cbVersion = [this, cbXPub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::init::version] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::milliseconds{ 1500 }) != std::future_status::ready) { + logger_->error("[JadeDevice::init::version] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] version response: {}", dump(msg)); + + const auto& netType = msg[QLatin1Literal("result")][QLatin1Literal("JADE_NETWORKS")].toString().toStdString(); + if ((netType != "ALL") && (netType != (testNet_ ? "TEST" : "MAIN"))) { + logger_->error("[JadeDevice::init] network type mismatch: {}", netType); + cb_->publicKeyReady(key()); + return; + } + + auto inXpub = std::make_shared(); + bs::hd::Path path; + path.append(bs::hd::hardFlag); + path.append(testNet_ ? bs::hd::CoinType::Bitcoin_test : bs::hd::CoinType::Bitcoin_main); + const QCborMap params = { {QLatin1Literal("network"), network()}, {QLatin1Literal("path"), convertPath(path)}}; + inXpub->request = getRequest(++seqId_, QLatin1Literal("get_xpub"), params); + + if (msg[QLatin1Literal("result")][QLatin1Literal("JADE_STATE")].toString().toStdString() == "LOCKED") { + if (cb_) { + cb_->requestHWPass(key(), true); + } + const QCborMap authParams{ {QLatin1Literal("network"), network()}, {QLatin1Literal("epoch"), epoch()} }; + auto inAuth = std::make_shared(); + inAuth->request = getRequest(++seqId_, QLatin1Literal("auth_user"), authParams); + const auto& cbAuth = [this, cbXPub, inXpub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::init::auth] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::seconds{ 30 }) != std::future_status::ready) { + logger_->error("[JadeDevice::init::auth] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] auth response: {}", dump(msg)); + + if (msg[QLatin1Literal("result")].isBool()) { + if (msg[QLatin1Literal("result")].isTrue()) { + processQueued(inXpub, cbXPub); + } + else { + logger_->error("[JadeDevice::init::auth] failed"); + cb_->publicKeyReady(key()); + return; + } + } + else { // forward request to PIN server + const auto& httpParams = msg[QLatin1Literal("result")][QLatin1Literal("http_request")][QLatin1Literal("params")]; + auto inHttp = std::make_shared(); + inHttp->url = httpParams[QLatin1Literal("urls")].toArray().at(0).toString().toStdString(); + inHttp->data = httpParams[QLatin1Literal("data")].toString().toStdString(); + const auto onReply = msg[QLatin1Literal("result")][QLatin1Literal("http_request")][QLatin1Literal("on-reply")].toString(); + + const auto& cbHttp = [this, onReply, cbXPub, inXpub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data || data->response.empty()) { + logger_->error("[JadeDevice::init::http] invalid data"); + cb_->publicKeyReady(key()); + return; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson(QByteArray::fromStdString(data->response)); + const auto& params = QCborMap::fromJsonObject(jsonDoc.object()); + auto inResp = std::make_shared(); + inResp->request = getRequest(++seqId_, onReply, params); + const auto& cbHandshake = [this, cbXPub, inXpub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::init::handshake] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::seconds{ 23 }) != std::future_status::ready) { + logger_->error("[JadeDevice::init::handshake] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] handshake response: {}", dump(msg)); + + const auto& httpParams = msg[QLatin1Literal("result")][QLatin1Literal("http_request")][QLatin1Literal("params")]; + auto inHttp = std::make_shared(); + inHttp->url = httpParams[QLatin1Literal("urls")].toArray().at(0).toString().toStdString(); + const auto& jsonData = httpParams[QLatin1Literal("data")].toJsonValue().toObject(); + inHttp->data = QJsonDocument(jsonData).toJson().toStdString(); + const auto onReply = msg[QLatin1Literal("result")][QLatin1Literal("http_request")][QLatin1Literal("on-reply")].toString(); + + const auto& cbReply = [this, onReply, cbXPub, inXpub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data || data->response.empty()) { + logger_->error("[JadeDevice::init::reply] invalid data"); + cb_->publicKeyReady(key()); + return; + } + QJsonDocument jsonDoc = QJsonDocument::fromJson(QByteArray::fromStdString(data->response)); + const auto& params = QCborMap::fromJsonObject(jsonDoc.object()); + auto inComplete = std::make_shared(); + inComplete->request = getRequest(++seqId_, onReply, params); + const auto& cbComplete = [this, cbXPub, inXpub](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::init::complete] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::seconds{ 3 }) != std::future_status::ready) { + logger_->error("[JadeDevice::init::complete] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] complete response: {}", dump(msg)); + if (msg[QLatin1Literal("result")].isBool() && msg[QLatin1Literal("result")].isTrue()) { + processQueued(inXpub, cbXPub); + } + else { + logger_->error("[JadeDevice::init::complete] handshake failed"); + cb_->publicKeyReady(key()); + } + }; + processQueued(inComplete, cbComplete); + }; + processQueued(inHttp, cbReply); + }; + processQueued(inResp, cbHandshake); + }; + processQueued(inHttp, cbHttp); + } + }; + processQueued(inAuth, cbAuth); + } + else { + processQueued(inXpub, cbXPub); + } + }; + processQueued(in, cbVersion); +} + +void JadeDevice::getPublicKeys() +{ + awaitingWalletInfo_ = {}; + awaitingWalletInfo_.type = bs::wallet::HardwareEncKey::WalletType::Jade; + awaitingWalletInfo_.xpubRoot = xpubRoot_.toBinStr(); + awaitingWalletInfo_.label = key().label; + awaitingWalletInfo_.deviceId = key().id; + awaitingWalletInfo_.vendor = key().vendor; + + const auto& requestXPub = [this](bs::hd::Purpose purp)->std::pair, bs::WorkerPool::callback> + { + auto inXpub = std::make_shared(); + bs::hd::Path path; + path.append(purp + bs::hd::hardFlag); + path.append(testNet_ ? bs::hd::CoinType::Bitcoin_test : bs::hd::CoinType::Bitcoin_main); + path.append(bs::hd::hardFlag); + const QCborMap params = { {QLatin1Literal("network"), network()}, {QLatin1Literal("path"), convertPath(path)} }; + inXpub->request = getRequest(++seqId_, QLatin1Literal("get_xpub"), params); + + return {inXpub , [this, purp](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::getPublicKeys::xpub] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::milliseconds{ 1500 }) != std::future_status::ready) { + logger_->error("[JadeDevice::getPublicKeys::xpub] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::getPublicKeys] xpub response: {}", dump(msg)); + + if (purp == bs::hd::Purpose::Native) { + awaitingWalletInfo_.xpubNativeSegwit = msg[QLatin1Literal("result")].toString().toStdString(); + } + else if (purp == bs::hd::Purpose::Nested) { + awaitingWalletInfo_.xpubNestedSegwit = msg[QLatin1Literal("result")].toString().toStdString(); + } + else { + logger_->error("[JadeDevice::getPublicKeys::xpub] unsupported wallet type {}", purp); + } + if (!awaitingWalletInfo_.xpubNativeSegwit.empty() && !awaitingWalletInfo_.xpubNestedSegwit.empty()) { + cb_->walletInfoReady(key(), awaitingWalletInfo_); + } + } }; + }; + const auto& inXpubNative = requestXPub(bs::hd::Purpose::Native); + processQueued(inXpubNative.first, inXpubNative.second); + const auto& inXpubNested = requestXPub(bs::hd::Purpose::Nested); + processQueued(inXpubNested.first, inXpubNested.second); +} + +void JadeDevice::signTX(const bs::core::wallet::TXSignRequest &reqTX) +{ + logger_->debug("[JadeDevice::signTX]"); + std::vector txHashes; + for (uint32_t i = 0; i < reqTX.armorySigner_.getTxInCount(); ++i) { + const auto& spender = reqTX.armorySigner_.getSpender(i); + if (!spender) { + logger_->warn("[JadeDevice::signTX] no spender at {}", i); + continue; + } + txHashes.push_back(spender->getUtxo().getTxHash()); + + auto bip32Paths = spender->getBip32Paths(); + if (bip32Paths.size() != 1) { + logger_->error("[TrezorDevice::signTX] {} BIP32 paths", bip32Paths.size()); + continue; + } + const auto& path = bip32Paths.begin()->second.getDerivationPathFromSeed(); + const QCborMap params = { {QLatin1Literal("network"), network()}, {QLatin1Literal("path"), convertPath(path)} }; + auto inXpub = std::make_shared(); + inXpub->request = getRequest(++seqId_, QLatin1Literal("get_xpub"), params); + + const auto& cbXPub = [this](const std::shared_ptr& out) + { + const auto& data = std::static_pointer_cast(out); + if (!data) { + logger_->error("[JadeDevice::signTX] invalid data"); + cb_->publicKeyReady(key()); + return; + } + if (data->futResponse.wait_for(std::chrono::milliseconds{ 1500 }) != std::future_status::ready) { + logger_->error("[JadeDevice::signTX] data timeout"); + cb_->publicKeyReady(key()); + return; + } + const auto& msg = data->futResponse.get(); + logger_->debug("[JadeDevice::init] xpub response: {}", dump(msg)); + }; + processQueued(inXpub, cbXPub); + } + logger_->debug("[JadeDevice::signTX] {} prevOuts requested", txHashes.size()); + cb_->needSupportingTXs(key(), txHashes); + awaitingTXreq_ = reqTX; +} + +void JadeDevice::retrieveXPubRoot() +{ + logger_->debug("[JadeDevice::retrieveXPubRoot]"); +} + +void JadeDevice::reset() +{ + awaitingTXreq_ = {}; + awaitingWalletInfo_ = {}; +} + +QString bs::hww::JadeDevice::network() const +{ + return testNet_ ? QLatin1Literal("testnet") : QLatin1Literal("mainnet"); +} + + +JadeSerialHandler::JadeSerialHandler(const std::shared_ptr& logger + , const QSerialPortInfo& spi) + : QObject(nullptr), logger_(logger), serial_(new QSerialPort(spi, this)) +{ + serial_->setBaudRate(QSerialPort::Baud115200); + serial_->setDataBits(QSerialPort::Data8); + serial_->setParity(QSerialPort::NoParity); + serial_->setStopBits(QSerialPort::OneStop); + + if (!Connect()) { + throw std::runtime_error("failed to open port " + spi.portName().toStdString()); + } +} + +JadeSerialHandler::~JadeSerialHandler() +{ + Disconnect(); +} + +std::shared_ptr JadeSerialHandler::processData(const std::shared_ptr& in) +{ + if (!serial_->isOpen()) { + return nullptr; + } + auto out = std::make_shared(); + if (in->needResponse) { + auto prom = std::make_shared>(); + out->futResponse = prom->get_future(); + requests_.push_back(prom); + } + logger_->debug("[JadeSerialHandler] sending: {}", dump(in->request)); + if (!in->request.empty()) { + const auto bytes = in->request.toCborValue().toCbor(); + write(bytes); + } + return out; +} + +bool bs::hww::JadeSerialHandler::Connect() +{ + if (serial_->isOpen()) { + return true; // already connected + } + if (serial_->open(QIODevice::ReadWrite)) { + // Connect 'data received' slot + connect(serial_, &QSerialPort::readyRead, this + , &JadeSerialHandler::onSerialDataReady); + return true; + } + Disconnect(); + return false; +} + +void bs::hww::JadeSerialHandler::Disconnect() +{ + disconnect(serial_, nullptr, this, nullptr); + if (serial_->isOpen()) { + serial_->close(); + } +} + +int bs::hww::JadeSerialHandler::write(const QByteArray& data) +{ + assert(serial_); + assert(serial_->isOpen()); + + int written = 0; + while (written != data.length()) { + const auto wrote = serial_->write(data.data() + written, qMin(256, data.length() - written)); + if (wrote < 0) { + logger_->debug("[{}] write error: {}", __func__, wrote); + Disconnect(); + return written; + } + else { + serial_->waitForBytesWritten(100); + written += wrote; + } + std::this_thread::sleep_for(std::chrono::milliseconds{ 100 }); + } + logger_->debug("[{}] {} bytes (of {}) written", __func__, written, data.size()); + return written; +} + +void bs::hww::JadeSerialHandler::parsePortion(const QByteArray& data) +{ + try { + // Collect data + unparsed_.append(data); + + // Try to parse cbor objects from byte buffer until it has no more complete objects + for (bool readNextObj = true; readNextObj; /*nothing - set in loop*/) { + QCborParserError err; + const QCborValue cbor = QCborValue::fromCbor(unparsed_, &err); + //logger_->debug("[{}] read type {}, result={}", __func__, cbor.type() + // , err.error.toString().toStdString()); + readNextObj = false; // In most cases we don't read another object + + if (err.error == QCborError::NoError && cbor.isMap()) { + const QCborMap msg = cbor.toMap(); + if (msg.contains(QCborValue(QLatin1Literal("log")))) { + // Print Jade log line immediately + logger_->info("[{}] {}", __func__, msg[QLatin1Literal("log")] + .toByteArray().toStdString()); + } + else { + //logger_->debug("[{}] ready: {}", __func__, dump(msg)); + if (requests_.empty()) { + logger_->error("[{}] unexpected response", __func__); + } + else { + requests_.front()->set_value(msg); + requests_.pop_front(); + } + } + + // Remove read object from m_data buffer + if (err.offset == unparsed_.length()) { + unparsed_.clear(); + } + else { + // We successfully read an object and there are still bytes left in the buffer - this + // is the one case where we loop and read again - make sure to preserve the remaining bytes. + const int remainder = static_cast(unparsed_.length() - err.offset); + unparsed_ = unparsed_.right(remainder); + readNextObj = true; + logger_->debug("[{}] {} more data after the message", __func__, remainder); + } + } + else if (err.error == QCborError::EndOfFile) { + // partial object - stop trying to read objects for now, await more data + if (unparsed_.length() > 0) { + logger_->debug("[{}] CBOR incomplete ({} bytes present) - awaiting more data" + , __func__, unparsed_.size()); + } + } + else { + // Unexpected parse error + logger_->warn("[{}] unexpected type {} and/or error: {}", __func__ + , (int)cbor.type(), err.error.toString().toStdString()); + Disconnect(); + } + } + } + catch (const std::exception& e) { + logger_->error("[{}] exception: {}", __func__, e.what()); + Disconnect(); + } + catch (...) { + logger_->error("[{}] exception", __func__); + Disconnect(); + } +} + +void JadeSerialHandler::onSerialDataReady() +{ + assert(serial_); + const auto& data = serial_->readAll(); + logger_->debug("[{}] {} bytes", __func__, data.size()); + + if (requests_.empty()) { + logger_->error("[{}] dropped {} bytes of serial data", __func__, data.size()); + return; + } + parsePortion(data); +} + +static size_t writeToString(void* ptr, size_t size, size_t count, std::string* stream) +{ + const size_t resSize = size * count; + stream->append((char*)ptr, resSize); + return resSize; +} + +JadeHttpHandler::JadeHttpHandler(const std::shared_ptr& logger) + : logger_(logger) +{ + curl_ = curl_easy_init(); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, writeToString); + curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl_, CURLOPT_POST, 1); + + curlHeaders_ = curl_slist_append(curlHeaders_, "Content-Type: application/json"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curlHeaders_); +} + +JadeHttpHandler::~JadeHttpHandler() +{ + curl_slist_free_all(curlHeaders_); + curl_easy_cleanup(curl_); +} + +std::shared_ptr JadeHttpHandler::processData(const std::shared_ptr& in) +{ + auto result = std::make_shared(); + if (!curl_) { + return result; + } + logger_->debug("[JadeHttpHandler::processData] request: {} {}", in->url, in->data); + curl_easy_setopt(curl_, CURLOPT_URL, in->url.c_str()); + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, in->data.data()); + std::string response; + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + + const auto res = curl_easy_perform(curl_); + if (res != CURLE_OK) { + logger_->error("[JadeHttpHandler::processData] failed to post to {}: {}", in->url, res); + return result; + } + result->response = response; + logger_->debug("[JadeHttpHandler::processData] response: {}", result->response); + return result; +} diff --git a/BlockSettleHW/jade/jadeDevice.h b/BlockSettleHW/jade/jadeDevice.h new file mode 100644 index 000000000..1b336638b --- /dev/null +++ b/BlockSettleHW/jade/jadeDevice.h @@ -0,0 +1,164 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef JADE_DEVICE_H +#define JADE_DEVICE_H + +#include +#include +#include +#include "Message/Worker.h" +#include "hwdeviceinterface.h" +#include "jadeClient.h" + +namespace spdlog { + class logger; +} +namespace bs { + namespace core { + namespace wallet { + struct TXSignRequest; + } + } +} +struct curl_slist; + +namespace bs { + namespace hww { + + struct JadeSerialIn : public bs::InData + { + ~JadeSerialIn() override = default; + QCborMap request; + bool needResponse{ true }; + }; + struct JadeSerialOut : public bs::OutData + { + ~JadeSerialOut() override = default; + std::future futResponse; + }; + + class JadeSerialHandler : public QObject + , public bs::HandlerImpl + { + Q_OBJECT + public: + JadeSerialHandler(const std::shared_ptr& + , const QSerialPortInfo& spi); + ~JadeSerialHandler() override; + + protected: + std::shared_ptr processData(const std::shared_ptr&) override; + + private slots: + void onSerialDataReady(); // Invoked when new serial data arrived + + private: + bool Connect(); + void Disconnect(); + int write(const QByteArray&); + void parsePortion(const QByteArray&); + + private: + std::shared_ptr logger_; + QSerialPort* serial_{ nullptr }; + std::deque>> requests_; + QByteArray unparsed_; + }; + + struct JadeHttpIn : public bs::InData + { + ~JadeHttpIn() override = default; + std::string url; + std::string data; + }; + struct JadeHttpOut : public bs::OutData + { + ~JadeHttpOut() override = default; + std::string response; + }; + + class JadeHttpHandler : public bs::HandlerImpl + { + public: + JadeHttpHandler(const std::shared_ptr&); + ~JadeHttpHandler() override; + + protected: + std::shared_ptr processData(const std::shared_ptr&) override; + + private: + std::shared_ptr logger_; + struct curl_slist* curlHeaders_{ NULL }; + void* curl_{ nullptr }; + }; + + + class JadeDevice : public DeviceInterface, protected WorkerPool + { + public: + JadeDevice(const std::shared_ptr& + , bool testNet, DeviceCallbacks*, const QSerialPortInfo&); + ~JadeDevice() override; + + static std::string idFromSerial(const QSerialPortInfo&); + DeviceKey key() const override; + DeviceType type() const override; + + // lifecycle + void init() override; + void cancel() override { WorkerPool::cancel(); } + void clearSession() override {} + void releaseConnection(); + + // operation + void getPublicKeys() override; + void signTX(const bs::core::wallet::TXSignRequest& reqTX) override; + void retrieveXPubRoot() override; + + // State + bool isBlocked() const override { + // There is no blocking state for Trezor + return false; + } + + protected: + std::shared_ptr worker(const std::shared_ptr&) override final; + + // operation result informing + void publicKeyReady() override {} //TODO: implement + void deviceTxStatusChanged(const std::string& status) override {} //TODO: implement + void operationFailed(const std::string& reason) override; + void requestForRescan() override {} //TODO: implement + void setSupportingTXs(const std::vector&) override; + + // Management + void cancelledOnDevice() override {} //TODO: implement + void invalidPin() override {} //TODO: implement + + private: + void reset(); + QString network() const; + + private: + std::shared_ptr logger_; + const bool testNet_; + DeviceCallbacks* cb_{ nullptr }; + const QSerialPortInfo endpoint_; + int seqId_{ 0 }; + mutable std::string walletId_; + const std::vector> handlers_; + bs::core::HwWalletInfo awaitingWalletInfo_; + bs::core::wallet::TXSignRequest awaitingTXreq_; + }; + + } //hw +} //bs +#endif // JADE_DEVICE_H diff --git a/BlockSettleHW/ledger/ledgerClient.cpp b/BlockSettleHW/ledger/ledgerClient.cpp index 6ba0ddebc..499b4c3f4 100644 --- a/BlockSettleHW/ledger/ledgerClient.cpp +++ b/BlockSettleHW/ledger/ledgerClient.cpp @@ -13,27 +13,26 @@ #include #include #endif - +#include +#include +#include #include "ledger/ledgerClient.h" #include "ledger/ledgerDevice.h" #include "ledger/hidapi/hidapi.h" -#include -#include "Wallets/SyncWalletsManager.h" - -#include -#include +#include "hwdevicemanager.h" +using namespace bs::hww; namespace { HidDeviceInfo fromHidOriginal(hid_device_info* info) { return { - QString::fromUtf8(info->path), + QString::fromUtf8(info->path).toStdString(), info->vendor_id, info->product_id, - QString::fromWCharArray(info->serial_number), + QString::fromWCharArray(info->serial_number).toStdString(), info->release_number, - QString::fromWCharArray(info->manufacturer_string), - QString::fromWCharArray(info->product_string), + QString::fromWCharArray(info->manufacturer_string).toStdString(), + QString::fromWCharArray(info->product_string).toStdString(), info->usage_page, info->usage, info->interface_number, @@ -41,82 +40,64 @@ namespace { } } -LedgerClient::LedgerClient(std::shared_ptr logger, std::shared_ptr walletManager, bool testNet, QObject *parent /*= nullptr*/) - : QObject(parent) - , logger_(logger) - , testNet_(testNet) - , walletManager_(walletManager) +LedgerClient::LedgerClient(const std::shared_ptr& logger + , bool testNet, DeviceCallbacks *cb) + : logger_(logger), testNet_(testNet), cb_(cb) { hidLock_ = std::make_shared(); } -QVector LedgerClient::deviceKeys() const +std::vector LedgerClient::deviceKeys() const { - QVector keys; + std::vector keys; keys.reserve(availableDevices_.size()); for (const auto device : availableDevices_) { - if (device->inited()) { - keys.push_back(device->key()); - } + keys.push_back(device->key()); } + logger_->debug("[LedgerClient::deviceKeys] {} key[s]", keys.size()); return keys; } -QPointer LedgerClient::getDevice(const QString& deviceId) +std::shared_ptr LedgerClient::getDevice(const std::string& deviceId) { for (auto device : availableDevices_) { - if (device->key().deviceId_ == deviceId) { + if (device->key().id == deviceId) { return device; } } - return nullptr; } -QString LedgerClient::lastScanError() const +std::string LedgerClient::lastScanError() const { return lastScanError_; } -void LedgerClient::scanDevices(AsyncCallBack&& cb) +void LedgerClient::scanDevices() { availableDevices_.clear(); + logger_->debug("[LedgerClient::scanDevices] start HID enumeration"); hid_device_info* info = hid_enumerate(0, 0); for (; info; info = info->next) { + /*logger_->debug("[{}] found: vendor {}, product {} ({}), serial {}, iface {}" + , __func__, info->vendor_id, info->product_id + , QString::fromWCharArray(info->product_string).toStdString() + , QString::fromWCharArray(info->serial_number).toStdString() + , info->interface_number);*/ if (checkLedgerDevice(info)) { - auto device = new LedgerDevice{ fromHidOriginal(info), testNet_, walletManager_, logger_, this, hidLock_}; - availableDevices_.push_back({ device }); + const auto& device = std::make_shared(fromHidOriginal(info) + , testNet_, logger_, cb_, hidLock_); + availableDevices_.push_back(device); } } - - if (availableDevices_.empty()) { - logger_->error( - "[LedgerClient] scanDevices - No ledger device available"); - } - else { - logger_->info( - "[LedgerClient] scanDevices - Enumerate request succeeded. Total device available : " - + QString::number(availableDevices_.size()).toUtf8() + "."); - } - hid_exit(); - // Init first one if (availableDevices_.empty()) { - if (cb) { - cb(); - } + logger_->info("[LedgerClient::scanDevices] no ledger devices available"); } else { - auto cbSaveScanError = [caller = QPointer(this), cbCopy = std::move(cb)]() { - caller->lastScanError_ = caller->availableDevices_[0]->lastError(); - - if (cbCopy) { - cbCopy(); - } - }; - - availableDevices_[0]->init(std::move(cbSaveScanError)); + logger_->info("[LedgerClient::scanDevices] found {} device[s]", availableDevices_.size()); } + cb_->scanningDone(); } diff --git a/BlockSettleHW/ledger/ledgerClient.h b/BlockSettleHW/ledger/ledgerClient.h index 3b14238f2..8fdcc61cd 100644 --- a/BlockSettleHW/ledger/ledgerClient.h +++ b/BlockSettleHW/ledger/ledgerClient.h @@ -11,14 +11,11 @@ #ifndef LEDGERCLIENT_H #define LEDGERCLIENT_H -#include "ledgerStructure.h" - #include #include +#include +#include "hwdeviceinterface.h" -#include - -class LedgerDevice; namespace spdlog { class logger; } @@ -28,30 +25,35 @@ namespace bs { } } -class LedgerClient : public QObject -{ - Q_OBJECT -public: - LedgerClient(std::shared_ptr logger, std::shared_ptr walletManager, bool testNet, QObject *parent = nullptr); - ~LedgerClient() override = default; +namespace bs { + namespace hww { + struct DeviceCallbacks; + class DeviceInterface; - void scanDevices(AsyncCallBack&& cb); + class LedgerClient + { + public: + LedgerClient(const std::shared_ptr&, bool testNet, DeviceCallbacks*); + ~LedgerClient() = default; - QVector deviceKeys() const; + void scanDevices(); - QPointer getDevice(const QString& deviceId); + std::vector deviceKeys() const; - QString lastScanError() const; + std::shared_ptr getDevice(const std::string& deviceId); -private: - QVector> availableDevices_; - bool testNet_; - QString lastScanError_; + std::string lastScanError() const; - std::shared_ptr logger_; - std::shared_ptr walletManager_; - std::shared_ptr hidLock_; + private: + std::shared_ptr logger_; + const bool testNet_; + DeviceCallbacks* cb_{ nullptr }; + std::vector> availableDevices_; + std::string lastScanError_; + std::shared_ptr hidLock_; + }; -}; + } //hw +} //bs #endif // LEDGERCLIENT_H diff --git a/BlockSettleHW/ledger/ledgerDevice.cpp b/BlockSettleHW/ledger/ledgerDevice.cpp index aa5214db3..ff02bf4b9 100644 --- a/BlockSettleHW/ledger/ledgerDevice.cpp +++ b/BlockSettleHW/ledger/ledgerDevice.cpp @@ -12,19 +12,40 @@ #include "ledger/ledgerDevice.h" #include "ledger/ledgerClient.h" #include "Assets.h" -#include "Wallets/ProtobufHeadlessUtils.h" #include "CoreWallet.h" +#include "hwdevicemanager.h" +#include "Wallets/ProtobufHeadlessUtils.h" #include "Wallets/SyncWalletsManager.h" #include "Wallets/SyncHDWallet.h" #include "ScopeGuard.h" #include "QByteArray" -#include "QDataStream" namespace { + class LedgerException : public std::runtime_error + { + public: + LedgerException(const std::string& desc) : std::runtime_error(desc) {} + }; + + class LedgerBrokenSequence : public LedgerException + { + public: + LedgerBrokenSequence() : LedgerException("Unexpected sequence number 191") + {} + }; + + class LedgerReconnectDevice : public LedgerException + { + public: + LedgerReconnectDevice() : LedgerException("Device needs to be reconnected") + {} + }; + const uint16_t kHidapiBrokenSequence = 191; - const std::string kHidapiSequence191 = "Unexpected sequence number 191"; - int sendApdu(hid_device* dongle, const QByteArray& command) { + + int sendApdu(hid_device* dongle, const QByteArray& command) + { int result = 0; QVector chunks; uint16_t chunkNumber = 0; @@ -64,34 +85,35 @@ namespace { break; } } - return result; } - uint16_t receiveApduResult(hid_device* dongle, QByteArray& response) { + uint16_t receiveApduResult(hid_device* dongle, QByteArray& response) + { response.clear(); uint16_t expectedChunkIndex = 0; - unsigned char buf[Ledger::CHUNK_MAX_BLOCK]; + int result = hid_read(dongle, buf, Ledger::CHUNK_MAX_BLOCK); if (result < 0) { return result; } QByteArray chunk(reinterpret_cast(buf), Ledger::CHUNK_MAX_BLOCK); - auto checkChunkIndex = [&chunk, &expectedChunkIndex]() { + const auto& checkChunkIndex = [&expectedChunkIndex](const QByteArray& chunk) + { auto chunkIndex = static_cast(((uint8_t)chunk[3] << 8) | (uint8_t)chunk[4]); if (chunkIndex != expectedChunkIndex++) { if (chunkIndex == static_cast(kHidapiBrokenSequence)) { - throw std::logic_error(kHidapiSequence191); + throw LedgerBrokenSequence(); } else { - throw std::logic_error("Unexpected sequence number"); + throw LedgerException("unexpected sequence number"); } } }; - checkChunkIndex(); + checkChunkIndex(chunk); int left = static_cast(((uint8_t)chunk[5] << 8) | (uint8_t)chunk[6]); response.append(chunk.mid(Ledger::FIRST_BLOCK_OFFSET, left)); @@ -105,7 +127,7 @@ namespace { } chunk = QByteArray(reinterpret_cast(buf), Ledger::CHUNK_MAX_BLOCK); - checkChunkIndex(); + checkChunkIndex(chunk); response.append(chunk.mid(Ledger::NEXT_BLOCK_OFFSET, left)); } @@ -113,7 +135,6 @@ namespace { auto resultCode = response.right(2); response.chop(2); return static_cast(((uint8_t)resultCode[0] << 8) | (uint8_t)resultCode[1]); - } QByteArray getApduHeader(uint8_t cla, uint8_t ins, uint8_t p1, uint8_t p2) { @@ -133,50 +154,24 @@ namespace { } } -LedgerDevice::LedgerDevice(HidDeviceInfo&& hidDeviceInfo, bool testNet, - std::shared_ptr walletManager - , const std::shared_ptr &logger, QObject* parent - , const std::shared_ptr& hidLock) - : HwDeviceInterface{parent} - , hidDeviceInfo_{std::move(hidDeviceInfo)} - , logger_{logger} - , testNet_{testNet} - , walletManager_{walletManager} - , hidLock_{hidLock} -{ -} +using namespace bs::hww; -LedgerDevice::~LedgerDevice() -{ - cancelCommandThread(); -} +LedgerDevice::LedgerDevice(const HidDeviceInfo& hidDeviceInfo, bool testNet + , const std::shared_ptr &logger, DeviceCallbacks* cb + , const std::shared_ptr& hidLock) + : WorkerPool(1, 1), hidDeviceInfo_{hidDeviceInfo} + , logger_{logger}, cb_(cb), testNet_{testNet}, hidLock_{hidLock} +{} DeviceKey LedgerDevice::key() const { - QString walletId; + std::string walletId; if (!xpubRoot_.empty()) { - auto expectedWalletId = bs::core::wallet::computeID( - BinaryData::fromString(xpubRoot_)).toBinStr(); - - auto importedWallets = walletManager_->getHwWallets( - bs::wallet::HardwareEncKey::WalletType::Ledger, {}); - - for (const auto imported : importedWallets) { - if (expectedWalletId == imported) { - walletId = QString::fromStdString(expectedWalletId); - break; - } - } + walletId = bs::core::wallet::computeID(xpubRoot_).toBinStr(); } - return { - hidDeviceInfo_.productString_, - hidDeviceInfo_.manufacturerString_, - hidDeviceInfo_.manufacturerString_, - walletId, - {}, - DeviceType::HWLedger - }; + return { hidDeviceInfo_.product, hidDeviceInfo_.serialNumber + , hidDeviceInfo_.manufacturer, walletId, DeviceType::HWLedger }; } DeviceType LedgerDevice::type() const @@ -184,341 +179,130 @@ DeviceType LedgerDevice::type() const return DeviceType::HWLedger; } -void LedgerDevice::init(AsyncCallBack&& cb /*= nullptr*/) +void LedgerDevice::init() { - auto saveRootKey = [caller = QPointer(this), cbCopy = std::move(cb)](QVariant result) { - caller->xpubRoot_ = result.value().info_.xpubRoot; - - if (cbCopy) { - cbCopy(); - } - }; - - auto commandThread = blankCommand(std::move(saveRootKey)); - commandThread->prepareGetRootKey(); - connect(commandThread, &LedgerCommandThread::finished, commandThread, &QObject::deleteLater); - commandThread->start(); + retrieveXPubRoot(); } -void LedgerDevice::cancel() -{ - cancelCommandThread(); -} -void LedgerDevice::getPublicKey(AsyncCallBackCall&& cb /*= nullptr*/) +bool DeviceIOHandler::writeData(const QByteArray& input, const std::string& logHeader) noexcept { - auto commandThread = blankCommand(std::move(cb)); - commandThread->prepareGetPublicKey(key()); - connect(commandThread, &LedgerCommandThread::finished, commandThread, &QObject::deleteLater); - commandThread->start(); -} - -void LedgerDevice::signTX(const bs::core::wallet::TXSignRequest& coreReq, AsyncCallBackCall&& cb /*= nullptr*/) -{ - // retrieve inputs paths - std::vector inputPathes; - for (int i = 0; i < coreReq.armorySigner_.getTxInCount(); ++i) { - auto spender = coreReq.armorySigner_.getSpender(i); - const auto& bip32Paths = spender->getBip32Paths(); - if (bip32Paths.size() != 1) { - throw std::logic_error("spender should only have one bip32 path"); - } - auto pathFromRoot = bip32Paths.begin()->second.getDerivationPathFromSeed(); - - bs::hd::Path path; - for (unsigned i=0; i 0) { - const auto purp = bs::hd::purpose(coreReq.change.address.getType()); - if (coreReq.change.index.empty()) { - throw std::logic_error(fmt::format("can't find change address index for '{}'", coreReq.change.address.display())); - } - - changePath = getDerivationPath(testNet_, purp); - changePath.append(bs::hd::Path::fromString(coreReq.change.index)); + logger_->debug("{} >>> {}", logHeader, input.toHex().toStdString()); + if (sendApdu(dongle_, input) < 0) { + logger_->error("{} Cannot write to device", logHeader); + return false; } - - // create different thread because hidapi is working in blocking mode - auto commandThread = blankCommand(std::move(cb)); - commandThread->prepareSignTx( - key(), coreReq, std::move(inputPathes), std::move(changePath)); - commandThread->start(); + return true; } -QPointer LedgerDevice::blankCommand(AsyncCallBackCall&& cb /*= nullptr*/) +void DeviceIOHandler::readData(QByteArray& output, const std::string& logHeader) { - commandThread_ = new LedgerCommandThread(hidDeviceInfo_, testNet_, logger_, this, hidLock_); - connect(commandThread_, &LedgerCommandThread::resultReady, this, [cbCopy = std::move(cb)](QVariant result) { - if (cbCopy) { - cbCopy(std::move(result)); - } - }); - connect(commandThread_, &LedgerCommandThread::info, this, [caller = QPointer(this)](QString info) { - if (!caller) { - return; - } - - if (info == HWInfoStatus::kTransaction) { - caller->isBlocked_ = true; - } - else if (info == HWInfoStatus::kReceiveSignedTx) { - caller->isBlocked_ = true; - } - caller->deviceTxStatusChanged(info); - }, Qt::BlockingQueuedConnection); - connect(commandThread_, &LedgerCommandThread::error, this, [caller = QPointer(this)](qint32 errorCode) { - if (!caller) { - return; - } - - QString error; - switch (errorCode) - { - case Ledger::SW_NO_ENVIRONMENT: - caller->requestForRescan(); - error = HWInfoStatus::kErrorNoEnvironment; - break; - case Ledger::SW_CANCELED_BY_USER: - caller->cancelledOnDevice(); - error = HWInfoStatus::kCancelledByUser; - break; - case Ledger::NO_DEVICE: - caller->requestForRescan(); - error = HWInfoStatus::kErrorNoDevice; - break; - case Ledger::SW_RECONNECT_DEVICE: - caller->requestForRescan(); - error = HWInfoStatus::kErrorReconnectDevice; - break; - case Ledger::NO_INPUTDATA: - default: - error = HWInfoStatus::kErrorInternalError; - break; + const auto res = receiveApduResult(dongle_, output); + if (res != Ledger::SW_OK) { + if (res == Ledger::SW_RECONNECT_DEVICE) { + logger_->debug("[{}] {} bytes read before reconnect", __func__, output.size()); + throw LedgerReconnectDevice(); } - - caller->lastError_ = error; - caller->operationFailed(error); - }, Qt::BlockingQueuedConnection); - connect(commandThread_, &LedgerCommandThread::finished, commandThread_, &QObject::deleteLater); - - return commandThread_; -} - -void LedgerDevice::cancelCommandThread() -{ - if (commandThread_ && commandThread_->isRunning()) { - commandThread_->disconnect(); - commandThread_->quit(); + logger_->error("{} Cannot read from device. APDU error code : {}", + logHeader, QByteArray::number(res, 16).toStdString()); + throw LedgerException("Can't read from device: " + std::to_string(res)); } + logger_->debug("{} <<< {}", logHeader, BinaryData::fromString(output.toStdString()).toHexStr() + "9000"); } -LedgerCommandThread::LedgerCommandThread(const HidDeviceInfo &hidDeviceInfo, bool testNet - , const std::shared_ptr &logger, QObject *parent - , const std::shared_ptr& hidLock) - : QThread{parent} - , hidDeviceInfo_{hidDeviceInfo} - , testNet_{testNet} - , logger_{logger} - , hidLock_{hidLock} -{ -} - -LedgerCommandThread::~LedgerCommandThread() -{ - releaseDevice(); -} - -void LedgerCommandThread::run() +bool DeviceIOHandler::initDevice() noexcept { - const std::lock_guard lock(*hidLock_); - - if (!initDevice()) { - logger_->info( - "[LedgerCommandThread] processTXLegacy - Cannot open device."); - emit error(Ledger::NO_DEVICE); - return; + if (hid_init() < 0 || hidDeviceInfo_.serialNumber.empty()) { + logger_->error("[DeviceIOHandler::initDevice] cannot init hid"); + return false; } - try { - switch (threadPurpose_) - { - case HardwareCommand::GetPublicKey: - processGetPublicKey(); - break; - case HardwareCommand::SignTX: - if (!coreReq_) { - logger_->error("[LedgerCommandThread] run - the core request is no valid"); - emit error(Ledger::NO_INPUTDATA); + { // make sure that user does not switch off device in the middle of operation + auto* info = hid_enumerate(hidDeviceInfo_.vendorId, hidDeviceInfo_.productId); + if (!info) { + logger_->error("[DeviceIOHandler::initDevice] device {} {} not found" + , hidDeviceInfo_.vendorId, hidDeviceInfo_.productId); + return false; + } + + bool bFound = false; + for (; info; info = info->next) { + if (checkLedgerDevice(info)) { + bFound = true; break; } - if (isNonSegwit(inputPaths_[0])) { - processTXLegacy(); - } - else { - processTXSegwit(); - } - break; - case HardwareCommand::GetRootPublicKey: - processGetRootKey(); - break; - case HardwareCommand::None: - default: - // Please add handler for a new command - assert(false); - break; } - } - catch (std::exception& exc) { - releaseDevice(); - logger_->error("[LedgerCommandThread] run - Done command with exception"); - emit error(lastError_); - if (threadPurpose_ == HardwareCommand::GetRootPublicKey) { - emit resultReady({}); + if (!bFound) { + logger_->error("[DeviceIOHandler::initDevice] device not found"); + return false; } - return; } - if (lastError_ != Ledger::SW_OK) { - emit error(lastError_); - } - - releaseDevice(); -} - -void LedgerCommandThread::prepareGetPublicKey(const DeviceKey &deviceKey) -{ - threadPurpose_ = HardwareCommand::GetPublicKey; - deviceKey_ = deviceKey; -} - -void LedgerCommandThread::prepareSignTx(const DeviceKey &deviceKey, - bs::core::wallet::TXSignRequest coreReq, - std::vector&& paths, bs::hd::Path&& changePath) -{ - threadPurpose_ = HardwareCommand::SignTX; - deviceKey_ = deviceKey; - coreReq_ = std::make_unique(std::move(coreReq)); - inputPaths_ = std::move(paths); - changePath_ = std::move(changePath); + std::unique_ptr serNumb(new wchar_t[hidDeviceInfo_.serialNumber.length() + 1]); + QString::fromStdString(hidDeviceInfo_.serialNumber).toWCharArray(serNumb.get()); + serNumb.get()[hidDeviceInfo_.serialNumber.length()] = 0x00; + dongle_ = hid_open(hidDeviceInfo_.vendorId, static_cast(hidDeviceInfo_.productId), serNumb.get()); + logger_->debug("[DeviceIOHandler::initDevice] {} {}: {}", hidDeviceInfo_.vendorId + , hidDeviceInfo_.productId, (dongle_ != NULL)); + return dongle_ != NULL; } -void LedgerCommandThread::prepareGetRootKey() +void DeviceIOHandler::releaseDevice() noexcept { - threadPurpose_ = HardwareCommand::GetRootPublicKey; + if (dongle_) { + hid_close(dongle_); + hid_exit(); + dongle_ = nullptr; + logger_->debug("[DeviceIOHandler::releaseDevice] {} {}", hidDeviceInfo_.vendorId + , hidDeviceInfo_.productId); + } } -void LedgerCommandThread::processGetPublicKey() +bool DeviceIOHandler::exchangeData(const QByteArray& input, QByteArray& output + , std::string&& logHeader) { - auto deviceKey = deviceKey_; - HwWalletWrapper walletInfo; - walletInfo.info_.type = bs::wallet::HardwareEncKey::WalletType::Ledger; - walletInfo.info_.vendor = deviceKey.vendor_.toStdString(); - walletInfo.info_.label = deviceKey.deviceLabel_.toStdString(); - walletInfo.info_.deviceId = {}; - - logger_->debug("[LedgerCommandThread] processGetPublicKey - Start retrieve root xpub key."); - - auto pubKey = retrievePublicKeyFromPath({ { bs::hd::hardFlag } }); - try { - walletInfo.info_.xpubRoot = pubKey.getBase58().toBinStr(); - } - catch (...) { - logger_->error("[LedgerCommandThread] getPublicKey - Cannot retrieve root xpub key."); - emit error(Ledger::INTERNAL_ERROR); - return; - } - - logger_->debug("[LedgerCommandThread] processGetPublicKey - Start retrieve nested segwit xpub key."); - pubKey = retrievePublicKeyFromPath(getDerivationPath(testNet_, bs::hd::Nested)); - try { - walletInfo.info_.xpubNestedSegwit = pubKey.getBase58().toBinStr(); - } - catch (...) { - logger_->error( - "[LedgerCommandThread] getPublicKey - Cannot retrieve nested segwit xpub key."); - emit error(Ledger::INTERNAL_ERROR); - return; - } - - logger_->debug("[LedgerCommandThread] processGetPublicKey - Start retrieve native segwit xpub key."); - pubKey = retrievePublicKeyFromPath(getDerivationPath(testNet_, bs::hd::Native)); - - try { - walletInfo.info_.xpubNativeSegwit = pubKey.getBase58().toBinStr(); - } - catch (...) { - logger_->error( - "[LedgerCommandThread] getPublicKey - Cannot retrieve native segwit xpub key."); - emit error(Ledger::INTERNAL_ERROR); - return; - } - - logger_->debug("[LedgerCommandThread] processGetPublicKey - Start retrieve legacy xpub key."); - pubKey = retrievePublicKeyFromPath(getDerivationPath(testNet_, bs::hd::NonSegWit)); - try { - walletInfo.info_.xpubLegacy = pubKey.getBase58().toBinStr(); - } - catch (...) { - logger_->error( - "[LedgerCommandThread] getPublicKey - Cannot retrieve legacy xpub key."); - emit error(Ledger::INTERNAL_ERROR); - return; - } - - if (!walletInfo.isValid()) { - logger_->error( - "[LedgerCommandThread] getPublicKey - Wallet info is not correct."); - emit error(Ledger::INTERNAL_ERROR); - return; - } - else { - logger_->debug("[LedgerCommandThread] getPublicKey - Operation succeeded."); + if (!writeData(input, logHeader)) { + return false; } - emit resultReady(QVariant::fromValue<>(walletInfo)); -} - -void LedgerCommandThread::processGetRootKey() -{ - HwWalletWrapper walletInfo; - walletInfo.info_.type = bs::wallet::HardwareEncKey::WalletType::Ledger; - auto pubKey = retrievePublicKeyFromPath({ { bs::hd::hardFlag } }); try { - walletInfo.info_.xpubRoot = pubKey.getBase58().toBinStr(); + readData(output, logHeader); + return true; } - catch (...) { - logger_->error( - "[LedgerCommandThread] getPublicKey - Cannot retrieve root xpub key."); - emit error(Ledger::INTERNAL_ERROR); - return; + catch (const LedgerBrokenSequence&) { + // Special case : sometimes hidapi return incorrect sequence_index as response + // and there is really now good solution for it, except restart dongle session + // till the moment error gone. + // https://github.com/obsidiansystems/ledger-app-tezos/blob/191-troubleshooting/README.md#error-unexpected-sequence-number-expected-0-got-191-on-macos + // (solution in link above is not working, given here for reference) + // Also it's a general issue for OSX really, but let's left it for all system, just in case + // And let's have 10 times threshold to avoid trying infinitively + static int maxAttempts = 10; + if (maxAttempts > 0) { + --maxAttempts; + ScopedGuard guard([] { + ++maxAttempts; + }); + releaseDevice(); + std::this_thread::sleep_for(std::chrono::milliseconds{ 100 }); + initDevice(); + return exchangeData(input, output, std::move(logHeader)); + } } - logger_->debug("[LedgerCommandThread] processGetPublicKey - Done retrieve root xpub key."); - - - emit resultReady(QVariant::fromValue<>(walletInfo)); -} - -BIP32_Node LedgerCommandThread::retrievePublicKeyFromPath(bs::hd::Path&& derivationPath) -{ - // Parent - std::unique_ptr parent = nullptr; - if (derivationPath.length() > 1) { - auto parentPath = derivationPath; - parentPath.pop(); - parent.reset(new BIP32_Node(getPublicKeyApdu(std::move(parentPath)))); + catch (const LedgerReconnectDevice&) { + logger_->error("{} device requires reconnection", logHeader); + /*releaseDevice(); + std::this_thread::sleep_for(std::chrono::milliseconds{100}); + initDevice(); + return exchangeData(input, output, std::move(logHeader));*/ + return false; } - - return getPublicKeyApdu(std::move(derivationPath), parent); + catch (const LedgerException&) {} + return false; } -BIP32_Node LedgerCommandThread::getPublicKeyApdu(bs::hd::Path&& derivationPath, const std::unique_ptr& parent) +BIP32_Node GetPubKeyHandler::getPublicKeyApdu(const bs::hd::Path& derivationPath + , const std::unique_ptr& parent) { QByteArray payload; payload.append(derivationPath.length()); @@ -531,16 +315,16 @@ BIP32_Node LedgerCommandThread::getPublicKeyApdu(bs::hd::Path&& derivationPath, command.append(payload); QByteArray response; - if (!exchangeData(command, response, "[LedgerCommandThread] getPublicKeyApdu - ")) { + if (!exchangeData(command, response, "[GetPubKeyHandler::getPublicKeyApdu]")) { return {}; } LedgerPublicKey pubKey; bool result = pubKey.parseFromResponse(response); - auto data = SecureBinaryData::fromString(pubKey.pubKey_.toStdString()); + auto data = SecureBinaryData::fromString(pubKey.pubKey.toStdString()); Armory::Assets::Asset_PublicKey pubKeyAsset(data); - SecureBinaryData chainCode = SecureBinaryData::fromString(pubKey.chainCode_.toStdString()); + SecureBinaryData chainCode = SecureBinaryData::fromString(pubKey.chainCode.toStdString()); uint32_t fingerprint = 0; if (parent) { @@ -558,116 +342,272 @@ BIP32_Node LedgerCommandThread::getPublicKeyApdu(bs::hd::Path&& derivationPath, return pubNode; } -QByteArray LedgerCommandThread::getTrustedInput(const BinaryData& hash, unsigned txOutId) +std::shared_ptr GetPubKeyHandler::processData(const std::shared_ptr& in) { - logger_->debug("[LedgerCommandThread] getTrustedInput - Start retrieve trusted input for legacy address."); + if (!in) { + return nullptr; + } + auto result = std::make_shared(); + const std::lock_guard lock(*hidLock_); + if (!initDevice()) { + logger_->info("[GetPubKeyHandler::processData] cannot init device"); + //emit error(Ledger::NO_DEVICE); + return result; + } + for (const auto& path : in->paths) { + result->pubKeys.push_back(retrievePublicKeyFromPath(path)); + } + releaseDevice(); + return result; +} - //find the supporting tx - auto tx = coreReq_->armorySigner_.getSupportingTx(hash); - QVector inputCommands; +BIP32_Node GetPubKeyHandler::retrievePublicKeyFromPath(const bs::hd::Path& derivationPath) +{ + std::unique_ptr parent; + if (derivationPath.length() > 1) { + auto parentPath = derivationPath; + parentPath.pop(); + parent = std::make_unique(getPublicKeyApdu(parentPath, {})); + } + return getPublicKeyApdu(derivationPath, parent); +} +void LedgerDevice::getPublicKeys() +{ + auto deviceKey = key(); + auto walletInfo = std::make_shared(); + walletInfo->type = bs::wallet::HardwareEncKey::WalletType::Ledger; + walletInfo->vendor = deviceKey.vendor; + walletInfo->label = deviceKey.label; + walletInfo->deviceId = {}; + + logger_->debug("[LedgerDevice::getPublicKeys] start retrieving device keys"); + auto inData = std::make_shared(); + inData->paths.push_back({ { bs::hd::hardFlag } }); + inData->paths.push_back(getDerivationPath(testNet_, bs::hd::Nested)); + inData->paths.push_back(getDerivationPath(testNet_, bs::hd::Native)); + inData->paths.push_back(getDerivationPath(testNet_, bs::hd::NonSegWit)); + + const auto& cb = [this, walletInfo](const std::shared_ptr& data) { - //trusted input request header - QByteArray txPayload; - writeUintBE(txPayload, txOutId); //outpoint index + const auto& result = std::static_pointer_cast(data); - writeUintLE(txPayload, tx.getVersion()); //supporting tx version - writeVarInt(txPayload, tx.getNumTxIn()); //supporting tx input count - auto command = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x00, 0x00, std::move(txPayload)); - inputCommands.push_back(std::move(command)); - } + if (result->pubKeys.size() != 4) { + logger_->error("[LedgerDevice::getPublicKeys] invalid amount of public " + "keys: {}", result->pubKeys.size()); + operationFailed("invalid amount of public keys: " + std::to_string(result->pubKeys.size())); + return; + } + auto pubKey = result->pubKeys.at(0); + try { + walletInfo->xpubRoot = pubKey.getBase58().toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[LedgerDevice::getPublicKeys] cannot retrieve root xpub key: {}", e.what()); + handleError(Ledger::INTERNAL_ERROR); + return; + } - //supporting tx inputs - for (unsigned i=0; ipubKeys.at(1); + try { + walletInfo->xpubNestedSegwit = pubKey.getBase58().toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[LedgerDevice::getPublicKeys] cannot retrieve nested segwit xpub key: {}", e.what()); + handleError(Ledger::INTERNAL_ERROR); + return; + } - //36 bytes of outpoint - QByteArray txInPayload; - txInPayload.push_back(QByteArray::fromRawData( - outpointRaw.getCharPtr(), outpointRaw.getSize())); + pubKey = result->pubKeys.at(2); + try { + walletInfo->xpubNativeSegwit = pubKey.getBase58().toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[LedgerDevice::getPublicKeys] cannot retrieve native segwit xpub key: {}", e.what()); + handleError(Ledger::INTERNAL_ERROR); + return; + } - //txin scriptSig size as varint - writeVarInt(txInPayload, scriptSig.getSize()); + pubKey = result->pubKeys.at(3); + try { + walletInfo->xpubLegacy = pubKey.getBase58().toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[LedgerDevice::getPublicKeys] cannot retrieve legacy xpub key: {}", e.what()); + handleError(Ledger::INTERNAL_ERROR); + return; + } - auto commandInput = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txInPayload)); - inputCommands.push_back(std::move(commandInput)); + const auto& isValid = [](const bs::core::HwWalletInfo& info) + { + return !info.xpubRoot.empty() && !info.xpubNestedSegwit.empty() && + !info.xpubNativeSegwit.empty() && !info.xpubLegacy.empty(); + }; + if (!isValid(*walletInfo)) { + logger_->error("[LedgerDevice::getPublicKeys] wallet info is invalid"); + handleError(Ledger::INTERNAL_ERROR); + return; + } + else { + logger_->debug("[LedgerDevice::getPublicKeys] operation succeeded"); + } + cb_->walletInfoReady(key(), *walletInfo); + }; + processQueued(inData, cb); +} - //txin scriptSig, assuming it's less than 251 bytes for the sake of simplicity - QByteArray txInScriptSig; - txInScriptSig.push_back(QByteArray::fromRawData( - scriptSig.toCharPtr(), scriptSig.getSize())); - writeUintLE(txInScriptSig, txIn.getSequence()); //sequence +void LedgerDevice::signTX(const bs::core::wallet::TXSignRequest& coreReq) +{ + try { + // retrieve inputs paths + std::vector inputPaths; + for (int i = 0; i < coreReq.armorySigner_.getTxInCount(); ++i) { + auto spender = coreReq.armorySigner_.getSpender(i); + const auto& bip32Paths = spender->getBip32Paths(); + if (bip32Paths.size() != 1) { + throw LedgerException("spender should only have one bip32 path"); + } + auto pathFromRoot = bip32Paths.begin()->second.getDerivationPathFromSeed(); - auto commandScriptSig = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txInScriptSig)); - inputCommands.push_back(std::move(commandScriptSig)); - } + bs::hd::Path path; + for (unsigned i = 0; i < pathFromRoot.size(); i++) { + path.append(pathFromRoot[i]); + } - { - //number of outputs - QByteArray txPayload; - writeVarInt(txPayload, tx.getNumTxOut()); //supporting tx input count - auto command = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txPayload)); - inputCommands.push_back(std::move(command)); - } + inputPaths.push_back(std::move(path)); + } - //supporting tx outputs - for (unsigned i=0; i 0) { + const auto purp = bs::hd::purpose(coreReq.change.address.getType()); + if (coreReq.change.index.empty()) { + throw LedgerException(fmt::format("can't find change address index for '{}'", coreReq.change.address.display())); + } - QByteArray txOutput; - writeUintLE(txOutput, txout.getValue()); //txout value - writeVarInt(txOutput, script.getSize()); //txout script len + changePath = getDerivationPath(testNet_, purp); + changePath.append(bs::hd::Path::fromString(coreReq.change.index)); + } - auto commandTxOut = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txOutput)); - inputCommands.push_back(std::move(commandTxOut)); + auto inPubData = std::make_shared(); + inPubData->paths = inputPaths; - //again, assuming the txout script is shorter than 255 bytes - QByteArray txOutScript; - txOutScript.push_back(QByteArray::fromRawData( - script.toCharPtr(), script.getSize())); + const auto& cb = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply) { + logger_->error("[LedgerDevice::signTX] invalid callback data"); + operationFailed("invalid data"); + return; + } + cb_->txSigned(key(), reply->serInputSigs); + }; + auto inData = std::make_shared(); + inData->key = key(); + inData->txReq = coreReq; + inData->inputPaths = std::move(inputPaths); + inData->changePath = std::move(changePath); - auto commandScript= getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txOutScript)); - inputCommands.push_back(std::move(commandScript)); + const auto& cbPub = [this, inData, cb](const std::shared_ptr& data) + { + const auto& pubResult = std::static_pointer_cast(data); + inData->inputNodes = std::move(pubResult->pubKeys); + processQueued(inData, cb); + }; + processQueued(inPubData, cbPub); } - - for (auto &inputCommand : inputCommands) { - QByteArray responseInput; - if (!exchangeData(inputCommand, responseInput, "[LedgerCommandThread] signTX - getting trusted input")) { - releaseDevice(); - throw std::runtime_error("failed to get trusted input"); - } + catch (const LedgerException& e) { + operationFailed(e.what()); } +} - //locktime - QByteArray locktime; - writeUintLE(locktime, tx.getLockTime()); - auto command = getApduCommand( - Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(locktime)); +void bs::hww::LedgerDevice::retrieveXPubRoot() +{ + auto deviceKey = key(); + auto walletInfo = std::make_shared(); + walletInfo->type = bs::wallet::HardwareEncKey::WalletType::Ledger; + walletInfo->vendor = deviceKey.vendor; + walletInfo->label = deviceKey.label; + walletInfo->deviceId = {}; - QByteArray trustedInput; - if (!exchangeData(command, trustedInput, "[LedgerCommandThread] signTX - getting trusted input")) { - releaseDevice(); - throw std::runtime_error("failed to get trusted input"); - } + auto inData = std::make_shared(); + inData->paths.push_back({ { bs::hd::hardFlag } }); - logger_->debug( - "[LedgerCommandThread] getTrustedInput - Done retrieve trusted input for legacy address."); + const auto& cb = [this, walletInfo](const std::shared_ptr& data) + { + const auto& result = std::static_pointer_cast(data); - return trustedInput; + if (result->pubKeys.size() != 1) { + logger_->error("[LedgerDevice::retrieveXPubRoot] invalid amount of public " + "keys: {}", result->pubKeys.size()); + handleError(Ledger::NO_INPUTDATA); + return; + } + const auto& pubKey = result->pubKeys.at(0); + if (xpubRoot_.empty()) { + xpubRoot_ = pubKey.getPublicKey(); + const auto& devKey = key(); + cb_->publicKeyReady(devKey); + return; + } + try { + walletInfo->xpubRoot = pubKey.getBase58().toBinStr(); + } + catch (const std::exception& e) { + logger_->error("[LedgerDevice::retrieveXPubRoot] cannot retrieve root xpub key: {}", e.what()); + handleError(Ledger::INTERNAL_ERROR); + return; + } + cb_->walletInfoReady(key(), *walletInfo); + }; + processQueued(inData, cb); } -// DO NOT DELETE JUST FOR HISTORICAL REFFERENCE -QByteArray LedgerCommandThread::getTrustedInputSegWit_outdated(const UTXO& utxo) +void bs::hww::LedgerDevice::requestForRescan() +{ +} + +std::shared_ptr bs::hww::LedgerDevice::worker(const std::shared_ptr&) +{ + const std::vector> handlers{ + std::make_shared(logger_, hidDeviceInfo_, hidLock_) + , std::make_shared(logger_, hidDeviceInfo_, hidLock_) + }; + return std::make_shared(handlers); +} + +void LedgerDevice::handleError(int32_t errorCode) +{ + std::string error; + switch (errorCode) + { + case Ledger::SW_NO_ENVIRONMENT: + requestForRescan(); + //error = InfoStatus::kErrorNoEnvironment; + break; + case Ledger::SW_CANCELED_BY_USER: + cancelledOnDevice(); + //error = HWInfoStatus::kCancelledByUser; + break; + case Ledger::NO_DEVICE: + requestForRescan(); + error = HWInfoStatus::kErrorNoDevice.toStdString(); + break; + case Ledger::SW_RECONNECT_DEVICE: + requestForRescan(); + error = HWInfoStatus::kErrorReconnectDevice.toStdString(); + break; + case Ledger::NO_INPUTDATA: + default: + error = HWInfoStatus::kErrorInternalError.toStdString(); + break; + } + lastError_ = error; + operationFailed(error); +} + +#if 0 // DO NOT DELETE JUST FOR HISTORICAL REFFERENCE +QByteArray getTrustedInputSegWit(const UTXO& utxo) { QByteArray trustedInput; trustedInput.push_back(QByteArray::fromStdString(utxo.getTxHash().toBinStr())); @@ -676,15 +616,15 @@ QByteArray LedgerCommandThread::getTrustedInputSegWit_outdated(const UTXO& utxo) return trustedInput; } +#endif //0 -void LedgerCommandThread::startUntrustedTransaction( - const std::vector& trustedInputs, +void SignTXHandler::startUntrustedTransaction(const std::vector& trustedInputs, const std::vector& redeemScripts, unsigned txOutIndex, bool isNew, bool isSW, bool isRbf) { - { + try { //setup untrusted transaction - logger_->debug("[LedgerCommandThread] startUntrustedTransaction - Start Init section"); + logger_->debug("[SignTXHandler::startUntrustedTransaction] start Init section"); QByteArray initPayload; writeUintLE(initPayload, Ledger::DEFAULT_VERSION); writeVarInt(initPayload, trustedInputs.size()); @@ -693,20 +633,24 @@ void LedgerCommandThread::startUntrustedTransaction( if (isSW) { p2 = isNew ? 0x02 : 0x80; } - auto initCommand = getApduCommand( - Ledger::CLA, Ledger::INS_HASH_INPUT_START, 0x00, p2, std::move(initPayload)); + auto initCommand = getApduCommand(Ledger::CLA, Ledger::INS_HASH_INPUT_START + , 0x00, p2, std::move(initPayload)); QByteArray responseInit; - if (!exchangeData(initCommand, responseInit, "[LedgerCommandThread] startUntrustedTransaction - InitPayload")) { + if (!exchangeData(initCommand, responseInit, "[SignTXHandler::startUntrustedTransaction] InitPayload")) { releaseDevice(); - throw std::runtime_error("failed to init untrusted tx"); + throw LedgerException("failed to init untrusted tx"); } - logger_->debug("[LedgerCommandThread] startUntrustedTransaction - Done Init section"); + logger_->debug("[SignTXHandler::startUntrustedTransaction] done Init section"); + } + catch (const LedgerException& e) { + logger_->error("[{}] {}", __func__, e.what()); + return; } //pass each input - logger_->debug("[LedgerCommandThread] startUntrustedTransaction - Start Input section"); - QVector inputCommands; + logger_->debug("[SignTXHandler::startUntrustedTransaction] start Input section"); + std::vector inputCommands; for (unsigned i=0; idebug("[SignTXHandler::startUntrustedTransaction] done Input section"); + } + catch (const LedgerException& e) { + logger_->error("[{}] {}", __func__, e.what()); + return; } - logger_->debug("[LedgerCommandThread] startUntrustedTransaction - Done Input section"); } -void LedgerCommandThread::finalizeInputFull() +void SignTXHandler::finalizeInputFull(const std::shared_ptr& inData) { - const bool hasChangeOutput = (changePath_.length() != 0); - if (hasChangeOutput) { - logger_->debug("[LedgerCommandThread] finalizeInputFull - Start Change section"); + if (inData->changePath.length() != 0) { + logger_->debug("[SignTXHandler::finalizeInputFull] start Change section"); // If tx has change, we send derivation path of return address in prio // before send all output addresses and change with it, so device could detect it QByteArray changeOutputPayload; - changeOutputPayload.append(static_cast(changePath_.length())); + changeOutputPayload.append(static_cast(inData->changePath.length())); - for (auto el : changePath_) { + for (const auto& el : inData->changePath) { writeUintBE(changeOutputPayload, el); } - auto changeCommand = getApduCommand(Ledger::CLA, Ledger::INS_HASH_INPUT_FINALIZE_FULL, 0xFF, 0x00, std::move(changeOutputPayload)); + auto changeCommand = getApduCommand(Ledger::CLA, Ledger::INS_HASH_INPUT_FINALIZE_FULL + , 0xFF, 0x00, std::move(changeOutputPayload)); QByteArray responseInput; - if (!exchangeData(changeCommand, responseInput, "[LedgerCommandThread] finalizeInputFull - changePayload ")) { + if (!exchangeData(changeCommand, responseInput, "[SignTXHandler::finalizeInputFull] changePayload ")) { releaseDevice(); return; } - logger_->debug("[LedgerCommandThread] finalizeInputFull - Done Change section"); + logger_->debug("[SignTXHandler::finalizeInputFull] done Change section"); } - logger_->debug("[LedgerCommandThread] finalizeInputFull - Start output section"); - size_t totalOutput = coreReq_->armorySigner_.getTxOutCount(); + logger_->debug("[SignTXHandler::finalizeInputFull] start output section"); + size_t totalOutput = inData->txReq.armorySigner_.getTxOutCount(); QByteArray outputFullPayload; writeVarInt(outputFullPayload, totalOutput); - for (unsigned i=0; iarmorySigner_.getTxOutCount(); i++) { - auto recipient = coreReq_->armorySigner_.getRecipient(i); + for (unsigned i=0; i < inData->txReq.armorySigner_.getTxOutCount(); i++) { + auto recipient = inData->txReq.armorySigner_.getRecipient(i); outputFullPayload.push_back(QByteArray::fromStdString(recipient->getSerializedScript().toBinStr())); } @@ -792,65 +742,74 @@ void LedgerCommandThread::finalizeInputFull() outputCommands.push_back(std::move(outputCommand)); } - emit info(HWInfoStatus::kPressButton); - - for (auto &outputCommand : outputCommands) { - QByteArray responseOutput; - if (!exchangeData(outputCommand, responseOutput, "[LedgerCommandThread] finalizeInputFull - outputPayload ")) { - releaseDevice(); - throw std::runtime_error("failed to upload recipients"); + //emit info(HWInfoStatus::kPressButton); + try { + for (auto& outputCommand : outputCommands) { + QByteArray responseOutput; + if (!exchangeData(outputCommand, responseOutput, "[SignTXHandler::finalizeInputFull] outputPayload")) { + releaseDevice(); + throw LedgerException("failed to upload recipients"); + } } + logger_->debug("[SignTXHandler::finalizeInputFull] done output section"); + } + catch (const LedgerException& e) { + logger_->error("[{}] {}", __func__, e.what()); + return; } - - logger_->debug("[LedgerCommandThread] finalizeInputFull - Done output section"); } -void LedgerCommandThread::processTXLegacy() +void SignTXHandler::processTXLegacy(const std::shared_ptr& inData + , const std::shared_ptr& outData) { //upload supporting tx to ledger, get trusted input back for our outpoints - emit info(HWInfoStatus::kTransaction); + //emit info(HWInfoStatus::kTransaction); std::vector trustedInputs; - for (unsigned i=0; iarmorySigner_.getTxInCount(); i++) { - auto spender = coreReq_->armorySigner_.getSpender(i); - auto trustedInput = getTrustedInput(spender->getOutputHash(), spender->getOutputIndex()); - trustedInputs.push_back(trustedInput); + for (unsigned i=0; i < inData->txReq.armorySigner_.getTxInCount(); i++) { + const auto& spender = inData->txReq.armorySigner_.getSpender(i); + const auto& trustedInput = getTrustedInput(inData, spender->getOutputHash() + , spender->getOutputIndex()); + if (!trustedInput.isEmpty()) { + trustedInputs.push_back(trustedInput); + } } // -- collect all redeem scripts std::vector redeemScripts; - for (int i = 0; i < coreReq_->armorySigner_.getTxInCount(); ++i) { - auto& utxo = coreReq_->armorySigner_.getSpender(i)->getUtxo(); + for (int i = 0; i < inData->txReq.armorySigner_.getTxInCount(); ++i) { + auto& utxo = inData->txReq.armorySigner_.getSpender(i)->getUtxo(); auto redeemScript = utxo.getScript(); - auto redeemScriptQ = QByteArray( - redeemScript.getCharPtr(), redeemScript.getSize()); + auto redeemScriptQ = QByteArray(redeemScript.getCharPtr(), redeemScript.getSize()); redeemScripts.push_back(redeemScriptQ); } // -- Start input upload section -- //upload the redeem script for each outpoint - QVector responseSigned; - for (int i = 0; i < coreReq_->armorySigner_.getTxInCount(); ++i) { + if (inData->inputPaths.size() != inData->txReq.armorySigner_.getTxInCount()) { + //TODO: report error + return; + } + std::map responseSigned; + for (int i = 0; i < inData->txReq.armorySigner_.getTxInCount(); ++i) { //pass true as this is a newly presented redeem script - startUntrustedTransaction( - trustedInputs, redeemScripts, i, i == 0, false, coreReq_->RBF); - + startUntrustedTransaction(trustedInputs, redeemScripts, i, i == 0, false + , inData->txReq.RBF); // -- Done input section -- // -- Start output section -- - //upload our recipients as serialized outputs - finalizeInputFull(); + finalizeInputFull(inData); // -- Done output section -- // At this point user verified all outputs and we can start signing inputs - emit info(HWInfoStatus::kReceiveSignedTx); + //emit info(HWInfoStatus::kReceiveSignedTx); // -- Start signing one by one all addresses -- logger_->debug("[LedgerCommandThread] processTXLegacy - Start signing section"); - auto &path = inputPaths_[i]; + auto &path = inData->inputPaths.at(i); QByteArray signPayload; signPayload.append(static_cast(path.length())); @@ -872,7 +831,7 @@ void LedgerCommandThread::processTXLegacy() return; } responseInputSign[0] = 0x30; // force first but to be 0x30 for a newer version of ledger - responseSigned.push_back(responseInputSign); + responseSigned[i] = responseInputSign; } logger_->debug("[LedgerCommandThread] processTXLegacy - Done signing section"); @@ -881,78 +840,77 @@ void LedgerCommandThread::processTXLegacy() // Done with device in this point // Composing and send data back - std::vector inputNodes; - for (auto const &path : inputPaths_) { - auto pubKeyNode = retrievePublicKeyFromPath(std::move(bs::hd::Path(path))); - inputNodes.push_back(pubKeyNode); - } - - emit info(HWInfoStatus::kTransactionFinished); + //emit info(HWInfoStatus::kTransactionFinished); // Debug check - debugPrintLegacyResult(responseSigned[0], inputNodes[0]); + debugPrintLegacyResult(inData, responseSigned.at(0), inData->inputNodes[0]); - sendTxSigningResult(responseSigned, inputNodes); + sendTxSigningResult(outData, responseSigned); } -void LedgerCommandThread::processTXSegwit() +void SignTXHandler::processTXSegwit(const std::shared_ptr& inData + , const std::shared_ptr& outData) { - emit info(HWInfoStatus::kTransaction); - // ---- Prepare all pib32 nodes for input from device ----- - auto segwitData = getSegwitData(); + //emit info(HWInfoStatus::kTransaction); + // ---- Prepare all bip32 nodes for input from device ----- + const auto& segwitData = getSegwitData(inData); + if (segwitData.empty()) { + logger_->error("[{}] empty segwit data", __func__); + return; + } //upload supporting tx to ledger, get trusted input back for our outpoints std::vector trustedInputs; - for (unsigned i=0; iarmorySigner_.getTxInCount(); i++) { - auto spender = coreReq_->armorySigner_.getSpender(i); - auto trustedInput = getTrustedInput(spender->getOutputHash(), spender->getOutputIndex()); - trustedInputs.push_back(trustedInput); + for (unsigned i=0; i < inData->txReq.armorySigner_.getTxInCount(); i++) { + const auto& spender = inData->txReq.armorySigner_.getSpender(i); + const auto& trustedInput = getTrustedInput(inData, spender->getOutputHash() + , spender->getOutputIndex()); + if (!trustedInput.isEmpty()) { + trustedInputs.push_back(trustedInput); + } } // -- Collect all redeem scripts std::vector redeemScripts; - for (int i = 0; i < coreReq_->armorySigner_.getTxInCount(); ++i) { - auto path = inputPaths_[i]; + for (int i = 0; i < inData->txReq.armorySigner_.getTxInCount(); ++i) { + const auto& path = inData->inputPaths.at(i); BinaryData redeemScriptWitness; if (isNativeSegwit(path)) { - auto& utxo = coreReq_->armorySigner_.getSpender(i)->getUtxo(); + const auto& utxo = inData->txReq.armorySigner_.getSpender(i)->getUtxo(); auto redeemScript = utxo.getScript(); - redeemScriptWitness = - BtcUtils::getP2WPKHWitnessScript(redeemScript.getSliceRef(2, 20)); + redeemScriptWitness = BtcUtils::getP2WPKHWitnessScript(redeemScript.getSliceRef(2, 20)); } else if (isNestedSegwit(path)) { - redeemScriptWitness = segwitData.redeemScripts_[i]; + redeemScriptWitness = segwitData.redeemScripts.at(i); } redeemScripts.push_back(QByteArray(redeemScriptWitness.toBinStr().c_str())); } // -- Start input upload section -- - { startUntrustedTransaction(trustedInputs, redeemScripts, - std::numeric_limits::max(), true, true, coreReq_->RBF); + std::numeric_limits::max(), true, true, inData->txReq.RBF); } - // -- Done input section -- // -- Start output section -- { //upload our recipients as serialized outputs - finalizeInputFull(); + finalizeInputFull(inData); } // -- Done output section -- // At this point user verified all outputs and we can start signing inputs - emit info(HWInfoStatus::kReceiveSignedTx); + //emit info(HWInfoStatus::kReceiveSignedTx); // -- Start signing one by one all addresses -- + std::map responseSigned; + for (int i = 0; i < inData->txReq.armorySigner_.getTxInCount(); ++i) { + const auto& path = inData->inputPaths.at(i); - QVector responseSigned; - for (int i = 0; i < coreReq_->armorySigner_.getTxInCount(); ++i) { - auto path = inputPaths_[i]; - - startUntrustedTransaction({ trustedInputs[i] }, { redeemScripts[i] }, 0, false, true, coreReq_->RBF); + startUntrustedTransaction({ trustedInputs.at(i) }, { redeemScripts.at(i) } + , 0, false, true, inData->txReq.RBF); QByteArray signPayload; signPayload.append(static_cast(path.length())); @@ -969,97 +927,93 @@ void LedgerCommandThread::processTXSegwit() auto commandSign = getApduCommand(Ledger::CLA, Ledger::INS_HASH_SIGN, 0x00, 0x00, std::move(signPayload)); QByteArray responseInputSign; - if (!exchangeData(commandSign, responseInputSign, "[LedgerCommandThread] signTX - Sign Payload")) { + if (!exchangeData(commandSign, responseInputSign, "[SignTXHandler::processTXSegwit] sign Payload")) { releaseDevice(); return; } responseInputSign[0] = 0x30; // force first but to be 0x30 for a newer version of ledger - responseSigned.push_back(responseInputSign); + responseSigned[i] = responseInputSign; } - emit info(HWInfoStatus::kTransactionFinished); + //emit info(HWInfoStatus::kTransactionFinished); // -- Done signing one by one all addresses -- - - sendTxSigningResult(responseSigned, segwitData.inputNodes_); + sendTxSigningResult(outData, responseSigned); } -SegwitInputData LedgerCommandThread::getSegwitData(void) +SegwitInputData SignTXHandler::getSegwitData(const std::shared_ptr& inData) { - logger_->info( - "[LedgerCommandThread] getSegwitData - Start retrieving segwit data."); - - SegwitInputData data; - for (unsigned i = 0; i < coreReq_->armorySigner_.getTxInCount(); i++) { - const auto& path = inputPaths_[i]; - auto spender = coreReq_->armorySigner_.getSpender(i); + logger_->info("[SignTXHandler::getSegwitData] start retrieving segwit data"); + try { + SegwitInputData data; + for (unsigned i = 0; i < inData->txReq.armorySigner_.getTxInCount(); i++) { + const auto& path = inData->inputPaths.at(i); + auto spender = inData->txReq.armorySigner_.getSpender(i); - auto pubKeyNode = retrievePublicKeyFromPath(std::move(bs::hd::Path(path))); - data.inputNodes_.push_back(pubKeyNode); + if (!isNestedSegwit(path)) { + continue; + } + const auto& pubKeyNode = inData->inputNodes.at(i); - if (!isNestedSegwit(path)) { - continue; - } + //recreate the p2wpkh & witness scripts + auto compressedKey = CryptoECDSA().CompressPoint(pubKeyNode.getPublicKey()); + auto pubKeyHash = BtcUtils::getHash160(compressedKey); - //recreate the p2wpkh & witness scripts - auto compressedKey = CryptoECDSA().CompressPoint(pubKeyNode.getPublicKey()); - auto pubKeyHash = BtcUtils::getHash160(compressedKey); + auto witnessScript = BtcUtils::getP2WPKHWitnessScript(pubKeyHash); - auto witnessScript = BtcUtils::getP2WPKHWitnessScript(pubKeyHash); + BinaryWriter bwSwScript; + bwSwScript.put_uint8_t(16); + bwSwScript.put_BinaryData(BtcUtils::getP2WPKHOutputScript(pubKeyHash)); + const auto& swScript = bwSwScript.getData(); - BinaryWriter bwSwScript; - bwSwScript.put_uint8_t(16); - bwSwScript.put_BinaryData(BtcUtils::getP2WPKHOutputScript(pubKeyHash)); - auto& swScript = bwSwScript.getData(); + /* sanity check: make sure the swScript is the preimage to the utxo's p2sh script */ - /* sanity check: make sure the swScript is the preimage to the utxo's p2sh script */ + //recreate p2sh hash + //auto p2shHash = BtcUtils::hash160(swScript); + auto p2shHash = BtcUtils::hash160(swScript.getSliceRef(1, 22)); - //recreate p2sh hash - //auto p2shHash = BtcUtils::hash160(swScript); - auto p2shHash = BtcUtils::hash160(swScript.getSliceRef(1, 22)); + //recreate p2sh script + auto p2shScript = BtcUtils::getP2SHScript(p2shHash); - //recreate p2sh script - auto p2shScript = BtcUtils::getP2SHScript(p2shHash); + //check vs utxo's script + if (spender->getOutputScript() != p2shScript) { + throw LedgerException("p2sh script mismatch"); + } - //check vs utxo's script - if (spender->getOutputScript() != p2shScript) { - throw std::runtime_error("p2sh script mismatch"); + data.preimages[i] = swScript; + data.redeemScripts[i] = std::move(witnessScript); } - - data.preimages_[i] = std::move(swScript); - data.redeemScripts_[i] = std::move(witnessScript); + logger_->info("[SignTXHandler::getSegwitData] done retrieving segwit data"); + return data; + } + catch (const LedgerException& e) { + logger_->error("[{}] {}", __func__, e.what()); + return {}; } - - logger_->info( - "[LedgerCommandThread] getSegwitData - Done retrieving segwit data."); - - return data; } -void LedgerCommandThread::sendTxSigningResult(const QVector& responseSigned, const std::vector& inputNodes) +void SignTXHandler::sendTxSigningResult(const std::shared_ptr& outData + , const std::map& responseSigned) { - Blocksettle::Communication::headless::InputSigs sigs; - for (std::size_t i = 0; i < responseSigned.size(); ++i) { - auto &signedInput = responseSigned[i]; - - auto *sig = sigs.add_inputsig(); - sig->set_index(static_cast(i)); - sig->set_data(signedInput.toStdString().c_str(), signedInput.size()); + BinaryWriter bw; + bw.put_var_int(responseSigned.size()); + for (const auto& signedInput : responseSigned) { + bw.put_uint32_t(signedInput.first); + bw.put_var_int(signedInput.second.size()); + bw.put_BinaryData(BinaryData::fromString(signedInput.second.toStdString())); } - - HWSignedTx wrapper; - wrapper.signedTx = sigs.SerializeAsString(); - emit resultReady(QVariant::fromValue(wrapper)); + outData->serInputSigs = bw.getData(); } -void LedgerCommandThread::debugPrintLegacyResult(const QByteArray& responseSigned, const BIP32_Node& node) +void SignTXHandler::debugPrintLegacyResult(const std::shared_ptr& inData + , const QByteArray& responseSigned, const BIP32_Node& node) { BinaryWriter bw; bw.put_uint32_t(1); //version bw.put_var_int(1); //txin count //inputs - auto utxo = coreReq_->armorySigner_.getSpender(0)->getUtxo(); + const auto& utxo = inData->txReq.armorySigner_.getSpender(0)->getUtxo(); //outpoint bw.put_BinaryData(utxo.getTxHash()); @@ -1082,123 +1036,149 @@ void LedgerCommandThread::debugPrintLegacyResult(const QByteArray& responseSigne bw.put_BinaryData(bwSigScript.getData()); //sequence - bw.put_uint32_t(coreReq_->RBF ? Ledger::DEFAULT_SEQUENCE - 2 : Ledger::DEFAULT_SEQUENCE); + bw.put_uint32_t(inData->txReq.RBF ? Ledger::DEFAULT_SEQUENCE - 2 : Ledger::DEFAULT_SEQUENCE); //txouts bw.put_var_int(1); //count - bw.put_BinaryData(coreReq_->armorySigner_.getRecipient(0)->getSerializedScript()); + bw.put_BinaryData(inData->txReq.armorySigner_.getRecipient(0)->getSerializedScript()); bw.put_uint32_t(0); - std::cout << bw.getData().toHexStr() << std::endl; + logger_->debug("[LedgerDevice::SignTXHandler] legacy result: {}", bw.getData().toHexStr()); } -bool LedgerCommandThread::initDevice() +std::shared_ptr SignTXHandler::processData(const std::shared_ptr& inData) { - if (hid_init() < 0 || hidDeviceInfo_.serialNumber_.isEmpty()) { - logger_->info( - "[LedgerCommandThread] getPublicKey - Cannot init hid."); - return false; + auto result = std::make_shared(); + if (!initDevice()) { + logger_->info("[SignTXHandler::processData] cannot init device"); + //emit error(Ledger::NO_DEVICE); + return result; } + if (!inData || !inData->txReq.isValid() || inData->inputPaths.empty()) { + logger_->error("[SignTXHandler::processData] invalid request"); + //emit error(Ledger::NO_INPUTDATA); + return result; + } + if (isNonSegwit(inData->inputPaths.at(0))) { + processTXLegacy(inData, result); + } + else { + processTXSegwit(inData, result); + } + releaseDevice(); + return result; +} - // make sure that user do not switch off device in the middle of operation - { - auto* info = hid_enumerate(hidDeviceInfo_.vendorId_, hidDeviceInfo_.productId_); - if (!info) { - return false; - } +QByteArray SignTXHandler::getTrustedInput(const std::shared_ptr& inData + , const BinaryData& hash, unsigned txOutId) +{ + logger_->debug("[SignTXHandler::getTrustedInput] start retrieve trusted input for legacy address"); - bool bFound = false; - for (; info; info = info->next) { - if (checkLedgerDevice(info)) { - bFound = true; - break; - } - } + //find the supporting tx + auto tx = inData->txReq.armorySigner_.getSupportingTx(hash); + std::vector inputCommands; - if (!bFound) { - return false; - } + { + //trusted input request header + QByteArray txPayload; + writeUintBE(txPayload, txOutId); //outpoint index + + writeUintLE(txPayload, tx.getVersion()); //supporting tx version + writeVarInt(txPayload, tx.getNumTxIn()); //supporting tx input count + auto command = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x00, 0x00, std::move(txPayload)); + inputCommands.push_back(std::move(command)); } - std::unique_ptr serNumb(new wchar_t[hidDeviceInfo_.serialNumber_.length() + 1]); - hidDeviceInfo_.serialNumber_.toWCharArray(serNumb.get()); - serNumb.get()[hidDeviceInfo_.serialNumber_.length()] = 0x00; - dongle_ = nullptr; - dongle_ = hid_open(hidDeviceInfo_.vendorId_, static_cast(hidDeviceInfo_.productId_), serNumb.get()); + //supporting tx inputs + for (unsigned i = 0; i < tx.getNumTxIn(); i++) { + auto txIn = tx.getTxInCopy(i); + auto outpoint = txIn.getOutPoint(); + auto outpointRaw = outpoint.serialize(); + auto scriptSig = txIn.getScriptRef(); - return dongle_ != nullptr; -} + //36 bytes of outpoint + QByteArray txInPayload; + txInPayload.push_back(QByteArray::fromRawData( + outpointRaw.getCharPtr(), outpointRaw.getSize())); -void LedgerCommandThread::releaseDevice() -{ - if (dongle_) { - hid_close(dongle_); - hid_exit(); - dongle_ = nullptr; - } -} + //txin scriptSig size as varint + writeVarInt(txInPayload, scriptSig.getSize()); -bool LedgerCommandThread::exchangeData(const QByteArray& input, - QByteArray& output, std::string&& logHeader) -{ - if (!writeData(input, logHeader)) { - return false; + auto commandInput = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txInPayload)); + inputCommands.push_back(std::move(commandInput)); + + //txin scriptSig, assuming it's less than 251 bytes for the sake of simplicity + QByteArray txInScriptSig; + txInScriptSig.push_back(QByteArray::fromRawData( + scriptSig.toCharPtr(), scriptSig.getSize())); + writeUintLE(txInScriptSig, txIn.getSequence()); //sequence + + auto commandScriptSig = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txInScriptSig)); + inputCommands.push_back(std::move(commandScriptSig)); } - try { - return readData(output, logHeader); + { + //number of outputs + QByteArray txPayload; + writeVarInt(txPayload, tx.getNumTxOut()); //supporting tx input count + auto command = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txPayload)); + inputCommands.push_back(std::move(command)); } - catch (std::exception &e){ - // Special case : sometimes hidapi return incorrect sequence_index as response - // and there is really now good solution for it, except restart dongle session - // till the moment error gone. - // https://github.com/obsidiansystems/ledger-app-tezos/blob/191-troubleshooting/README.md#error-unexpected-sequence-number-expected-0-got-191-on-macos - // (solution in link above is not working, given here for reference) - // Also it's a general issue for OSX really, but let's left it for all system, just in case - // And let's have 10 times threshold to avoid trying infinitively - static int maxAttempts = 10; - if (e.what() == kHidapiSequence191 && maxAttempts > 0) { - --maxAttempts; - ScopedGuard guard([] { - ++maxAttempts; - }); - releaseDevice(); - initDevice(); - output.clear(); + //supporting tx outputs + for (unsigned i = 0; i < tx.getNumTxOut(); i++) { + auto txout = tx.getTxOutCopy(i); + auto script = txout.getScriptRef(); - return exchangeData(input, output, std::move(logHeader)); - } - else { - throw e; - } - } -} + QByteArray txOutput; + writeUintLE(txOutput, txout.getValue()); //txout value + writeVarInt(txOutput, script.getSize()); //txout script len -// Do not use this function anywhere except inside exchangeData -bool LedgerCommandThread::writeData(const QByteArray& input, const std::string& logHeader) -{ - logger_->debug("{} - >>> {}", logHeader, input.toHex().toStdString()); - if (sendApdu(dongle_, input) < 0) { - logger_->error("{} - Cannot write to device.", logHeader); - throw std::logic_error("Cannot write to device"); - } + auto commandTxOut = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txOutput)); + inputCommands.push_back(std::move(commandTxOut)); - return true; -} + //again, assuming the txout script is shorter than 255 bytes + QByteArray txOutScript; + txOutScript.push_back(QByteArray::fromRawData( + script.toCharPtr(), script.getSize())); -// Do not use this function anywhere except inside exchangeData -bool LedgerCommandThread::readData(QByteArray& output, const std::string& logHeader) -{ - auto res = receiveApduResult(dongle_, output); - if (res != Ledger::SW_OK) { - logger_->error("{} - Cannot read from device. APDU error code : {}", - logHeader, QByteArray::number(res, 16).toStdString()); - lastError_ = res; - throw std::logic_error("Can't read from device"); + auto commandScript = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(txOutScript)); + inputCommands.push_back(std::move(commandScript)); } - logger_->debug("{} - <<< {}", logHeader, BinaryData::fromString(output.toStdString()).toHexStr() + "9000"); - return true; + try { + for (auto& inputCommand : inputCommands) { + QByteArray responseInput; + if (!exchangeData(inputCommand, responseInput, "[LedgerCommandThread] signTX - getting trusted input")) { + releaseDevice(); + throw LedgerException("failed to get trusted input"); + } + } + + //locktime + QByteArray locktime; + writeUintLE(locktime, tx.getLockTime()); + auto command = getApduCommand( + Ledger::CLA, Ledger::INS_GET_TRUSTED_INPUT, 0x80, 0x00, std::move(locktime)); + + QByteArray trustedInput; + if (!exchangeData(command, trustedInput, "[SignTXHandler::getTrustedInput] ")) { + releaseDevice(); + throw LedgerException("failed to get trusted input"); + } + + logger_->debug("[SignTXHandler::getTrustedInput] done retrieve trusted input for legacy address"); + return trustedInput; + } + catch (const LedgerException& e) { + logger_->error("[{}] {}", __func__, e.what()); + return {}; + } } diff --git a/BlockSettleHW/ledger/ledgerDevice.h b/BlockSettleHW/ledger/ledgerDevice.h index 914238402..ee8697612 100644 --- a/BlockSettleHW/ledger/ledgerDevice.h +++ b/BlockSettleHW/ledger/ledgerDevice.h @@ -11,160 +11,172 @@ #ifndef LEDGERDEVICE_H #define LEDGERDEVICE_H -#include "ledger/ledgerStructure.h" +#include +#include "BinaryData.h" #include "hwdeviceinterface.h" +#include "ledger/ledgerStructure.h" #include "ledger/hidapi/hidapi.h" -#include "BinaryData.h" - -#include +#include "Message/Worker.h" namespace spdlog { class logger; } namespace bs { - namespace sync { - class WalletsManager; - } namespace core { namespace wallet { struct TXSignRequest; } } -} - -class LedgerCommandThread; -class LedgerDevice : public HwDeviceInterface -{ - Q_OBJECT - -public: - LedgerDevice(HidDeviceInfo&& hidDeviceInfo, bool testNet, - std::shared_ptr walletManager - , const std::shared_ptr &logger, QObject* parent - , const std::shared_ptr& hidLock); - ~LedgerDevice() override; - - DeviceKey key() const override; - DeviceType type() const override; - - // lifecycle - void init(AsyncCallBack&& cb = nullptr) override; - void cancel() override; - void clearSession(AsyncCallBack&& cb = nullptr) {} - - // operation - void getPublicKey(AsyncCallBackCall&& cb = nullptr) override; - void signTX(const bs::core::wallet::TXSignRequest &reqTX, AsyncCallBackCall&& cb = nullptr) override; - void retrieveXPubRoot(AsyncCallBack&& cb) override {} // no special rule for ledger device - - bool isBlocked() override { - return isBlocked_; - } - QString lastError() override { - return lastError_; - } -private: - QPointer blankCommand(AsyncCallBackCall&& cb = nullptr); - void cancelCommandThread(); - -private: - HidDeviceInfo hidDeviceInfo_; - bool testNet_{}; - std::shared_ptr logger_; - std::shared_ptr walletManager_; - QPointer commandThread_; - bool isBlocked_{}; - QString lastError_{}; - std::shared_ptr hidLock_; -}; - -class LedgerCommandThread : public QThread -{ - Q_OBJECT -public: - LedgerCommandThread(const HidDeviceInfo &hidDeviceInfo, bool testNet, - const std::shared_ptr &logger, QObject *parent, const std::shared_ptr& hidLock); - ~LedgerCommandThread() override; - - void run() override; - - void prepareGetPublicKey(const DeviceKey &deviceKey); - void prepareSignTx( - const DeviceKey &deviceKey, - bs::core::wallet::TXSignRequest coreReq, - std::vector&& paths, bs::hd::Path&& changePath); - void prepareGetRootKey(); - -signals: - // Done with success - void resultReady(QVariant const &result); - - // Done with error - void error(qint32 erroCode); - - // Intermediate state - void info(QString message); - -protected: - // Device management - bool initDevice(); - void releaseDevice(); - - // APDU commands processing - bool exchangeData(const QByteArray& input, QByteArray& output, std::string&& logHeader); - // Do not use those functions straight away, use exchangeData instead - bool writeData(const QByteArray& input, const std::string& logHeader); - bool readData(QByteArray& output, const std::string& logHeader); - - // Get public key processing - void processGetPublicKey(); - void processGetRootKey(); - BIP32_Node retrievePublicKeyFromPath(bs::hd::Path&& derivationPath); - BIP32_Node getPublicKeyApdu(bs::hd::Path&& derivationPath, const std::unique_ptr& parent = nullptr); - - // Sign tx processing - QByteArray getTrustedInput(const BinaryData&, unsigned); - QByteArray getTrustedInputSegWit_outdated(const UTXO&); - - void startUntrustedTransaction( - const std::vector&, const std::vector&, - unsigned, bool, bool, bool); - void finalizeInputFull(); - void processTXLegacy(); - void processTXSegwit(); - - // Getter - SegwitInputData getSegwitData(); - - // Tx result - void sendTxSigningResult(const QVector& responseSigned, const std::vector& inputNodes); - - -private: - void debugPrintLegacyResult(const QByteArray& responseSigned, const BIP32_Node& node); - -private: - HidDeviceInfo hidDeviceInfo_; - bool testNet_{}; - std::shared_ptr logger_; - hid_device* dongle_ = nullptr; - - enum class HardwareCommand { - None, - GetPublicKey, - SignTX, - GetRootPublicKey - }; - - // Thread purpose data - HardwareCommand threadPurpose_; - DeviceKey deviceKey_; - std::unique_ptr coreReq_{}; - std::vector inputPaths_; - bs::hd::Path changePath_; - uint32_t lastError_ = 0x9000; - std::shared_ptr hidLock_; -}; + namespace hww { + struct DeviceCallbacks; + + class LedgerDevice : public DeviceInterface, protected WorkerPool + { + public: + LedgerDevice(const HidDeviceInfo& hidDeviceInfo, bool testNet + , const std::shared_ptr&, DeviceCallbacks* + , const std::shared_ptr& hidLock); + ~LedgerDevice() override = default; + + DeviceKey key() const override; + DeviceType type() const override; + + // lifecycle + void init() override; + void clearSession() {} + void cancel() override { WorkerPool::cancel(); } + + // operation + void getPublicKeys() override; + void signTX(const bs::core::wallet::TXSignRequest& reqTX) override; + void retrieveXPubRoot() override; + + bool isBlocked() const override { + return isBlocked_; + } + std::string lastError() const override { + return lastError_; + } + + protected: + void requestForRescan() override; + std::shared_ptr worker(const std::shared_ptr&) override; + + // operation result informing + void publicKeyReady() override {} //TODO: implement + void deviceTxStatusChanged(const std::string& status) override {} //TODO: implement + void operationFailed(const std::string& reason) override {} //TODO: implement + + // Management + void cancelledOnDevice() override {} //TODO: implement + void invalidPin() override {} //TODO: implement + + private: + void handleError(int32_t errorCode); + + private: + const HidDeviceInfo hidDeviceInfo_; + const bool testNet_; + std::shared_ptr logger_; + DeviceCallbacks* cb_{ nullptr }; + bool isBlocked_{false}; + std::string lastError_{}; + std::shared_ptr hidLock_; + }; + + + class DeviceIOHandler + { + public: + DeviceIOHandler(const std::shared_ptr& logger + , const HidDeviceInfo& devInfo, const std::shared_ptr& hidLock) + : logger_(logger), hidDeviceInfo_(devInfo), hidLock_(hidLock) + {} + ~DeviceIOHandler() { + releaseDevice(); + } + + bool initDevice() noexcept; + void releaseDevice() noexcept; + bool writeData(const QByteArray& input, const std::string& logHeader) noexcept; + void readData(QByteArray& output, const std::string& logHeader); + bool exchangeData(const QByteArray& input, QByteArray& output, std::string&& logHeader); + + protected: + std::shared_ptr logger_; + const HidDeviceInfo hidDeviceInfo_; + std::shared_ptr hidLock_; + hid_device* dongle_{ nullptr }; + }; + + struct SignTXIn : public bs::InData + { + ~SignTXIn() override = default; + DeviceKey key; + bs::core::wallet::TXSignRequest txReq; + std::vector inputPaths; + bs::hd::Path changePath; + std::vector inputNodes; + }; + struct SignTXOut : public bs::OutData + { + ~SignTXOut() override = default; + SecureBinaryData serInputSigs; + }; + + class SignTXHandler : public bs::HandlerImpl, protected DeviceIOHandler + { + public: + SignTXHandler(const std::shared_ptr& logger + , const HidDeviceInfo& devInfo, const std::shared_ptr& hidLock) + : DeviceIOHandler(logger, devInfo, hidLock) {} + + protected: + std::shared_ptr processData(const std::shared_ptr&) override; + + private: + void processTXLegacy(const std::shared_ptr&, const std::shared_ptr&); + void processTXSegwit(const std::shared_ptr&, const std::shared_ptr&); + QByteArray getTrustedInput(const std::shared_ptr&, const BinaryData& hash + , unsigned txOutId); + SegwitInputData getSegwitData(const std::shared_ptr&); + void sendTxSigningResult(const std::shared_ptr&, const std::map&); + void startUntrustedTransaction(const std::vector& trustedInputs + , const std::vector& redeemScripts, unsigned txOutIndex + , bool isNew, bool isSW, bool isRbf); + void finalizeInputFull(const std::shared_ptr&); + void debugPrintLegacyResult(const std::shared_ptr& + , const QByteArray& responseSigned, const BIP32_Node&); + }; + + struct PubKeyIn : public bs::InData + { + ~PubKeyIn() override = default; + std::vector paths; + }; + struct PubKeyOut : public bs::OutData + { + ~PubKeyOut() override = default; + std::vector pubKeys; + }; + + class GetPubKeyHandler : public bs::HandlerImpl, protected DeviceIOHandler + { + public: + GetPubKeyHandler(const std::shared_ptr& logger + , const HidDeviceInfo& devInfo, const std::shared_ptr& hidLock) + : DeviceIOHandler(logger, devInfo, hidLock) + {} + + protected: + BIP32_Node getPublicKeyApdu(const bs::hd::Path&, const std::unique_ptr& parent); + BIP32_Node retrievePublicKeyFromPath(const bs::hd::Path&); + + std::shared_ptr processData(const std::shared_ptr&) override; + }; + + } //hw +} //bs #endif // LEDGERDEVICE_H diff --git a/BlockSettleHW/ledger/ledgerStructure.cpp b/BlockSettleHW/ledger/ledgerStructure.cpp index ca1963ba2..d604dc070 100644 --- a/BlockSettleHW/ledger/ledgerStructure.cpp +++ b/BlockSettleHW/ledger/ledgerStructure.cpp @@ -26,7 +26,8 @@ void writeVarInt(QByteArray &output, size_t size) { } } -bool checkLedgerDevice(hid_device_info* info) { +bool checkLedgerDevice(hid_device_info* info) +{ if (!info) { return false; } diff --git a/BlockSettleHW/ledger/ledgerStructure.h b/BlockSettleHW/ledger/ledgerStructure.h index acaafac3c..7cc270eef 100644 --- a/BlockSettleHW/ledger/ledgerStructure.h +++ b/BlockSettleHW/ledger/ledgerStructure.h @@ -11,8 +11,10 @@ #ifndef LEDGERSTRUCTURE_H #define LEDGERSTRUCTURE_H -#include "hwcommonstructure.h" -#include "QDataStream" +#include +#include +#include "BinaryData.h" +#include "BIP32_Node.h" namespace Ledger { @@ -144,29 +146,34 @@ void writeUintLE(QByteArray& out, T value) { void writeVarInt(QByteArray &output, size_t size); struct HidDeviceInfo { - QString path_; - uint16_t vendorId_; - uint16_t productId_; - QString serialNumber_; - uint16_t releaseNumber_; - QString manufacturerString_; - QString productString_; - uint16_t usagePage_; - uint16_t usage_; - int interfaceNumber_; + std::string path; + uint16_t vendorId; + uint16_t productId; + std::string serialNumber; + uint16_t releaseNumber; + std::string manufacturer; + std::string product; + uint16_t usagePage; + uint16_t usage; + int interfaceNumber; }; -struct SegwitInputData { - std::unordered_map preimages_; - std::unordered_map redeemScripts_; - std::vector inputNodes_; +struct SegwitInputData +{ + std::map preimages; + std::map redeemScripts; + + bool empty() const + { + return (preimages.empty() && redeemScripts.empty()); + } }; struct LedgerPublicKey { - QByteArray pubKey_; - QByteArray address_; - QByteArray chainCode_; + QByteArray pubKey; + QByteArray address; + QByteArray chainCode; bool parseFromResponse(QByteArray response) { QDataStream stream(response); @@ -174,26 +181,26 @@ struct LedgerPublicKey uint8_t pubKeyLength; stream >> pubKeyLength; - pubKey_.clear(); - pubKey_.resize(pubKeyLength); - stream.readRawData(pubKey_.data(), pubKeyLength); + pubKey.clear(); + pubKey.resize(pubKeyLength); + stream.readRawData(pubKey.data(), pubKeyLength); uint8_t addressLength; stream >> addressLength; - address_.clear(); - address_.resize(addressLength); - stream.readRawData(address_.data(), addressLength); + address.clear(); + address.resize(addressLength); + stream.readRawData(address.data(), addressLength); - chainCode_.clear(); - chainCode_.resize(Ledger::CHAIN_CODE_SIZE); - stream.readRawData(chainCode_.data(), Ledger::CHAIN_CODE_SIZE); + chainCode.clear(); + chainCode.resize(Ledger::CHAIN_CODE_SIZE); + stream.readRawData(chainCode.data(), Ledger::CHAIN_CODE_SIZE); return stream.atEnd(); } bool isValid() { - return !pubKey_.isEmpty() && !address_.isEmpty() && !chainCode_.isEmpty(); + return !pubKey.isEmpty() && !address.isEmpty() && !chainCode.isEmpty(); } }; diff --git a/BlockSettleHW/trezor/trezorClient.cpp b/BlockSettleHW/trezor/trezorClient.cpp index c1a440089..29efe4095 100644 --- a/BlockSettleHW/trezor/trezorClient.cpp +++ b/BlockSettleHW/trezor/trezorClient.cpp @@ -1,7 +1,7 @@ /* *********************************************************************************** -* Copyright (C) 2020 - 2021, BlockSettle AB +* Copyright (C) 2020 - 2023, BlockSettle AB * Distributed under the GNU Affero General Public License (AGPL v3) * See LICENSE or http://www.gnu.org/licenses/agpl.html * @@ -9,331 +9,255 @@ */ #include "trezorClient.h" -#include "ConnectionManager.h" +#include +#include "hwdevicemanager.h" #include "trezorDevice.h" #include "Wallets/SyncWalletsManager.h" #include "Wallets/SyncHDWallet.h" #include "ScopeGuard.h" -#include -#include -#include -#include -#include -#include -#include +using namespace bs::hww; -TrezorClient::TrezorClient(const std::shared_ptr& connectionManager, - std::shared_ptr walletManager, bool testNet, QObject* parent /*= nullptr*/) - : QObject(parent) - , connectionManager_(connectionManager) - , walletManager_(walletManager) - , testNet_(testNet) -{ -} - -QByteArray TrezorClient::getSessionId() -{ - return deviceData_.sessionId_; -} +TrezorClient::TrezorClient(const std::shared_ptr& logger + , bool testNet, DeviceCallbacks* cb) + : bs::WorkerPool(1, 1), logger_(logger), cb_(cb), testNet_(testNet) +{} -void TrezorClient::initConnection(bool force, AsyncCallBack&& cb) +void TrezorClient::initConnection() { - auto initCallBack = [this, cbCopy = std::move(cb), force](QNetworkReply* reply) mutable { - ScopedGuard guard([cb = std::move(cbCopy)]{ - if (cb) { - cb(); - } - }); - if (!reply || reply->error() != QNetworkReply::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] initConnection - Network error : " + reply->errorString().toUtf8()); + const auto& cb = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty() || reply->response.empty()) { + logger_->error("[TrezorClient::initConnection] network error: {}" + , (reply && !reply->error.empty()) ? reply->error : ""); return; } - - QByteArray loadData = reply ? reply->readAll().simplified() : ""; - - QJsonParseError jsonError; - QJsonDocument loadDoc = QJsonDocument::fromJson(loadData, &jsonError); - if (jsonError.error != QJsonParseError::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] initConnection - Invalid json structure . Parsing error : " + jsonError.errorString().toUtf8()); - return; - } - - const auto bridgeInfo = loadDoc.object(); - connectionManager_->GetLogger()->info( - "[TrezorClient] initConnection - Connection initialized. Bridge version : " - + bridgeInfo.value(QString::fromUtf8("version")).toString().toUtf8()); - - state_ = State::Init; - emit initialized(); - - enumDevices(force, std::move(guard.releaseCb())); - reply->deleteLater(); - }; - - connectionManager_->GetLogger()->info("[TrezorClient] Initialize connection"); - postToTrezor("/", std::move(initCallBack), true); -} - -void TrezorClient::initConnection(QString&& deviceId, bool force, AsyncCallBackCall&& cb /*= nullptr*/) -{ - AsyncCallBack cbWrapper = [copyDeviceId = std::move(deviceId), originCb = std::move(cb)]() { - originCb({ copyDeviceId }); - }; - - initConnection(force, std::move(cbWrapper)); -} - -void TrezorClient::releaseConnection(AsyncCallBack&& cb) -{ - if (deviceData_.sessionId_.isEmpty()) { - cleanDeviceData(); - if (cb) { - cb(); + nlohmann::json response; + try { + response = nlohmann::json::parse(reply->response); } - return; - } - - auto releaseCallback = [this, cbCopy = std::move(cb)](QNetworkReply* reply) mutable { - ScopedGuard ensureCb([this, cb = std::move(cbCopy)]{ - cleanDeviceData(); - if (cb) { - cb(); - } - }); - - if (!reply || reply->error() != QNetworkReply::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] releaseConnection - Network error : " + reply->errorString().toUtf8()); + catch (const nlohmann::json::exception& e) { + logger_->error("[TrezorClient::initConnection] failed to parse '{}': {}" + , reply->response, e.what()); return; } - connectionManager_->GetLogger()->info( - "[TrezorClient] releaseConnection - Connection successfully released"); + logger_->info("[TrezorClient::initConnection] connection inited, bridge version: {}" + , response["version"].get()); - state_ = State::Released; - emit deviceReleased(); - - reply->deleteLater(); + state_ = trezor::State::Init; + //emit initialized(); }; - - connectionManager_->GetLogger()->info("[TrezorClient] Release connection. Connection id: " - + deviceData_.sessionId_); - - QByteArray releaseUrl = "/release/" + deviceData_.sessionId_; - postToTrezor(std::move(releaseUrl), std::move(releaseCallback)); - + logger_->info("[TrezorClient::initConnection]"); + auto inData = std::make_shared(); + inData->path = "/"; + processQueued(inData, cb); } -void TrezorClient::postToTrezor(QByteArray&& urlMethod, std::function &&cb, bool timeout /* = false */) +void bs::hww::TrezorClient::releaseConnection() { - post(std::move(urlMethod), std::move(cb), QByteArray(), timeout); + for (const auto& device : devices_) { + device->releaseConnection(); + } } -void TrezorClient::postToTrezorInput(QByteArray&& urlMethod, std::function &&cb, QByteArray&& input) +std::vector TrezorClient::deviceKeys() const { - post(std::move(urlMethod), std::move(cb), std::move(input)); + std::vector result; + for (const auto& dev : devices_) { + result.push_back(dev->key()); + } + logger_->debug("[TrezorClient::deviceKeys] {} key[s]", result.size()); + return result; } -void TrezorClient::call(QByteArray&& input, AsyncCallBackCall&& cb) +std::shared_ptr TrezorClient::getDevice(const std::string& deviceId) { - auto callCallback = [this, cbCopy = std::move(cb)](QNetworkReply* reply) mutable { - - if (!reply || reply->error() != QNetworkReply::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] call - Network error : " + reply->errorString().toUtf8()); - if (cbCopy) { - cbCopy({}); - } - return; + for (const auto& dev : devices_) { + if (dev->key().id == deviceId) { + return dev; } - - QByteArray loadData = reply->readAll().simplified(); - cbCopy(std::move(loadData)); - }; - - connectionManager_->GetLogger()->info("[TrezorClient] Call to trezor."); - - QByteArray callUrl = "/call/" + deviceData_.sessionId_; - postToTrezorInput(std::move(callUrl), std::move(callCallback), std::move(input)); -} - -QVector TrezorClient::deviceKeys() const -{ - if (!trezorDevice_) { - return {}; } - auto key = trezorDevice_->key(); - return { key }; + return nullptr; } -QPointer TrezorClient::getTrezorDevice(const QString& deviceId) +std::shared_ptr TrezorClient::worker(const std::shared_ptr&) { - // #TREZOR_INTEGRATION: need lookup for several devices - if (!trezorDevice_) { - return nullptr; - } - - assert(trezorDevice_->key().deviceId_ == deviceId); - return trezorDevice_; + const std::vector> handlers{ std::make_shared + (logger_, trezorEndPoint_) }; + return std::make_shared(handlers); } -void TrezorClient::enumDevices(bool forceAcquire, AsyncCallBack&& cb) +void TrezorClient::listDevices() { - auto enumCallback = [this, cbCopy = std::move(cb), forceAcquire](QNetworkReply* reply) mutable { - ScopedGuard ensureCb([cb = std::move(cbCopy)]{ - if (cb) { - cb(); + if (state_ == trezor::State::None) { + initConnection(); + } + const auto& cb = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty() || reply->response.empty()) { + if (!reply) { + logger_->error("[TrezorClient::listDevices] invalid reply type"); } - }); - - if (!reply || reply->error() != QNetworkReply::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] enumDevices - Network error : " + reply->errorString().toUtf8()); + else { + logger_->error("[TrezorClient::listDevices] network error: {}" + , !reply->error.empty() ? reply->error : ""); + } + cb_->scanningDone(); return; } - - QByteArray loadData = reply ? reply->readAll().simplified() : ""; - - QJsonParseError jsonError; - QJsonDocument loadDoc = QJsonDocument::fromJson(loadData, &jsonError); - if (jsonError.error != QJsonParseError::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] enumDevices - Invalid json structure . Parsing error : " + jsonError.errorString().toUtf8()); - return; + nlohmann::json response; + try { + response = nlohmann::json::parse(reply->response); } - - QJsonArray devices = loadDoc.array(); - const int deviceCount = devices.count(); - if (deviceCount == 0) { - connectionManager_->GetLogger()->info( - "[TrezorClient] enumDevices - No trezor device available"); + catch (const nlohmann::json::exception& e) { + logger_->error("[TrezorClient::listDevices] failed to parse '{}': {}" + , reply->response, e.what()); + cb_->scanningDone(); return; } - QVector trezorDevices; - for (const QJsonValueRef &deviceRef : devices) { - const QJsonObject deviceObj = deviceRef.toObject(); - trezorDevices.push_back({ - deviceObj[QLatin1String("path")].toString().toUtf8(), - deviceObj[QLatin1String("vendor")].toString().toUtf8(), - deviceObj[QLatin1String("product")].toString().toUtf8(), - deviceObj[QLatin1String("session")].toString().toUtf8(), - deviceObj[QLatin1String("debug")].toString().toUtf8(), - deviceObj[QLatin1String("debugSession")].toString().toUtf8() }); + std::vector trezorDevices; + for (const auto& device : response) { + const auto& session = device["session"].is_null() ? "null" + : device["session"].get(); + const auto& debugSession = device["debugSession"].is_null() ? "null" + : device["debugSession"].get(); + trezorDevices.push_back({device["path"].get() + , device["vendor"].get(), device["product"].get() + , session, device["debug"].get(), debugSession }); } + logger_->info("[TrezorClient::listDevices] enumeration finished, #devices: {}" + , trezorDevices.size()); + state_ = trezor::State::Enumerated; - // If there will be a few trezor devices connected, let's choose first one for now - // later we could expand this functionality to many of them - if (!forceAcquire && trezorDevice_ && trezorDevices.first().sessionId_ == deviceData_.sessionId_) { - // this is our previous session so we could go straight away on it - return; + nbDevices_ = trezorDevices.size(); + if (nbDevices_ == 0) { + cb_->scanningDone(); + } + else { + for (const auto& dev : trezorDevices) { + acquireDevice(dev); + } } - - deviceData_ = trezorDevices.first(); - connectionManager_->GetLogger()->info( - "[TrezorClient] enumDevices - Enumerate request succeeded. Total device available : " - + QString::number(deviceCount).toUtf8() + ". Trying to acquire first one..."); - - state_ = State::Enumerated; - emit devicesScanned(); - - acquireDevice(std::move(ensureCb.releaseCb())); - reply->deleteLater(); }; - - connectionManager_->GetLogger()->info("[TrezorClient] Request to enumerate devices."); - postToTrezor("/enumerate", std::move(enumCallback)); + logger_->info("[TrezorClient::listDevices]"); + auto inData = std::make_shared(); + inData->path = "/enumerate"; + processQueued(inData, cb); } -void TrezorClient::acquireDevice(AsyncCallBack&& cb) +void TrezorClient::acquireDevice(const trezor::DeviceData& devData, bool init) { - QByteArray previousSessionId = deviceData_.sessionId_.isEmpty() ? - "null" : deviceData_.sessionId_; - - auto acquireCallback = [this, previousSessionId, cbCopy = std::move(cb)](QNetworkReply* reply) mutable { - ScopedGuard ensureCb([cb = std::move(cbCopy)]{ - if (cb) { - cb(); - } - }); - - if (!reply || reply->error() != QNetworkReply::NoError) { - connectionManager_->GetLogger()->error( - "[TrezorClient] acquireDevice - Network error : " + reply->errorString().toUtf8()); + const auto& prevSessionId = devices_.empty() ? "null" + : devices_.at(devices_.size() - 1)->data().sessionId; + auto inData = std::make_shared(); + inData->path = "/acquire/" + devData.path + "/" + prevSessionId; + + auto acquireCallback = [this, prevSessionId, devData, init] + (const std::shared_ptr& data) + { + --nbDevices_; + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty() || reply->response.empty()) { + logger_->error("[TrezorClient::acquireDevice] network error: {}" + , (reply && !reply->error.empty()) ? reply->error : ""); return; } - - QByteArray loadData = reply ? reply->readAll().simplified() : ""; - QJsonObject acuiredDevice = QJsonDocument::fromJson(loadData).object(); - - deviceData_.sessionId_ = acuiredDevice[QLatin1String("session")].toString().toUtf8(); - - if (deviceData_.sessionId_.isEmpty() || deviceData_.sessionId_ == previousSessionId) { - connectionManager_->GetLogger()->error( - "[TrezorClient] acquireDevice - Cannot acquire device"); + nlohmann::json response; + try { + response = nlohmann::json::parse(reply->response); + } + catch (const nlohmann::json::exception& e) { + logger_->error("[TrezorClient::acquireDevice] failed to parse '{}': {}", reply->response, e.what()); return; } - connectionManager_->GetLogger()->info("[TrezorClient] Connection has successfully acquired. Old connection id: " - + previousSessionId + ", new connection id : " + deviceData_.sessionId_); + auto devDataCopy = devData; + devDataCopy.sessionId = response["session"].is_null() ? "null" + : response["session"].get(); - state_ = State::Acquired; - emit deviceReady(); + if (devDataCopy.sessionId.empty() || devDataCopy.sessionId == prevSessionId) { + logger_->error("[TrezorClient::acquireDevice] cannot acquire device"); + return; + } - trezorDevice_ = new TrezorDevice(connectionManager_, walletManager_, testNet_, { this }, this) ; - trezorDevice_->init(std::move(ensureCb.releaseCb())); + logger_->info("[TrezorClient::acquireDevice] Connection has successfully acquired. Old " + "connection id: {}, new connection id: {}", prevSessionId, devData.sessionId); - reply->deleteLater(); + state_ = trezor::State::Acquired; + const auto& newDevice = std::make_shared(logger_, devDataCopy + , testNet_, cb_, trezorEndPoint_); + newDevice->init(); + devices_.push_back(newDevice); }; + logger_->info("[TrezorClient::acquireDevice] old session id: {}", prevSessionId); + processQueued(inData, acquireCallback); +} - connectionManager_->GetLogger()->info("[TrezorClient] Acquire new connection. Old connection id: " - + previousSessionId); - QByteArray acquireUrl = "/acquire/" + deviceData_.path_ + "/" + previousSessionId; - postToTrezor(std::move(acquireUrl), std::move(acquireCallback)); +namespace { + static const std::string kBlockSettleOrigin{ "Origin: https://blocksettle.trezor.io" }; } -void TrezorClient::post(QByteArray&& urlMethod, std::function &&cb, QByteArray&& input, bool timeout /* = false*/) +static size_t writeToString(void* ptr, size_t size, size_t count, std::string* stream) { - QNetworkRequest request; - request.setRawHeader({ "Origin" }, { blocksettleOrigin }); - request.setUrl(QUrl(QString::fromLocal8Bit(trezorEndPoint_ + urlMethod))); - - if (!input.isEmpty()) { - request.setHeader(QNetworkRequest::ContentTypeHeader, { QByteArray("application/x-www-form-urlencoded") }); - } + const size_t resSize = size * count; + stream->append((char*)ptr, resSize); + return resSize; +} - QNetworkReply *reply = QNetworkAccessManager().post(request, input); - auto connection = connect(reply, &QNetworkReply::finished, this - , [cbCopy = cb, repCopy = reply, sender = QPointer(this)] - { - if (!sender) { - return; // TREZOR client already destroyed - } +TrezorPostHandler::TrezorPostHandler(const std::shared_ptr& logger + , const std::string& baseURL) + : logger_(logger), baseURL_(baseURL) +{ + curl_ = curl_easy_init(); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, writeToString); - cbCopy(repCopy); - repCopy->deleteLater(); - }); + curlHeaders_ = curl_slist_append(curlHeaders_, kBlockSettleOrigin.c_str()); + //curlHeaders_ = curl_slist_append(curlHeaders_, "content-type: application/x-www-form-urlencoded;"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curlHeaders_); + curl_easy_setopt(curl_, CURLOPT_POST, 1); +} - // Timeout - if (timeout) { - QTimer::singleShot(2000, [replyCopy = QPointer(reply)]() { - if (!replyCopy) { - return; - } - replyCopy->abort(); - }); - } +bs::hww::TrezorPostHandler::~TrezorPostHandler() +{ + curl_slist_free_all(curlHeaders_); + curl_easy_cleanup(curl_); } -void TrezorClient::cleanDeviceData() +std::shared_ptr bs::hww::TrezorPostHandler::processData(const std::shared_ptr& inData) { - if (trezorDevice_) { - trezorDevice_->deleteLater(); - trezorDevice_ = nullptr; + auto result = std::make_shared(); + if (!curl_) { + result->error = "curl not inited"; + return result; + } + const std::string url{ baseURL_ + inData->path }; + logger_->debug("[{}] request: '{}' to {}", __func__, inData->input, url); + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + if (!inData->input.empty()) { + curl_easy_setopt(curl_, CURLOPT_POSTFIELDS, inData->input.data()); + } + if (inData->timeout) { + curl_easy_setopt(curl_, CURLOPT_TIMEOUT_MS, 10000L); + } + else { + curl_easy_setopt(curl_, CURLOPT_TIMEOUT, 120000L); + } + + std::string response; + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + + const auto res = curl_easy_perform(curl_); + if (res != CURLE_OK) { + result->error = fmt::format("failed to post to {}: {}", url, res); + result->timedOut = (res == CURLE_OPERATION_TIMEDOUT); + return result; } - deviceData_ = {}; + result->response = response; + logger_->debug("[{}] response: {}", __func__, result->response); + return result; } diff --git a/BlockSettleHW/trezor/trezorClient.h b/BlockSettleHW/trezor/trezorClient.h index 5f3fac1f6..4fa35ce18 100644 --- a/BlockSettleHW/trezor/trezorClient.h +++ b/BlockSettleHW/trezor/trezorClient.h @@ -1,7 +1,7 @@ /* *********************************************************************************** -* Copyright (C) 2020 - 2021, BlockSettle AB +* Copyright (C) 2020 - 2023, BlockSettle AB * Distributed under the GNU Affero General Public License (AGPL v3) * See LICENSE or http://www.gnu.org/licenses/agpl.html * @@ -11,77 +11,96 @@ #ifndef TREZORCLIENT_H #define TREZORCLIENT_H -#include "trezorStructure.h" #include +#include +#include "hwdeviceinterface.h" +#include "Message/Worker.h" +#include "trezorStructure.h" -#include -#include -#include - -class ConnectionManager; -class QNetworkRequest; -class TrezorDevice; - -namespace bs { - namespace sync { - class WalletsManager; - } +namespace spdlog { + class logger; } +struct curl_slist; -class TrezorClient : public QObject -{ - Q_OBJECT - -public: - - TrezorClient(const std::shared_ptr& connectionManager_, - std::shared_ptr walletManager, bool testNet, QObject* parent = nullptr); - ~TrezorClient() override = default; - - QByteArray getSessionId(); - - void initConnection(bool force, AsyncCallBack&& cb = nullptr); - void initConnection(QString&& deviceId, bool force, AsyncCallBackCall&& cb = nullptr); - void releaseConnection(AsyncCallBack&& cb = nullptr); - - void call(QByteArray&& input, AsyncCallBackCall&& cb); - - QVector deviceKeys() const; - QPointer getTrezorDevice(const QString& deviceId); - -private: - void postToTrezor(QByteArray&& urlMethod, std::function &&cb, bool timeout = false); - void postToTrezorInput(QByteArray&& urlMethod, std::function &&cb, QByteArray&& input); - - void enumDevices(bool forceAcquire, AsyncCallBack&& cb = nullptr); - void acquireDevice(AsyncCallBack&& cb = nullptr); - void post(QByteArray&& urlMethod, std::function &&cb, QByteArray&& input, bool timeout = false); - - void cleanDeviceData(); - -signals: - void initialized(); - void devicesScanned(); - void deviceReady(); - void deviceReleased(); - - void publicKeyReady(); - void onRequestPinMatrix(); - -private: - std::shared_ptr connectionManager_; - std::shared_ptr walletManager_; - - const QByteArray trezorEndPoint_ = "http://127.0.0.1:21325"; - const QByteArray blocksettleOrigin = "https://blocksettle.trezor.io"; - DeviceData deviceData_; - State state_ = State::None; - bool testNet_{}; - - // There should really be a bunch of devices - QPointer trezorDevice_{}; - - -}; +namespace bs { + namespace hww { + class TrezorDevice; + class DeviceCallbacks; + + struct TrezorPostIn : public bs::InData + { + ~TrezorPostIn() override = default; + std::string path; + std::string input; + bool timeout{ true }; + }; + struct TrezorPostOut : public bs::OutData + { + ~TrezorPostOut() override = default; + std::string response; + std::string error; + bool timedOut{ false }; + }; + + class TrezorPostHandler : public bs::HandlerImpl + { + public: + TrezorPostHandler(const std::shared_ptr& + , const std::string& baseURL); + ~TrezorPostHandler() override; + + protected: + std::shared_ptr processData(const std::shared_ptr&) override; + + private: + std::shared_ptr logger_; + const std::string baseURL_; + struct curl_slist* curlHeaders_{ NULL }; + void* curl_{ nullptr }; + }; + + + class TrezorClient : protected bs::WorkerPool + { + friend class TrezorDevice; + public: + TrezorClient(const std::shared_ptr& + , bool testNet, DeviceCallbacks*); + ~TrezorClient() override = default; + + void initConnection(); + void releaseConnection(); + void listDevices(); + + std::vector deviceKeys() const; + std::shared_ptr getDevice(const std::string& deviceId); + + protected: + std::shared_ptr worker(const std::shared_ptr&) override final; + + private: + void acquireDevice(const trezor::DeviceData&, bool init = false); + + //former signals + void initialized(); + void devicesScanned(); + void deviceReady(); + void deviceReleased(); + + void publicKeyReady(); + void onRequestPinMatrix(); + + private: + std::shared_ptr logger_; + DeviceCallbacks* cb_{ nullptr }; + const std::string trezorEndPoint_{ "http://127.0.0.1:21325" }; + trezor::State state_{ trezor::State::None }; + bool testNet_{}; + std::vector> devices_; + unsigned int nbDevices_{ 0 }; + }; + + } //hw +} //bs #endif // TREZORCLIENT_H diff --git a/BlockSettleHW/trezor/trezorDevice.cpp b/BlockSettleHW/trezor/trezorDevice.cpp index 1f580e25a..659225fe3 100644 --- a/BlockSettleHW/trezor/trezorDevice.cpp +++ b/BlockSettleHW/trezor/trezorDevice.cpp @@ -8,22 +8,27 @@ ********************************************************************************** */ +#include +#include +#include "hwdevicemanager.h" #include "trezorDevice.h" #include "trezorClient.h" -#include "ConnectionManager.h" -#include "headless.pb.h" #include "Wallets/ProtobufHeadlessUtils.h" #include "CoreWallet.h" #include "Wallets/SyncWalletsManager.h" #include "Wallets/SyncHDWallet.h" -#include - +#include "headless.pb.h" -// Protobuf +// Trezor interface (source - https://github.com/trezor/trezor-common/tree/master/protob) +#include "trezor/generated_proto/messages.pb.h" +#include "trezor/generated_proto/messages-management.pb.h" +#include "trezor/generated_proto/messages-common.pb.h" +#include "trezor/generated_proto/messages-bitcoin.pb.h" #include using namespace hw::trezor::messages; +using namespace bs::hww; namespace { const auto kModel1 = "1"; @@ -60,14 +65,14 @@ namespace { return packed; } - MessageData unpackMessage(const QByteArray& response) + trezor::MessageData unpackMessage(const QByteArray& response) { QDataStream stream(response); - MessageData ret; + trezor::MessageData ret; - ret.msg_type_ = QByteArray::fromHex(response.mid(0, 4).toHex()).toInt(nullptr, 16); - ret.length_ = QByteArray::fromHex(response.mid(4, 8).toHex()).toInt(nullptr, 16); - ret.message_ = QByteArray::fromHex(response.mid(12, 2 * ret.length_)).toStdString(); + ret.type = QByteArray::fromHex(response.mid(0, 4).toHex()).toInt(nullptr, 16); + ret.length = QByteArray::fromHex(response.mid(4, 8).toHex()).toInt(nullptr, 16); + ret.message = QByteArray::fromHex(response.mid(12, 2 * ret.length)).toStdString(); return ret; } @@ -82,51 +87,61 @@ namespace { return output; } - const std::string tesNetCoin = "Testnet"; + static const std::string kTestNetCoin = "Testnet"; } -TrezorDevice::TrezorDevice(const std::shared_ptr &connectionManager, std::shared_ptr walletManager - , bool testNet, const QPointer &client, QObject* parent) - : HwDeviceInterface(parent) - , connectionManager_(connectionManager) - , walletManager_(walletManager) - , client_(std::move(client)) - , testNet_(testNet) -{ -} +TrezorDevice::TrezorDevice(const std::shared_ptr &logger + , const trezor::DeviceData& data, bool testNet, DeviceCallbacks* cb + , const std::string& endpoint) + : bs::WorkerPool(1, 1) + , logger_(logger), data_(data), testNet_(testNet), cb_(cb), endpoint_(endpoint) + , features_{std::make_shared()} +{} TrezorDevice::~TrezorDevice() = default; -DeviceKey TrezorDevice::key() const +std::shared_ptr TrezorDevice::worker(const std::shared_ptr&) { - QString walletId; - QString status; - if (!xpubRoot_.empty()) { - auto expectedWalletId = bs::core::wallet::computeID( - BinaryData::fromString(xpubRoot_)).toBinStr(); + const std::vector> handlers{ std::make_shared + (logger_, endpoint_) }; + return std::make_shared(handlers); +} - auto importedWallets = walletManager_->getHwWallets( - bs::wallet::HardwareEncKey::WalletType::Trezor, features_.device_id()); +void bs::hww::TrezorDevice::operationFailed(const std::string& reason) +{ + releaseConnection(); + cb_->operationFailed(features_->device_id(), reason); +} - for (const auto &imported : importedWallets) { - if (expectedWalletId == imported) { - walletId = QString::fromStdString(expectedWalletId); - break; - } +void TrezorDevice::releaseConnection() +{ + auto releaseCallback = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty()) { + logger_->error("[TrezorDevice::releaseConnection] network error: {}" + , (reply && !reply->error.empty()) ? reply->error : ""); + return; } - } - else { - status = tr("Not initialized"); - } - return { - QString::fromStdString(features_.label()) - , QString::fromStdString(features_.device_id()) - , QString::fromStdString(features_.vendor()) - , walletId - , status - , type() + logger_->info("[TrezorClient] releaseConnection - Connection successfully released"); + + state_ = trezor::State::Released; + //emit deviceReleased(); }; + auto inData = std::make_shared(); + inData->path = "/release/" + data_.sessionId; + processQueued(inData, releaseCallback); +} + +DeviceKey TrezorDevice::key() const +{ + std::string walletId; + if (!xpubRoot_.empty()) { + walletId = bs::core::wallet::computeID(xpubRoot_).toBinStr(); + } + return { features_->label(), features_->device_id(), features_->vendor() + , walletId, type() }; } DeviceType TrezorDevice::type() const @@ -134,199 +149,291 @@ DeviceType TrezorDevice::type() const return DeviceType::HWTrezor; } -void TrezorDevice::init(AsyncCallBack&& cb) +void TrezorDevice::init() { - connectionManager_->GetLogger()->debug("[TrezorDevice] init - start init call "); + if (state_ == trezor::State::Init) { + logger_->debug("[TrezorDevice::init] already inited"); + return; + } + logger_->debug("[TrezorDevice::init] start"); management::Initialize message; - message.set_session_id(client_->getSessionId()); - - setCallbackNoData(MessageType_Features, std::move(cb)); - makeCall(message); + message.set_session_id(data_.sessionId); + const auto& cb = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty()) { + logger_->error("[TrezorDevice::makeCall] comm error: {}", (reply && !reply->error.empty()) ? reply->error : ""); + operationFailed("comm error"); + cancel(); + reset(); + return; + } + const auto& msg = unpackMessage(QByteArray::fromStdString(reply->response)); + handleMessage(msg); + }; + auto inData = std::make_shared(); + inData->path = "/call/" + data_.sessionId; + inData->input = packMessage(message).toStdString(); + processQueued(inData, cb); } -void TrezorDevice::getPublicKey(AsyncCallBackCall&& cb) +struct NoDataOut : public bs::OutData +{ + ~NoDataOut() override = default; + MessageType msgType; +}; + +struct XPubOut : public bs::OutData +{ + ~XPubOut() override = default; + std::string xpub; +}; + +struct TXOut : public bs::OutData +{ + ~TXOut() override = default; + std::string signedTX; +}; + +void TrezorDevice::getPublicKeys() { awaitingWalletInfo_ = {}; // General data - awaitingWalletInfo_.info_.type = bs::wallet::HardwareEncKey::WalletType::Trezor; - awaitingWalletInfo_.info_.label = features_.label(); - awaitingWalletInfo_.info_.deviceId = features_.device_id(); - awaitingWalletInfo_.info_.vendor = features_.vendor(); - awaitingWalletInfo_.info_.xpubRoot = xpubRoot_; - - awaitingWalletInfo_.isFirmwareSupported_ = isFirmwareSupported(); - if (!awaitingWalletInfo_.isFirmwareSupported_) { - awaitingWalletInfo_.firmwareSupportedMsg_ = firmwareSupportedVersion(); - cb(QVariant::fromValue<>(awaitingWalletInfo_)); + awaitingWalletInfo_.type = bs::wallet::HardwareEncKey::WalletType::Trezor; + awaitingWalletInfo_.label = features_->label(); + awaitingWalletInfo_.deviceId = features_->device_id(); + awaitingWalletInfo_.vendor = features_->vendor(); + awaitingWalletInfo_.xpubRoot = xpubRoot_.toBinStr(); + + if (!isFirmwareSupported()) { + logger_->warn("[TrezorDevice::getPublicKeys] unsupported firmware. {}" + , firmwareSupportedVersion()); + cb_->walletInfoReady(key(), awaitingWalletInfo_); return; } - // We cannot get all data from one call so we make four calls: - // fetching first address for "m/0'" as wallet id - // fetching first address for "m/84'" as native segwit xpub - // fetching first address for "m/49'" as nested segwit xpub - // fetching first address for "m/44'" as legacy xpub - - AsyncCallBackCall cbLegacy = [this, cb = std::move(cb)](QVariant &&data) mutable { - awaitingWalletInfo_.info_.xpubLegacy = data.toByteArray().toStdString(); - - cb(QVariant::fromValue<>(awaitingWalletInfo_)); - }; - - AsyncCallBackCall cbNested = [this, cbLegacy = std::move(cbLegacy)](QVariant &&data) mutable { - awaitingWalletInfo_.info_.xpubNestedSegwit = data.toByteArray().toStdString(); - - connectionManager_->GetLogger()->debug("[TrezorDevice] init - start retrieving legacy public key from device " - + features_.label()); - bitcoin::GetPublicKey message; - for (const uint32_t add : getDerivationPath(testNet_, bs::hd::Purpose::NonSegWit)) { - message.add_address_n(add); - } - if (testNet_) { - message.set_coin_name(tesNetCoin); + const auto& cbLegacy = [this](const std::shared_ptr& data) + { + const auto& reply = std::dynamic_pointer_cast(data); + if (!reply) { + logger_->error("[TrezorDevice::getPublicKeys::legacy] invalid callback data"); + return; } - - setDataCallback(MessageType_PublicKey, std::move(cbLegacy)); - makeCall(message); + awaitingWalletInfo_.xpubLegacy = reply->xpub; + cb_->walletInfoReady(key(), awaitingWalletInfo_); }; - AsyncCallBackCall cbNative = [this, cbNested = std::move(cbNested)](QVariant &&data) mutable { - awaitingWalletInfo_.info_.xpubNativeSegwit = data.toByteArray().toStdString(); - - connectionManager_->GetLogger()->debug("[TrezorDevice] init - start retrieving nested segwit public key from device " - + features_.label()); - bitcoin::GetPublicKey message; - for (const uint32_t add : getDerivationPath(testNet_, bs::hd::Purpose::Nested)) { - message.add_address_n(add); + const auto& cbNested = [this](const std::shared_ptr& data) + { + const auto& reply = std::dynamic_pointer_cast(data); + if (!reply) { + logger_->error("[TrezorDevice::getPublicKeys::nested] invalid callback data"); + return; } + awaitingWalletInfo_.xpubNestedSegwit = reply->xpub; + }; - if (testNet_) { - message.set_coin_name(tesNetCoin); + const auto& cbNative = [this](const std::shared_ptr &data) + { + const auto& reply = std::dynamic_pointer_cast(data); + if (!reply) { + logger_->error("[TrezorDevice::getPublicKeys::native] invalid callback data"); + return; } - - setDataCallback(MessageType_PublicKey, std::move(cbNested)); - makeCall(message); + awaitingWalletInfo_.xpubNativeSegwit = reply->xpub; }; - - connectionManager_->GetLogger()->debug("[TrezorDevice] init - start retrieving native segwit public key from device " - + features_.label()); + logger_->debug("[TrezorDevice::getPublicKeys] start public keys from device {}" + , features_->label()); bitcoin::GetPublicKey message; for (const uint32_t add : getDerivationPath(testNet_, bs::hd::Purpose::Native)) { message.add_address_n(add); } - if (testNet_) { - message.set_coin_name(tesNetCoin); + message.set_coin_name(kTestNetCoin); } + makeCall(message, cbNative); - setDataCallback(MessageType_PublicKey, std::move(cbNative)); - makeCall(message); + message.clear_address_n(); + for (const uint32_t add : getDerivationPath(testNet_, bs::hd::Purpose::Nested)) { + message.add_address_n(add); + } + makeCall(message, cbNested); + + message.clear_address_n(); + for (const uint32_t add : getDerivationPath(testNet_, bs::hd::Purpose::NonSegWit)) { + message.add_address_n(add); + } + makeCall(message, cbLegacy); } -void TrezorDevice::setMatrixPin(const std::string& pin) +void TrezorDevice::setMatrixPin(const SecureBinaryData& pin) { - connectionManager_->GetLogger()->debug("[TrezorDevice] setMatrixPin - send matrix pin response"); + logger_->debug("[TrezorDevice::setMatrixPin] {}", data_.path); common::PinMatrixAck message; - message.set_pin(pin); + message.set_pin(pin.toBinStr()); makeCall(message); } -void TrezorDevice::setPassword(const std::string& password, bool enterOnDevice) +void TrezorDevice::setPassword(const SecureBinaryData& password, bool enterOnDevice) { - connectionManager_->GetLogger()->debug("[TrezorDevice] setPassword - send passphrase response"); + logger_->debug("[TrezorDevice::setPassword] {}", data_.path); common::PassphraseAck message; if (enterOnDevice) { message.set_on_device(true); } else { - message.set_passphrase(password); + message.set_passphrase(password.toBinStr()); } makeCall(message); } void TrezorDevice::cancel() { - connectionManager_->GetLogger()->debug("[TrezorDevice] cancel previous operation"); + logger_->debug("[TrezorDevice::cancel]"); management::Cancel message; makeCall(message); - sendTxMessage(HWInfoStatus::kCancelledByUser); + sendTxMessage(/*HWInfoStatus::kCancelledByUser*/"cancelled by user"); } -void TrezorDevice::clearSession(AsyncCallBack&& cb) +void TrezorDevice::clearSession() { - connectionManager_->GetLogger()->debug("[TrezorDevice] cancel previous operation"); - management::ClearSession message; - - if (cb) { - setCallbackNoData(MessageType_Success, std::move(cb)); - } - + logger_->debug("[TrezorDevice::clearSession]"); + management::EndSession message; makeCall(message); } +void TrezorDevice::setSupportingTXs(const std::vector& txs) +{ + if (!currentTxSignReq_) { + logger_->warn("[{}] no current sign TX operation in progress", __func__); + return; + } + for (const auto& tx : txs) { + currentTxSignReq_->armorySigner_.addSupportingTx(tx); + logger_->debug("[{}] added supporting TX {}", __func__, tx.getThisHash().toHexStr(true)); + } +} -void TrezorDevice::signTX(const bs::core::wallet::TXSignRequest &reqTX, AsyncCallBackCall&& cb /*= nullptr*/) +void TrezorDevice::signTX(const bs::core::wallet::TXSignRequest &reqTX) { - currentTxSignReq_.reset(new bs::core::wallet::TXSignRequest(reqTX)); - connectionManager_->GetLogger()->debug("[TrezorDevice] SignTX - specify init data to " + features_.label()); + currentTxSignReq_ = std::make_unique(reqTX); + logger_->debug("[TrezorDevice::signTX] {}", features_->label()); + + std::vector txHashes; + for (uint32_t i = 0; i < currentTxSignReq_->armorySigner_.getTxInCount(); ++i) { + const auto& spender = currentTxSignReq_->armorySigner_.getSpender(i); + if (!spender) { + logger_->warn("[{}] no spender at {}", __func__, i); + continue; + } + txHashes.push_back(spender->getUtxo().getTxHash()); + } + cb_->needSupportingTXs(key(), txHashes); bitcoin::SignTx message; message.set_inputs_count(currentTxSignReq_->armorySigner_.getTxInCount()); message.set_outputs_count(currentTxSignReq_->armorySigner_.getTxOutCount()); if (testNet_) { - message.set_coin_name(tesNetCoin); - } - - if (cb) { - setDataCallback(MessageType_TxRequest, std::move(cb)); + message.set_coin_name(kTestNetCoin); } - - awaitingTransaction_ = {}; - makeCall(message); + const auto& cb = [this, reqTX](const std::shared_ptr& data) + { + const auto& reply = std::dynamic_pointer_cast(data); + if (!reply) { + logger_->error("[TrezorDevice::signTX] invalid callback data"); + return; + } + // According to architecture, Trezor allow us to sign tx with incorrect + // passphrase, so let's check that the final tx is correct. In Ledger case + // this situation is impossible, since the wallets with different passphrase will be treated + // as different devices, which will be verified in sign part. + try { + std::map> utxoMap; + for (unsigned i = 0; i < reqTX.armorySigner_.getTxInCount(); i++) { + const auto& utxo = reqTX.armorySigner_.getSpender(i)->getUtxo(); + auto& idMap = utxoMap[utxo.getTxHash()]; + idMap.emplace(utxo.getTxOutIndex(), utxo); + } + unsigned flags = SCRIPT_VERIFY_P2SH | SCRIPT_VERIFY_SEGWIT | SCRIPT_VERIFY_P2SH_SHA256; + bool validSign = Armory::Signer::Signer::verify(SecureBinaryData::fromString(reply->signedTX) + , utxoMap, flags, true).isValid(); + if (!validSign) { + SPDLOG_LOGGER_ERROR(logger_, "sign verification failed"); + operationFailed("Signing failed. Please ensure you typed the correct passphrase."); + return; + } + } + catch (const std::exception& e) { + SPDLOG_LOGGER_ERROR(logger_, "sign verification failed: {}", e.what()); + operationFailed("Signing failed. Please ensure you typed the correct passphrase."); + return; + } + cb_->txSigned(key(), SecureBinaryData::fromString(reply->signedTX)); + }; + logger_->debug("[{}] {}", __func__, message.DebugString()); + makeCall(message, cb); } -void TrezorDevice::retrieveXPubRoot(AsyncCallBack&& cb) +void TrezorDevice::retrieveXPubRoot() { - // Fetching walletId - connectionManager_->GetLogger()->debug("[TrezorDevice] init - start retrieving root public key from device " - + features_.label()); + logger_->debug("[TrezorDevice::retrieveXPubRoot] start retrieving root public" + " key from device {}", features_->label()); bitcoin::GetPublicKey message; message.add_address_n(bs::hd::hardFlag); if (testNet_) { - message.set_coin_name(tesNetCoin); + message.set_coin_name(kTestNetCoin); } - auto saveXpubRoot = [caller = QPointer(this), cb = std::move(cb)](QVariant&& data) { - if (!caller) { + const auto& saveXpubRoot = [this](const std::shared_ptr& data) + { + const auto& reply = std::dynamic_pointer_cast(data); + if (!reply) { + logger_->error("[TrezorDevice::retrieveXPubRoot] invalid callback data"); return; } - - caller->xpubRoot_ = data.toByteArray().toStdString(); - if (cb) { - cb(); - } + logger_->debug("[TrezorDevice::retrieveXPubRoot] {}: {}", features_->label(), reply->xpub); + xpubRoot_ = BinaryData::fromString(reply->xpub); + const auto& devKey = key(); + cb_->publicKeyReady(devKey); }; - - setDataCallback(MessageType_PublicKey, std::move(saveXpubRoot)); - makeCall(message); + makeCall(message, saveXpubRoot); } -void TrezorDevice::makeCall(const google::protobuf::Message &msg) +void TrezorDevice::makeCall(const google::protobuf::Message &msg + , const bs::WorkerPool::callback& callback) { - client_->call(packMessage(msg), [this](QVariant&& answer) { - if (answer.isNull()) { - emit operationFailed(QLatin1String("Network error")); - resetCaches(); + if (state_ != trezor::State::Init) { + logger_->debug("[{}] re-initing device", __func__); + init(); + } + auto cb = callback; + if ((cb == nullptr) && !awaitingCallbacks_.empty()) { + cb = awaitingCallbacks_.front(); + awaitingCallbacks_.pop_front(); + } + const auto& cbWrap = [this, cb](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || !reply->error.empty()) { + logger_->error("[TrezorDevice::makeCall] network error: {}" + , (reply && !reply->error.empty()) ? reply->error : (data ? "" : "")); + operationFailed("comm error"); + cancel(); + reset(); + return; } - - MessageData data = unpackMessage(answer.toByteArray()); - handleMessage(data); - }); + const auto& msg = unpackMessage(QByteArray::fromStdString(reply->response)); + handleMessage(msg, cb); + }; + auto inData = std::make_shared(); + inData->path = "/call/" + data_.sessionId; + inData->input = packMessage(msg).toStdString(); + processQueued(inData, cbWrap); } -void TrezorDevice::handleMessage(const MessageData& data) +void TrezorDevice::handleMessage(const trezor::MessageData& data, const bs::WorkerPool::callback& cb) { - switch (static_cast(data.msg_type_)) { + switch (static_cast(data.type)) { case MessageType_Success: { common::Success success; @@ -335,33 +442,42 @@ void TrezorDevice::handleMessage(const MessageData& data) break; case MessageType_Failure: { + state_ = trezor::State::None; common::Failure failure; if (parseResponse(failure, data)) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage last message failure " - + getJSONReadableMessage(failure)); + logger_->warn("[TrezorDevice::handleMessage] last message failure: {}" + , getJSONReadableMessage(failure)); } - sendTxMessage(QString::fromStdString(failure.message())); - resetCaches(); + sendTxMessage(failure.message()); + reset(); switch (failure.code()) { case common::Failure_FailureType_Failure_ActionCancelled: - emit cancelledOnDevice(); + cancelledOnDevice(); break; case common::Failure_FailureType_Failure_PinInvalid: - emit invalidPin(); + invalidPin(); break; default: - emit operationFailed(QString::fromStdString(failure.message())); + operationFailed(failure.message()); break; } } break; case MessageType_Features: { - if (parseResponse(features_, data)) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage Features, model: '{}' - {}.{}.{}" - , features_.model(), features_.major_version(), features_.minor_version(), features_.patch_version()); + if (parseResponse(*features_, data)) { + state_ = trezor::State::Init; + logger_->debug("[TrezorDevice::handleMessage] features: model '{}' v{}.{}.{}" + , features_->model(), features_->major_version(), features_->minor_version(), features_->patch_version()); // + getJSONReadableMessage(features_)); + retrieveXPubRoot(); + if (cb_) { + cb_->scanningDone(); + } + } + else { + logger_->error("[TrezorDevice::handleMessage] failed to parse features response"); } } break; @@ -369,21 +485,22 @@ void TrezorDevice::handleMessage(const MessageData& data) { common::ButtonRequest request; if (parseResponse(request, data)) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage ButtonRequest " - + getJSONReadableMessage(request)); + logger_->debug("[TrezorDevice::handleMessage] ButtonRequest {}", getJSONReadableMessage(request)); } common::ButtonAck response; - makeCall(response); - sendTxMessage(HWInfoStatus::kPressButton); + makeCall(response, cb); + sendTxMessage(/*HWInfoStatus::kPressButton*/"press the button"); txSignedByUser_ = true; } - break; + return; case MessageType_PinMatrixRequest: { common::PinMatrixRequest request; if (parseResponse(request, data)) { - emit requestPinMatrix(); - sendTxMessage(HWInfoStatus::kRequestPin); + if (cb_) { + cb_->requestPinMatrix(key()); + } + sendTxMessage(/*HWInfoStatus::kRequestPin*/"enter pin"); } } break; @@ -391,8 +508,11 @@ void TrezorDevice::handleMessage(const MessageData& data) { common::PassphraseRequest request; if (parseResponse(request, data)) { - emit requestHWPass(hasCapability(management::Features_Capability_Capability_PassphraseEntry)); - sendTxMessage(HWInfoStatus::kRequestPassphrase); + if (cb_) { + cb_->requestHWPass(key(), + hasCapability(management::Features_Capability_Capability_PassphraseEntry)); + } + sendTxMessage(/*HWInfoStatus::kRequestPassphrase*/"enter passphrase"); } } break; @@ -400,10 +520,21 @@ void TrezorDevice::handleMessage(const MessageData& data) { bitcoin::PublicKey publicKey; if (parseResponse(publicKey, data)) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage PublicKey" //); - + getJSONReadableMessage(publicKey)); + logger_->debug("[TrezorDevice::handleMessage] public key: {}" + , getJSONReadableMessage(publicKey)); + } + auto callback = cb; + if (!callback && !awaitingCallbacks_.empty()) { + logger_->debug("[TrezorDevice::handleMessage::PublicKey] retrieving pooled callback"); + callback = awaitingCallbacks_.front(); + awaitingCallbacks_.pop_front(); + } + if (callback) { + const auto& outData = std::make_shared(); + outData->xpub = publicKey.xpub(); + callback(outData); + return; } - dataCallback(MessageType_PublicKey, QByteArray::fromStdString(publicKey.xpub())); } break; case MessageType_Address: @@ -414,85 +545,56 @@ void TrezorDevice::handleMessage(const MessageData& data) break; case MessageType_TxRequest: { - handleTxRequest(data); - sendTxMessage(txSignedByUser_ ? HWInfoStatus::kReceiveSignedTx : HWInfoStatus::kTransaction); + handleTxRequest(data, cb); + sendTxMessage(txSignedByUser_ ? /*HWInfoStatus::kReceiveSignedTx*/"signed TX" + : /*HWInfoStatus::kTransaction*/"transaction"); } - break; + return; default: - { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage " + std::to_string(data.msg_type_) + " - Unhandled message type"); - } + logger_->info("[TrezorDevice::handleMessage] {} - Unhandled message type", data.type); break; } - - callbackNoData(static_cast(data.msg_type_)); + if (cb) { +/* const auto& noData = std::make_shared(); + noData->msgType = static_cast(data.type); + cb(noData);*/ + awaitingCallbacks_.push_back(cb); + } } -bool TrezorDevice::parseResponse(google::protobuf::Message &msg, const MessageData& data) +bool TrezorDevice::parseResponse(google::protobuf::Message &msg, const trezor::MessageData& data) { - bool ok = msg.ParseFromString(data.message_); + bool ok = msg.ParseFromString(data.message); if (ok) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage " + - std::to_string(data.msg_type_) + " - successfully parsed response"); + logger_->debug("[TrezorDevice::parseResponse] {} - successfully parsed " + "response", data.type); } else { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage " + - std::to_string(data.msg_type_) + " - failed to parse response"); + logger_->error("[TrezorDevice::parseResponse] {} - failed to parse response" + , data.type); } - return ok; } -void TrezorDevice::resetCaches() +void TrezorDevice::reset() { - awaitingCallbackNoData_.clear(); - awaitingCallbackData_.clear(); - currentTxSignReq_.reset(nullptr); - awaitingTransaction_ = {}; + currentTxSignReq_.reset(); + awaitingSignedTX_.clear(); awaitingWalletInfo_ = {}; + awaitingCallbacks_.clear(); } -void TrezorDevice::setCallbackNoData(MessageType type, AsyncCallBack&& cb) -{ - awaitingCallbackNoData_[type] = std::move(cb); -} - -void TrezorDevice::callbackNoData(MessageType type) -{ - auto iAwaiting = awaitingCallbackNoData_.find(type); - if (iAwaiting != awaitingCallbackNoData_.end()) { - auto cb = std::move(iAwaiting->second); - awaitingCallbackNoData_.erase(iAwaiting); - cb(); - } -} - -void TrezorDevice::setDataCallback(MessageType type, AsyncCallBackCall&& cb) -{ - awaitingCallbackData_[type] = std::move(cb); -} - -void TrezorDevice::dataCallback(MessageType type, QVariant&& response) -{ - auto iAwaiting = awaitingCallbackData_.find(type); - if (iAwaiting != awaitingCallbackData_.end()) { - auto cb = std::move(iAwaiting->second); - awaitingCallbackData_.erase(iAwaiting); - cb(std::move(response)); - } -} - -void TrezorDevice::handleTxRequest(const MessageData& data) +void TrezorDevice::handleTxRequest(const trezor::MessageData& data + , const bs::WorkerPool::callback& cb) { assert(currentTxSignReq_); bitcoin::TxRequest txRequest; if (parseResponse(txRequest, data)) { - connectionManager_->GetLogger()->debug("[TrezorDevice] handleMessage TxRequest " - + getJSONReadableMessage(txRequest)); + logger_->debug("[TrezorDevice::handleTxRequest] {}", getJSONReadableMessage(txRequest)); } if (txRequest.has_serialized() && txRequest.serialized().has_serialized_tx()) { - awaitingTransaction_.signedTx += txRequest.serialized().serialized_tx(); + awaitingSignedTX_ += txRequest.serialized().serialized_tx(); } bitcoin::TxAck txAck; @@ -512,10 +614,9 @@ void TrezorDevice::handleTxRequest(const MessageData& data) input->set_script_sig(txIn.getScript().toBinStr()); } - connectionManager_->GetLogger()->debug("[TrezorDevice] handleTxRequest TXINPUT for prev hash" - + getJSONReadableMessage(txAck)); - - makeCall(txAck); + logger_->debug("[TrezorDevice::handleTxRequest] TXINPUT for prev hash: {}" + , getJSONReadableMessage(txAck)); + makeCall(txAck, cb); break; } @@ -523,15 +624,19 @@ void TrezorDevice::handleTxRequest(const MessageData& data) bitcoin::TxAck_TransactionType_TxInputType *input = type->add_inputs(); const int index = txRequest.details().request_index(); + logger_->debug("[TrezorDevice::handleTxRequest] TXINPUT index={}, txInCount={}" + , index, currentTxSignReq_->armorySigner_.getTxInCount()); assert(index >= 0 && index < currentTxSignReq_->armorySigner_.getTxInCount()); auto spender = currentTxSignReq_->armorySigner_.getSpender(index); auto utxo = spender->getUtxo(); auto address = bs::Address::fromUTXO(utxo); const auto purp = bs::hd::purpose(address.getType()); + logger_->debug("[TrezorDevice::handleTxRequest] TXINPUT address {}", address.display()); auto bip32Paths = spender->getBip32Paths(); if (bip32Paths.size() != 1) { + logger_->error("[TrezorDevice::handleTxRequest] TXINPUT {} BIP32 paths", bip32Paths.size()); throw std::logic_error("unexpected pubkey count for spender"); } const auto& path = bip32Paths.begin()->second.getDerivationPathFromSeed(); @@ -562,13 +667,11 @@ void TrezorDevice::handleTxRequest(const MessageData& data) if (currentTxSignReq_->RBF) { input->set_sequence(UINT32_MAX - 2); } - txAck.set_allocated_tx(type); - connectionManager_->GetLogger()->debug("[TrezorDevice] handleTxRequest TXINPUT" - + getJSONReadableMessage(txAck)); - - makeCall(txAck); + logger_->debug("[TrezorDevice::handleTxRequest] TXINPUT response {}" + , getJSONReadableMessage(txAck)); + makeCall(txAck, cb); } break; case bitcoin::TxRequest_RequestType_TXOUTPUT: @@ -583,10 +686,10 @@ void TrezorDevice::handleTxRequest(const MessageData& data) binOutput->set_script_pubkey(txOut.getScript().toBinStr()); } - connectionManager_->GetLogger()->debug("[TrezorDevice] handleTxRequest TXOUTPUT for prev hash" - + getJSONReadableMessage(txAck)); + logger_->debug("[TrezorDevice::handleTxRequest] TXOUTPUT for prev hash: {}" + , getJSONReadableMessage(txAck)); - makeCall(txAck); + makeCall(txAck, cb); break; } @@ -600,7 +703,7 @@ void TrezorDevice::handleTxRequest(const MessageData& data) if (currentTxSignReq_->change.address != address) { // general output output->set_address(address.display()); output->set_amount(bsOutput->getValue()); - output->set_script_type(bitcoin::TxAck_TransactionType_TxOutputType_OutputScriptType_PAYTOADDRESS); + output->set_script_type(bitcoin::PAYTOADDRESS); } else { const auto &change = currentTxSignReq_->change; output->set_amount(change.value); @@ -608,7 +711,9 @@ void TrezorDevice::handleTxRequest(const MessageData& data) const auto purp = bs::hd::purpose(change.address.getType()); if (change.index.empty()) { - throw std::logic_error(fmt::format("can't find change address index for '{}'", change.address.display())); + const auto& errorMsg = fmt::format("can't find change address index for '{}'", change.address.display()); + logger_->error("[TrezorDevice::handleTxRequest::TXOUTPUT] {}", errorMsg); + throw std::logic_error(errorMsg); } auto path = getDerivationPath(testNet_, purp); @@ -618,27 +723,27 @@ void TrezorDevice::handleTxRequest(const MessageData& data) } const auto changeType = change.address.getType(); - bitcoin::TxAck_TransactionType_TxOutputType_OutputScriptType scriptType; + bitcoin::OutputScriptType scriptType; if (changeType == AddressEntryType_P2SH) { - scriptType = bitcoin::TxAck_TransactionType_TxOutputType_OutputScriptType_PAYTOP2SHWITNESS; + scriptType = bitcoin::PAYTOP2SHWITNESS; } else if (changeType == AddressEntryType_P2WPKH) { - scriptType = bitcoin::TxAck_TransactionType_TxOutputType_OutputScriptType_PAYTOWITNESS; + scriptType = bitcoin::PAYTOWITNESS; } else if (changeType == AddressEntryType_P2PKH) { - scriptType = bitcoin::TxAck_TransactionType_TxOutputType_OutputScriptType_PAYTOADDRESS; + scriptType = bitcoin::PAYTOADDRESS; } else { - throw std::runtime_error(fmt::format("unexpected changeType: {}", static_cast(changeType))); + const auto& errorMsg = fmt::format("unexpected change type: {}", (int)changeType); + logger_->error("[TrezorDevice::handleTxRequest::TXOUTPUT] {}", errorMsg); + throw std::runtime_error(errorMsg); } - output->set_script_type(scriptType); } txAck.set_allocated_tx(type); - connectionManager_->GetLogger()->debug("[TrezorDevice] handleTxRequest TXOUTPUT" - + getJSONReadableMessage(txAck)); - - makeCall(txAck); + logger_->debug("[TrezorDevice::handleTxRequest] TXOUTPUT response: {}" + , getJSONReadableMessage(txAck)); + makeCall(txAck, cb); } break; case bitcoin::TxRequest_RequestType_TXMETA: @@ -646,39 +751,58 @@ void TrezorDevice::handleTxRequest(const MessageData& data) // Return previous tx details for legacy inputs // See https://wiki.trezor.io/Developers_guide:Message_Workflows auto tx = prevTx(txRequest); +#ifdef TREZOR_NEW_STYLE_TXMETA + bitcoin::TxAckPrevMeta msg; + auto msgTX = msg.mutable_tx(); +#else auto data = txAck.mutable_tx(); +#endif if (tx.isInitialized()) { +#ifdef TREZOR_NEW_STYLE_TXMETA + msgTX->set_version(tx.getVersion()); + msgTX->set_lock_time(tx.getLockTime()); + msgTX->set_inputs_count(tx.getNumTxIn()); + msgTX->set_outputs_count(tx.getNumTxOut()); +#else data->set_version(tx.getVersion()); data->set_lock_time(tx.getLockTime()); data->set_inputs_cnt(tx.getNumTxIn()); data->set_outputs_cnt(tx.getNumTxOut()); +#endif } - - connectionManager_->GetLogger()->debug("[TrezorDevice] handleTxRequest TXMETA" - + getJSONReadableMessage(txAck)); - - makeCall(txAck); +#ifdef TREZOR_NEW_STYLE_TXMETA + logger_->debug("[TrezorDevice::handleTxRequest] TXMETA response: {}", getJSONReadableMessage(msg)); + makeCall(msg, cb); +#else + logger_->debug("[TrezorDevice::handleTxRequest] TXMETA response: {}", getJSONReadableMessage(txAck)); + makeCall(txAck, cb); +#endif } break; case bitcoin::TxRequest_RequestType_TXFINISHED: { - dataCallback(MessageType_TxRequest, QVariant::fromValue<>(awaitingTransaction_)); - sendTxMessage(HWInfoStatus::kTransactionFinished); - resetCaches(); + if (cb) { + const auto& txOut = std::make_shared(); + txOut->signedTX = awaitingSignedTX_; + cb(txOut); + } + sendTxMessage(/*HWInfoStatus::kTransactionFinished*/"TX finished"); + reset(); } break; default: + logger_->error("[TrezorDevice::handleTxRequest] unhandled request type {}" + , txRequest.request_type()); break; } } -void TrezorDevice::sendTxMessage(const QString& status) +void TrezorDevice::sendTxMessage(const std::string &status) { if (!currentTxSignReq_) { return; } - - emit deviceTxStatusChanged(status); + //emit deviceTxStatusChanged(status); } Tx TrezorDevice::prevTx(const bitcoin::TxRequest &txRequest) @@ -690,45 +814,45 @@ Tx TrezorDevice::prevTx(const bitcoin::TxRequest &txRequest) } catch (const std::exception&) { - SPDLOG_LOGGER_ERROR(connectionManager_->GetLogger(), "can't find prev TX {}", txHash.toHexStr(1)); + SPDLOG_LOGGER_ERROR(logger_, "can't find prev TX {}", txHash.toHexStr(1)); return {}; } } -bool TrezorDevice::hasCapability(management::Features::Capability cap) const +bool TrezorDevice::hasCapability(const management::Features_Capability& cap) const { - return std::find(features_.capabilities().begin(), features_.capabilities().end(), cap) - != features_.capabilities().end(); + return std::find(features_->capabilities().begin(), features_->capabilities().end(), cap) + != features_->capabilities().end(); } bool TrezorDevice::isFirmwareSupported() const { - auto verIt = kMinVersion.find(features_.model()); + auto verIt = kMinVersion.find(features_->model()); if (verIt == kMinVersion.end()) { return false; } const auto &minVer = verIt->second; - if (features_.major_version() > minVer[0]) { + if (features_->major_version() > minVer[0]) { return true; } - if (features_.major_version() < minVer[0]) { + if (features_->major_version() < minVer[0]) { return false; } - if (features_.minor_version() > minVer[1]) { + if (features_->minor_version() > minVer[1]) { return true; } - if (features_.minor_version() < minVer[1]) { + if (features_->minor_version() < minVer[1]) { return false; } - return features_.patch_version() >= minVer[2]; + return features_->patch_version() >= minVer[2]; } std::string TrezorDevice::firmwareSupportedVersion() const { - auto verIt = kMinVersion.find(features_.model()); + auto verIt = kMinVersion.find(features_->model()); if (verIt == kMinVersion.end()) { - return fmt::format("Unknown model: {}", features_.model()); + return fmt::format("Unknown model: {}", features_->model()); } const auto &minVer = verIt->second; return fmt::format("Please update wallet firmware to version {}.{}.{} or later" diff --git a/BlockSettleHW/trezor/trezorDevice.h b/BlockSettleHW/trezor/trezorDevice.h index 3dcc6d838..7a8e44d16 100644 --- a/BlockSettleHW/trezor/trezorDevice.h +++ b/BlockSettleHW/trezor/trezorDevice.h @@ -11,110 +11,120 @@ #ifndef TREZORDEVICE_H #define TREZORDEVICE_H +#include "Message/Worker.h" #include "trezorStructure.h" #include "hwdeviceinterface.h" -#include -#include -#include - - -// Trezor interface (source - https://github.com/trezor/trezor-common/tree/master/protob) -#include "trezor/generated_proto/messages-management.pb.h" -#include "trezor/generated_proto/messages-common.pb.h" -#include "trezor/generated_proto/messages-bitcoin.pb.h" -#include "trezor/generated_proto/messages.pb.h" - - -class ConnectionManager; -class QNetworkRequest; -class TrezorClient; +#include "trezorClient.h" +namespace spdlog { + class logger; +} namespace bs { namespace core { namespace wallet { struct TXSignRequest; } } - namespace sync { - class WalletsManager; - } } - -class TrezorDevice : public HwDeviceInterface -{ - Q_OBJECT - -public: - TrezorDevice(const std::shared_ptr & - , std::shared_ptr walletManager, bool testNet - , const QPointer &, QObject* parent = nullptr); - ~TrezorDevice() override; - - DeviceKey key() const override; - DeviceType type() const override; - - - // lifecycle - void init(AsyncCallBack&& cb = nullptr) override; - void cancel() override; - void clearSession(AsyncCallBack&& cb = nullptr) override; - - // operation - void getPublicKey(AsyncCallBackCall&& cb = nullptr) override; - void signTX(const bs::core::wallet::TXSignRequest& reqTX, AsyncCallBackCall&& cb = nullptr) override; - void retrieveXPubRoot(AsyncCallBack&& cb) override; - - // Management - void setMatrixPin(const std::string& pin) override; - void setPassword(const std::string& password, bool enterOnDevice) override; - - // State - bool isBlocked() override { - // There is no blocking state for Trezor - return false; +namespace hw { + namespace trezor { + namespace messages { + namespace bitcoin { + class TxRequest; + } + namespace management { + class Features; + enum Features_Capability: int; + } + enum MessageType: int; + } } +} -private: - void makeCall(const google::protobuf::Message &msg); - - void handleMessage(const MessageData& data); - bool parseResponse(google::protobuf::Message &msg, const MessageData& data); - - // callbacks - void resetCaches(); - - void setCallbackNoData(hw::trezor::messages::MessageType, AsyncCallBack&& cb); - void callbackNoData(hw::trezor::messages::MessageType); - - void setDataCallback(hw::trezor::messages::MessageType, AsyncCallBackCall&& cb); - void dataCallback(hw::trezor::messages::MessageType, QVariant&& response); - - void handleTxRequest(const MessageData& data); - void sendTxMessage(const QString& status); - - // Returns previous Tx for legacy inputs - // Trezor could request non-existing hash if wrong passphrase entered - Tx prevTx(const hw::trezor::messages::bitcoin::TxRequest &txRequest); - -private: - bool hasCapability(hw::trezor::messages::management::Features::Capability cap) const; - - bool isFirmwareSupported() const; - std::string firmwareSupportedVersion() const; - - std::shared_ptr connectionManager_{}; - std::shared_ptr walletManager_{}; - - QPointer client_{}; - hw::trezor::messages::management::Features features_{}; - bool testNet_{}; - std::unique_ptr currentTxSignReq_; - HWSignedTx awaitingTransaction_; - HwWalletWrapper awaitingWalletInfo_; - - bool txSignedByUser_ = false; - std::unordered_map awaitingCallbackNoData_; - std::unordered_map awaitingCallbackData_; -}; - +namespace bs { + namespace hww { + + class TrezorDevice : public DeviceInterface, protected WorkerPool + { + public: + TrezorDevice(const std::shared_ptr&, const trezor::DeviceData& + , bool testNet, DeviceCallbacks*, const std::string& endpoint); + ~TrezorDevice() override; + + trezor::DeviceData data() const { return data_; } + DeviceKey key() const override; + DeviceType type() const override; + + // lifecycle + void init() override; + void cancel() override; + void clearSession() override; + void releaseConnection(); + + // operation + void getPublicKeys() override; + void signTX(const bs::core::wallet::TXSignRequest& reqTX) override; + void retrieveXPubRoot() override; + + void setSupportingTXs(const std::vector&) override; + + // Management + void setMatrixPin(const SecureBinaryData& pin) override; + void setPassword(const SecureBinaryData& password, bool enterOnDevice) override; + + // State + bool isBlocked() const override { + // There is no blocking state for Trezor + return false; + } + + protected: + std::shared_ptr worker(const std::shared_ptr&) override final; + + // operation result informing + void publicKeyReady() override {} //TODO: implement + void deviceTxStatusChanged(const std::string& status) override {} //TODO: implement + void operationFailed(const std::string& reason) override; + void requestForRescan() override {} //TODO: implement + + // Management + void cancelledOnDevice() override {} //TODO: implement + void invalidPin() override {} //TODO: implement + + private: + void makeCall(const google::protobuf::Message&, const bs::WorkerPool::callback& cb = nullptr); + void handleMessage(const trezor::MessageData&, const bs::WorkerPool::callback& cb = nullptr); + bool parseResponse(google::protobuf::Message&, const trezor::MessageData&); + + void reset(); + + void handleTxRequest(const trezor::MessageData&, const bs::WorkerPool::callback& cb); + void sendTxMessage(const std::string& status); + + // Returns previous Tx for legacy inputs + // Trezor could request non-existing hash if wrong passphrase entered + Tx prevTx(const ::hw::trezor::messages::bitcoin::TxRequest& txRequest); + + bool hasCapability(const ::hw::trezor::messages::management::Features_Capability&) const; + bool isFirmwareSupported() const; + std::string firmwareSupportedVersion() const; + + private: + std::shared_ptr logger_; + const trezor::DeviceData data_; + const bool testNet_; + DeviceCallbacks* cb_{ nullptr }; + const std::string endpoint_; + trezor::State state_{ trezor::State::None }; + std::shared_ptr<::hw::trezor::messages::management::Features> features_{}; + + std::unique_ptr currentTxSignReq_; + bs::core::HwWalletInfo awaitingWalletInfo_; + std::string awaitingSignedTX_; + std::deque awaitingCallbacks_; + bool txSignedByUser_{ false }; + }; + + } //hw +} //bs #endif // TREZORDEVICE_H diff --git a/BlockSettleHW/trezor/trezorStructure.h b/BlockSettleHW/trezor/trezorStructure.h index 0b71a300d..72cd8bf9e 100644 --- a/BlockSettleHW/trezor/trezorStructure.h +++ b/BlockSettleHW/trezor/trezorStructure.h @@ -11,26 +11,50 @@ #ifndef TREZORSTRUCTURE_H #define TREZORSTRUCTURE_H -#include "hwcommonstructure.h" - -enum class State { - None = 0, - Init, - Enumerated, - Acquired, - Released -}; - -struct MessageData -{ - int msg_type_ = -1; - int length_ = -1; - std::string message_; -}; - -namespace HWInfoStatus { - const QString kRequestPassphrase = QObject::tr("Please enter the trezor passphrase"); - const QString kRequestPin = QObject::tr("Please enter the pin from device"); -} +#include + +namespace bs { + namespace hww { + namespace trezor { + + struct DeviceData + { + std::string path; + int vendor; + int product; + std::string sessionId; + bool debug{ false }; + std::string debugSession; + }; + + enum class State { + None = 0, + Init, + Enumerated, + Acquired, + Released + }; + + struct MessageData + { + int type = -1; + int length = -1; + std::string message; + }; + + enum class InfoStatus { + Unknown, + RequestPassphrase, + RequestPIN + }; + + } //trezor + } //hw +} //bs + +//namespace HWInfoStatus { + //const QString kRequestPassphrase = QObject::tr("Please enter the trezor passphrase"); + //const QString kRequestPin = QObject::tr("Please enter the pin from device"); +//} #endif // TREZORSTRUCTURE_H diff --git a/BlockSettleSigner/interfaces/GUI_QML/QMLApp.cpp b/BlockSettleSigner/interfaces/GUI_QML/QMLApp.cpp index a00697208..f188f307b 100644 --- a/BlockSettleSigner/interfaces/GUI_QML/QMLApp.cpp +++ b/BlockSettleSigner/interfaces/GUI_QML/QMLApp.cpp @@ -111,7 +111,7 @@ QMLAppObj::QMLAppObj(SignerAdapter *adapter, const std::shared_ptr - images/bs_logo.png images/full_logo.png images/notification_critical.png images/notification_info.png diff --git a/BlockSettleUILib/BSTerminalSplashScreen.cpp b/BlockSettleUILib/BSTerminalSplashScreen.cpp index 0186456fb..d768e5e5a 100644 --- a/BlockSettleUILib/BSTerminalSplashScreen.cpp +++ b/BlockSettleUILib/BSTerminalSplashScreen.cpp @@ -23,23 +23,25 @@ BSTerminalSplashScreen::BSTerminalSplashScreen(const QPixmap& splash_image) progress_->setMinimum(0); progress_->setMaximum(100); progress_->setValue(0); - progress_->setMinimumWidth(this->width()); - progress_->setMaximumHeight(10); - - blockSettleLabel_ = new QLabel(this); - blockSettleLabel_->setText(QLatin1String("BLOCKSETTLE TERMINAL")); - blockSettleLabel_->move(30, 140); - blockSettleLabel_->setStyleSheet(QLatin1String("font-size: 18px; color: white")); - - progress_->setStyleSheet(QLatin1String("text-align: center; font-size: 8px; border-width: 0px;")); - SetTipText(tr("Loading")); + progress_->setMinimumWidth(this->width() - 10); + progress_->setMaximumHeight(8); + progress_->move(5, this->height() - 10); + + //blockSettleLabel_ = new QLabel(this); + //blockSettleLabel_->setText(QLatin1String("BLOCKSETTLE TERMINAL")); + //blockSettleLabel_->move(30, 140); + //blockSettleLabel_->setStyleSheet(QLatin1String("font-size: 18px; color: white")); + + progress_->setStyleSheet(QLatin1String("QProgressBar::chunk{background-color:#45A6FF}; background:#191E2A; text-align: center; font-size: 8px; border-width: 1px; border-color: #3C435A")); + //SetTipText(tr("Loading")); + progress_->setFormat(QString::fromStdString("")); } BSTerminalSplashScreen::~BSTerminalSplashScreen() = default; void BSTerminalSplashScreen::SetTipText(const QString& tip) { - progress_->setFormat(tip + QString::fromStdString(" %p%")); + //progress_->setFormat(tip + QString::fromStdString(" %p%")); // QApplication::processEvents(); } diff --git a/BlockSettleUILib/CreateTransactionDialogAdvanced.cpp b/BlockSettleUILib/CreateTransactionDialogAdvanced.cpp index 02c884aea..87df52926 100644 --- a/BlockSettleUILib/CreateTransactionDialogAdvanced.cpp +++ b/BlockSettleUILib/CreateTransactionDialogAdvanced.cpp @@ -445,7 +445,9 @@ void CreateTransactionDialogAdvanced::setRBFinputs(const Tx &tx) QMetaObject::invokeMethod(this, lbdSetInputs); } }; +#ifdef OLD_WALLETS_CODE wallet->getRBFTxOutList(cbRBFUtxos); +#endif } }; //armory_->getTXsByHash(txHashSet, cbTXs, true); diff --git a/BlockSettleUILib/InfoDialogs/StartupDialog.cpp b/BlockSettleUILib/InfoDialogs/StartupDialog.cpp index 338fe0b1f..f09b2e3e1 100644 --- a/BlockSettleUILib/InfoDialogs/StartupDialog.cpp +++ b/BlockSettleUILib/InfoDialogs/StartupDialog.cpp @@ -16,7 +16,7 @@ #include "ApplicationSettings.h" #include "ui_StartupDialog.h" -#include "ArmoryServersProvider.h" +#include "../../Core/ArmoryServersProvider.h" namespace { const char *kLicenseFilePath = "://resources/license.html"; diff --git a/BlockSettleUILib/NotificationCenter.cpp b/BlockSettleUILib/NotificationCenter.cpp index b9d85985f..e2285b47c 100644 --- a/BlockSettleUILib/NotificationCenter.cpp +++ b/BlockSettleUILib/NotificationCenter.cpp @@ -308,7 +308,7 @@ void NotificationTrayIconResponder::notificationAction(const QString &action) newVersionMessage_ = true; messageClicked(); } else if (action == c_newOkAction) { - newChatMessage_ = true; +// newChatMessage_ = true; messageClicked(); } } diff --git a/BlockSettleUILib/Settings/ArmoryServersViewModel.cpp b/BlockSettleUILib/Settings/ArmoryServersViewModel.cpp index dc106ddaf..c0c174186 100644 --- a/BlockSettleUILib/Settings/ArmoryServersViewModel.cpp +++ b/BlockSettleUILib/Settings/ArmoryServersViewModel.cpp @@ -20,7 +20,7 @@ ArmoryServersViewModel::ArmoryServersViewModel(const std::shared_ptrservers(); + servers_.clear(); + for (const auto& server : serversProvider_->servers()) { + servers_.append(server); + } endResetModel(); } diff --git a/BlockSettleUILib/Settings/ArmoryServersViewModel.h b/BlockSettleUILib/Settings/ArmoryServersViewModel.h index 04da7fe70..cf0d6eb5a 100644 --- a/BlockSettleUILib/Settings/ArmoryServersViewModel.h +++ b/BlockSettleUILib/Settings/ArmoryServersViewModel.h @@ -17,7 +17,7 @@ #include "AuthAddress.h" #include "BinaryData.h" #include "ApplicationSettings.h" -#include "ArmoryServersProvider.h" +#include "../../Core/ArmoryServersProvider.h" class ArmoryServersViewModel : public QAbstractTableModel diff --git a/BlockSettleUILib/Settings/ArmoryServersWidget.cpp b/BlockSettleUILib/Settings/ArmoryServersWidget.cpp index a79c284f7..03aba190b 100644 --- a/BlockSettleUILib/Settings/ArmoryServersWidget.cpp +++ b/BlockSettleUILib/Settings/ArmoryServersWidget.cpp @@ -198,11 +198,11 @@ void ArmoryServersWidget::onAddServer() return; ArmoryServer server; - server.name = ui_->lineEditName->text(); + server.name = ui_->lineEditName->text().toStdString(); server.netType = static_cast(ui_->comboBoxNetworkType->currentIndex() - 1); - server.armoryDBIp = ui_->lineEditAddress->text(); - server.armoryDBPort = ui_->spinBoxPort->value(); - server.armoryDBKey = ui_->lineEditKey->text(); + server.armoryDBIp = ui_->lineEditAddress->text().toStdString(); + server.armoryDBPort = std::to_string(ui_->spinBoxPort->value()); + server.armoryDBKey = ui_->lineEditKey->text().toStdString(); if (armoryServersProvider_) { bool ok = armoryServersProvider_->add(server); @@ -261,11 +261,11 @@ void ArmoryServersWidget::onEdit() ui_->stackedWidgetAddSave->setCurrentWidget(ui_->pageSaveServerButton); - ui_->lineEditName->setText(server.name); + ui_->lineEditName->setText(QString::fromStdString(server.name)); ui_->comboBoxNetworkType->setCurrentIndex(static_cast(server.netType) + 1); - ui_->lineEditAddress->setText(server.armoryDBIp); - ui_->spinBoxPort->setValue(server.armoryDBPort); - ui_->lineEditKey->setText(server.armoryDBKey); + ui_->lineEditAddress->setText(QString::fromStdString(server.armoryDBIp)); + ui_->spinBoxPort->setValue(std::stoi(server.armoryDBPort)); + ui_->lineEditKey->setText(QString::fromStdString(server.armoryDBKey)); } void ArmoryServersWidget::onSelect() @@ -290,11 +290,11 @@ void ArmoryServersWidget::onSave() } ArmoryServer server; - server.name = ui_->lineEditName->text(); + server.name = ui_->lineEditName->text().toStdString(); server.netType = static_cast(ui_->comboBoxNetworkType->currentIndex() - 1); - server.armoryDBIp = ui_->lineEditAddress->text(); - server.armoryDBPort = ui_->spinBoxPort->value(); - server.armoryDBKey = ui_->lineEditKey->text(); + server.armoryDBIp = ui_->lineEditAddress->text().toStdString(); + server.armoryDBPort = std::to_string(ui_->spinBoxPort->value()); + server.armoryDBKey = ui_->lineEditKey->text().toStdString(); if (armoryServersProvider_) { bool ok = armoryServersProvider_->replace(index, server); @@ -339,7 +339,7 @@ void ArmoryServersWidget::setupServerFromSelected(bool needUpdate) return; } if (armoryServersProvider_) { - armoryServersProvider_->setupServer(index, needUpdate); + armoryServersProvider_->setupServer(index); setRowSelected(armoryServersProvider_->indexOfCurrent()); } else { @@ -397,14 +397,14 @@ void ArmoryServersWidget::onFormChanged() bool valid = false; if (acceptable) { ArmoryServer armoryHost; - armoryHost.name = ui_->lineEditName->text(); - armoryHost.armoryDBIp = ui_->lineEditAddress->text(); - armoryHost.armoryDBPort = ui_->spinBoxPort->value(); - armoryHost.armoryDBKey = ui_->lineEditKey->text(); + armoryHost.name = ui_->lineEditName->text().toStdString(); + armoryHost.armoryDBIp = ui_->lineEditAddress->text().toStdString(); + armoryHost.armoryDBPort = std::to_string(ui_->spinBoxPort->value()); + armoryHost.armoryDBKey = ui_->lineEditKey->text().toStdString(); valid = armoryHost.isValid(); if (valid) { if (armoryServersProvider_) { - exists = armoryServersProvider_->indexOf(armoryHost.name) != -1 + exists = armoryServersProvider_->indexOf(QString::fromStdString(armoryHost.name)) != -1 || armoryServersProvider_->indexOf(armoryHost) != -1; } else { diff --git a/BlockSettleUILib/Settings/ArmoryServersWidget.h b/BlockSettleUILib/Settings/ArmoryServersWidget.h index f37071b4a..c32ffd34e 100644 --- a/BlockSettleUILib/Settings/ArmoryServersWidget.h +++ b/BlockSettleUILib/Settings/ArmoryServersWidget.h @@ -15,7 +15,7 @@ #include #include -#include "ArmoryServersProvider.h" +#include "../Core/ArmoryServersProvider.h" #include "ArmoryServersViewModel.h" namespace Ui { diff --git a/BlockSettleUILib/Settings/ConfigDialog.cpp b/BlockSettleUILib/Settings/ConfigDialog.cpp index a6ebba26d..421999169 100644 --- a/BlockSettleUILib/Settings/ConfigDialog.cpp +++ b/BlockSettleUILib/Settings/ConfigDialog.cpp @@ -10,7 +10,7 @@ */ #include "ConfigDialog.h" -#include "ArmoryServersProvider.h" +#include "../../Core/ArmoryServersProvider.h" #include "AssetManager.h" #include "GeneralSettingsPage.h" #include "NetworkSettingsPage.h" diff --git a/BlockSettleUILib/Settings/ConfigDialog.h b/BlockSettleUILib/Settings/ConfigDialog.h index a66cff364..f25a52f21 100644 --- a/BlockSettleUILib/Settings/ConfigDialog.h +++ b/BlockSettleUILib/Settings/ConfigDialog.h @@ -14,7 +14,7 @@ #include #include #include "ApplicationSettings.h" -#include "ArmoryServersProvider.h" +#include "../../Core/ArmoryServersProvider.h" #include "Settings/SignersProvider.h" #include "Wallets/SignContainer.h" diff --git a/BlockSettleUILib/Settings/NetworkSettingsPage.cpp b/BlockSettleUILib/Settings/NetworkSettingsPage.cpp index f8fe5d9c1..999a2b33c 100644 --- a/BlockSettleUILib/Settings/NetworkSettingsPage.cpp +++ b/BlockSettleUILib/Settings/NetworkSettingsPage.cpp @@ -144,7 +144,7 @@ void NetworkSettingsPage::initSettings() { if (armoryServersProvider_) { armoryServerModel_ = new ArmoryServersViewModel(armoryServersProvider_); - connect(armoryServersProvider_.get(), &ArmoryServersProvider::dataChanged, this, &NetworkSettingsPage::displayArmorySettings); + //connect(armoryServersProvider_.get(), &ArmoryServersProvider::dataChanged, this, &NetworkSettingsPage::displayArmorySettings); } else { armoryServerModel_ = new ArmoryServersViewModel(this); @@ -177,7 +177,10 @@ void NetworkSettingsPage::displayArmorySettings() // set index of selected server selectedServer = armoryServersProvider_->getArmorySettings(); selectedServerIndex = armoryServersProvider_->indexOfCurrent(); - connectedServerSettings = armoryServersProvider_->connectedArmorySettings(); + const auto& connectedIdx = armoryServersProvider_->indexOfCurrent(); + if (connectedIdx >= 0) { + connectedServerSettings = armoryServersProvider_->servers().at(connectedIdx); + } connectedServerIndex = armoryServersProvider_->indexOfConnected(); } else { @@ -196,9 +199,9 @@ void NetworkSettingsPage::displayArmorySettings() // display info of connected server ui_->labelArmoryServerNetwork->setText(connectedServerSettings.netType == NetworkType::MainNet ? tr("MainNet") : tr("TestNet")); - ui_->labelArmoryServerAddress->setText(connectedServerSettings.armoryDBIp); - ui_->labelArmoryServerPort->setText(QString::number(connectedServerSettings.armoryDBPort)); - ui_->labelArmoryServerKey->setText(connectedServerSettings.armoryDBKey); + ui_->labelArmoryServerAddress->setText(QString::fromStdString(connectedServerSettings.armoryDBIp)); + ui_->labelArmoryServerPort->setText(QString::fromStdString(connectedServerSettings.armoryDBPort)); + ui_->labelArmoryServerKey->setText(QString::fromStdString(connectedServerSettings.armoryDBKey)); // display tip if configuration was changed if (selectedServerIndex != connectedServerIndex @@ -273,8 +276,15 @@ void NetworkSettingsPage::onEnvSelected(int envIndex) const auto env = ApplicationSettings::EnvConfiguration(envIndex); const int armoryIndex = ui_->comboBoxArmoryServer->currentIndex(); int serverIndex = armoryIndex; - const auto &armoryServers = armoryServersProvider_ ? armoryServersProvider_->servers() - : armoryServers_; + QList armoryServers; + if (armoryServersProvider_) { + for (const auto& server : armoryServersProvider_->servers()) { + armoryServers.append(server); + } + } + else { + armoryServers = armoryServers_; + } if (armoryIndex < 0 || armoryIndex >= armoryServers.count()) { return; } @@ -292,8 +302,15 @@ void NetworkSettingsPage::onEnvSelected(int envIndex) void NetworkSettingsPage::onArmorySelected(int armoryIndex) { int envIndex = ui_->comboBoxEnvironment->currentIndex(); - auto armoryServers = armoryServersProvider_ ? armoryServersProvider_->servers() - : armoryServers_; + QList armoryServers; + if (armoryServersProvider_) { + for (const auto& server : armoryServersProvider_->servers()) { + armoryServers.append(server); + } + } + else { + armoryServers = armoryServers_; + } if (armoryIndex < 0 || armoryIndex >= armoryServers.count()) { return; } diff --git a/BlockSettleUILib/TransactionDetailsWidget.cpp b/BlockSettleUILib/TransactionDetailsWidget.cpp index 7cf9d920e..70056f111 100644 --- a/BlockSettleUILib/TransactionDetailsWidget.cpp +++ b/BlockSettleUILib/TransactionDetailsWidget.cpp @@ -11,7 +11,6 @@ #include "TransactionDetailsWidget.h" #include "ui_TransactionDetailsWidget.h" #include "BTCNumericTypes.h" -#include "BlockObj.h" #include "CheckRecipSigner.h" #include "UiUtils.h" #include "Wallets/SyncWallet.h" diff --git a/BlockSettleUILib/TransactionsViewModel.cpp b/BlockSettleUILib/TransactionsViewModel.cpp index 15398dae1..7b664264a 100644 --- a/BlockSettleUILib/TransactionsViewModel.cpp +++ b/BlockSettleUILib/TransactionsViewModel.cpp @@ -595,8 +595,9 @@ void TransactionsViewModel::onLedgerEntries(const std::string &, uint32_t std::vector txWallet; txWallet.reserve(entries.size()); for (const auto &entry : entries) { - const auto &walletId = entry.walletIds.empty() ? std::string{} : *(entry.walletIds.cbegin()); - txWallet.push_back({ entry.txHash, walletId, entry.value }); + std::vector walletIds; + walletIds.insert(walletIds.end(), entry.walletIds.cbegin(), entry.walletIds.cend()); + txWallet.push_back({ entry.txHash, walletIds, entry.value }); } emit needTXDetails(txWallet, true, {}); } @@ -625,8 +626,9 @@ void TransactionsViewModel::onZCsInvalidated(const std::vector& txHa txWallet.reserve(invalidatedNodes_.size()); for (const auto& invNode : invalidatedNodes_) { const auto& entry = invNode.second->item()->txEntry; - const auto& walletId = entry.walletIds.empty() ? std::string{} : *(entry.walletIds.cbegin()); - txWallet.push_back({ entry.txHash, walletId, entry.value }); + std::vector walletIds; + walletIds.insert(walletIds.end(), entry.walletIds.cbegin(), entry.walletIds.cend()); + txWallet.push_back({ entry.txHash, walletIds, entry.value }); } emit needTXDetails(txWallet, false, {}); } @@ -637,9 +639,9 @@ void TransactionsViewModel::onTXDetails(const std::vectorcanMixLeaves()) { - if (hdWallet->isHardwareOfflineWallet() && !(walletTypes & WalletsTypes::WatchOnly)) { + if (hdWallet->isHardwareWallet() && hdWallet->isOffline() && + !(walletTypes & WalletsTypes::WatchOnly)) { continue; } diff --git a/BlockSettleUILib/UtxoReservationManager.cpp b/BlockSettleUILib/UtxoReservationManager.cpp index d52b873b6..62a0784b4 100644 --- a/BlockSettleUILib/UtxoReservationManager.cpp +++ b/BlockSettleUILib/UtxoReservationManager.cpp @@ -70,10 +70,11 @@ namespace { result->utxosMap.emplace(walletId, std::move(utxos)); cbDone(); }; +#ifdef OLD_WALLETS_CODE if (!wallet->getSpendableTxOutList(cbWrapNormal, UINT64_MAX, false)) { return false; } - +#endif auto cbWrapZc = [result, size = wallets.size(), walletId = wallet->walletId(), cbDone] (std::vector utxos) { @@ -81,9 +82,11 @@ namespace { result->utxosMapZc.emplace(walletId, std::move(utxos)); cbDone(); }; +#ifdef OLD_WALLETS_CODE if (!wallet->getSpendableZCList(cbWrapZc)) { return false; } +#endif } return true; } diff --git a/CMakeLists.txt b/CMakeLists.txt index fc5e14c8b..65b05270b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,10 @@ add_definitions(-DSTATIC_BUILD) add_definitions(-DSPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_DEBUG) add_definitions(-DBUILD_WALLETS) +IF(UNIX AND NOT APPLE) +set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath='$ORIGIN'") +ENDIF() + IF(CMAKE_BUILD_TYPE STREQUAL "Debug") IF(BSTERMINAL_SHARED_LIBS) SET(THIRD_PARTY_BUILD_MODE "debug-shared") @@ -45,10 +49,14 @@ ELSE() ENDIF() add_definitions(-DNOMINMAX) +add_definitions(-DCURL_STATICLIB) # Force Armory to ignore Crypto++ and use libbtc instead. add_definitions(-DLIBBTC_ONLY) +add_definitions(-DQT_IS_AVAILABLE) +add_definitions(-DQT_UI_IS_AVAILABLE) + IF (APPLE) add_definitions(-DMDB_USE_POSIX_SEM=1) ENDIF(APPLE) @@ -60,7 +68,7 @@ ENDIF (PRODUCTION_BUILD) # Force 64-bit builds and min target for macOS. If the min target changes, # update generate.py too. if(APPLE) - set(CMAKE_OSX_DEPLOYMENT_TARGET 10.12 CACHE STRING "" FORCE) + set(CMAKE_OSX_DEPLOYMENT_TARGET 10.15 CACHE STRING "" FORCE) set(CMAKE_OSX_ARCHITECTURES "x86_64" CACHE STRING "macOS architecture to build; 64-bit is expected" FORCE) endif(APPLE) @@ -170,6 +178,7 @@ FIND_PACKAGE(Qt5QuickControls2 REQUIRED) FIND_PACKAGE(Qt5Charts REQUIRED) FIND_PACKAGE(Qt5Sql REQUIRED) FIND_PACKAGE(Qt5Svg REQUIRED) +FIND_PACKAGE(Qt5SerialPort REQUIRED) INCLUDE_DIRECTORIES(${Qt5Core_INCLUDE_DIRS}) INCLUDE_DIRECTORIES(${Qt5Gui_INCLUDE_DIRS}) @@ -184,6 +193,7 @@ INCLUDE_DIRECTORIES(${Qt5Sql_INCLUDE_DIRS}) INCLUDE_DIRECTORIES(${Qt5Svg_INCLUDE_DIRS}) IF ( UNIX AND NOT APPLE ) + add_compile_options(-fPIC) FIND_PACKAGE(Qt5DBus REQUIRED) ADD_DEFINITIONS( "-DBS_USE_DBUS" ) INCLUDE_DIRECTORIES(${Qt5DBus_INCLUDE_DIRS}) @@ -254,8 +264,8 @@ if (UNIX) ENDIF() endif (UNIX) - -# setup libwebsockets +# use only existing lws temporarily, don't try to build it (build fails on Windows) +ADD_DEFINITIONS(-DBUILD_LWS=1) SET(WS_PACKAGE_ROOT ${THIRD_PARTY_COMMON_DIR}/libwebsockets) SET(WS_LIB_DIR ${WS_PACKAGE_ROOT}/lib) SET(WS_INCLUDE_DIR ${WS_PACKAGE_ROOT}/include) @@ -267,14 +277,14 @@ IF (WIN32) SET(WS_LIB_NAME websockets_static) ENDIF() ELSE(WIN32) - IF(BSTERMINAL_SHARED_LIBS) +# IF(BSTERMINAL_SHARED_LIBS) SET(WS_LIB_NAME libwebsockets.so) - ELSE(BSTERMINAL_SHARED_LIBS) - SET(WS_LIB_NAME libwebsockets.a) - ENDIF() +# ELSE(BSTERMINAL_SHARED_LIBS) +# SET(WS_LIB_NAME libwebsockets.a) +# ENDIF() ENDIF(WIN32) -FIND_LIBRARY(WS_LIB NAMES ${WS_LIB_NAME} PATHS ${WS_LIB_DIR} NO_DEFAULT_PATH ) +FIND_LIBRARY(WS_LIB NAMES ${WS_LIB_NAME} PATHS ${WS_LIB_DIR}) IF(NOT WS_LIB) MESSAGE(FATAL_ERROR "Could not find libwebsockets in ${WS_LIB_DIR}") ENDIF() @@ -336,22 +346,46 @@ IF( NOT LIBCP_LIB) ENDIF() INCLUDE_DIRECTORIES( ${LIBCP_INCLUDE_DIR} ) -SET(MPIR_PACKAGE_ROOT ${THIRD_PARTY_COMMON_DIR}/mpir) -SET(MPIR_LIB_DIR ${MPIR_PACKAGE_ROOT}/lib) IF (WIN32) + SET(MPIR_PACKAGE_ROOT ${THIRD_PARTY_COMMON_DIR}/mpir) + SET(MPIR_LIB_DIR ${MPIR_PACKAGE_ROOT}/lib) SET(MPIR_LIB_NAME mpir) -ELSE(WIN32) - IF(BSTERMINAL_SHARED_LIBS) - SET(MPIR_LIB_NAME libgmp.so) - ELSE() - SET(MPIR_LIB_NAME libgmp.a) + FIND_LIBRARY(MPIR_LIB NAMES ${MPIR_LIB_NAME} PATHS ${MPIR_LIB_DIR}) + IF( NOT MPIR_LIB) + MESSAGE( FATAL_ERROR "Could not find MPIR lib in ${MPIR_LIB_DIR}") ENDIF() +#ELSE(WIN32) +# SET(MPIR_LIB_DIR ${MPIR_PACKAGE_ROOT}/lib) +# IF(BSTERMINAL_SHARED_LIBS) +# SET(MPIR_LIB_NAME libgmp.so) +# ELSE() +# SET(MPIR_LIB_NAME libgmp.a) +# ENDIF() ENDIF(WIN32) -FIND_LIBRARY( MPIR_LIB NAMES ${MPIR_LIB_NAME} PATHS ${MPIR_LIB_DIR} NO_DEFAULT_PATH ) -IF( NOT MPIR_LIB) - MESSAGE( FATAL_ERROR "Could not find MPIR lib in ${MPIR_LIB_DIR}") + + +SET(CURL_ROOT ${THIRD_PARTY_COMMON_DIR}/curl) +SET(CURL_INCLUDE_DIR ${CURL_ROOT}/include) + +set(CURL_LIBRARY "-lcurl") +find_package(CURL REQUIRED) + +IF (WIN32) + IF (CMAKE_BUILD_TYPE STREQUAL "Debug") + SET(CURL_LIB_NAME libcurl-d.lib) + ELSE () + SET(CURL_LIB_NAME libcurl.lib) + ENDIF () +ELSE(WIN32) + SET(CURL_LIB_NAME libcurl-d.a libcurl.a curl-d curl) +ENDIF(WIN32) + +FIND_LIBRARY( CURL_LIB NAMES ${CURL_LIB_NAME} PATHS ${CURL_ROOT}/lib NO_DEFAULT_PATH ) +IF( NOT CURL_LIB) + MESSAGE( FATAL_ERROR "Could not find curl lib ${CURL_LIB_NAME} in ${CURL_ROOT}/lib") ENDIF() + IF(BUILD_TESTS) SET(GTEST_PACKAGE_ROOT ${THIRD_PARTY_COMMON_DIR}/Gtest) INCLUDE_DIRECTORIES( ${GTEST_PACKAGE_ROOT}/include ) @@ -375,14 +409,7 @@ ENDIF(BUILD_TESTS) SET( CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${THIRD_PARTY_COMMON_DIR}/Protobuf ) FIND_PACKAGE( Protobuf REQUIRED ) INCLUDE_DIRECTORIES( ${PROTOBUF_INCLUDE_DIRS} ) - -IF ( UNIX ) - GET_FILENAME_COMPONENT(ProtoLibDir ${Protobuf_LIBRARY} DIRECTORY) - FIND_LIBRARY( PROTO_LIB NAMES "libprotobuf.a" PATHS ${ProtoLibDir} NO_DEFAULT_PATH ) -ELSE ( UNIX ) - SET( PROTO_LIB ${PROTOBUF_LIBRARIES} ) -ENDIF ( UNIX ) - +SET( PROTO_LIB ${PROTOBUF_LIBRARIES} ) SET( PROTO_ROOT_DIR ${TERMINAL_GUI_ROOT} ) SET( PATH_TO_GENERATED ${TERMINAL_GUI_ROOT}/generated_proto ) FILE( MAKE_DIRECTORY ${PATH_TO_GENERATED} ) @@ -477,9 +504,12 @@ IF(WIN32) LIST(APPEND QT_QUICK_LIBS ${QT5_QML_ROOT}/QtQml/Models.2/modelsplugind.lib + ${QT5_QML_ROOT}/QtQml/qmlplugind.lib ${QT5_QML_ROOT}/QtQuick.2/qtquick2plugind.lib ${QT5_QML_ROOT}/QtQuick/Controls/qtquickcontrolsplugind.lib ${QT5_QML_ROOT}/QtQuick/Controls.2/qtquickcontrols2plugind.lib + ${QT5_QML_ROOT}/QtQuick/Dialogs/dialogplugind.lib + ${QT5_QML_ROOT}/QtQuick/Dialogs/Private/dialogsprivateplugind.lib ${QT5_QML_ROOT}/QtQuick/Layouts/qquicklayoutsplugind.lib ${QT5_QML_ROOT}/QtQuick/Templates.2/qtquicktemplates2plugind.lib ${QT5_QML_ROOT}/QtQuick/Window.2/windowplugind.lib @@ -527,9 +557,12 @@ IF(WIN32) LIST(APPEND QT_QUICK_LIBS ${QT5_QML_ROOT}/QtQml/Models.2/modelsplugin.lib + ${QT5_QML_ROOT}/QtQml/qmlplugin.lib ${QT5_QML_ROOT}/QtQuick.2/qtquick2plugin.lib ${QT5_QML_ROOT}/QtQuick/Controls/qtquickcontrolsplugin.lib ${QT5_QML_ROOT}/QtQuick/Controls.2/qtquickcontrols2plugin.lib + ${QT5_QML_ROOT}/QtQuick/Dialogs/dialogplugin.lib + ${QT5_QML_ROOT}/QtQuick/Dialogs/Private/dialogsprivateplugin.lib ${QT5_QML_ROOT}/QtQuick/Layouts/qquicklayoutsplugin.lib ${QT5_QML_ROOT}/QtQuick/Templates.2/qtquicktemplates2plugin.lib ${QT5_QML_ROOT}/QtQuick/Window.2/windowplugin.lib @@ -608,9 +641,12 @@ ELSE(WIN32) ) SET(QT_QUICK_LIBS ${QT5_QML_ROOT}/QtQml/Models.2/libmodelsplugin_debug.a + ${QT5_QML_ROOT}/QtQml/libqmlplugin_debug.a ${QT5_QML_ROOT}/QtQuick.2/libqtquick2plugin_debug.a ${QT5_QML_ROOT}/QtQuick/Controls/libqtquickcontrolsplugin_debug.a ${QT5_QML_ROOT}/QtQuick/Controls.2/libqtquickcontrols2plugin_debug.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/libdialogplugin_debug.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/Private/libdialogsprivateplugin_debug.a ${QT5_QML_ROOT}/QtQuick/Layouts/libqquicklayoutsplugin_debug.a ${QT5_QML_ROOT}/QtQuick/Templates.2/libqtquicktemplates2plugin_debug.a ${QT5_QML_ROOT}/QtQuick/Window.2/libwindowplugin_debug.a @@ -645,9 +681,12 @@ ELSE(WIN32) ) SET(QT_QUICK_LIBS ${QT5_QML_ROOT}/QtQml/Models.2/libmodelsplugin.a + ${QT5_QML_ROOT}/QtQml/libqmlplugin.a ${QT5_QML_ROOT}/QtQuick.2/libqtquick2plugin.a ${QT5_QML_ROOT}/QtQuick/Controls/libqtquickcontrolsplugin.a ${QT5_QML_ROOT}/QtQuick/Controls.2/libqtquickcontrols2plugin.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/libdialogplugin.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/Private/libdialogsprivateplugin.a ${QT5_QML_ROOT}/QtQuick/Layouts/libqquicklayoutsplugin.a ${QT5_QML_ROOT}/QtQuick/Templates.2/libqtquicktemplates2plugin.a ${QT5_QML_ROOT}/QtQuick/Window.2/libwindowplugin.a @@ -683,8 +722,8 @@ ELSE(WIN32) libQt5ThemeSupport.a libQt5EventDispatcherSupport.a libQt5FontDatabaseSupport.a - libQt5LinuxAccessibilitySupport.a - libQt5AccessibilitySupport.a +# libQt5LinuxAccessibilitySupport.a +# libQt5AccessibilitySupport.a libQt5EdidSupport.a ) LIST(APPEND QT_LINUX_LIBS @@ -726,7 +765,6 @@ ELSE(WIN32) ${ZLIB_LIBRARIES} png m - double-conversion.a dl cups udev @@ -736,9 +774,12 @@ ELSE(WIN32) SET(QT_QUICK_LIBS ${QT5_QML_ROOT}/QtQml/Models.2/libmodelsplugin.a + ${QT5_QML_ROOT}/QtQml/libqmlplugin.a ${QT5_QML_ROOT}/QtQuick.2/libqtquick2plugin.a ${QT5_QML_ROOT}/QtQuick/Controls/libqtquickcontrolsplugin.a ${QT5_QML_ROOT}/QtQuick/Controls.2/libqtquickcontrols2plugin.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/libdialogplugin.a + ${QT5_QML_ROOT}/QtQuick/Dialogs/Private/libdialogsprivateplugin.a ${QT5_QML_ROOT}/QtQuick/PrivateWidgets/libwidgetsplugin.a ${QT5_QML_ROOT}/QtQuick/Layouts/libqquicklayoutsplugin.a ${QT5_QML_ROOT}/QtQuick/Templates.2/libqtquicktemplates2plugin.a @@ -790,11 +831,6 @@ IF(BSTERMINAL_SHARED_LIBS) add_custom_command(TARGET ${COPY_SHARED_LIBS_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different $<$:${QRENCODE_LIB_DLL}> $<$>:${QRENCODE_LIB_DLL}> ${LIBRARY_OUTPUT_PATH}/${CMAKE_BUILD_TYPE}) - # websockets - STRING(REPLACE ".lib" ".dll" WS_LIB_DLL ${WS_LIB}) - add_custom_command(TARGET ${COPY_SHARED_LIBS_NAME} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different $<$:${WS_LIB_DLL}> $<$>:${WS_LIB_DLL}> ${LIBRARY_OUTPUT_PATH}/${CMAKE_BUILD_TYPE}) - string(REGEX REPLACE "([0-9])\.([0-9])\.([0-9]\.*)" "\\1;\\2" VERSION_MATCHES ${OPENSSL_VERSION}) list(GET VERSION_MATCHES 0 version_major) list(GET VERSION_MATCHES 1 version_minor) @@ -835,10 +871,9 @@ ADD_SUBDIRECTORY(common/WalletsLib) ADD_SUBDIRECTORY(common/cppForSwig) ADD_SUBDIRECTORY(common/CommonLib) ADD_SUBDIRECTORY(CommonUI) -#ADD_SUBDIRECTORY(BlockSettleHW) +ADD_SUBDIRECTORY(BlockSettleHW) ADD_SUBDIRECTORY(BlockSettleApp) -#ADD_SUBDIRECTORY(BlockSettleSigner) IF(BUILD_TESTS) ADD_SUBDIRECTORY(UnitTests) diff --git a/Core/ApiAdapter.cpp b/Core/ApiAdapter.cpp index 5a8f9e04d..8cfa28e28 100644 --- a/Core/ApiAdapter.cpp +++ b/Core/ApiAdapter.cpp @@ -10,7 +10,10 @@ */ #include "ApiAdapter.h" #include +#include "common.pb.h" +using namespace bs::message; +using namespace BlockSettle::Common; class ApiRouter : public bs::message::Router { @@ -75,7 +78,7 @@ class ApiBusGateway : public ApiBusAdapter return false; // ignore unsolicited broadcasts } - bool process(const bs::message::Envelope &env) override + ProcessingResult process(const bs::message::Envelope &env) override { auto envCopy = env; envCopy.setId(0); @@ -87,23 +90,25 @@ class ApiBusGateway : public ApiBusAdapter std::unique_lock lock(mtxIdMap_); idMap_[envCopy.foreignId()] = { env.foreignId(), env.sender }; } - return true; + return ProcessingResult::Success; } else { - return false; + return ProcessingResult::Retry; } } else if (env.receiver->isFallback()) { if (env.isRequest()) { envCopy.receiver = userTermBroadcast_; - return parent_->pushFill(envCopy); + return parent_->pushFill(envCopy) ? ProcessingResult::Success + : ProcessingResult::Retry; } else { std::unique_lock lock(mtxIdMap_); const auto &itId = idMap_.find(env.responseId()); if (itId == idMap_.end()) { envCopy.receiver = userTermBroadcast_; - return parent_->pushFill(envCopy); + return parent_->pushFill(envCopy) ? ProcessingResult::Success + : ProcessingResult::Retry; } else { envCopy = bs::message::Envelope::makeResponse(parent_->user_ @@ -113,14 +118,14 @@ class ApiBusGateway : public ApiBusAdapter if (parent_->pushFill(envCopy)) { idMap_.erase(env.responseId()); } - return true; + return ProcessingResult::Success; } } } - return true; + return ProcessingResult::Success; } - bool pushToApiBus(const bs::message::Envelope &env) + ProcessingResult pushToApiBus(const bs::message::Envelope &env) { auto envCopy = env; envCopy.setId(0); @@ -136,17 +141,17 @@ class ApiBusGateway : public ApiBusAdapter if (pushFill(envCopy)) { idMap_.erase(itIdMap); - return true; + return ProcessingResult::Success; } - return false; + return ProcessingResult::Retry; } } - bool rc = pushFill(envCopy); + const bool rc = pushFill(envCopy); if (rc && env.isRequest()) { std::unique_lock lock(mtxIdMap_); idMap_[envCopy.foreignId()] = { env.foreignId(), env.sender }; } - return rc; + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } private: @@ -199,24 +204,49 @@ void ApiAdapter::add(const std::shared_ptr &adapter) } } const auto userId = ++nextApiUser_; - logger_->debug("[{}] {} has id {}", __func__, adapter->name(), userId); + logger_->debug("[{}] {} has user {}", __func__, adapter->name(), userId); adapter->setUserId(userId); apiBus_->addAdapter(adapter); } -bool ApiAdapter::process(const bs::message::Envelope &env) +ProcessingResult ApiAdapter::process(const bs::message::Envelope &env) { - RelayAdapter::process(env); if (env.receiver->value() == user_->value()) { + if (!env.isRequest() && !queue_->isSame()) { + auto envResp = bs::message::Envelope::makeResponse(env.sender, env.receiver + , {}, env.responseId()); + queue_->pushFill(envResp); + } + if (env.message.empty()) { + return ProcessingResult::Success; + } return gwAdapter_->pushToApiBus(env); } - return true; + return RelayAdapter::process(env); } bool ApiAdapter::processBroadcast(const bs::message::Envelope& env) { if (RelayAdapter::processBroadcast(env)) { - return gwAdapter_->pushToApiBus(env); + gwAdapter_->pushToApiBus(env); + return true; } return false; } + +bool ApiAdapter::processTimeout(const bs::message::Envelope& env) +{ + if (!env.receiver) { + return false; + } + return true; +} + + +bool ApiBusAdapter::pushFill(bs::message::Envelope& env) +{ + if (!queue_) { + return false; + } + return queue_->pushFill(env); +} diff --git a/Core/ApiAdapter.h b/Core/ApiAdapter.h index f95782014..a2a6ae888 100644 --- a/Core/ApiAdapter.h +++ b/Core/ApiAdapter.h @@ -72,6 +72,9 @@ class ApiBusAdapter : public bs::message::Adapter user_->setName(name()); } +protected: + bool pushFill(bs::message::Envelope&) override; + protected: std::shared_ptr user_; }; @@ -84,8 +87,9 @@ class ApiAdapter : public bs::message::RelayAdapter, public bs::MainLoopRuner ApiAdapter(const std::shared_ptr &); ~ApiAdapter() override = default; - bool process(const bs::message::Envelope &) override; - bool processBroadcast(const bs::message::Envelope& env) override; + bs::message::ProcessingResult process(const bs::message::Envelope&) override; + bool processBroadcast(const bs::message::Envelope&) override; + bool processTimeout(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_, fallbackUser_ }; } std::string name() const override { return "API"; } diff --git a/Core/ApiJson.cpp b/Core/ApiJson.cpp index 97bfd9096..d6004e730 100644 --- a/Core/ApiJson.cpp +++ b/Core/ApiJson.cpp @@ -34,7 +34,7 @@ ApiJsonAdapter::ApiJsonAdapter(const std::shared_ptr &logger) , userSettings_(UserTerminal::create(TerminalUsers::Settings)) {} -bool ApiJsonAdapter::process(const Envelope &env) +ProcessingResult ApiJsonAdapter::process(const Envelope &env) { if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { @@ -58,7 +58,7 @@ bool ApiJsonAdapter::process(const Envelope &env) return processOnChainTrack(env); case TerminalUsers::Assets: return processAssets(env); - default: break; + default: return ProcessingResult::Ignored; } } else if (env.receiver && (env.sender->value() == user_->value()) @@ -70,8 +70,9 @@ bool ApiJsonAdapter::process(const Envelope &env) else { logger_->warn("[{}] non-terminal #{} user {}", __func__, env.foreignId() , env.sender->name()); + return ProcessingResult::Error; } - return true; + return ProcessingResult::Success; } bool ApiJsonAdapter::processBroadcast(const bs::message::Envelope& env) @@ -79,25 +80,25 @@ bool ApiJsonAdapter::processBroadcast(const bs::message::Envelope& env) if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { case TerminalUsers::System: - return processAdminMessage(env); + return (processAdminMessage(env) != ProcessingResult::Ignored); case TerminalUsers::Blockchain: - return processBlockchain(env); + return (processBlockchain(env) != ProcessingResult::Ignored); case TerminalUsers::Signer: - return processSigner(env); + return (processSigner(env) != ProcessingResult::Ignored); case TerminalUsers::Wallets: - return processWallets(env); + return (processWallets(env) != ProcessingResult::Ignored); case TerminalUsers::BsServer: - return processBsServer(env); + return (processBsServer(env) != ProcessingResult::Ignored); case TerminalUsers::Settlement: - return processSettlement(env); + return (processSettlement(env) != ProcessingResult::Ignored); case TerminalUsers::Matching: - return processMatching(env); + return (processMatching(env) != ProcessingResult::Ignored); case TerminalUsers::MktData: - return processMktData(env); + return (processMktData(env) != ProcessingResult::Ignored); case TerminalUsers::OnChainTracker: - return processOnChainTrack(env); + return (processOnChainTrack(env) != ProcessingResult::Ignored); case TerminalUsers::Assets: - return processAssets(env); + return (processAssets(env) != ProcessingResult::Ignored); default: break; } } @@ -234,12 +235,12 @@ void ApiJsonAdapter::OnClientDisconnected(const std::string& clientId) , connectedClients_.size()); } -bool ApiJsonAdapter::processSettings(const Envelope &env) +ProcessingResult ApiJsonAdapter::processSettings(const Envelope &env) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetResponse: @@ -270,12 +271,12 @@ bool ApiJsonAdapter::processSettings(const Envelope &env) clientPubKeys_.insert(clientKey); } break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) +ProcessingResult ApiJsonAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) { std::map settings; for (const auto &setting : response.responses()) { @@ -294,26 +295,26 @@ bool ApiJsonAdapter::processSettingsGetResponse(const SettingsMessage_SettingsRe break; } } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processAdminMessage(const Envelope &env) +ProcessingResult ApiJsonAdapter::processAdminMessage(const Envelope &env) { AdministrativeMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse admin msg #{}", __func__, env.foreignId()); - return false; + return ProcessingResult::Error; } switch (msg.data_case()) { case AdministrativeMessage::kStart: processStart(); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processBlockchain(const Envelope &env) +ProcessingResult ApiJsonAdapter::processBlockchain(const Envelope &env) { ArmoryMessage msg; if (!msg.ParseFromString(env.message)) { @@ -322,7 +323,7 @@ bool ApiJsonAdapter::processBlockchain(const Envelope &env) if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case ArmoryMessage::kStateChanged: @@ -350,12 +351,12 @@ bool ApiJsonAdapter::processBlockchain(const Envelope &env) case ArmoryMessage::kZcInvalidated: sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processSigner(const Envelope &env) +ProcessingResult ApiJsonAdapter::processSigner(const Envelope &env) { SignerMessage msg; if (!msg.ParseFromString(env.message)) { @@ -364,7 +365,7 @@ bool ApiJsonAdapter::processSigner(const Envelope &env) if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SignerMessage::kState: @@ -376,17 +377,17 @@ bool ApiJsonAdapter::processSigner(const Envelope &env) case SignerMessage::kSignTxResponse: sendReplyToClient(env.responseId(), msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processWallets(const Envelope &env) +ProcessingResult ApiJsonAdapter::processWallets(const Envelope &env) { WalletsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case WalletsMessage::kWalletLoaded: @@ -405,34 +406,34 @@ bool ApiJsonAdapter::processWallets(const Envelope &env) sendReplyToClient(env.responseId(), msg, env.sender); } break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processOnChainTrack(const Envelope &env) +ProcessingResult ApiJsonAdapter::processOnChainTrack(const Envelope &env) { OnChainTrackMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case OnChainTrackMessage::kAuthState: case OnChainTrackMessage::kVerifiedAuthAddresses: sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processAssets(const bs::message::Envelope& env) +ProcessingResult ApiJsonAdapter::processAssets(const bs::message::Envelope& env) { AssetsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case AssetsMessage::kSubmittedAuthAddrs: @@ -443,9 +444,9 @@ bool ApiJsonAdapter::processAssets(const bs::message::Envelope& env) case AssetsMessage::kBalance: sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } void ApiJsonAdapter::processStart() @@ -466,12 +467,12 @@ void ApiJsonAdapter::processStart() pushRequest(user_, userSettings_, msg.SerializeAsString()); } -bool ApiJsonAdapter::processBsServer(const bs::message::Envelope& env) +ProcessingResult ApiJsonAdapter::processBsServer(const bs::message::Envelope& env) { BsServerMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case BsServerMessage::kStartLoginResult: @@ -486,17 +487,17 @@ bool ApiJsonAdapter::processBsServer(const bs::message::Envelope& env) loggedInUser_.clear(); sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processSettlement(const bs::message::Envelope& env) +ProcessingResult ApiJsonAdapter::processSettlement(const bs::message::Envelope& env) { SettlementMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettlementMessage::kQuote: @@ -508,17 +509,17 @@ bool ApiJsonAdapter::processSettlement(const bs::message::Envelope& env) case SettlementMessage::kQuoteReqNotif: sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processMatching(const bs::message::Envelope& env) +ProcessingResult ApiJsonAdapter::processMatching(const bs::message::Envelope& env) { MatchingMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MatchingMessage::kLoggedIn: @@ -530,17 +531,17 @@ bool ApiJsonAdapter::processMatching(const bs::message::Envelope& env) loggedInUser_.clear(); sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool ApiJsonAdapter::processMktData(const bs::message::Envelope& env) +ProcessingResult ApiJsonAdapter::processMktData(const bs::message::Envelope& env) { MktDataMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MktDataMessage::kDisconnected: @@ -549,9 +550,9 @@ bool ApiJsonAdapter::processMktData(const bs::message::Envelope& env) case MktDataMessage::kPriceUpdate: sendReplyToClient(0, msg, env.sender); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } bool ApiJsonAdapter::hasRequest(uint64_t msgId) const diff --git a/Core/ApiJson.h b/Core/ApiJson.h index f10527c5e..7aba427db 100644 --- a/Core/ApiJson.h +++ b/Core/ApiJson.h @@ -32,7 +32,7 @@ class ApiJsonAdapter : public ApiBusAdapter, public ServerConnectionListener ApiJsonAdapter(const std::shared_ptr &); ~ApiJsonAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -44,19 +44,19 @@ class ApiJsonAdapter : public ApiBusAdapter, public ServerConnectionListener void OnClientDisconnected(const std::string& clientId) override; private: - bool processSettings(const bs::message::Envelope &); - bool processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - - bool processAdminMessage(const bs::message::Envelope &); - bool processAssets(const bs::message::Envelope&); - bool processBlockchain(const bs::message::Envelope&); - bool processBsServer(const bs::message::Envelope&); - bool processMatching(const bs::message::Envelope&); - bool processMktData(const bs::message::Envelope&); - bool processOnChainTrack(const bs::message::Envelope&); - bool processSettlement(const bs::message::Envelope&); - bool processSigner(const bs::message::Envelope&); - bool processWallets(const bs::message::Envelope&); + bs::message::ProcessingResult processSettings(const bs::message::Envelope &); + bs::message::ProcessingResult processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + + bs::message::ProcessingResult processAdminMessage(const bs::message::Envelope &); + bs::message::ProcessingResult processAssets(const bs::message::Envelope&); + bs::message::ProcessingResult processBlockchain(const bs::message::Envelope&); + bs::message::ProcessingResult processBsServer(const bs::message::Envelope&); + bs::message::ProcessingResult processMatching(const bs::message::Envelope&); + bs::message::ProcessingResult processMktData(const bs::message::Envelope&); + bs::message::ProcessingResult processOnChainTrack(const bs::message::Envelope&); + bs::message::ProcessingResult processSettlement(const bs::message::Envelope&); + bs::message::ProcessingResult processSigner(const bs::message::Envelope&); + bs::message::ProcessingResult processWallets(const bs::message::Envelope&); void processStart(); diff --git a/Core/ArmoryServersProvider.cpp b/Core/ArmoryServersProvider.cpp new file mode 100644 index 000000000..4a63833fe --- /dev/null +++ b/Core/ArmoryServersProvider.cpp @@ -0,0 +1,332 @@ +/* + +*********************************************************************************** +* Copyright (C) 2019 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "ArmoryServersProvider.h" +#include "BootstrapDataManager.h" +#include +#include +#include + +namespace { + + // Change to true if local ArmoryDB auto-start should be reverted (not tested, see BST-2131) + const bool kEnableLocalAutostart = false; + + enum class ServerIndices { + MainNet = 0, + TestNet, + //LocalhostMainNet, + //LocalHostTestNet + }; + +} // namespace + +const std::vector ArmoryServersProvider::defaultServers_ = { + ArmoryServer::fromTextSettings(ARMORY_BLOCKSETTLE_NAME":0:mainnet.blocksettle.com:9003:"), + ArmoryServer::fromTextSettings(ARMORY_BLOCKSETTLE_NAME":1:testnet.blocksettle.com:19003:"), +/* ArmoryServer::fromTextSettings(kEnableLocalAutostart ? + QStringLiteral("%1:0:127.0.0.1::").arg(QObject::tr("Local Auto-launch Node")) : + QStringLiteral("%1:0:127.0.0.1::").arg(QObject::tr("Local BlockSettleDB Node"))), + ArmoryServer::fromTextSettings(kEnableLocalAutostart ? + QStringLiteral("%1:1:127.0.0.1::").arg(QObject::tr("Local Auto-launch Node")) : + QStringLiteral("%1:1:127.0.0.1:81:").arg(QObject::tr("Local BlockSettleDB Node")))*/ +}; + +const int ArmoryServersProvider::kDefaultServersCount = ArmoryServersProvider::defaultServers_.size(); + +ArmoryServersProvider::ArmoryServersProvider(const std::shared_ptr &appSettings + , const std::shared_ptr& logger) + : appSettings_(appSettings), logger_(logger) +{} + +std::vector ArmoryServersProvider::servers() const +{ + QStringList userServers = appSettings_->get(ApplicationSettings::armoryServers); + std::vector servers; + servers.reserve(userServers.size() + kDefaultServersCount); + + // #1 add MainNet blocksettle server + ArmoryServer bsMainNet = defaultServers_.at(static_cast(ServerIndices::MainNet)); + //bsMainNet.armoryDBKey = QString::fromStdString(bootstrapDataManager_->getArmoryMainnetKey()); //hard-code key + servers.push_back(bsMainNet); + + // #2 add TestNet blocksettle server + ArmoryServer bsTestNet = defaultServers_.at(static_cast(ServerIndices::TestNet)); + //bsTestNet.armoryDBKey = QString::fromStdString(bootstrapDataManager_->getArmoryTestnetKey()); + servers.push_back(bsTestNet); + +/* // #3 add localhost node MainNet + ArmoryServer localMainNet = defaultServers_.at(static_cast(ServerIndices::LocalhostMainNet)); + localMainNet.armoryDBPort = appSettings_->GetDefaultArmoryLocalPort(NetworkType::MainNet); + localMainNet.runLocally = kEnableLocalAutostart; + servers.push_back(localMainNet); + + // #4 add localhost node TestNet + ArmoryServer localTestNet = defaultServers_.at(static_cast(ServerIndices::LocalHostTestNet)); + localTestNet.armoryDBPort = appSettings_->GetDefaultArmoryLocalPort(NetworkType::TestNet); + localTestNet.runLocally = kEnableLocalAutostart; + servers.push_back(localTestNet);*/ + + for (const QString &srv : userServers) { + const auto& server = ArmoryServer::fromTextSettings(srv.toStdString()); + servers.push_back(server); + } + return servers; +} + +ArmorySettings ArmoryServersProvider::getArmorySettings() const +{ + ArmorySettings settings; + + settings.netType = appSettings_->get(ApplicationSettings::netType); + settings.armoryDBIp = appSettings_->get(ApplicationSettings::armoryDbIp); + settings.armoryDBPort = std::to_string(appSettings_->GetArmoryRemotePort()); + settings.runLocally = appSettings_->get(ApplicationSettings::runArmoryLocally); + + const int serverIndex = indexOf(static_cast(settings)); + if (serverIndex >= 0) { + settings.armoryDBKey = servers().at(serverIndex).armoryDBKey; + } + + settings.socketType = appSettings_->GetArmorySocketType(); + + settings.armoryExecutablePath = QDir::cleanPath(appSettings_->get(ApplicationSettings::armoryPathName)).toStdString(); + settings.dbDir = appSettings_->GetDBDir().toStdString(); + settings.bitcoinBlocksDir = appSettings_->GetBitcoinBlocksDir().toStdString(); + settings.dataDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation).toStdString(); + + return settings; +} + +int ArmoryServersProvider::indexOfCurrent() const +{ + return indexOf(static_cast(getArmorySettings())); +} + +int ArmoryServersProvider::indexOfConnected() const +{ + return lastConnectedIndex_; +} + +int ArmoryServersProvider::indexOf(const QString &name) const +{ + // naive implementation + const auto& s = servers(); + for (int i = 0; i < s.size(); ++i) { + if (s.at(i).name == name.toStdString()) { + return i; + } + } + return -1; +} + +int ArmoryServersProvider::indexOf(const ArmoryServer &server) const +{ + const auto& srvrs = servers(); + for (int i = 0; i < srvrs.size(); ++i) { + const auto& s = srvrs.at(i); +#ifdef MSG_DEBUGGING + logger_->debug("[{}] {}: {} vs {}, {} vs {}, {} vs {}, {} vs {}", __func__ + , i, s.name, server.name, (int)s.netType, (int)server.netType + , s.armoryDBIp, server.armoryDBIp, s.armoryDBPort, server.armoryDBPort); +#endif + if ((server.name.empty() || (s.name == server.name)) && (s.netType == server.netType) + && (s.armoryDBIp == server.armoryDBIp) && (s.armoryDBPort == server.armoryDBPort)) { + return i; + } + } + return -1; +} + +int ArmoryServersProvider::indexOfIpPort(const std::string &srvIPPort) const +{ + QString ipPort = QString::fromStdString(srvIPPort); + QStringList ipPortList = ipPort.split(QStringLiteral(":")); + if (ipPortList.size() != 2) { + return -1; + } + + for (int i = 0; i < servers().size(); ++i) { + if ((servers().at(i).armoryDBIp == ipPortList.at(0).toStdString()) + && (servers().at(i).armoryDBPort == ipPortList.at(1).toStdString())) { + return i; + } + } + return -1; +} + +int ArmoryServersProvider::getIndexOfMainNetServer() +{ + return static_cast(ServerIndices::MainNet); +} + +int ArmoryServersProvider::getIndexOfTestNetServer() +{ + return static_cast(ServerIndices::TestNet); +} + +bool ArmoryServersProvider::add(const ArmoryServer &server) +{ + if (server.armoryDBPort.empty()) { + return false; + } + const int armoryPort = std::stoi(server.armoryDBPort); + if (armoryPort < 1 || armoryPort > USHRT_MAX) { + return false; + } + if (server.name.empty()) { + return false; + } + + const auto& serversData = servers(); + // check if server with already exist + for (const ArmoryServer &s : serversData) { + if (s.name == server.name) { + return false; + } + if (s.armoryDBIp == server.armoryDBIp + && s.armoryDBPort == server.armoryDBPort + && s.netType == server.netType) { + return false; + } + } + + QStringList serversTxt = appSettings_->get(ApplicationSettings::armoryServers); + + serversTxt.append(QString::fromStdString(server.toTextSettings())); + appSettings_->set(ApplicationSettings::armoryServers, serversTxt); + return true; +} + +bool ArmoryServersProvider::replace(int index, const ArmoryServer &server) +{ + if (server.armoryDBPort.empty() || server.name.empty()) { + return false; + } + if (index < kDefaultServersCount) { + return false; + } + + const auto& serversData = servers(); + if (index >= serversData.size()) { + return false; + } + + // check if server with already exist + for (int i = 0; i < serversData.size(); ++i) { + if (i == index) continue; + + const ArmoryServer &s = serversData.at(i); + if (s.name == server.name) { + return false; + } + if (s.armoryDBIp == server.armoryDBIp + && s.armoryDBPort == server.armoryDBPort + && s.netType == server.netType) { + return false; + } + } + + QStringList serversTxt = appSettings_->get(ApplicationSettings::armoryServers); + if (index - kDefaultServersCount >= serversTxt.size()) { + return false; + } + + logger_->debug("[{}] {}", __func__, server.toTextSettings()); + serversTxt.replace(index - kDefaultServersCount, QString::fromStdString(server.toTextSettings())); + appSettings_->set(ApplicationSettings::armoryServers, serversTxt); + return true; +} + +bool ArmoryServersProvider::remove(int index) +{ + if (index < kDefaultServersCount) { + return false; + } + + QStringList servers = appSettings_->get(ApplicationSettings::armoryServers); + int indexToRemove = index - kDefaultServersCount; + if (indexToRemove >= 0 && indexToRemove < servers.size()){ + servers.removeAt(indexToRemove); + appSettings_->set(ApplicationSettings::armoryServers, servers); + return true; + } + else { + return false; + } +} + +NetworkType ArmoryServersProvider::setupServer(int index) +{ + NetworkType netType{ NetworkType::Invalid }; + const auto& srvList = servers(); + if (index >= 0 && index < srvList.size()) { + const auto& server = srvList.at(index); + appSettings_->set(ApplicationSettings::armoryDbName, QString::fromStdString(server.name)); + appSettings_->set(ApplicationSettings::armoryDbIp, QString::fromStdString(server.armoryDBIp)); + appSettings_->set(ApplicationSettings::armoryDbPort, QString::fromStdString(server.armoryDBPort)); + appSettings_->set(ApplicationSettings::netType, static_cast(server.netType)); + appSettings_->set(ApplicationSettings::runArmoryLocally, server.runLocally); + netType = server.netType; + } + lastConnectedIndex_ = index; + return netType; +} + +int ArmoryServersProvider::addKey(const QString &address, int port, const QString &key) +{ + // find server + int index = -1; + for (int i = 0; i < servers().size(); ++i) { + if ((servers().at(i).armoryDBIp == address.toStdString()) + && (servers().at(i).armoryDBPort == std::to_string(port))) { + index = i; + break; + } + } + + if (index == -1){ + return -1; + } + + if (index < ArmoryServersProvider::kDefaultServersCount) { + return -1; + } + + QStringList servers = appSettings_->get(ApplicationSettings::armoryServers); + QString serverTxt = servers.at(index - ArmoryServersProvider::kDefaultServersCount); + ArmoryServer server = ArmoryServer::fromTextSettings(serverTxt.toStdString()); + server.armoryDBKey = key.toStdString(); + servers[index - ArmoryServersProvider::kDefaultServersCount] = QString::fromStdString(server.toTextSettings()); + + appSettings_->set(ApplicationSettings::armoryServers, servers); + return index; +} + +int ArmoryServersProvider::addKey(const std::string &srvIPPort, const BinaryData &srvPubKey) +{ + QString ipPort = QString::fromStdString(srvIPPort); + QStringList ipPortList = ipPort.split(QStringLiteral(":")); + if (ipPortList.size() == 2) { + return addKey(ipPortList.at(0), ipPortList.at(1).toInt() + , QString::fromStdString(srvPubKey.toHexStr())); + } + return -1; +} + +void ArmoryServersProvider::setConnectedArmorySettings(const ArmorySettings ¤tArmorySettings) +{ + lastConnectedIndex_ = indexOf(currentArmorySettings); +} + +bool ArmoryServersProvider::isDefault(int index) const +{ + return index == 0 || index == 1; +} diff --git a/Core/ArmoryServersProvider.h b/Core/ArmoryServersProvider.h new file mode 100644 index 000000000..e040c2014 --- /dev/null +++ b/Core/ArmoryServersProvider.h @@ -0,0 +1,67 @@ +/* + +*********************************************************************************** +* Copyright (C) 2019 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef ARMORY_SERVERS_PROVIDER_H +#define ARMORY_SERVERS_PROVIDER_H + +#include "ApplicationSettings.h" +#include "BinaryData.h" + +#define ARMORY_BLOCKSETTLE_NAME "BlockSettle" + +#define MAINNET_ARMORY_BLOCKSETTLE_ADDRESS "armory.blocksettle.com" +#define MAINNET_ARMORY_BLOCKSETTLE_PORT 80 + +#define TESTNET_ARMORY_BLOCKSETTLE_ADDRESS "armory-testnet.blocksettle.com" +#define TESTNET_ARMORY_BLOCKSETTLE_PORT 80 + +namespace spdlog { + class logger; +} + +class ArmoryServersProvider +{ +public: + ArmoryServersProvider(const std::shared_ptr& + , const std::shared_ptr &); + + std::vector servers() const; + ArmorySettings getArmorySettings() const; + + int indexOfCurrent() const; // index of server which set in ini file + int indexOfConnected() const; // index of server currently connected + int indexOf(const QString &name) const; + int indexOf(const ArmoryServer &server) const; + int indexOfIpPort(const std::string &srvIPPort) const; + static int getIndexOfMainNetServer(); + static int getIndexOfTestNetServer(); + + bool add(const ArmoryServer &server); + bool replace(int index, const ArmoryServer &server); + bool remove(int index); + NetworkType setupServer(int index); + + int addKey(const QString &address, int port, const QString &key); + int addKey(const std::string &srvIPPort, const BinaryData &srvPubKey); + + void setConnectedArmorySettings(const ArmorySettings &); + + // if default armory used + bool isDefault(int index) const; + static const int kDefaultServersCount; + +private: + std::shared_ptr appSettings_; + std::shared_ptr logger_; + int lastConnectedIndex_{ -1 }; + static const std::vector defaultServers_; +}; + +#endif // ARMORY_SERVERS_PROVIDER_H diff --git a/Core/AssetsAdapter.cpp b/Core/AssetsAdapter.cpp index f633376cf..8d7b8fa15 100644 --- a/Core/AssetsAdapter.cpp +++ b/Core/AssetsAdapter.cpp @@ -26,13 +26,13 @@ AssetsAdapter::AssetsAdapter(const std::shared_ptr &logger) assetMgr_ = std::make_unique(logger, this); } -bool AssetsAdapter::process(const bs::message::Envelope &env) +ProcessingResult AssetsAdapter::process(const bs::message::Envelope &env) { if (env.sender->value() == bs::message::TerminalUsers::Settings) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings message #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetResponse: @@ -44,7 +44,7 @@ bool AssetsAdapter::process(const bs::message::Envelope &env) MatchingMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse matching message #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MatchingMessage::kSubmittedAuthAddresses: @@ -56,7 +56,7 @@ bool AssetsAdapter::process(const bs::message::Envelope &env) AssetsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own message #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case AssetsMessage::kSubmitAuthAddress: @@ -64,7 +64,7 @@ bool AssetsAdapter::process(const bs::message::Envelope &env) default: break; } } - return true; + return ProcessingResult::Ignored; } bool AssetsAdapter::processBroadcast(const bs::message::Envelope& env) @@ -106,7 +106,8 @@ bool AssetsAdapter::processBroadcast(const bs::message::Envelope& env) } switch (msg.data_case()) { case BsServerMessage::kBalanceUpdated: - return processBalance(msg.balance_updated().currency(), msg.balance_updated().value()); + return (processBalance(msg.balance_updated().currency(), msg.balance_updated().value()) + != ProcessingResult::Ignored); default: break; } } @@ -141,14 +142,14 @@ void AssetsAdapter::onSecuritiesChanged() { } -bool AssetsAdapter::processGetSettings(const SettingsMessage_SettingsResponse& response) +ProcessingResult AssetsAdapter::processGetSettings(const SettingsMessage_SettingsResponse& response) { for (const auto& setting : response.responses()) { switch (setting.request().index()) { default: break; } } - return true; + return ProcessingResult::Ignored; } bool AssetsAdapter::onMatchingLogin(const MatchingMessage_LoggedIn&) @@ -159,29 +160,32 @@ bool AssetsAdapter::onMatchingLogin(const MatchingMessage_LoggedIn&) , msg.SerializeAsString()); } -bool AssetsAdapter::processSubmittedAuth(const MatchingMessage_SubmittedAuthAddresses& response) +ProcessingResult AssetsAdapter::processSubmittedAuth(const MatchingMessage_SubmittedAuthAddresses& response) { AssetsMessage msg; auto msgBC = msg.mutable_submitted_auth_addrs(); for (const auto& addr : response.addresses()) { msgBC->add_addresses(addr); } - return pushBroadcast(user_, msg.SerializeAsString()); + pushBroadcast(user_, msg.SerializeAsString()); + return ProcessingResult::Success; } -bool AssetsAdapter::processSubmitAuth(const std::string& address) +ProcessingResult AssetsAdapter::processSubmitAuth(const std::string& address) { // currently using Celer storage for this, but this might changed at some point MatchingMessage msg; msg.set_submit_auth_address(address); - return pushRequest(user_, UserTerminal::create(TerminalUsers::Matching) + pushRequest(user_, UserTerminal::create(TerminalUsers::Matching) , msg.SerializeAsString()); + return ProcessingResult::Success; } -bool AssetsAdapter::processBalance(const std::string& currency, double value) +ProcessingResult AssetsAdapter::processBalance(const std::string& currency, double value) { AssetsMessage msg; auto msgBal = msg.mutable_balance(); msgBal->set_currency(currency); msgBal->set_value(value); - return pushBroadcast(user_, msg.SerializeAsString()); + pushBroadcast(user_, msg.SerializeAsString()); + return ProcessingResult::Success; } diff --git a/Core/AssetsAdapter.h b/Core/AssetsAdapter.h index ae88c1905..7ecd91910 100644 --- a/Core/AssetsAdapter.h +++ b/Core/AssetsAdapter.h @@ -33,7 +33,7 @@ class AssetsAdapter : public bs::message::Adapter AssetsAdapter(const std::shared_ptr &); ~AssetsAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -50,11 +50,11 @@ class AssetsAdapter : public bs::message::Adapter void onSecuritiesChanged() override; //internal processing - bool processGetSettings(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + bs::message::ProcessingResult processGetSettings(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); bool onMatchingLogin(const BlockSettle::Terminal::MatchingMessage_LoggedIn&); - bool processSubmittedAuth(const BlockSettle::Terminal::MatchingMessage_SubmittedAuthAddresses&); - bool processSubmitAuth(const std::string&); - bool processBalance(const std::string& currency, double); + bs::message::ProcessingResult processSubmittedAuth(const BlockSettle::Terminal::MatchingMessage_SubmittedAuthAddresses&); + bs::message::ProcessingResult processSubmitAuth(const std::string&); + bs::message::ProcessingResult processBalance(const std::string& currency, double); private: std::shared_ptr logger_; @@ -62,5 +62,4 @@ class AssetsAdapter : public bs::message::Adapter std::unique_ptr assetMgr_; }; - #endif // ASSETS_ADAPTER_H diff --git a/Core/BsServerAdapter.cpp b/Core/BsServerAdapter.cpp index db008164e..89dd42434 100644 --- a/Core/BsServerAdapter.cpp +++ b/Core/BsServerAdapter.cpp @@ -39,14 +39,14 @@ BsServerAdapter::BsServerAdapter(const std::shared_ptr &logger) connMgr_->setCaBundle(bs::caBundlePtr(), bs::caBundleSize()); } -bool BsServerAdapter::process(const bs::message::Envelope &env) +ProcessingResult BsServerAdapter::process(const bs::message::Envelope &env) { if (env.sender->value() == TerminalUsers::Settings) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings message #{}", __func__ , env.foreignId()); - return true; + return ProcessingResult::Error; } if (msg.data_case() == SettingsMessage::kGetResponse) { return processLocalSettings(msg.get_response()); @@ -55,7 +55,7 @@ bool BsServerAdapter::process(const bs::message::Envelope &env) else if (env.receiver->value() == user_->value()) { return processOwnRequest(env); } - return true; + return ProcessingResult::Ignored; } bool BsServerAdapter::processBroadcast(const bs::message::Envelope& env) @@ -90,12 +90,12 @@ void BsServerAdapter::start() , msg.SerializeAsString()); } -bool BsServerAdapter::processOwnRequest(const Envelope &env) +ProcessingResult BsServerAdapter::processOwnRequest(const Envelope &env) { BsServerMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own request #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case BsServerMessage::kOpenConnection: @@ -131,10 +131,10 @@ bool BsServerAdapter::processOwnRequest(const Envelope &env) #endif default: break; } - return true; + return ProcessingResult::Ignored; } -bool BsServerAdapter::processLocalSettings(const SettingsMessage_SettingsResponse &response) +ProcessingResult BsServerAdapter::processLocalSettings(const SettingsMessage_SettingsResponse &response) { for (const auto &setting : response.responses()) { switch (static_cast(setting.request().index())) { @@ -152,37 +152,37 @@ bool BsServerAdapter::processLocalSettings(const SettingsMessage_SettingsRespons default: break; } } - return true; + return ProcessingResult::Success; } -bool BsServerAdapter::processPuBKeyResponse(bool allowed) +ProcessingResult BsServerAdapter::processPuBKeyResponse(bool allowed) { if (!futPuBkey_) { logger_->error("[{}] not waiting for PuB key permission", __func__); - return true; + return ProcessingResult::Error; } futPuBkey_->setValue(allowed); futPuBkey_.reset(); - return true; + return ProcessingResult::Success; } -bool BsServerAdapter::processTimeout(const std::string& id) +ProcessingResult BsServerAdapter::processTimeout(const std::string& id) { const auto& itTO = timeouts_.find(id); if (itTO == timeouts_.end()) { logger_->error("[{}] unknown timeout {}", __func__, id); - return true; + return ProcessingResult::Error; } itTO->second(); timeouts_.erase(itTO); - return true; + return ProcessingResult::Success; } -bool BsServerAdapter::processOpenConnection() +ProcessingResult BsServerAdapter::processOpenConnection() { if (connected_) { logger_->error("[{}] already connected", __func__); - return true; + return ProcessingResult::Error; } #if 0 bsClient_ = std::make_unique(logger_, this); @@ -215,13 +215,13 @@ bool BsServerAdapter::processOpenConnection() } bsClient_->setConnection(std::move(connection)); #endif - return true; + return ProcessingResult::Success; } -bool BsServerAdapter::processStartLogin(const std::string& login) +ProcessingResult BsServerAdapter::processStartLogin(const std::string& login) { if (!connected_) { - return false; // wait for connection to complete + return ProcessingResult::Retry; // wait for connection to complete } if (currentLogin_.empty()) { currentLogin_ = login; @@ -232,19 +232,19 @@ bool BsServerAdapter::processStartLogin(const std::string& login) else { //onStartLoginDone(true, {}); } - return true; + return ProcessingResult::Success; } -bool BsServerAdapter::processCancelLogin() +ProcessingResult BsServerAdapter::processCancelLogin() { if (currentLogin_.empty()) { logger_->warn("[BsServerAdapter::processCancelLogin] no login started - ignoring request"); - return true; + return ProcessingResult::Ignored; } #if 0 bsClient_->cancelLogin(); #endif - return true; + return ProcessingResult::Success; } #if 0 diff --git a/Core/BsServerAdapter.h b/Core/BsServerAdapter.h index 27480f787..c05f2a6b8 100644 --- a/Core/BsServerAdapter.h +++ b/Core/BsServerAdapter.h @@ -43,7 +43,7 @@ class BsServerAdapter : public bs::message::Adapter BsServerAdapter(const std::shared_ptr &); ~BsServerAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -51,13 +51,13 @@ class BsServerAdapter : public bs::message::Adapter private: void start(); - bool processOwnRequest(const bs::message::Envelope &); - bool processLocalSettings(const BlockSettle::Terminal::SettingsMessage_SettingsResponse &); - bool processPuBKeyResponse(bool); - bool processTimeout(const std::string& id); - bool processOpenConnection(); - bool processStartLogin(const std::string&); - bool processCancelLogin(); + bs::message::ProcessingResult processOwnRequest(const bs::message::Envelope &); + bs::message::ProcessingResult processLocalSettings(const BlockSettle::Terminal::SettingsMessage_SettingsResponse &); + bs::message::ProcessingResult processPuBKeyResponse(bool); + bs::message::ProcessingResult processTimeout(const std::string& id); + bs::message::ProcessingResult processOpenConnection(); + bs::message::ProcessingResult processStartLogin(const std::string&); + bs::message::ProcessingResult processCancelLogin(); //bool processSubmitAuthAddr(const bs::message::Envelope&, const std::string &addr); //void processUpdateOrders(const Blocksettle::Communication::ProxyTerminalPb::Response_UpdateOrders&); //void processUnsignedPayin(const Blocksettle::Communication::ProxyTerminalPb::Response_UnsignedPayinRequest&); @@ -81,5 +81,4 @@ class BsServerAdapter : public bs::message::Adapter std::unordered_map> timeouts_; }; - #endif // BS_SERVER_ADAPTER_H diff --git a/Core/MDHistAdapter.cpp b/Core/MDHistAdapter.cpp index 92ae0aa88..dd1c3d770 100644 --- a/Core/MDHistAdapter.cpp +++ b/Core/MDHistAdapter.cpp @@ -23,9 +23,9 @@ MDHistAdapter::MDHistAdapter(const std::shared_ptr &logger) , user_(std::make_shared(bs::message::TerminalUsers::MDHistory)) {} -bool MDHistAdapter::process(const bs::message::Envelope &env) +ProcessingResult MDHistAdapter::process(const bs::message::Envelope &) { - return true; + return ProcessingResult::Ignored; } bool MDHistAdapter::processBroadcast(const bs::message::Envelope& env) diff --git a/Core/MDHistAdapter.h b/Core/MDHistAdapter.h index 3737b248e..82e8df697 100644 --- a/Core/MDHistAdapter.h +++ b/Core/MDHistAdapter.h @@ -23,7 +23,7 @@ class MDHistAdapter : public bs::message::Adapter MDHistAdapter(const std::shared_ptr &); ~MDHistAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } diff --git a/Core/MktDataAdapter.cpp b/Core/MktDataAdapter.cpp index b5e153027..1b3aead08 100644 --- a/Core/MktDataAdapter.cpp +++ b/Core/MktDataAdapter.cpp @@ -34,14 +34,14 @@ MktDataAdapter::MktDataAdapter(const std::shared_ptr &logger) , true, false); } -bool MktDataAdapter::process(const bs::message::Envelope &env) +ProcessingResult MktDataAdapter::process(const bs::message::Envelope &env) { if (env.receiver->value() == user_->value()) { MktDataMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own request #{}" , __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MktDataMessage::kStartConnection: @@ -51,7 +51,7 @@ bool MktDataAdapter::process(const bs::message::Envelope &env) break; } } - return true; + return ProcessingResult::Ignored; } bool MktDataAdapter::processBroadcast(const bs::message::Envelope& env) @@ -179,15 +179,15 @@ void MktDataAdapter::sendTrade(const bs::network::NewTrade& trade) pushBroadcast(user_, msg.SerializeAsString(), true); } -bool MktDataAdapter::processStartConnection(int e) +ProcessingResult MktDataAdapter::processStartConnection(int e) { if (connected_) { logger_->debug("[{}] already connected", __func__); - return true; + return ProcessingResult::Success; } const auto env = static_cast(e); mdProvider_->SetConnectionSettings(PubKeyLoader::serverHostName(PubKeyLoader::KeyType::MdServer, env) , PubKeyLoader::serverHttpsPort()); mdProvider_->MDLicenseAccepted(); - return true; + return ProcessingResult::Success; } diff --git a/Core/MktDataAdapter.h b/Core/MktDataAdapter.h index e2bee9398..91ef7d696 100644 --- a/Core/MktDataAdapter.h +++ b/Core/MktDataAdapter.h @@ -25,7 +25,7 @@ class MktDataAdapter : public bs::message::Adapter, public MDCallbackTarget MktDataAdapter(const std::shared_ptr &); ~MktDataAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -50,7 +50,7 @@ class MktDataAdapter : public bs::message::Adapter, public MDCallbackTarget private: void sendTrade(const bs::network::NewTrade&); - bool processStartConnection(int env); + bs::message::ProcessingResult processStartConnection(int env); private: std::shared_ptr logger_; diff --git a/Core/SettingsAdapter.cpp b/Core/SettingsAdapter.cpp index 504efd497..baba3fb9a 100644 --- a/Core/SettingsAdapter.cpp +++ b/Core/SettingsAdapter.cpp @@ -41,9 +41,6 @@ SettingsAdapter::SettingsAdapter(const std::shared_ptr &set logMgr_->add(appSettings_->GetLogsConfig()); logger_ = logMgr_->logger(); - if (!appSettings_->get(ApplicationSettings::initialized)) { - appSettings_->SetDefaultSettings(true); - } appSettings_->selectNetwork(); logger_->debug("Settings loaded from {}", appSettings_->GetSettingsPath().toStdString()); @@ -67,18 +64,16 @@ SettingsAdapter::SettingsAdapter(const std::shared_ptr &set , filePathInResources.toStdString()); } } - - armoryServersProvider_ = std::make_shared(settings, bootstrapDataManager_); - signersProvider_ = std::make_shared(appSettings_); + armoryServersProvider_ = std::make_shared(settings, logger_); } -bool SettingsAdapter::process(const bs::message::Envelope &env) +ProcessingResult SettingsAdapter::process(const bs::message::Envelope &env) { if (env.receiver->value() == TerminalUsers::Settings) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetRequest: @@ -99,18 +94,6 @@ bool SettingsAdapter::process(const bs::message::Envelope &env) return processUpdArmoryServer(env, msg.upd_armory_server()); case SettingsMessage::kSignerRequest: return processSignerSettings(env); - case SettingsMessage::kSignerSetKey: - return processSignerSetKey(msg.signer_set_key()); - case SettingsMessage::kSignerReset: - return processSignerReset(); - case SettingsMessage::kSignerServersGet: - return processGetSigners(env); - case SettingsMessage::kSetSignerServer: - return processSetSigner(env, msg.set_signer_server()); - case SettingsMessage::kAddSignerServer: - return processAddSigner(env, msg.add_signer_server()); - case SettingsMessage::kDelSignerServer: - return processDelSigner(env, msg.del_signer_server()); case SettingsMessage::kStateGet: return processGetState(env); case SettingsMessage::kReset: @@ -131,7 +114,10 @@ bool SettingsAdapter::process(const bs::message::Envelope &env) break; } } - return true; + else { + return ProcessingResult::Success; + } + return ProcessingResult::Ignored; } bool SettingsAdapter::processBroadcast(const bs::message::Envelope& env) @@ -143,40 +129,56 @@ bool SettingsAdapter::processBroadcast(const bs::message::Envelope& env) return false; } if (msg.data_case() == ArmoryMessage::kSettingsRequest) { - ArmoryMessage msgReply; - auto msgResp = msgReply.mutable_settings_response(); - const auto& armorySettings = armoryServersProvider_->getArmorySettings(); - armoryServersProvider_->setConnectedArmorySettings(armorySettings); - msgResp->set_socket_type(armorySettings.socketType); - msgResp->set_network_type((int)armorySettings.netType); - msgResp->set_host(armorySettings.armoryDBIp.toStdString()); - msgResp->set_port(std::to_string(armorySettings.armoryDBPort)); - msgResp->set_bip15x_key(armorySettings.armoryDBKey.toStdString()); - msgResp->set_run_locally(armorySettings.runLocally); - msgResp->set_data_dir(armorySettings.dataDir.toStdString()); - msgResp->set_executable_path(armorySettings.armoryExecutablePath.toStdString()); - msgResp->set_bitcoin_dir(armorySettings.bitcoinBlocksDir.toStdString()); - msgResp->set_db_dir(armorySettings.dbDir.toStdString()); - msgResp->set_cache_file_name(appSettings_->get(ApplicationSettings::txCacheFileName)); - pushResponse(user_, env, msgReply.SerializeAsString()); + sendSettings(armoryServersProvider_->getArmorySettings()); + logger_->debug("current={}, connected={}", armoryServersProvider_->indexOfCurrent() + , armoryServersProvider_->indexOfConnected()); return true; } } return false; } -bool SettingsAdapter::processRemoteSettings(uint64_t msgId) +void SettingsAdapter::sendSettings(const ArmorySettings& armorySettings, bool netTypeChanged) +{ + if (!armorySettings.empty()) { + ArmoryMessage msg; + auto msgResp = msg.mutable_settings_response(); + armoryServersProvider_->setConnectedArmorySettings(armorySettings); + msgResp->set_socket_type(armorySettings.socketType); + msgResp->set_network_type((int)armorySettings.netType); + msgResp->set_host(armorySettings.armoryDBIp); + msgResp->set_port(armorySettings.armoryDBPort); + msgResp->set_bip15x_key(armorySettings.armoryDBKey); + msgResp->set_run_locally(armorySettings.runLocally); + msgResp->set_data_dir(armorySettings.dataDir); + msgResp->set_executable_path(armorySettings.armoryExecutablePath); + msgResp->set_bitcoin_dir(armorySettings.bitcoinBlocksDir); + msgResp->set_db_dir(armorySettings.dbDir); + msgResp->set_cache_file_name(appSettings_->get(ApplicationSettings::txCacheFileName)); + pushRequest(user_, bs::message::UserTerminal::create(bs::message::TerminalUsers::Blockchain) + , msg.SerializeAsString(), {}, 3, std::chrono::seconds{10}); +#ifdef MSG_DEBUGGING + logger_->debug("[{}] {}", __func__, msg.DebugString()); +#endif + } + if (netTypeChanged) { + logger_->debug("[{}] network type changed - reloading wallets", __func__); + } + processSignerSettings({}); +} + +ProcessingResult SettingsAdapter::processRemoteSettings(uint64_t msgId) { const auto &itReq = remoteSetReqs_.find(msgId); if (itReq == remoteSetReqs_.end()) { logger_->error("[{}] failed to find remote settings request #{}", __func__ , msgId); - return true; + return ProcessingResult::Error; } - return true; + return ProcessingResult::Ignored; } -bool SettingsAdapter::processGetState(const bs::message::Envelope& env) +ProcessingResult SettingsAdapter::processGetState(const bs::message::Envelope& env) { SettingsMessage msg; auto msgResp = msg.mutable_state(); @@ -187,10 +189,11 @@ bool SettingsAdapter::processGetState(const bs::message::Envelope& env) setReq->set_index(static_cast(st.first)); setFromQVariant(st.second, setReq, setResp); } - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } -bool SettingsAdapter::processReset(const bs::message::Envelope& env +ProcessingResult SettingsAdapter::processReset(const bs::message::Envelope& env , const SettingsMessage_SettingsRequest& request) { SettingsMessage msg; @@ -205,10 +208,11 @@ bool SettingsAdapter::processReset(const bs::message::Envelope& env const auto& value = appSettings_->get(setting); setFromQVariant(value, setReq, setResp); } - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } -bool SettingsAdapter::processResetToState(const bs::message::Envelope& env +ProcessingResult SettingsAdapter::processResetToState(const bs::message::Envelope& env , const SettingsMessage_SettingsResponse& request) { for (const auto& req : request.responses()) { @@ -218,10 +222,11 @@ bool SettingsAdapter::processResetToState(const bs::message::Envelope& env } SettingsMessage msg; *msg.mutable_state() = request; - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } -bool SettingsAdapter::processBootstrap(const bs::message::Envelope &env +ProcessingResult SettingsAdapter::processBootstrap(const bs::message::Envelope &env , const std::string& bsData) { SettingsMessage msg; @@ -246,8 +251,9 @@ bool SettingsAdapter::processBootstrap(const bs::message::Envelope &env else { logger_->error("[{}] failed to set bootstrap data", __func__); } - return pushResponse(user_, bsData.empty() ? env.sender : nullptr + pushResponse(user_, bsData.empty() ? env.sender : nullptr , msg.SerializeAsString(), bsData.empty() ? env.foreignId() : 0); + return ProcessingResult::Success; } static bs::network::ws::PrivateKey readOrCreatePrivateKey(const std::string& filename) @@ -274,7 +280,7 @@ static bs::network::ws::PrivateKey readOrCreatePrivateKey(const std::string& fil return result; } -bool SettingsAdapter::processApiPrivKey(const bs::message::Envelope& env) +ProcessingResult SettingsAdapter::processApiPrivKey(const bs::message::Envelope& env) { //FIXME: should be re-implemented to avoid storing plain private key in a file on disk const auto &apiKeyFN = appSettings_->AppendToWritableDir( QString::fromStdString("apiPrivKey")).toStdString(); @@ -282,7 +288,8 @@ bool SettingsAdapter::processApiPrivKey(const bs::message::Envelope& env) SettingsMessage msg; SecureBinaryData privKey(apiPrivKey.data(), apiPrivKey.size()); msg.set_api_privkey(privKey.toBinStr()); - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } static std::set readClientKeys(const std::string& filename) @@ -305,7 +312,7 @@ static std::set readClientKeys(const std::string& filename) return result; } -bool SettingsAdapter::processApiClientsList(const bs::message::Envelope& env) +ProcessingResult SettingsAdapter::processApiClientsList(const bs::message::Envelope& env) { const auto& apiKeysFN = appSettings_->AppendToWritableDir( QString::fromStdString("apiClientPubKeys")).toStdString(); @@ -318,7 +325,8 @@ bool SettingsAdapter::processApiClientsList(const bs::message::Envelope& env) for (const auto& clientKey : clientKeys) { msgResp->add_pub_keys(clientKey); } - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } std::string SettingsAdapter::guiMode() const @@ -329,7 +337,7 @@ std::string SettingsAdapter::guiMode() const return appSettings_->guiMode().toStdString(); } -bool SettingsAdapter::processGetRequest(const bs::message::Envelope &env +ProcessingResult SettingsAdapter::processGetRequest(const bs::message::Envelope &env , const SettingsMessage_SettingsRequest &request) { unsigned int nbFetched = 0; @@ -338,7 +346,7 @@ bool SettingsAdapter::processGetRequest(const bs::message::Envelope &env for (const auto &req : request.requests()) { auto resp = msgResp->add_responses(); resp->set_allocated_request(new SettingRequest(req)); - if (req.source() == SettingSource_Local) { + if ((req.source() == SettingSource_Local) || (req.source() == SettingSource_Unknown)) { switch (req.index()) { case SetIdx_BlockSettleSignAddress: resp->set_s(appSettings_->GetBlocksettleSignAddress()); @@ -421,12 +429,13 @@ bool SettingsAdapter::processGetRequest(const bs::message::Envelope &env } } if (nbFetched > 0) { - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } - return true; + return ProcessingResult::Ignored; } -bool SettingsAdapter::processPutRequest(const SettingsMessage_SettingsResponse &request) +ProcessingResult SettingsAdapter::processPutRequest(const SettingsMessage_SettingsResponse &request) { unsigned int nbUpdates = 0; for (const auto &req : request.responses()) { @@ -489,37 +498,54 @@ bool SettingsAdapter::processPutRequest(const SettingsMessage_SettingsResponse & if (nbUpdates) { SettingsMessage msg; *(msg.mutable_settings_updated()) = request; - return pushBroadcast(user_, msg.SerializeAsString()); + pushBroadcast(user_, msg.SerializeAsString()); + return ProcessingResult::Success; } - return true; + return ProcessingResult::Ignored; } -bool SettingsAdapter::processArmoryServer(const BlockSettle::Terminal::SettingsMessage_ArmoryServer &request) +ProcessingResult SettingsAdapter::processArmoryServer(const BlockSettle::Terminal::SettingsMessage_ArmoryServer &request) { int selIndex = 0; for (const auto &server : armoryServersProvider_->servers()) { - if ((server.name == QString::fromStdString(request.server_name())) + if ((server.name == request.server_name()) && (server.netType == static_cast(request.network_type()))) { break; } selIndex++; } if (selIndex >= armoryServersProvider_->servers().size()) { - logger_->error("[{}] failed to find Armory server {}", __func__, request.server_name()); - return true; + logger_->error("[{}] failed to find Armory server {} #{}", __func__, request.server_name(), selIndex); + return ProcessingResult::Error; } armoryServersProvider_->setupServer(selIndex); appSettings_->selectNetwork(); + return ProcessingResult::Success; } -bool SettingsAdapter::processSetArmoryServer(const bs::message::Envelope& env, int index) +ProcessingResult SettingsAdapter::processSetArmoryServer(const bs::message::Envelope& env, int index) { - armoryServersProvider_->setupServer(index); - appSettings_->selectNetwork(); - return processGetArmoryServers(env); + if (armoryServersProvider_->indexOfConnected() == index) { + return ProcessingResult::Ignored; + } + logger_->debug("[{}] #{}", __func__, index); + const auto prevNetType = armoryServersProvider_->getArmorySettings().netType; + const auto newNetType = armoryServersProvider_->setupServer(index); + if (newNetType == NetworkType::Invalid) { + logger_->error("[{}] Failed to setup server #{}", __func__, index); + return ProcessingResult::Error; + } + const bool netTypeChanged = newNetType != prevNetType; + if (netTypeChanged) { + appSettings_->selectNetwork(); + logger_->debug("[{}] net {} selected", __func__ + , (int)appSettings_->get(ApplicationSettings::Setting::netType)); + } + sendSettings(armoryServersProvider_->getArmorySettings(), netTypeChanged); + return ProcessingResult::Success; } -bool SettingsAdapter::processGetArmoryServers(const bs::message::Envelope& env) +ProcessingResult SettingsAdapter::processGetArmoryServers(const bs::message::Envelope& env) { SettingsMessage msg; auto msgResp = msg.mutable_armory_servers(); @@ -528,32 +554,33 @@ bool SettingsAdapter::processGetArmoryServers(const bs::message::Envelope& env) for (const auto& server : armoryServersProvider_->servers()) { auto msgSrv = msgResp->add_servers(); msgSrv->set_network_type((int)server.netType); - msgSrv->set_server_name(server.name.toStdString()); - msgSrv->set_server_address(server.armoryDBIp.toStdString()); - msgSrv->set_server_port(std::to_string(server.armoryDBPort)); - msgSrv->set_server_key(server.armoryDBKey.toStdString()); + msgSrv->set_server_name(server.name); + msgSrv->set_server_address(server.armoryDBIp); + msgSrv->set_server_port(server.armoryDBPort); + msgSrv->set_server_key(server.armoryDBKey); msgSrv->set_run_locally(server.runLocally); msgSrv->set_one_way_auth(server.oneWayAuth_); msgSrv->set_password(server.password.toBinStr()); } - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; } static ArmoryServer fromMessage(const SettingsMessage_ArmoryServer& msg) { ArmoryServer result; - result.name = QString::fromStdString(msg.server_name()); + result.name = msg.server_name(); result.netType = static_cast(msg.network_type()); - result.armoryDBIp = QString::fromStdString(msg.server_address()); - result.armoryDBPort = std::stoi(msg.server_port()); - result.armoryDBKey = QString::fromStdString(msg.server_key()); + result.armoryDBIp = msg.server_address(); + result.armoryDBPort = msg.server_port(); + result.armoryDBKey = msg.server_key(); result.password = SecureBinaryData::fromString(msg.password()); result.runLocally = msg.run_locally(); result.oneWayAuth_ = msg.one_way_auth(); return result; } -bool SettingsAdapter::processAddArmoryServer(const bs::message::Envelope& env +ProcessingResult SettingsAdapter::processAddArmoryServer(const bs::message::Envelope& env , const SettingsMessage_ArmoryServer& request) { const auto& server = fromMessage(request); @@ -566,7 +593,7 @@ bool SettingsAdapter::processAddArmoryServer(const bs::message::Envelope& env return processGetArmoryServers(env); } -bool SettingsAdapter::processDelArmoryServer(const bs::message::Envelope& env +ProcessingResult SettingsAdapter::processDelArmoryServer(const bs::message::Envelope& env , int index) { if (!armoryServersProvider_->remove(index)) { @@ -575,7 +602,7 @@ bool SettingsAdapter::processDelArmoryServer(const bs::message::Envelope& env return processGetArmoryServers(env); } -bool SettingsAdapter::processUpdArmoryServer(const bs::message::Envelope& env +ProcessingResult SettingsAdapter::processUpdArmoryServer(const bs::message::Envelope& env , const SettingsMessage_ArmoryServerUpdate& request) { const auto& server = fromMessage(request.server()); @@ -585,95 +612,37 @@ bool SettingsAdapter::processUpdArmoryServer(const bs::message::Envelope& env return processGetArmoryServers(env); } -bool SettingsAdapter::processSignerSettings(const bs::message::Envelope &env) +ProcessingResult SettingsAdapter::processSignerSettings(const bs::message::Envelope& env) { SettingsMessage msg; auto msgResp = msg.mutable_signer_response(); - msgResp->set_is_local(signersProvider_->currentSignerIsLocal()); - msgResp->set_network_type(appSettings_->get(ApplicationSettings::netType)); - - const auto &signerHost = signersProvider_->getCurrentSigner(); - msgResp->set_name(signerHost.name.toStdString()); - msgResp->set_host(signerHost.address.toStdString()); - msgResp->set_port(std::to_string(signerHost.port)); - msgResp->set_key(signerHost.key.toStdString()); - msgResp->set_id(signerHost.serverId()); - msgResp->set_remote_keys_dir(signersProvider_->remoteSignerKeysDir()); - msgResp->set_remote_keys_file(signersProvider_->remoteSignerKeysFile()); - - for (const auto &signer : signersProvider_->signers()) { - auto keyVal = msgResp->add_client_keys(); - keyVal->set_key(signer.serverId()); - keyVal->set_value(signer.key.toStdString()); - } + const auto netType = appSettings_->get(ApplicationSettings::netType); + logger_->debug("[{}] network type: {}", __func__, netType); + msgResp->set_network_type(netType); msgResp->set_home_dir(appSettings_->GetHomeDir().toStdString()); - msgResp->set_auto_sign_spend_limit(appSettings_->get(ApplicationSettings::autoSignSpendLimit)); - - return pushResponse(user_, env, msg.SerializeAsString()); -} - -bool SettingsAdapter::processSignerSetKey(const SettingsMessage_SignerSetKey &request) -{ - signersProvider_->addKey(request.server_id(), request.new_key()); - return true; -} - -bool SettingsAdapter::processSignerReset() -{ - signersProvider_->setupSigner(0, true); - return true; -} - -bool SettingsAdapter::processGetSigners(const bs::message::Envelope& env) -{ - SettingsMessage msg; - auto msgResp = msg.mutable_signer_servers(); - msgResp->set_own_key(signersProvider_->remoteSignerOwnKey().toHexStr()); - msgResp->set_idx_current(signersProvider_->indexOfCurrent()); - for (const auto& signer : signersProvider_->signers()) { - auto msgSrv = msgResp->add_servers(); - msgSrv->set_name(signer.name.toStdString()); - msgSrv->set_host(signer.address.toStdString()); - msgSrv->set_port(std::to_string(signer.port)); - msgSrv->set_key(signer.key.toStdString()); + //msgResp->set_auto_sign_spend_limit(appSettings_->get(ApplicationSettings::autoSignSpendLimit)); + if (env.sender) { + pushResponse(user_, env, msg.SerializeAsString()); } - return pushResponse(user_, env, msg.SerializeAsString()); -} - -bool SettingsAdapter::processSetSigner(const bs::message::Envelope& env - , int index) -{ - signersProvider_->setupSigner(index); - return processGetSigners(env); -} - -static SignerHost fromMessage(const SettingsMessage_SignerServer& msg) -{ - SignerHost result; - result.name = QString::fromStdString(msg.name()); - result.address = QString::fromStdString(msg.host()); - result.port = std::stoi(msg.port()); - result.key = QString::fromStdString(msg.key()); - return result; -} - -bool SettingsAdapter::processAddSigner(const bs::message::Envelope& env - , const SettingsMessage_SignerServer& request) -{ - const auto& signer = fromMessage(request); - signersProvider_->add(signer); - signersProvider_->setupSigner(signersProvider_->indexOf(signer)); - return processGetSigners(env); + else { + pushResponse(user_, bs::message::UserTerminal::create(bs::message::TerminalUsers::Signer) + , msg.SerializeAsString()); + } + return ProcessingResult::Success; } -bool SettingsAdapter::processDelSigner(const bs::message::Envelope& env - , int index) +#include +static std::string convertPathname(const QString& pathname) { - signersProvider_->remove(index); - return processGetSigners(env); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + const QByteArray baFilename = QTextCodec::codecForLocale()->fromUnicode(pathname); +#else + auto fromUtf16 = QStringEncoder(QStringEncoder::System); + const QByteArray baFilename = fromUtf16(mFileName); +#endif + return baFilename.toStdString(); } - void bs::message::setFromQVariant(const QVariant& val, SettingRequest* req, SettingResponse* resp) { switch (val.type()) { diff --git a/Core/SettingsAdapter.h b/Core/SettingsAdapter.h index 5999dc89b..bafc866cd 100644 --- a/Core/SettingsAdapter.h +++ b/Core/SettingsAdapter.h @@ -14,6 +14,7 @@ #include #include #include +#include "ArmorySettings.h" #include "Message/Adapter.h" #include "TerminalMessage.h" @@ -58,7 +59,7 @@ class SettingsAdapter : public bs::message::Adapter , const QStringList &appArgs); ~SettingsAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -68,34 +69,28 @@ class SettingsAdapter : public bs::message::Adapter std::string guiMode() const; private: - bool processGetRequest(const bs::message::Envelope & + bs::message::ProcessingResult processGetRequest(const bs::message::Envelope & , const BlockSettle::Terminal::SettingsMessage_SettingsRequest &); - bool processPutRequest(const BlockSettle::Terminal::SettingsMessage_SettingsResponse &); - bool processArmoryServer(const BlockSettle::Terminal::SettingsMessage_ArmoryServer &); - bool processSetArmoryServer(const bs::message::Envelope&, int index); - bool processGetArmoryServers(const bs::message::Envelope&); - bool processAddArmoryServer(const bs::message::Envelope& + bs::message::ProcessingResult processPutRequest(const BlockSettle::Terminal::SettingsMessage_SettingsResponse &); + bs::message::ProcessingResult processArmoryServer(const BlockSettle::Terminal::SettingsMessage_ArmoryServer &); + bs::message::ProcessingResult processSetArmoryServer(const bs::message::Envelope&, int index); + bs::message::ProcessingResult processGetArmoryServers(const bs::message::Envelope&); + bs::message::ProcessingResult processAddArmoryServer(const bs::message::Envelope& , const BlockSettle::Terminal::SettingsMessage_ArmoryServer&); - bool processDelArmoryServer(const bs::message::Envelope&, int index); - bool processUpdArmoryServer(const bs::message::Envelope& + bs::message::ProcessingResult processDelArmoryServer(const bs::message::Envelope&, int index); + bs::message::ProcessingResult processUpdArmoryServer(const bs::message::Envelope& , const BlockSettle::Terminal::SettingsMessage_ArmoryServerUpdate&); - bool processSignerSettings(const bs::message::Envelope &); - bool processSignerSetKey(const BlockSettle::Terminal::SettingsMessage_SignerSetKey &); - bool processSignerReset(); - bool processGetSigners(const bs::message::Envelope&); - bool processSetSigner(const bs::message::Envelope&, int); - bool processAddSigner(const bs::message::Envelope& - , const BlockSettle::Terminal::SettingsMessage_SignerServer&); - bool processDelSigner(const bs::message::Envelope&, int); - bool processRemoteSettings(uint64_t msgId); - bool processGetState(const bs::message::Envelope&); - bool processReset(const bs::message::Envelope& + bs::message::ProcessingResult processSignerSettings(const bs::message::Envelope&); + void sendSettings(const ArmorySettings&, bool netTypeChanged = false); + bs::message::ProcessingResult processRemoteSettings(uint64_t msgId); + bs::message::ProcessingResult processGetState(const bs::message::Envelope&); + bs::message::ProcessingResult processReset(const bs::message::Envelope& , const BlockSettle::Terminal::SettingsMessage_SettingsRequest&); - bool processResetToState(const bs::message::Envelope& + bs::message::ProcessingResult processResetToState(const bs::message::Envelope& , const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - bool processBootstrap(const bs::message::Envelope&, const std::string&); - bool processApiPrivKey(const bs::message::Envelope&); - bool processApiClientsList(const bs::message::Envelope&); + bs::message::ProcessingResult processBootstrap(const bs::message::Envelope&, const std::string&); + bs::message::ProcessingResult processApiPrivKey(const bs::message::Envelope&); + bs::message::ProcessingResult processApiClientsList(const bs::message::Envelope&); private: std::shared_ptr user_; @@ -104,8 +99,7 @@ class SettingsAdapter : public bs::message::Adapter std::shared_ptr appSettings_; std::shared_ptr bootstrapDataManager_; std::shared_ptr armoryServersProvider_; - std::shared_ptr signersProvider_; - std::shared_ptr ccFileManager_; + //std::shared_ptr ccFileManager_; std::shared_ptr tradeSettings_; std::map remoteSetReqs_; diff --git a/Core/SignerAdapter.cpp b/Core/SignerAdapter.cpp index 35b1de300..7425d8cef 100644 --- a/Core/SignerAdapter.cpp +++ b/Core/SignerAdapter.cpp @@ -1,7 +1,7 @@ /* *********************************************************************************** -* Copyright (C) 2020 - 2021, BlockSettle AB +* Copyright (C) 2020 - 2023, BlockSettle AB * Distributed under the GNU Affero General Public License (AGPL v3) * See LICENSE or http://www.gnu.org/licenses/agpl.html * @@ -9,12 +9,15 @@ */ #include "SignerAdapter.h" +#include #include +#include "bip39/bip39.h" #include "Adapters/SignerClient.h" #include "CoreWalletsManager.h" +#include "SystemFileUtils.h" +#include "TerminalMessage.h" #include "Wallets/InprocSigner.h" #include "Wallets/ProtobufHeadlessUtils.h" -#include "TerminalMessage.h" #include "common.pb.h" #include "terminal.pb.h" @@ -37,13 +40,13 @@ std::unique_ptr SignerAdapter::createClient() const return client; } -bool SignerAdapter::process(const bs::message::Envelope &env) +ProcessingResult SignerAdapter::process(const bs::message::Envelope &env) { if (env.isRequest()) { SignerMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } return processOwnRequest(env, msg); } @@ -51,7 +54,7 @@ bool SignerAdapter::process(const bs::message::Envelope &env) SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kSignerResponse: @@ -61,7 +64,7 @@ bool SignerAdapter::process(const bs::message::Envelope &env) default: break; } } - return true; + return ProcessingResult::Ignored; } bool SignerAdapter::processBroadcast(const bs::message::Envelope& env) @@ -91,13 +94,14 @@ void SignerAdapter::start() walletsReady(); return; } + logger_->debug("[SignerAdapter::start]"); SettingsMessage msg; msg.mutable_signer_request(); pushRequest(user_, UserTerminal::create(TerminalUsers::Settings) , msg.SerializeAsString()); } -bool SignerAdapter::processOwnRequest(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processOwnRequest(const bs::message::Envelope &env , const SignerMessage &request) { switch (request.data_case()) { @@ -133,20 +137,38 @@ bool SignerAdapter::processOwnRequest(const bs::message::Envelope &env case SignerMessage::kDialogRequest: return processDialogRequest(env, request.dialog_request()); case SignerMessage::kCreateWallet: - return processCreateWallet(env, false, request.create_wallet()); + return processCreateWallet(env, true, request.create_wallet()); case SignerMessage::kImportWallet: return processCreateWallet(env, true, request.import_wallet()); case SignerMessage::kDeleteWallet: return processDeleteWallet(env, request.delete_wallet()); + case SignerMessage::kImportHwWallet: + return processImportHwWallet(env, request.import_hw_wallet()); + case SignerMessage::kExportWoWalletRequest: + return processExportWoWallet(env, request.export_wo_wallet_request()); + case SignerMessage::kChangeWalletPass: + return processChangeWalletPass(env, request.change_wallet_pass()); + case SignerMessage::kGetWalletSeed: + return processGetWalletSeed(env, request.get_wallet_seed()); + case SignerMessage::kImportWoWallet: + return processImportWoWallet(env, request.import_wo_wallet()); + case SignerMessage::kSetWalletName: + return processWalletRename(env, request.set_wallet_name().wallet().wallet_id() + , request.set_wallet_name().new_name()); default: logger_->warn("[{}] unknown signer request: {}", __func__, request.data_case()); break; } - return true; + return ProcessingResult::Ignored; } -bool SignerAdapter::processSignerSettings(const SettingsMessage_SignerServer &response) +ProcessingResult SignerAdapter::processSignerSettings(const SettingsMessage_SignerServer &response) { + { + SignerMessage msg; + msg.mutable_wallets_reset(); + pushBroadcast(user_, msg.SerializeAsString(), true); + } curServerId_ = response.id(); netType_ = static_cast(response.network_type()); walletsDir_ = response.home_dir(); @@ -163,7 +185,8 @@ bool SignerAdapter::processSignerSettings(const SettingsMessage_SignerServer &re logger_->info("[{}] loading wallets from {}", __func__, walletsDir_); signer_->Start(); walletsChanged(); - return sendComponentLoading(); + sendComponentLoading(); + return ProcessingResult::Success; } void SignerAdapter::walletsChanged(bool rescan) @@ -228,14 +251,15 @@ bool SignerAdapter::sendComponentLoading() static const auto &adminUser = UserTerminal::create(TerminalUsers::System); AdministrativeMessage msg; msg.set_component_loading(user_->value()); - return pushBroadcast(adminUser, msg.SerializeAsString()); + pushBroadcast(adminUser, msg.SerializeAsString()); + return true; } -bool SignerAdapter::processNewKeyResponse(bool acceptNewKey) +ProcessingResult SignerAdapter::processNewKeyResponse(bool acceptNewKey) { if (!connFuture_) { logger_->error("[{}] new key comparison wasn't requested", __func__); - return true; + return ProcessingResult::Error; } connFuture_->setValue(acceptNewKey); if (acceptNewKey) { @@ -247,10 +271,10 @@ bool SignerAdapter::processNewKeyResponse(bool acceptNewKey) , msg.SerializeAsString()); } connFuture_.reset(); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processStartWalletSync(const bs::message::Envelope &env) +ProcessingResult SignerAdapter::processStartWalletSync(const bs::message::Envelope &env) { requests_.put(env.foreignId(), env.sender); const auto &cbWallets = [this, msgId=env.foreignId()] @@ -285,10 +309,10 @@ bool SignerAdapter::processStartWalletSync(const bs::message::Envelope &env) pushResponse(user_, sender, msg.SerializeAsString(), msgId); }; signer_->syncWalletInfo(cbWallets); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncAddresses(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processSyncAddresses(const bs::message::Envelope &env , const SignerMessage_SyncAddresses &request) { requests_.put(env.foreignId(), env.sender); @@ -311,10 +335,10 @@ bool SignerAdapter::processSyncAddresses(const bs::message::Envelope &env addrSet.insert(BinaryData::fromString(addr)); } signer_->syncAddressBatch(request.wallet_id(), addrSet, cb); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncNewAddresses(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processSyncNewAddresses(const bs::message::Envelope &env , const SignerMessage_SyncNewAddresses &request) { requests_.put(env.foreignId(), env.sender); @@ -335,7 +359,7 @@ bool SignerAdapter::processSyncNewAddresses(const bs::message::Envelope &env }; if (request.indices_size() != 1) { logger_->error("[{}] not a single new address request", __func__); - return true; + return ProcessingResult::Error; } signer_->syncNewAddress(request.wallet_id(), request.indices(0), cb); } @@ -364,10 +388,10 @@ bool SignerAdapter::processSyncNewAddresses(const bs::message::Envelope &env } signer_->syncNewAddresses(request.wallet_id(), indices, cb); } - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processExtendAddrChain(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processExtendAddrChain(const bs::message::Envelope &env , const SignerMessage_ExtendAddrChain &request) { requests_.put(env.foreignId(), env.sender); @@ -389,10 +413,10 @@ bool SignerAdapter::processExtendAddrChain(const bs::message::Envelope &env pushResponse(user_, sender, msg.SerializeAsString(), msgId); }; signer_->extendAddressChain(request.wallet_id(), request.count(), request.ext_int(), cb); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncWallet(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processSyncWallet(const bs::message::Envelope &env , const std::string &walletId) { requests_.put(env.foreignId(), env.sender); @@ -403,11 +427,13 @@ bool SignerAdapter::processSyncWallet(const bs::message::Envelope &env if (!sender) { return; } + logger_->debug("[SignerAdapter::processSyncWallet] wallet {} asset type: {}", walletId, data.assetType); SignerMessage msg; auto msgResp = msg.mutable_wallet_synced(); msgResp->set_wallet_id(walletId); msgResp->set_high_ext_index(data.highestExtIndex); msgResp->set_high_int_index(data.highestIntIndex); + msgResp->set_asset_type((int)data.assetType); for (const auto &addr : data.addresses) { auto msgAddr = msgResp->add_addresses(); @@ -429,10 +455,10 @@ bool SignerAdapter::processSyncWallet(const bs::message::Envelope &env pushResponse(user_, sender, msg.SerializeAsString(), msgId); }; signer_->syncWallet(walletId, cb); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncHdWallet(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processSyncHdWallet(const bs::message::Envelope &env , const std::string &walletId) { requests_.put(env.foreignId(), env.sender); @@ -451,27 +477,27 @@ bool SignerAdapter::processSyncHdWallet(const bs::message::Envelope &env pushResponse(user_, sender, msg.SerializeAsString(), msgId); }; signer_->syncHDWallet(walletId, cb); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncAddrComment(const SignerMessage_SyncAddressComment &request) +ProcessingResult SignerAdapter::processSyncAddrComment(const SignerMessage_SyncAddressComment &request) { try { signer_->syncAddressComment(request.wallet_id() , bs::Address::fromAddressString(request.address()), request.comment()); } catch (const std::exception &) {} - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processSyncTxComment(const SignerMessage_SyncTxComment &request) +ProcessingResult SignerAdapter::processSyncTxComment(const SignerMessage_SyncTxComment &request) { signer_->syncTxComment(request.wallet_id() , BinaryData::fromString(request.tx_hash()), request.comment()); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processGetRootPubKey(const bs::message::Envelope &env +ProcessingResult SignerAdapter::processGetRootPubKey(const bs::message::Envelope &env , const std::string &walletId) { requests_.put(env.foreignId(), env.sender); @@ -491,20 +517,22 @@ bool SignerAdapter::processGetRootPubKey(const bs::message::Envelope &env pushResponse(user_, sender, msg.SerializeAsString(), msgId); }; signer_->getRootPubkey(walletId, cb); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processDelHdRoot(const std::string &walletId) +ProcessingResult SignerAdapter::processDelHdRoot(const std::string &walletId) { - return (signer_->DeleteHDRoot(walletId) > 0); + signer_->DeleteHDRoot(walletId); + return ProcessingResult::Success; } -bool SignerAdapter::processDelHdLeaf(const std::string &walletId) +ProcessingResult SignerAdapter::processDelHdLeaf(const std::string &walletId) { - return (signer_->DeleteHDLeaf(walletId) > 0); + signer_->DeleteHDLeaf(walletId); + return ProcessingResult::Success; } -bool SignerAdapter::processSignTx(const bs::message::Envelope& env +ProcessingResult SignerAdapter::processSignTx(const bs::message::Envelope& env , const SignerMessage_SignTxRequest& request) { const auto& cbSigned = [this, env, id=request.id()] @@ -519,15 +547,55 @@ bool SignerAdapter::processSignTx(const bs::message::Envelope& env msgResp->set_error_text(errorReason); pushResponse(user_, env, msg.SerializeAsString()); }; - const auto& txReq = bs::signer::pbTxRequestToCore(request.tx_request(), logger_); - passphrase_ = SecureBinaryData::fromString(request.passphrase()); - signer_->signTXRequest(txReq, cbSigned - , static_cast(request.sign_mode()) - , request.keep_dup_recips()); - return true; + auto txReq = bs::signer::pbTxRequestToCore(request.tx_request(), logger_); + logger_->debug("[{}] ({} wallet id[s])", __func__, txReq.walletIds.size()); + bs::core::WalletsManager::HDWalletPtr hdWallet; + if ((txReq.walletIds.size() == 1)) { + hdWallet = walletsMgr_->getHDWalletById(txReq.walletIds.at(0)); + if (hdWallet) { + if (!hdWallet->isHardwareWallet()) { + logger_->warn("[{}] {} not a hardware wallet", __func__, txReq.walletIds.at(0)); + hdWallet.reset(); + } + } + else { + logger_->error("[{}] failed to get HD wallet by {}", __func__, txReq.walletIds.at(0)); + } + } + if (hdWallet) { + const auto& signData = SecureBinaryData::fromString(request.passphrase()); + hdWallet->pushPasswordPrompt([signData]() { return signData; }); + SignerMessage msg; + auto msgResp = msg.mutable_sign_tx_response(); + try { + logger_->debug("[{}] lock time: {}", __func__, txReq.armorySigner_.getLockTime()); + const auto& signedTX = hdWallet->signTXRequestWithWallet(txReq); + if (signedTX.empty()) { + throw std::runtime_error("signer returned empty TX"); + } + logger_->debug("[{}] signed TX: {}", __func__, signedTX.toHexStr()); + if (!txReq.armorySigner_.verify()) { + logger_->error("[{}] verification failed", __func__); + } + msgResp->set_signed_tx(signedTX.toBinStr()); + } + catch (const std::exception& e) { + msgResp->set_error_text(e.what()); + } + hdWallet->popPasswordPrompt(); + pushResponse(user_, env, msg.SerializeAsString()); + } + else { + logger_->debug("[{}] multi-leaf signing ({} leaves)", __func__, txReq.walletIds.size()); + passphrase_ = SecureBinaryData::fromString(request.passphrase()); + signer_->signTXRequest(txReq, cbSigned + , static_cast(request.sign_mode()) + , request.keep_dup_recips()); + } + return ProcessingResult::Success; } -bool SignerAdapter::processResolvePubSpenders(const bs::message::Envelope& env +ProcessingResult SignerAdapter::processResolvePubSpenders(const bs::message::Envelope& env , const bs::core::wallet::TXSignRequest& txReq) { const auto& cbResolve = [this, env](bs::error::ErrorCode result @@ -542,21 +610,21 @@ bool SignerAdapter::processResolvePubSpenders(const bs::message::Envelope& env if (signer_->resolvePublicSpenders(txReq, cbResolve) == 0) { logger_->error("[{}] failed to send", __func__); } - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processAutoSignRequest(const bs::message::Envelope& env +ProcessingResult SignerAdapter::processAutoSignRequest(const bs::message::Envelope& env , const SignerMessage_AutoSign& request) { autoSignRequests_[request.wallet_id()] = env; QVariantMap data; data[QLatin1String("rootId")] = QString::fromStdString(request.wallet_id()); data[QLatin1String("enable")] = request.enable(); - return (signer_->customDialogRequest(bs::signer::ui::GeneralDialogType::ActivateAutoSign - , data) != 0); + signer_->customDialogRequest(bs::signer::ui::GeneralDialogType::ActivateAutoSign, data); + return ProcessingResult::Success; } -bool SignerAdapter::processDialogRequest(const bs::message::Envelope& +ProcessingResult SignerAdapter::processDialogRequest(const bs::message::Envelope& , const SignerMessage_DialogRequest& request) { const auto& dlgType = static_cast(request.dialog_type()); @@ -564,10 +632,11 @@ bool SignerAdapter::processDialogRequest(const bs::message::Envelope& for (const auto& d : request.data()) { data[QString::fromStdString(d.key())] = QString::fromStdString(d.value()); } - return (signer_->customDialogRequest(dlgType, data) != 0); + signer_->customDialogRequest(dlgType, data); + return ProcessingResult::Success; } -bool SignerAdapter::processCreateWallet(const bs::message::Envelope& env +ProcessingResult SignerAdapter::processCreateWallet(const bs::message::Envelope& env , bool rescan, const SignerMessage_CreateWalletRequest& w) { bs::wallet::PasswordData pwdData; @@ -578,34 +647,266 @@ bool SignerAdapter::processCreateWallet(const bs::message::Envelope& env SignerMessage msg; auto msgResp = msg.mutable_created_wallet(); try { - const auto& seed = w.xpriv_key().empty() ? bs::core::wallet::Seed(SecureBinaryData::fromString(w.seed()), netType_) + const auto& seed = w.xpriv_key().empty() ? bs::core::wallet::Seed(w.seed(), netType_) : bs::core::wallet::Seed::fromXpriv(SecureBinaryData::fromString(w.xpriv_key()), netType_); const auto& wallet = walletsMgr_->createWallet(w.name(), w.description(), seed , walletsDir_, pwdData, w.primary()); msgResp->set_wallet_id(wallet->walletId()); walletsChanged(rescan); - logger_->debug("[{}] wallet {} created", __func__, wallet->walletId()); + logger_->debug("[{}] wallet {} created (rescan: {})", __func__, wallet->walletId(), rescan); } catch (const std::exception& e) { logger_->error("[{}] failed to create wallet: {}", __func__, e.what()); msgResp->set_error_msg(e.what()); } pushResponse(user_, env, msg.SerializeAsString()); - return true; + return ProcessingResult::Success; } -bool SignerAdapter::processDeleteWallet(const bs::message::Envelope& env - , const std::string& rootId) +bs::message::ProcessingResult SignerAdapter::processImportHwWallet(const bs::message::Envelope& env + , const BlockSettle::Common::SignerMessage_ImportHWWallet& request) { + logger_->debug("[{}] {}", __func__, request.DebugString()); + const bs::core::HwWalletInfo hwwInfo{ static_cast(request.type()) + , request.vendor(), request.label(), request.device_id(), request.xpub_root() + , request.xpub_nested_segwit(), request.xpub_native_segwit(), request.xpub_legacy() }; SignerMessage msg; - const auto& hdWallet = walletsMgr_->getHDWalletById(rootId); + auto msgResp = msg.mutable_created_wallet(); + try { + logger_->debug("[{}] label: {}, vendor: {}", __func__, hwwInfo.label, hwwInfo.vendor); + const auto& hwWallet = std::make_shared(netType_ + , hwwInfo, walletsDir_, logger_); + walletsMgr_->addWallet(hwWallet); + msgResp->set_wallet_id(hwWallet->walletId()); + walletsChanged(true); + logger_->debug("[{}] wallet {} created", __func__, hwWallet->walletId()); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to create HW wallet: {}", __func__, e.what()); + msgResp->set_error_msg(e.what()); + } + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Success; +} + +ProcessingResult SignerAdapter::processDeleteWallet(const bs::message::Envelope& env + , const SignerMessage_WalletRequest& request) +{ + SignerMessage msg; + if (!isPasswordValid(request.wallet_id(), SecureBinaryData::fromString(request.password()))) { + logger_->warn("[{}] {} password check failed", __func__, request.wallet_id()); + msg.set_wallet_deleted(""); + pushBroadcast(user_, msg.SerializeAsString()); + return ProcessingResult::Error; + } + const auto& hdWallet = walletsMgr_->getHDWalletById(request.wallet_id()); if (hdWallet && walletsMgr_->deleteWalletFile(hdWallet)) { - msg.set_wallet_deleted(rootId); + msg.set_wallet_deleted(request.wallet_id()); } else { msg.set_wallet_deleted(""); } pushBroadcast(user_, msg.SerializeAsString()); - return true; + return ProcessingResult::Success; +} + +ProcessingResult SignerAdapter::processExportWoWallet(const bs::message::Envelope& env + , const SignerMessage_ExportWoWalletRequest& request) +{ + SignerMessage msg; + msg.set_export_wo_wallet_response(""); + const auto& hdWallet = walletsMgr_->getHDWalletById(request.wallet().wallet_id()); + if (!hdWallet) { + logger_->error("[{}] wallet {} not found", __func__, request.wallet().wallet_id()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + if (hdWallet->isWatchingOnly()) { + const auto& srcPathName = hdWallet->getFileName(); + const auto& fileName = SystemFileUtils::getFileName(srcPathName); + const auto& dstPathName = request.output_dir() + "/" + fileName; + try { + std::filesystem::copy(srcPathName, dstPathName); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to copy {} -> {}: {}", __func__, srcPathName, dstPathName, e.what()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + logger_->debug("[{}] copied {} to {}", __func__, request.wallet().wallet_id(), dstPathName); + msg.set_export_wo_wallet_response(fileName); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; + } + auto woWallet = hdWallet->createWatchingOnly(); + if (!woWallet) { + logger_->error("[{}] WO export {} failed", __func__, request.wallet().wallet_id()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + const auto srcPathName = woWallet->getFileName(); + const std::string netType = (hdWallet->networkType() == NetworkType::TestNet) ? "Testnet_" : ""; + const std::string fileName = "BlockSettle_" + netType + hdWallet->walletId() + "_watchonly.lmdb"; + const auto& dstPathName = request.output_dir() + "/" + fileName; + woWallet->shutdown(); + try { + std::filesystem::rename(srcPathName, dstPathName); + std::filesystem::remove(srcPathName + "-lock"); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to move {} -> {}: {}", __func__, srcPathName, dstPathName, e.what()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + + logger_->debug("[{}] exported {} to {}", __func__, request.wallet().wallet_id(), dstPathName); + msg.set_export_wo_wallet_response(fileName); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Success; +} + +bs::message::ProcessingResult SignerAdapter::processChangeWalletPass(const bs::message::Envelope& env + , const SignerMessage_ChangeWalletPassword& request) +{ + const auto& hdWallet = walletsMgr_->getHDWalletById(request.wallet().wallet_id()); + if (!hdWallet) { + logger_->error("[{}] wallet {} not found", __func__, request.wallet().wallet_id()); + return ProcessingResult::Error; + } + bool result = true; + { + const auto oldPass = SecureBinaryData::fromString(request.wallet().password()); + const bs::wallet::PasswordData newPass{ SecureBinaryData::fromString(request.new_password()) + , {bs::wallet::EncryptionType::Password} }; + const bs::core::WalletPasswordScoped lock(hdWallet, oldPass); + result = hdWallet->changePassword({bs::wallet::EncryptionType::Password}, newPass); + } + SignerMessage msg; + msg.set_wallet_pass_changed(result); + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Success; +} + +bool SignerAdapter::isPasswordValid(const std::string& walletId + , const SecureBinaryData& password) const +{ + const auto& hdWallet = walletsMgr_->getHDWalletById(walletId); + if (!hdWallet) { + logger_->error("[{}] wallet {} not found", __func__, walletId); + return false; + } + if (hdWallet->isWatchingOnly() || hdWallet->isHardwareWallet()) { + if (password.empty()) { + return true; + } + else { + return false; + } + } + try { + const bs::core::WalletPasswordScoped lock(hdWallet, password); + const auto& seed = hdWallet->getDecryptedSeed(); + if (seed.empty()) { + throw std::runtime_error("seed is empty"); + } + logger_->debug("[{}] seed id: {}, walletId: {}, pass size: {}", __func__, seed.getWalletId(), walletId, password.getSize()); + return (seed.getWalletId() == walletId); + } + catch (const Armory::Wallets::WalletException& e) { + logger_->error("[{}] failed to decrypt wallet {}: {}", __func__, walletId, e.what()); + } + catch (const Armory::Wallets::Encryption::DecryptedDataContainerException&) { + logger_->error("[{}] failed to decrypt wallet {}", __func__, walletId); + } + catch (const std::exception& e) { + logger_->error("[{}] {}: {}", __func__, walletId, e.what()); + } + return false; +} + +bs::message::ProcessingResult SignerAdapter::processGetWalletSeed(const bs::message::Envelope& env + , const SignerMessage_WalletRequest& request) +{ + SignerMessage msg; + auto msgResp = msg.mutable_wallet_seed(); + msgResp->set_wallet_id(request.wallet_id()); + const auto& hdWallet = walletsMgr_->getHDWalletById(request.wallet_id()); + if (!hdWallet) { + logger_->error("[{}] wallet {} not found", __func__, request.wallet_id()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + const auto& password = SecureBinaryData::fromString(request.password()); + if (!isPasswordValid(request.wallet_id(), password)) { + logger_->warn("[{}] invalid password supplied", __func__); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + try { + const bs::core::WalletPasswordScoped lock(hdWallet, password); + const auto& seed = hdWallet->getDecryptedSeed(); + if (seed.empty()) { + logger_->warn("[{}] empty seed", __func__); + } + msgResp->set_xpriv(seed.toXpriv().toBinStr()); + msgResp->set_bip39_seed(seed.seed().toBinStr()); //TODO: check that seed.seed() has spaces + } + catch (const Armory::Wallets::WalletException& e) { + logger_->error("[{}] failed to decrypt wallet {}: {}", __func__, request.wallet_id(), e.what()); + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + catch (const Armory::Wallets::Encryption::DecryptedDataContainerException&) { + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + catch (const std::exception&) { + pushResponse(user_, env, msg.SerializeAsString()); + return ProcessingResult::Error; + } + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Success; +} + +bs::message::ProcessingResult SignerAdapter::processImportWoWallet(const bs::message::Envelope& env + , const std::string& filename) +{ + SignerMessage msg; + auto msgResp = msg.mutable_created_wallet(); + const auto& fn = SystemFileUtils::getFileName(filename); + const auto& targetFile = walletsDir_ + "/" + fn; + try { + std::filesystem::copy(filename, targetFile); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to copy {} to wallets dir {}: {}", __func__ + , filename, walletsDir_, e.what()); + msgResp->set_error_msg("failed to copy " + fn); + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Error; + } + const auto& wallet = signer_->importWoWallet(netType_, targetFile); + if (!wallet) { + msgResp->set_error_msg("failed to import " + fn); + pushResponse(user_, env, msg.SerializeAsString()); + return bs::message::ProcessingResult::Error; + } + msgResp->set_wallet_id(wallet->walletId()); + pushResponse(user_, env, msg.SerializeAsString()); + walletsChanged(); + walletsReady(); + return bs::message::ProcessingResult::Success; +} + +bs::message::ProcessingResult SignerAdapter::processWalletRename(const bs::message::Envelope& + , const std::string& walletId, const std::string& newName) +{ + const auto& hdWallet = walletsMgr_->getHDWalletById(walletId); + if (!hdWallet) { + logger_->error("[{}] wallet {} not found", __func__, walletId); + return bs::message::ProcessingResult::Error; + } + hdWallet->setName(newName); + return bs::message::ProcessingResult::Success; } diff --git a/Core/SignerAdapter.h b/Core/SignerAdapter.h index 1a942dc57..8b1b2b447 100644 --- a/Core/SignerAdapter.h +++ b/Core/SignerAdapter.h @@ -29,10 +29,13 @@ namespace BlockSettle { namespace Common { class SignerMessage; class SignerMessage_AutoSign; + class SignerMessage_ChangeWalletPassword; class SignerMessage_CreateWalletRequest; class SignerMessage_DialogRequest; + class SignerMessage_ExportWoWalletRequest; class SignerMessage_ExtendAddrChain; class SignerMessage_GetSettlPayinAddr; + class SignerMessage_ImportHWWallet; class SignerMessage_SetSettlementId; class SignerMessage_SignSettlementTx; class SignerMessage_SignTxRequest; @@ -40,6 +43,7 @@ namespace BlockSettle { class SignerMessage_SyncAddressComment; class SignerMessage_SyncNewAddresses; class SignerMessage_SyncTxComment; + class SignerMessage_WalletRequest; } namespace Terminal { class SettingsMessage_SignerServer; @@ -55,7 +59,7 @@ class SignerAdapter : public bs::message::Adapter, public SignerCallbackTarget , const std::shared_ptr &signer = nullptr); ~SignerAdapter() override = default; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -73,38 +77,52 @@ class SignerAdapter : public bs::message::Adapter, public SignerCallbackTarget void newWalletPrompt() override; void autoSignStateChanged(bs::error::ErrorCode , const std::string& walletId) override; + bool isPasswordValid(const std::string& walletId, const SecureBinaryData& password) const; - bool processOwnRequest(const bs::message::Envelope & + bs::message::ProcessingResult processOwnRequest(const bs::message::Envelope & , const BlockSettle::Common::SignerMessage &); - bool processSignerSettings(const BlockSettle::Terminal::SettingsMessage_SignerServer &); - bool processNewKeyResponse(bool); + bs::message::ProcessingResult processSignerSettings(const BlockSettle::Terminal::SettingsMessage_SignerServer &); + bs::message::ProcessingResult processNewKeyResponse(bool); bool sendComponentLoading(); - bool processStartWalletSync(const bs::message::Envelope &); - bool processSyncAddresses(const bs::message::Envelope & + bs::message::ProcessingResult processStartWalletSync(const bs::message::Envelope &); + bs::message::ProcessingResult processSyncAddresses(const bs::message::Envelope & , const BlockSettle::Common::SignerMessage_SyncAddresses &); - bool processSyncNewAddresses(const bs::message::Envelope & + bs::message::ProcessingResult processSyncNewAddresses(const bs::message::Envelope & , const BlockSettle::Common::SignerMessage_SyncNewAddresses &); - bool processExtendAddrChain(const bs::message::Envelope & + bs::message::ProcessingResult processExtendAddrChain(const bs::message::Envelope & , const BlockSettle::Common::SignerMessage_ExtendAddrChain &); - bool processSyncWallet(const bs::message::Envelope &, const std::string &walletId); - bool processSyncHdWallet(const bs::message::Envelope &, const std::string &walletId); - bool processSyncAddrComment(const BlockSettle::Common::SignerMessage_SyncAddressComment &); - bool processSyncTxComment(const BlockSettle::Common::SignerMessage_SyncTxComment &); - bool processGetRootPubKey(const bs::message::Envelope &, const std::string &walletId); - bool processDelHdRoot(const std::string &walletId); - bool processDelHdLeaf(const std::string &walletId); - bool processSignTx(const bs::message::Envelope& + bs::message::ProcessingResult processSyncWallet(const bs::message::Envelope &, const std::string &walletId); + bs::message::ProcessingResult processSyncHdWallet(const bs::message::Envelope &, const std::string &walletId); + bs::message::ProcessingResult processSyncAddrComment(const BlockSettle::Common::SignerMessage_SyncAddressComment &); + bs::message::ProcessingResult processSyncTxComment(const BlockSettle::Common::SignerMessage_SyncTxComment &); + bs::message::ProcessingResult processGetRootPubKey(const bs::message::Envelope &, const std::string &walletId); + bs::message::ProcessingResult processDelHdRoot(const std::string &walletId); + bs::message::ProcessingResult processDelHdLeaf(const std::string &walletId); + bs::message::ProcessingResult processSignTx(const bs::message::Envelope& , const BlockSettle::Common::SignerMessage_SignTxRequest&); - bool processResolvePubSpenders(const bs::message::Envelope& + bs::message::ProcessingResult processResolvePubSpenders(const bs::message::Envelope& , const bs::core::wallet::TXSignRequest&); - bool processAutoSignRequest(const bs::message::Envelope& + bs::message::ProcessingResult processAutoSignRequest(const bs::message::Envelope& , const BlockSettle::Common::SignerMessage_AutoSign&); - bool processDialogRequest(const bs::message::Envelope& + bs::message::ProcessingResult processDialogRequest(const bs::message::Envelope& , const BlockSettle::Common::SignerMessage_DialogRequest&); - bool processCreateWallet(const bs::message::Envelope&, bool rescan + bs::message::ProcessingResult processCreateWallet(const bs::message::Envelope&, bool rescan , const BlockSettle::Common::SignerMessage_CreateWalletRequest&); - bool processDeleteWallet(const bs::message::Envelope&, const std::string& rootId); + bs::message::ProcessingResult processImportHwWallet(const bs::message::Envelope& + , const BlockSettle::Common::SignerMessage_ImportHWWallet&); + bs::message::ProcessingResult processDeleteWallet(const bs::message::Envelope& + , const BlockSettle::Common::SignerMessage_WalletRequest&); + bs::message::ProcessingResult processExportWoWallet(const bs::message::Envelope& + , const BlockSettle::Common::SignerMessage_ExportWoWalletRequest&); + bs::message::ProcessingResult processChangeWalletPass(const bs::message::Envelope& + , const BlockSettle::Common::SignerMessage_ChangeWalletPassword&); + bs::message::ProcessingResult processGetWalletSeed(const bs::message::Envelope& + , const BlockSettle::Common::SignerMessage_WalletRequest&); + bs::message::ProcessingResult processImportWoWallet(const bs::message::Envelope& + , const std::string& filename); + bs::message::ProcessingResult processWalletRename(const bs::message::Envelope& + , const std::string& walletId, const std::string& newName); private: std::shared_ptr logger_; @@ -123,5 +141,4 @@ class SignerAdapter : public bs::message::Adapter, public SignerCallbackTarget SecureBinaryData passphrase_; }; - #endif // SIGNER_ADAPTER_H diff --git a/Core/TerminalMessage.h b/Core/TerminalMessage.h index 206c373cf..bdbe1ee9f 100644 --- a/Core/TerminalMessage.h +++ b/Core/TerminalMessage.h @@ -48,6 +48,7 @@ namespace bs { OnChainTracker,// Auth & CC tracker combined in one adapter Settlement, // All settlements (FX, XBT, CC) for both dealer and requester Chat, // Chat network routines + HWWallets, // Hardware wallets device manager UsersCount }; @@ -100,7 +101,7 @@ namespace bs { std::shared_ptr logger_; std::map> queues_; std::shared_ptr runnableAdapter_; - std::shared_ptr relayAdapter_; + std::shared_ptr relayAdapter_; }; } // namespace message diff --git a/Deploy/Windows/bsterminal.nsi b/Deploy/Windows/bsterminal.nsi index 7f1452401..15885ed1a 100644 --- a/Deploy/Windows/bsterminal.nsi +++ b/Deploy/Windows/bsterminal.nsi @@ -1,283 +1,221 @@ -Name "BlockSettle Terminal" -SetCompressor /SOLID lzma - -# General Symbol Definitions -!define COMPANY "BlockSettle AB" -!define URL http://blocksettle.com/ -!define VERSION "0.91.2" -!define PRODUCT_NAME "BlockSettle Terminal" - -# MultiUser Symbol Definitions -!define MULTIUSER_EXECUTIONLEVEL Highest -!define MULTIUSER_INSTALLMODE_COMMANDLINE -!define MULTIUSER_INSTALLMODE_INSTDIR BlockSettle -!define MULTIUSER_USE_PROGRAMFILES64 - -# MUI Symbol Definitions -!define MUI_ICON bs.ico -!define MUI_FINISHPAGE_NOAUTOCLOSE -!define MUI_STARTMENUPAGE_DEFAULTFOLDER BlockSettle -!define MUI_FINISHPAGE_RUN $INSTDIR\blocksettle.exe -!define MUI_UNICON bs.ico -!define MUI_UNFINISHPAGE_NOAUTOCLOSE - -# Included files -!include MultiUser.nsh -!include Sections.nsh -!include MUI2.nsh -!include logiclib.nsh -!include x64.nsh -!include "WordFunc.nsh" - -#BlockSettle Branding -!define MUI_UNWELCOMEFINISHPAGE_BITMAP "resources\nsis3-banner.bmp" ; -!define MUI_WELCOMEFINISHPAGE_BITMAP "resources\nsis3-banner.bmp" ; - -#language settings -!define MUI_LANGDLL_REGISTRY_ROOT SHELL_CONTEXT -!define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language" -!define MUI_LANGDLL_ALLLANGUAGES -!define MUI_LANGDLL_ALWAYSSHOW - -# Variables -Var StartMenuGroup - -# Installer pages -!insertmacro MUI_PAGE_WELCOME -!insertmacro MUI_PAGE_LICENSE LICENSE -#!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "ComponentsLeave" -#!insertmacro MUI_PAGE_COMPONENTS -!insertmacro MUI_PAGE_DIRECTORY -!insertmacro MUI_PAGE_STARTMENU Application $StartMenuGroup -!insertmacro MUI_PAGE_INSTFILES -!insertmacro MUI_PAGE_FINISH -!insertmacro MUI_UNPAGE_CONFIRM -!insertmacro MUI_UNPAGE_INSTFILES - -# Installer languages -!insertmacro MUI_LANGUAGE English - -# Installer attributes -OutFile bsterminal_installer.exe -# No need to set InstallDir here, MultiUser init will set it ($PROGRAMFILES64 for admins, local app data for regular users) -# InstallDir "$PROGRAMFILES64\BlockSettle" -CRCCheck on -XPStyle on -Icon bs.ico -ShowInstDetails show -AutoCloseWindow true -LicenseData LICENSE -VIProductVersion "${VERSION}.0" -VIAddVersionKey ProductName "${PRODUCT_NAME}" -VIAddVersionKey ProductVersion "${VERSION}" -VIAddVersionKey CompanyName "${COMPANY}" -VIAddVersionKey CompanyWebsite "${URL}" -VIAddVersionKey Comments "" -VIAddVersionKey FileVersion "${VERSION}" -VIAddVersionKey FileDescription "BlockSettle Terminal Installer" -VIAddVersionKey LegalCopyright "Copyright (C) 2016-2019 BlockSettle AB" -UninstallIcon bs.ico -ShowUninstDetails show - -#registry key for unisntalling -!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" - -!macro CREATE_SMGROUP_SHORTCUT NAME PATH ARGS - Push "${ARGS}" - Push "${NAME}" - Push "${PATH}" - Call CreateSMGroupShortcut -!macroend - -# Component selection stuff -Section "Terminal" SEC_TERM -SectionEnd - -Section "Signer" SEC_SIGN -SectionEnd - -#LangString DESC_SEC_TERM ${LANG_ENGLISH} "Main terminal binary" -#LangString DESC_SEC_SIGN ${LANG_ENGLISH} "Signer process binary" - -!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN - !insertmacro MUI_DESCRIPTION_TEXT ${SEC_TERM} "Main terminal binary" - !insertmacro MUI_DESCRIPTION_TEXT ${SEC_SIGN} "Signer process binary" -!insertmacro MUI_FUNCTION_DESCRIPTION_END - -Function ComponentsLeave - SectionGetFlags ${SEC_TERM} $0 - StrCmp $0 1 End - SectionGetFlags ${SEC_SIGN} $0 - StrCmp $0 1 End - MessageBox MB_OK "You should select at least one component" - Abort - End: -FunctionEnd - -Section "install" - ${If} ${RunningX64} - SetOutPath $INSTDIR - RmDir /r $INSTDIR - SetOverwrite on - File ..\..\build_terminal\RelWithDebInfo\bin\RelWithDebInfo\libzmq-v141-mt-4_3_2.dll - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\concrt140.dll" - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\msvcp140.dll" - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\msvcp140_1.dll" - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\msvcp140_2.dll" - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\vccorlib140.dll" - File "C:\Program Files (x86)\Microsoft Visual Studio\2017\Community\VC\Redist\MSVC\14.16.27012\x64\Microsoft.VC141.CRT\vcruntime140.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-console-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-datetime-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-debug-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-errorhandling-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-file-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-file-l1-2-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-file-l2-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-handle-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-heap-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-interlocked-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-libraryloader-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-localization-l1-2-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-memory-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-namedpipe-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-processenvironment-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-processthreads-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-processthreads-l1-1-1.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-profile-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-rtlsupport-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-string-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-synch-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-synch-l1-2-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-sysinfo-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-timezone-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-core-util-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-conio-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-convert-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-environment-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-filesystem-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-heap-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-locale-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-math-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-multibyte-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-private-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-process-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-runtime-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-stdio-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-string-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-time-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\api-ms-win-crt-utility-l1-1-0.dll" - File "C:\Program Files (x86)\Windows Kits\10\Redist\ucrt\DLLs\x64\ucrtbase.dll" -# ${If} ${SectionIsSelected} ${SEC_TERM} - File ..\..\build_terminal\RelWithDebInfo\bin\RelWithDebInfo\blocksettle.exe -# ${Endif} -# ${If} ${SectionIsSelected} ${SEC_SIGN} - File ..\..\build_terminal\RelWithDebInfo\bin\RelWithDebInfo\blocksettle_signer.exe -# ${Endif} - SetOutPath $INSTDIR\scripts -# File ..\..\Scripts\DealerAutoQuote.qml -# File ..\..\Scripts\RFQBot.qml - SetOutPath $INSTDIR - CreateShortcut "$DESKTOP\BlockSettle Terminal.lnk" $INSTDIR\blocksettle.exe - CreateShortcut "$DESKTOP\BlockSettle Signer.lnk" $INSTDIR\blocksettle_signer.exe - !insertmacro CREATE_SMGROUP_SHORTCUT "BlockSettle Terminal" "$INSTDIR\blocksettle.exe" "" - !insertmacro CREATE_SMGROUP_SHORTCUT "BlockSettle Signer" "$INSTDIR\blocksettle_signer.exe" "" - - WriteUninstaller $INSTDIR\uninstall.exe - !insertmacro MUI_STARTMENU_WRITE_BEGIN Application - SetOutPath $SMPROGRAMS\$StartMenuGroup - CreateShortcut "$SMPROGRAMS\$StartMenuGroup\Uninstall.lnk" $INSTDIR\uninstall.exe - !insertmacro MUI_STARTMENU_WRITE_END - ${Else} - # 32 bit code - MessageBox MB_OK "You cannot install this version on a 32-bit system" - ${EndIf} -SectionEnd -#post install registry handling -Section -Post - #To be used when running the uninstaller - WriteRegStr SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" - WriteRegStr SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninstall.exe" -SectionEnd - - -# Uninstaller sections -!macro DELETE_SMGROUP_SHORTCUT NAME - Push "${NAME}" - Call un.DeleteSMGroupShortcut -!macroend - -Section "Uninstall" - Delete /REBOOTOK "$DESKTOP\BlockSettle Terminal.lnk" - Delete /REBOOTOK "$DESKTOP\BlockSettle Signer.lnk" - !insertmacro DELETE_SMGROUP_SHORTCUT "BlockSettle Terminal" - !insertmacro DELETE_SMGROUP_SHORTCUT "BlockSettle Signer" - Delete /REBOOTOK $INSTDIR\blocksettle.exe - Delete /REBOOTOK $INSTDIR\blocksettle_signer.exe - Delete /REBOOTOK $INSTDIR\libzmq-v141-mt-4_3_2.dll - Delete /REBOOTOK $INSTDIR\msvcp140.dll - Delete /REBOOTOK $INSTDIR\vcruntime140.dll - RmDir /r /REBOOTOK $INSTDIR - - Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\Uninstall.lnk" - Delete /REBOOTOK $INSTDIR\uninstall.exe - RmDir /r /REBOOTOK $SMPROGRAMS\$StartMenuGroup - RmDir /r /REBOOTOK $INSTDIR - Push $R0 - StrCpy $R0 $StartMenuGroup 1 - StrCmp $R0 ">" no_smgroup - - DeleteRegKey SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" -no_smgroup: - Pop $R0 -SectionEnd - -# Installer functions -Function .onInit - ; Avoid running the installer if BlockSettle Terminal Installer is already running, - System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}InstMutex") i .r1 ?e' - Pop $R0 - StrCmp $R0 0 +3 - MessageBox MB_OK|MB_ICONEXCLAMATION \ - "The ${PRODUCT_NAME} Installer is already running." - Abort - - ClearErrors - - InitPluginsDir - !insertmacro MULTIUSER_INIT - -FunctionEnd - -Function CreateSMGroupShortcut - Exch $R0 ;PATH - Exch - Exch $R1 ;NAME - Exch 2 - Exch $R3 - Push $R2 - StrCpy $R2 $StartMenuGroup 1 - StrCmp $R2 ">" no_smgroup - SetOutPath $SMPROGRAMS\$StartMenuGroup - CreateShortcut "$SMPROGRAMS\$StartMenuGroup\$R1.lnk" $R0 $R3 -no_smgroup: - Pop $R2 - Pop $R1 - Pop $R0 -FunctionEnd - -# Uninstaller functions -Function un.onInit - !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuGroup - !insertmacro MULTIUSER_UNINIT -FunctionEnd - -Function un.DeleteSMGroupShortcut - Exch $R1 ;NAME - Push $R2 - StrCpy $R2 $StartMenuGroup 1 - StrCmp $R2 ">" no_smgroup - Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\$R1.lnk" -no_smgroup: - Pop $R2 - Pop $R1 -FunctionEnd +Name "BlockSettle Terminal" +SetCompressor /SOLID lzma + +# General Symbol Definitions +!define COMPANY "BlockSettle AB" +!define URL http://blocksettle.com/ +!define VERSION "0.91.2" +!define PRODUCT_NAME "BlockSettle Terminal" + +# MultiUser Symbol Definitions +!define MULTIUSER_EXECUTIONLEVEL Highest +!define MULTIUSER_INSTALLMODE_COMMANDLINE +!define MULTIUSER_INSTALLMODE_INSTDIR BlockSettle +!define MULTIUSER_USE_PROGRAMFILES64 + +# MUI Symbol Definitions +!define MUI_ICON bs.ico +!define MUI_FINISHPAGE_NOAUTOCLOSE +!define MUI_STARTMENUPAGE_DEFAULTFOLDER BlockSettle +!define MUI_FINISHPAGE_RUN $INSTDIR\blocksettle.exe +!define MUI_UNICON bs.ico +!define MUI_UNFINISHPAGE_NOAUTOCLOSE + +# Included files +!include MultiUser.nsh +!include Sections.nsh +!include MUI2.nsh +!include logiclib.nsh +!include x64.nsh +!include "WordFunc.nsh" + +#BlockSettle Branding +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "resources\nsis3-banner.bmp" ; +!define MUI_WELCOMEFINISHPAGE_BITMAP "resources\nsis3-banner.bmp" ; + +#language settings +!define MUI_LANGDLL_REGISTRY_ROOT SHELL_CONTEXT +!define MUI_LANGDLL_REGISTRY_VALUENAME "NSIS:Language" +!define MUI_LANGDLL_ALLLANGUAGES +!define MUI_LANGDLL_ALWAYSSHOW + +# Variables +Var StartMenuGroup + +# Installer pages +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_LICENSE LICENSE +#!define MUI_PAGE_CUSTOMFUNCTION_LEAVE "ComponentsLeave" +#!insertmacro MUI_PAGE_COMPONENTS +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_STARTMENU Application $StartMenuGroup +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +# Installer languages +!insertmacro MUI_LANGUAGE English + +# Installer attributes +OutFile bsterminal_installer.exe +# No need to set InstallDir here, MultiUser init will set it ($PROGRAMFILES64 for admins, local app data for regular users) +# InstallDir "$PROGRAMFILES64\BlockSettle" +CRCCheck on +XPStyle on +Icon bs.ico +ShowInstDetails show +AutoCloseWindow true +LicenseData LICENSE +VIProductVersion "${VERSION}.0" +VIAddVersionKey ProductName "${PRODUCT_NAME}" +VIAddVersionKey ProductVersion "${VERSION}" +VIAddVersionKey CompanyName "${COMPANY}" +VIAddVersionKey CompanyWebsite "${URL}" +VIAddVersionKey Comments "" +VIAddVersionKey FileVersion "${VERSION}" +VIAddVersionKey FileDescription "BlockSettle Terminal Installer" +VIAddVersionKey LegalCopyright "Copyright (C) 2016-2023 BlockSettle AB" +UninstallIcon bs.ico +ShowUninstDetails show + +#registry key for unisntalling +!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" + +!macro CREATE_SMGROUP_SHORTCUT NAME PATH ARGS + Push "${ARGS}" + Push "${NAME}" + Push "${PATH}" + Call CreateSMGroupShortcut +!macroend + +# Component selection stuff +Section "Terminal" SEC_TERM +SectionEnd + + +#LangString DESC_SEC_TERM ${LANG_ENGLISH} "Main terminal binary" + +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SEC_TERM} "Main terminal binary" +!insertmacro MUI_FUNCTION_DESCRIPTION_END + +Function ComponentsLeave + SectionGetFlags ${SEC_TERM} $0 + StrCmp $0 1 End + MessageBox MB_OK "You should select at least one component" + Abort + End: +FunctionEnd + +Section "install" + ${If} ${RunningX64} + SetOutPath $INSTDIR + RmDir /r $INSTDIR + SetOverwrite on +# ${If} ${SectionIsSelected} ${SEC_TERM} + File ..\..\build_terminal\RelWithDebInfo\bin\RelWithDebInfo\blocksettle.exe +# ${Endif} + SetOutPath $INSTDIR\scripts +# File ..\..\Scripts\DealerAutoQuote.qml +# File ..\..\Scripts\RFQBot.qml + SetOutPath $INSTDIR + CreateShortcut "$DESKTOP\BlockSettle Terminal.lnk" $INSTDIR\blocksettle.exe + !insertmacro CREATE_SMGROUP_SHORTCUT "BlockSettle Terminal" "$INSTDIR\blocksettle.exe" "" + + WriteUninstaller $INSTDIR\uninstall.exe + !insertmacro MUI_STARTMENU_WRITE_BEGIN Application + SetOutPath $SMPROGRAMS\$StartMenuGroup + CreateShortcut "$SMPROGRAMS\$StartMenuGroup\Uninstall.lnk" $INSTDIR\uninstall.exe + !insertmacro MUI_STARTMENU_WRITE_END + ${Else} + # 32 bit code + MessageBox MB_OK "You cannot install this version on a 32-bit system" + ${EndIf} +SectionEnd +#post install registry handling +Section -Post + #To be used when running the uninstaller + WriteRegStr SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" + WriteRegStr SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninstall.exe" +SectionEnd + + +# Uninstaller sections +!macro DELETE_SMGROUP_SHORTCUT NAME + Push "${NAME}" + Call un.DeleteSMGroupShortcut +!macroend + +Section "Uninstall" + Delete /REBOOTOK "$DESKTOP\BlockSettle Terminal.lnk" + !insertmacro DELETE_SMGROUP_SHORTCUT "BlockSettle Terminal" + Delete /REBOOTOK $INSTDIR\blocksettle.exe + #Delete /REBOOTOK $INSTDIR\libzmq-v141-mt-4_3_2.dll + #Delete /REBOOTOK $INSTDIR\msvcp140.dll + #Delete /REBOOTOK $INSTDIR\vcruntime140.dll + RmDir /r /REBOOTOK $INSTDIR + + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\Uninstall.lnk" + Delete /REBOOTOK $INSTDIR\uninstall.exe + RmDir /r /REBOOTOK $SMPROGRAMS\$StartMenuGroup + RmDir /r /REBOOTOK $INSTDIR + Push $R0 + StrCpy $R0 $StartMenuGroup 1 + StrCmp $R0 ">" no_smgroup + + DeleteRegKey SHELL_CONTEXT "${PRODUCT_UNINST_KEY}" +no_smgroup: + Pop $R0 +SectionEnd + +# Installer functions +Function .onInit + ; Avoid running the installer if BlockSettle Terminal Installer is already running, + System::Call 'kernel32::CreateMutexA(i 0, i 0, t "${PRODUCT_NAME}InstMutex") i .r1 ?e' + Pop $R0 + StrCmp $R0 0 +3 + MessageBox MB_OK|MB_ICONEXCLAMATION \ + "The ${PRODUCT_NAME} Installer is already running." + Abort + + ClearErrors + + InitPluginsDir + !insertmacro MULTIUSER_INIT + +FunctionEnd + +Function CreateSMGroupShortcut + Exch $R0 ;PATH + Exch + Exch $R1 ;NAME + Exch 2 + Exch $R3 + Push $R2 + StrCpy $R2 $StartMenuGroup 1 + StrCmp $R2 ">" no_smgroup + SetOutPath $SMPROGRAMS\$StartMenuGroup + CreateShortcut "$SMPROGRAMS\$StartMenuGroup\$R1.lnk" $R0 $R3 +no_smgroup: + Pop $R2 + Pop $R1 + Pop $R0 +FunctionEnd + +# Uninstaller functions +Function un.onInit + !insertmacro MUI_STARTMENU_GETFOLDER Application $StartMenuGroup + !insertmacro MULTIUSER_UNINIT +FunctionEnd + +Function un.DeleteSMGroupShortcut + Exch $R1 ;NAME + Push $R2 + StrCpy $R2 $StartMenuGroup 1 + StrCmp $R2 ">" no_smgroup + Delete /REBOOTOK "$SMPROGRAMS\$StartMenuGroup\$R1.lnk" +no_smgroup: + Pop $R2 + Pop $R1 +FunctionEnd diff --git a/Deploy/deploy.sh b/Deploy/deploy.sh index 0c09acc77..0694a4a9d 100755 --- a/Deploy/deploy.sh +++ b/Deploy/deploy.sh @@ -19,7 +19,6 @@ rm -rf Ubuntu/usr/share/blocksettle/scripts mkdir -p Ubuntu/usr/share/blocksettle/scripts cp $binpath/blocksettle Ubuntu/usr/bin/ -cp $binpath/blocksettle_signer Ubuntu/usr/bin/ #cp $scriptpath/DealerAutoQuote.qml Ubuntu/usr/share/blocksettle/scripts/ #cp $scriptpath/RFQBot.qml Ubuntu/usr/share/blocksettle/scripts/ @@ -27,6 +26,5 @@ dpkg -b Ubuntu bsterminal.deb echo "deb package generated" rm -f Ubuntu/usr/bin/blocksettle -rm -f Ubuntu/usr/bin/blocksettle_signer rm -f Ubuntu/usr/share/blocksettle/scripts/* rm -f Ubuntu/lib/x86_64-linux-gnu/* diff --git a/GUI/CMakeLists.txt b/GUI/CMakeLists.txt index 249175835..483f4af98 100644 --- a/GUI/CMakeLists.txt +++ b/GUI/CMakeLists.txt @@ -1,7 +1,7 @@ # # # *********************************************************************************** -# * Copyright (C) 2020 - 2022, BlockSettle AB +# * Copyright (C) 2020 - 2023, BlockSettle AB # * Distributed under the GNU Affero General Public License (AGPL v3) # * See LICENSE or http://www.gnu.org/licenses/agpl.html # * @@ -9,5 +9,7 @@ # # +INCLUDE_DIRECTORIES(${BS_HW_LIB_INCLUDE_DIR}) + ADD_SUBDIRECTORY(QtWidgets) ADD_SUBDIRECTORY(QtQuick) diff --git a/GUI/QtQuick/AddressFilterModel.cpp b/GUI/QtQuick/AddressFilterModel.cpp new file mode 100644 index 000000000..a1585bd3b --- /dev/null +++ b/GUI/QtQuick/AddressFilterModel.cpp @@ -0,0 +1,161 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "AddressFilterModel.h" +#include "AddressListModel.h" +#include + +AddressFilterModel::AddressFilterModel(std::shared_ptr settings) + : QSortFilterProxyModel() + , settings_(settings) +{ + setDynamicSortFilter(true); + sort(2, Qt::AscendingOrder); + connect(this, &AddressFilterModel::changed, this, &AddressFilterModel::invalidate); + + if (settings_ != nullptr) + { + connect(settings_.get(), &SettingsController::reset, this, [this]() + { + if (settings_->hasParam(ApplicationSettings::Setting::AddressFilterHideUsed)) { + hideUsed_ = settings_->getParam(ApplicationSettings::Setting::AddressFilterHideUsed).toBool(); + } + if (settings_->hasParam(ApplicationSettings::Setting::AddressFilterHideInternal)) { + hideInternal_ = settings_->getParam(ApplicationSettings::Setting::AddressFilterHideInternal).toBool(); + } + if (settings_->hasParam(ApplicationSettings::Setting::AddressFilterHideExternal)) { + hideExternal_ = settings_->getParam(ApplicationSettings::Setting::AddressFilterHideExternal).toBool(); + } + if (settings_->hasParam(ApplicationSettings::Setting::AddressFilterHideEmpty)) { + hideEmpty_ = settings_->getParam(ApplicationSettings::Setting::AddressFilterHideEmpty).toBool(); + } + emit changed(); + }); + } +} + +bool AddressFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + if (hideUsed_) { + const auto txCountIndex = sourceModel()->index(source_row, 1); + const auto bananceIndex = sourceModel()->index(source_row, 2); + if (sourceModel()->data(txCountIndex, QmlAddressListModel::TableRoles::TableDataRole) != 0 && + qFuzzyIsNull(sourceModel()->data(bananceIndex, QmlAddressListModel::TableRoles::TableDataRole).toDouble())) + { + return false; + } + } + + if (hideEmpty_) { + const auto bananceIndex = sourceModel()->index(source_row, 2); + if (qFuzzyIsNull(sourceModel()->data(bananceIndex, QmlAddressListModel::TableRoles::TableDataRole).toDouble())) + { + return false; + } + } + + const auto assetTypeIndex = sourceModel()->index(source_row, 0); + const auto assetType = sourceModel()->data(assetTypeIndex, QmlAddressListModel::TableRoles::AddressTypeRole).toString(); + if (hideInternal_) { + if (assetType.length() > 0 && assetType.at(0).toLatin1() == '1') + { + return false; + } + } + if (hideExternal_) { + if (assetType.length() > 0 && assetType.at(0).toLatin1() == '0') + { + return false; + } + } + + return true; +} + +bool AddressFilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + try { + const auto leftAmount = sourceModel()->data(sourceModel()->index(left.row(), 2) + , QmlAddressListModel::TableRoles::TableDataRole).toDouble(); + const auto leftIndex = sourceModel()->data(sourceModel()->index(left.row(), 0) + , QmlAddressListModel::TableRoles::AddressTypeRole).toString().remove(0, 2).toInt(); + const auto leftTypeStr = sourceModel()->data(sourceModel()->index(left.row(), 0) + , QmlAddressListModel::TableRoles::AddressTypeRole).toString(); + const auto rightAmount = sourceModel()->data(sourceModel()->index(right.row(), 2) + , QmlAddressListModel::TableRoles::TableDataRole).toDouble(); + const auto rightIndex = sourceModel()->data(sourceModel()->index(right.row(), 0) + , QmlAddressListModel::TableRoles::AddressTypeRole).toString().remove(0, 2).toInt(); + const auto rightTypeStr = sourceModel()->data(sourceModel()->index(right.row(), 0) + , QmlAddressListModel::TableRoles::AddressTypeRole).toString(); + const auto leftType = leftTypeStr.isEmpty() ? 0 : leftTypeStr.at(0); + const auto rightType = rightTypeStr.isEmpty() ? 0 : rightTypeStr.at(0); + return (!qFuzzyIsNull(leftAmount) && qFuzzyIsNull(rightAmount)) + || (qFuzzyIsNull(leftAmount) == qFuzzyIsNull(rightAmount) && leftIndex < rightIndex) + || (qFuzzyIsNull(leftAmount) == qFuzzyIsNull(rightAmount) && leftIndex == rightIndex && leftType < rightType); + } + catch (...) {} + return false; +} + +bool AddressFilterModel::hideUsed() const +{ + return hideUsed_; +} + +bool AddressFilterModel::hideInternal() const +{ + return hideInternal_; +} + +bool AddressFilterModel::hideExternal() const +{ + return hideExternal_; +} + +bool AddressFilterModel::hideEmpty() const +{ + return hideEmpty_; +} + +void AddressFilterModel::setHideUsed(bool hideUsed) noexcept +{ + if (hideUsed != hideUsed_) { + hideUsed_ = hideUsed; + settings_->setParam(ApplicationSettings::Setting::AddressFilterHideUsed, hideUsed_); + emit changed(); + } +} + +void AddressFilterModel::setHideInternal(bool hideInternal) noexcept +{ + if (hideInternal_ != hideInternal) { + hideInternal_ = hideInternal; + settings_->setParam(ApplicationSettings::Setting::AddressFilterHideInternal, hideInternal_); + emit changed(); + } +} + +void AddressFilterModel::setHideExternal(bool hideExternal) noexcept +{ + if (hideExternal_ != hideExternal) { + hideExternal_ = hideExternal; + settings_->setParam(ApplicationSettings::Setting::AddressFilterHideExternal, hideExternal_); + emit changed(); + } +} + +void AddressFilterModel::setHideEmpty(bool hideEmpty) noexcept +{ + if (hideEmpty_ != hideEmpty) { + hideEmpty_ = hideEmpty; + settings_->setParam(ApplicationSettings::Setting::AddressFilterHideEmpty, hideEmpty_); + emit changed(); + } +} diff --git a/GUI/QtQuick/AddressFilterModel.h b/GUI/QtQuick/AddressFilterModel.h new file mode 100644 index 000000000..d8592d298 --- /dev/null +++ b/GUI/QtQuick/AddressFilterModel.h @@ -0,0 +1,50 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include +#include "SettingsController.h" + +class AddressFilterModel: public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(bool hideUsed READ hideUsed WRITE setHideUsed NOTIFY changed) + Q_PROPERTY(bool hideInternal READ hideInternal WRITE setHideInternal NOTIFY changed) + Q_PROPERTY(bool hideExternal READ hideExternal WRITE setHideExternal NOTIFY changed) + Q_PROPERTY(bool hideEmpty READ hideEmpty WRITE setHideEmpty NOTIFY changed) + +public: + AddressFilterModel(std::shared_ptr settings); + + bool hideUsed() const; + bool hideInternal() const; + bool hideExternal() const; + bool hideEmpty() const; + void setHideUsed(bool hideUsed) noexcept; + void setHideInternal(bool hideInternal) noexcept; + void setHideExternal(bool hideExternal) noexcept; + void setHideEmpty(bool hideEmpty) noexcept; + +signals: + void changed(); + +protected: + bool filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex & left, const QModelIndex & right) const override; + +private: + bool hideUsed_ { true }; + bool hideInternal_ { false }; + bool hideExternal_ { false }; + bool hideEmpty_ { false }; + std::shared_ptr settings_; +}; diff --git a/GUI/QtQuick/AddressListModel.cpp b/GUI/QtQuick/AddressListModel.cpp new file mode 100644 index 000000000..0e4a2d4b5 --- /dev/null +++ b/GUI/QtQuick/AddressListModel.cpp @@ -0,0 +1,201 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "AddressListModel.h" +#include +#include "ColorScheme.h" +#include "Utils.h" + +namespace +{ + static const QHash kRoles{ + {Qt::DisplayRole, "address"}, + {QmlAddressListModel::TableDataRole, "tableData"}, + {QmlAddressListModel::ColorRole, "dataColor"}, + {QmlAddressListModel::AddressTypeRole, "addressType"}, + {QmlAddressListModel::AssetTypeRole, "assetType"} }; +} + +QmlAddressListModel::QmlAddressListModel(const std::shared_ptr& logger, QObject* parent) + : QAbstractTableModel(parent), logger_(logger), header_({ tr("Address"), tr("#Tx"), tr("Balance (BTC)"), tr("Comment") }) +{} + +int QmlAddressListModel::rowCount(const QModelIndex&) const +{ + return table_.size(); +} + +int QmlAddressListModel::columnCount(const QModelIndex&) const +{ + return header_.size(); +} + +QVariant QmlAddressListModel::data(const QModelIndex& index, int role) const +{ + const int row = index.row(); + if (row < 0) { + return {}; + } + try { + switch (role) + { + case Qt::DisplayRole: + return table_.at(row).at(0); + case TableDataRole: + { + switch (index.column()) { + case 0: return table_.at(row).at(0); + case 1: return QString::number(getTransactionCount(addresses_.at(row).id())); + case 2: return getAddressBalance(addresses_.at(row).id()); + case 3: return table_.at(row).at(1); + default: return QString{}; + } + } + break; + case ColorRole: return QColorConstants::White; + case AddressTypeRole: return table_.at(row).at(2); + case AssetTypeRole: return table_.at(row).at(3); + default: break; + } + } + catch (const std::exception&) { + return QString{}; + } + return QVariant(); +} + +QHash QmlAddressListModel::roleNames() const +{ + return kRoles; +} + +void QmlAddressListModel::addRow(const std::string& walletId, const QVector& row) +{ + logger_->debug("[{}] {} {}", __func__, walletId, row.at(0).toStdString()); + if (walletId != expectedWalletId_) { + logger_->warn("[QmlAddressListModel::addRow] wallet {} not expected ({})", walletId, expectedWalletId_); + return; + } + try { + addresses_.push_back(bs::Address::fromAddressString(row.at(0).toStdString())); + } + catch (const std::exception&) { + logger_->warn("[{}] {} invalid address {}", __func__, walletId, row.at(0).toStdString()); + addresses_.push_back(bs::Address{}); + } + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + table_.append(row); + endInsertRows(); +} + +void QmlAddressListModel::addRows(const std::string& walletId, const QVector>& rows) +{ + logger_->debug("[{}] {} {} rows", __func__, walletId, rows.size()); + if (walletId != expectedWalletId_) { + logger_->warn("[QmlAddressListModel::addRows] wallet {} not expected ({})", walletId, expectedWalletId_); + return; + } + if (rows.empty()) { + return; + } + for (const auto& row : rows) { + try { + const auto& addr = bs::Address::fromAddressString(row.at(0).toStdString()); + bool found = false; + for (const auto& a : addresses_) { + if (a == addr) { + found = true; + break; + } + } + if (!found) { + addresses_.push_back(addr); + } + } + catch (const std::exception&) { + addresses_.push_back(bs::Address{}); + } + } + logger_->debug("[{}] {} rows / {} addresses", __func__, rows.size(), addresses_.size()); + QVector> newRows; + for (const auto& row : rows) { + bool found = false; + for (const auto& r : table_) { + if (r.at(0) == row.at(0)) { + found = true; + break; + } + } + if (!found) { + for (const auto& r : newRows) { + if (r.at(0) == row.at(0)) { + found = true; + break; + } + } + } + if (!found) { + newRows.append(row); + } + } + bool found = false; + if (!newRows.empty()) { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + newRows.size() - 1); + table_.append(newRows); + endInsertRows(); + } +} + +void QmlAddressListModel::updateRow(const BinaryData& addrPubKey, uint64_t bal, uint32_t nbTx) +{ + pendingBalances_[addrPubKey] = { bal, nbTx }; + for (int i = 0; i < table_.size(); ++i) { + const auto& addr = addresses_.at(i); + // logger_->debug("[QmlAddressListModel::updateRow] {} {} {}", addr.display(), bal, nbTx); + if (addr.id() == addrPubKey) { + emit dataChanged(createIndex(i, 1), createIndex(i, 2)); + break; + } + } +} + +void QmlAddressListModel::reset(const std::string& expectedWalletId) +{ + logger_->debug("[{}] {}", __func__, expectedWalletId); + expectedWalletId_ = expectedWalletId; + beginResetModel(); + addresses_.clear(); + table_.clear(); + endResetModel(); +} + +QVariant QmlAddressListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_.at(section); + } + return QVariant(); +} + +quint32 QmlAddressListModel::getTransactionCount(const BinaryData& address) const +{ + if (pendingBalances_.count(address) > 0) { + return pendingBalances_.at(address).nbTx; + } + return 0; +} + +QString QmlAddressListModel::getAddressBalance(const BinaryData& address) const +{ + if (pendingBalances_.count(address) > 0) { + return gui_utils::satoshiToQString(pendingBalances_.at(address).balance); + } + return gui_utils::satoshiToQString(0);; +} diff --git a/GUI/QtQuick/AddressListModel.h b/GUI/QtQuick/AddressListModel.h new file mode 100644 index 000000000..c65b65340 --- /dev/null +++ b/GUI/QtQuick/AddressListModel.h @@ -0,0 +1,71 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef ADDRESS_LIST_MODEL_H +#define ADDRESS_LIST_MODEL_H + +#include +#include +#include +#include +#include "Address.h" +#include "BinaryData.h" + +namespace spdlog +{ + class logger; +} + +class QmlAddressListModel: public QAbstractTableModel +{ + Q_OBJECT +public: + enum TableRoles + { + TableDataRole = Qt::UserRole + 1, + ColorRole, + AddressTypeRole, + AssetTypeRole + }; + Q_ENUM(TableRoles) + + QmlAddressListModel(const std::shared_ptr&, QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void addRow(const std::string& walletId, const QVector&); + void addRows(const std::string& walletId, const QVector>&); + void updateRow(const BinaryData& addr, uint64_t bal, uint32_t nbTx); + void reset(const std::string& expectedWalletId); + +private: + quint32 getTransactionCount(const BinaryData& address) const; + QString getAddressBalance(const BinaryData& address) const; + +private: + std::shared_ptr logger_; + const QStringList header_; + QVector> table_; + std::vector addresses_; + + struct PendingBalance + { + uint64_t balance{ 0 }; + uint32_t nbTx{ 0 }; + }; + std::map pendingBalances_; + std::string expectedWalletId_; +}; + +#endif // ADDRESS_LIST_MODEL_H diff --git a/GUI/QtQuick/ArmoryServersModel.cpp b/GUI/QtQuick/ArmoryServersModel.cpp new file mode 100644 index 000000000..5e3619ec1 --- /dev/null +++ b/GUI/QtQuick/ArmoryServersModel.cpp @@ -0,0 +1,197 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "ArmoryServersModel.h" +#include "ArmoryServersProvider.h" + +#include + +namespace { + static const QHash kRoleNames{ + {Qt::DisplayRole, "display"}, + {ArmoryServersModel::TableDataRole, "tableData"}, + {ArmoryServersModel::NameRole, "name"}, + {ArmoryServersModel::NetTypeRole, "netType"}, + {ArmoryServersModel::AddressRole, "address"}, + {ArmoryServersModel::PortRole, "port"}, + {ArmoryServersModel::KeyRole, "key"}, + {ArmoryServersModel::DefaultServerRole, "isDefault"}, + {ArmoryServersModel::CurrentServerRole, "isCurrent"}, + }; +} + +ArmoryServersModel::ArmoryServersModel(const std::shared_ptr &logger, QObject* parent) + : QAbstractListModel(parent) + , logger_(logger) +{ + connect(this, &ArmoryServersModel::modelReset, + this, &ArmoryServersModel::rowCountChanged); + connect(this, &ArmoryServersModel::rowsInserted, + this, &ArmoryServersModel::rowCountChanged); + connect(this, &ArmoryServersModel::rowsRemoved, + this, &ArmoryServersModel::rowCountChanged); +} + +void ArmoryServersModel::setCurrent(int value) +{ + if (value < 0) { + value = connected_; + } + if ((value < 0) || (current_ == value)) { + return; + } + current_ = value; + emit currentChanged(value); + const auto dataIndex = index(current_, 0); + emit dataChanged(dataIndex, dataIndex, { ArmoryServersModel::TableRoles::CurrentServerRole }); +} + +void ArmoryServersModel::setData(int curIdx, int connIdx + , const std::vector& data) +{ + QMetaObject::invokeMethod(this, [this, curIdx, connIdx, data] { + beginResetModel(); + data_ = data; + endResetModel(); + if (connected_ != connIdx) { + connected_ = connIdx; + emit connectedChanged(); + } + setCurrent(curIdx); + }); +} + +void ArmoryServersModel::add(const ArmoryServer& srv) +{ + beginInsertRows(QModelIndex{}, rowCount(), rowCount()); + data_.push_back(srv); + endInsertRows(); +} + +// netType==0 => MainNet, netType==1 => TestNet +void ArmoryServersModel::add(QString name, QString armoryDBIp, int armoryDBPort, int netType, QString armoryDBKey) +{ + ArmoryServer server; + server.name = name.toStdString(); + server.armoryDBPort = std::to_string(armoryDBPort); + server.armoryDBIp = armoryDBIp.toStdString(); + server.armoryDBKey = armoryDBKey.toStdString(); + if (netType == 0) { + server.netType = NetworkType::MainNet; + } + else if (netType == 1) { + server.netType = NetworkType::TestNet; + } + + QMetaObject::invokeMethod(this, [this, server] { + add(server); + setCurrent(rowCount() - 1); + }); +} + +bool ArmoryServersModel::del(int idx) +{ + if ((idx >= rowCount()) || (idx < 0) || (idx < ArmoryServersProvider::kDefaultServersCount)) { + return false; + } + QMetaObject::invokeMethod(this, [this, idx] { + beginRemoveRows(QModelIndex{}, idx, idx); + data_.erase(data_.cbegin() + idx); + endRemoveRows(); + }); + if (idx == current()) { + //@dvajdual dont sure - what server must be default? + setCurrent(0); + } + return true; +} + +int ArmoryServersModel::rowCount(const QModelIndex&) const +{ + return data_.size(); +} + +QVariant ArmoryServersModel::data(const QModelIndex& index, int role) const +{ + if(!index.isValid() || index.row() > rowCount()) { + return QVariant(); + } + + int row = index.row(); + + switch (role) { + case Qt::DisplayRole: return (data_.at(row).netType == NetworkType::MainNet) + ? (QString::fromStdString(data_.at(row).name) + QLatin1String(" (Mainnet)")) + : (QString::fromStdString(data_.at(row).name) + QLatin1String(" (Testnet)")); + case NameRole: return QString::fromStdString(data_.at(row).name); + case NetTypeRole: return (int)data_.at(row).netType; + case AddressRole: return QString::fromStdString(data_.at(row).armoryDBIp); + case PortRole: return std::stoi(data_.at(row).armoryDBPort); + case KeyRole: return QString::fromStdString(data_.at(row).armoryDBKey); + case DefaultServerRole: return (index.row() < ArmoryServersProvider::kDefaultServersCount) && (index.row() < rowCount()); + case CurrentServerRole: return (index.row() == current()); + default: return QVariant(); + } + return QVariant(); +} + +bool ArmoryServersModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid() || index.row() > rowCount()) { + return false; + } + + int row = index.row(); + if (role == CurrentServerRole) { + setCurrent(row); + } + else if (isEditable(index.row())) { + switch (role) + { + case NameRole: + data_.at(row).name = value.toString().toStdString(); + break; + case NetTypeRole: + data_.at(row).netType = static_cast(value.toInt()); + break; + case AddressRole: + data_.at(row).armoryDBIp = value.toString().toStdString(); + break; + case PortRole: + data_.at(row).armoryDBPort = std::to_string(value.toInt()); + break; + case KeyRole: + data_.at(row).armoryDBKey = value.toString().toStdString(); + break; + default: break; + } + } + emit changed(index.row()); + emit dataChanged(index, index, { role }); + return true; +} + +QHash ArmoryServersModel::roleNames() const +{ + return kRoleNames; +} + +bool ArmoryServersModel::isEditable(int row) const +{ + return !data(index(row), DefaultServerRole).toBool(); +} + +QString ArmoryServersModel::currentNetworkName() const +{ + if (current_ >= 0 && current_ < data_.size()) { + return QString::fromStdString(data_.at(current_).name); + } + return QString::fromLatin1(""); +} diff --git a/GUI/QtQuick/ArmoryServersModel.h b/GUI/QtQuick/ArmoryServersModel.h new file mode 100644 index 000000000..f03294624 --- /dev/null +++ b/GUI/QtQuick/ArmoryServersModel.h @@ -0,0 +1,72 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include + +#include +#include "ArmorySettings.h" + +namespace spdlog { + class logger; +} + +class ArmoryServersModel: public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int current READ current WRITE setCurrent NOTIFY currentChanged) + Q_PROPERTY(int connected READ connected NOTIFY connectedChanged) + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + Q_PROPERTY(QString currentNetworkName READ currentNetworkName NOTIFY currentChanged) + +public: + enum TableRoles { + TableDataRole = Qt::UserRole + 1, NameRole, NetTypeRole, AddressRole, + PortRole, KeyRole, DefaultServerRole, CurrentServerRole + }; + Q_ENUM(TableRoles) + + ArmoryServersModel(const std::shared_ptr&, QObject* parent = nullptr); + + void setCurrent (int value); + void setData(int curIdx, int connIdx, const std::vector&); + void add(const ArmoryServer&); + // netType==0 => MainNet, netType==1 => TestNet + void add(QString name, QString armoryDBIp, int armoryDBPort, int netType, QString armoryDBKey); + bool del(int idx); + auto data() const { return data_; } + auto data(int idx) const { return data_.at(idx); } + + int rowCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + bool setData(const QModelIndex& index, const QVariant& value, int role = Qt::EditRole) override; + QHash roleNames() const override; + + bool isEditable(int row) const; + + QString currentNetworkName() const; + +signals: + void changed(int); + void currentChanged(int index); + void connectedChanged(); + void rowCountChanged(); + +private: + int current() const { return current_; } + int connected() const { return connected_; } + +private: + std::shared_ptr logger_; + int current_{ -1 }; + int connected_{ -1 }; + std::vector data_; +}; diff --git a/GUI/QtQuick/CMakeLists.txt b/GUI/QtQuick/CMakeLists.txt index 2aa835366..ec394ed5a 100644 --- a/GUI/QtQuick/CMakeLists.txt +++ b/GUI/QtQuick/CMakeLists.txt @@ -13,10 +13,10 @@ CMAKE_MINIMUM_REQUIRED(VERSION 3.3) PROJECT(${TERMINAL_GUI_QTQUICK_NAME}) -FILE(GLOB SOURCES +FILE(GLOB_RECURSE SOURCES *.cpp ) -FILE(GLOB HEADERS +FILE(GLOB_RECURSE HEADERS *.h ) @@ -35,6 +35,7 @@ INCLUDE_DIRECTORIES(${Qt5Network_INCLUDE_DIRS} ) INCLUDE_DIRECTORIES(${Qt5Qml_INCLUDE_DIRS} ) INCLUDE_DIRECTORIES(${Qt5DBus_INCLUDE_DIRS} ) INCLUDE_DIRECTORIES(${Qt5Charts_INCLUDE_DIRS} ) +INCLUDE_DIRECTORIES(${CURL_INCLUDE_DIR}) qt5_add_resources(GENERATED_RESOURCES qtquick.qrc) diff --git a/GUI/QtQuick/ColorScheme.h b/GUI/QtQuick/ColorScheme.h new file mode 100644 index 000000000..233745544 --- /dev/null +++ b/GUI/QtQuick/ColorScheme.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace ColorScheme { + static const QColor tableHeaderColor = QColor("#7A88B0"); + static const QColor tableTextColor = QColor("#FFFFFF"); + + static const QColor transactionConfirmationZero = QColor("#EB6060"); + static const QColor transactionConfirmationHigh = QColor("#67D2A3"); +} diff --git a/GUI/QtQuick/FeeSuggModel.cpp b/GUI/QtQuick/FeeSuggModel.cpp new file mode 100644 index 000000000..51248bfb1 --- /dev/null +++ b/GUI/QtQuick/FeeSuggModel.cpp @@ -0,0 +1,132 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "FeeSuggModel.h" +#include "Address.h" +#include "BTCNumericTypes.h" + +#include + +namespace { + static const QHash kRoles{ + {FeeSuggestionModel::TextRole, "text"}, + {FeeSuggestionModel::BlocksRole, "nb_blocks"}, + {FeeSuggestionModel::TimeRole, "time"}, + {FeeSuggestionModel::ValueRole, "value"} + }; +} + +FeeSuggestionModel::FeeSuggestionModel(const std::shared_ptr& logger, QObject* parent) + : QAbstractTableModel(parent), logger_(logger) +{} + +int FeeSuggestionModel::rowCount(const QModelIndex &) const +{ + return data_.size() + 1; +} + +int FeeSuggestionModel::columnCount(const QModelIndex &) const +{ + return 1; +} + +QVariant FeeSuggestionModel::data(const QModelIndex& index, int role) const +{ + if (index.row() == data_.size()) { + switch (role) { + case TextRole: + return tr("Manual Fee Selection"); + case BlocksRole: + case TimeRole: + return 0; + case ValueRole: + return QString(); + default: break; + } + } + switch (role) { + case TextRole: + return tr("%1 blocks (%2): %3").arg(data_.at(index.row()).nbBlocks) + .arg(data_.at(index.row()).estTime).arg(QString::number(data_.at(index.row()).fpb, 'f', 1)); + case BlocksRole: + return data_.at(index.row()).nbBlocks; + case TimeRole: + return data_.at(index.row()).estTime; + case ValueRole: + return data_.at(index.row()).fpb; + default: break; + } + return QVariant(); +} + +QHash FeeSuggestionModel::roleNames() const +{ + return kRoles; +} + +std::map FeeSuggestionModel::feeLevels() +{ + return { + { 2, tr("20 minutes")}, + { 4, tr("40 minutes")}, + { 6, tr("60 minutes")}, + { 12, tr("2 hours")}, + { 24, tr("4 hours")}, + { 48, tr("8 hours")}, + { 144, tr("24 hours")}, + { 504, tr("3 days")}, + { 1008, tr("7 days")} + }; +} + +void FeeSuggestionModel::addRows(const std::map& feeLevels) +{ + if (feeLevels.empty()) { + return; + } + const auto& levelMapping = FeeSuggestionModel::feeLevels(); + decltype(data_) newRows; + for (const auto& feeLevel : feeLevels) { + QString estTime; + const auto& itLevel = levelMapping.find(feeLevel.first); + if (itLevel == levelMapping.end()) { + estTime = tr("%1 minutes").arg(feeLevel.first * 10); + } + else { + estTime = itLevel->second; + } + FeeSuggestion row{ feeLevel.first, std::move(estTime), feeLevel.second }; + newRows.emplace_back(std::move(row)); + } + QMetaObject::invokeMethod(this, [this, newRows] { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + newRows.size() - 1); + data_.insert(data_.cend(), newRows.begin(), newRows.end()); + endInsertRows(); + emit changed(); + }); +} + +void FeeSuggestionModel::clear() +{ + QMetaObject::invokeMethod(this, [this] { + beginResetModel(); + data_.clear(); + endResetModel(); + emit changed(); + }); +} + +float FeeSuggestionModel::fastestFee() const +{ + if (!data_.empty()) { + return data_.at(0).fpb; + } + return 0.0; +} diff --git a/GUI/QtQuick/FeeSuggModel.h b/GUI/QtQuick/FeeSuggModel.h new file mode 100644 index 000000000..dee23ae2d --- /dev/null +++ b/GUI/QtQuick/FeeSuggModel.h @@ -0,0 +1,59 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef FEE_SUGG_MODEL_H +#define FEE_SUGG_MODEL_H + +#include +#include +#include +#include +#include "BinaryData.h" + +namespace spdlog { + class logger; +} + +class FeeSuggestionModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int rowCount READ rowCount NOTIFY changed) + Q_PROPERTY(float fastestFee READ fastestFee NOTIFY changed) + +public: + enum TableRoles { TextRole = Qt::DisplayRole, BlocksRole = Qt::UserRole, TimeRole, ValueRole }; + FeeSuggestionModel(const std::shared_ptr&, QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + static std::map feeLevels(); + struct FeeSuggestion { + uint32_t nbBlocks; + QString estTime; + float fpb; + }; + void addRows(const std::map&); + void clear(); + +signals: + void changed(); + +private: + float fastestFee() const; + +private: + std::shared_ptr logger_; + std::vector data_; +}; + +#endif // FEE_SUGG_MODEL_H diff --git a/GUI/QtQuick/LeverexPlugin.cpp b/GUI/QtQuick/LeverexPlugin.cpp new file mode 100644 index 000000000..f23ad51dc --- /dev/null +++ b/GUI/QtQuick/LeverexPlugin.cpp @@ -0,0 +1,18 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "LeverexPlugin.h" +#include + +LeverexPlugin::LeverexPlugin(QObject* parent) + : Plugin(parent) +{ + qmlRegisterInterface("LeverexPlugin"); +} diff --git a/GUI/QtQuick/LeverexPlugin.h b/GUI/QtQuick/LeverexPlugin.h new file mode 100644 index 000000000..d6f37ea9f --- /dev/null +++ b/GUI/QtQuick/LeverexPlugin.h @@ -0,0 +1,29 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include "Plugin.h" + +class LeverexPlugin: public Plugin +{ + Q_OBJECT +public: + LeverexPlugin(QObject *parent); + + QString name() const override { return QLatin1Literal("Leverex"); } + QString description() const override { return tr("Leverage made simple"); } + QString icon() const override { return QLatin1Literal("qrc:/images/leverex_plugin.png"); } + QString path() const override { return {}; } + + Q_INVOKABLE void init() override {} + +private: +}; diff --git a/GUI/QtQuick/PaperBackupWriter.cpp b/GUI/QtQuick/PaperBackupWriter.cpp new file mode 100644 index 000000000..8eccae4aa --- /dev/null +++ b/GUI/QtQuick/PaperBackupWriter.cpp @@ -0,0 +1,149 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +#include "PaperBackupWriter.h" + +#include +#include +#include +#include +#include +#include + +WalletBackupPdfWriter::WalletBackupPdfWriter(const QString &walletId + , const QStringList& seed + , const QPixmap &qr) + : walletId_(walletId) + , seed_(seed) + , qr_(qr) +{ +} + +bool WalletBackupPdfWriter::write(const QString &fileName) +{ + QFile f(fileName); + bool success = f.open(QIODevice::WriteOnly); + + if (!success) { + return false; + } + + QPdfWriter pdf(fileName); + + pdf.setPageSize(QPagedPaintDevice::A4); + pdf.setResolution(kResolution); + + qreal width = (kTotalWidthInches - kMarginInches * 2.0) * kResolution; + qreal height = (kTotalHeightInches - kMarginInches * 2.0) * kResolution; + + QPageLayout layout = pdf.pageLayout(); + layout.setUnits(QPageLayout::Inch); + layout.setMargins(QMarginsF(kMarginInches, kMarginInches, kMarginInches, kMarginInches)); + pdf.setPageLayout(layout); + + QPainter p(&pdf); + draw(p, width, height); + p.end(); + + f.close(); + success = (f.error() == QFileDevice::NoError && f.size() > 0); + + return success; +} + +QPixmap WalletBackupPdfWriter::getPreview(int width, double marginScale) +{ + int viewportWidth = width; + int viewportHeight = qRound(viewportWidth * kTotalHeightInches / kTotalWidthInches); + + int windowWidth = qRound((kTotalWidthInches - kMarginInches * 2.0) * kResolution); + int windowHeight = qRound((kTotalHeightInches - kMarginInches * 2.0) * kResolution); + + int viewportMargin = qRound(kMarginInches / kTotalWidthInches * viewportWidth * marginScale); + + QPixmap image(viewportWidth, viewportHeight); + image.fill(Qt::white); + + QPainter painter(&image); + + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + painter.setViewport(viewportMargin, viewportMargin + , viewportWidth - 2 * viewportMargin + , viewportHeight - 2 * viewportMargin); + + painter.setWindow(0, 0, windowWidth, windowHeight); + + // The code in draw does not work correctly with other sizes than A4 and 1200 DPI. + // So we keep logical sizes and use viewport an window instead. + draw(painter, windowWidth, windowHeight); + + painter.end(); + + return image; +} + +void WalletBackupPdfWriter::print(QPrinter *printer) +{ + int printerResolution = printer->resolution(); + + int viewportWidth = qRound((kTotalWidthInches - kMarginInches * 2.0) * printerResolution); + int viewportHeight = qRound((kTotalHeightInches - kMarginInches * 2.0) * printerResolution); + + printer->setPageMargins(QMarginsF(kMarginInches, kMarginInches, kMarginInches, kMarginInches), QPageLayout::Inch); + + int windowWidth = qRound((kTotalWidthInches - kMarginInches * 2.0) * kResolution); + int windowHeight = qRound((kTotalHeightInches - kMarginInches * 2.0) * kResolution); + + QPainter painter(printer); + + painter.setRenderHint(QPainter::SmoothPixmapTransform); + painter.setViewport(0, 0, viewportWidth, viewportHeight); + painter.setWindow(0, 0, windowWidth, windowHeight); + + // The code in draw does not work correctly with sizes other than A4/Letter and 1200 DPI. + // So we keep logical sizes and use viewport an window instead. + draw(painter, windowWidth, windowHeight); +} + +void WalletBackupPdfWriter::draw(QPainter &p, qreal width, qreal height) +{ + const auto longSeed = seed_.length() > 12; + + QFont font = p.font(); + font.setPixelSize(150); + font.setBold(true); + + QPixmap logo = QPixmap(QString::fromUtf8(longSeed ? ":/images/RPK24.png" : ":/images/RPK12.png")); + p.drawPixmap(QRect(0, 0, width, height), logo); + + qreal relWidth = width / logo.width(); + qreal relHeight = height / logo.height(); + + + p.setFont(font); + + // Magic numbers is pixel positions of given controls on RPK_template.png + p.drawText(QPointF(140 * relWidth, 2020 * relHeight), walletId_); + + const auto topLeftX = 240; + const auto topLeftY = 2415; + const auto deltaX = 480; + const auto deltaY = 93; + for (auto i = 0; i < seed_.size() / 3; ++i) { + for (auto j = 0; j < 3; ++j) { + p.drawText(QPointF((topLeftX + j * deltaX) * relWidth, (topLeftY + i * deltaY) * relHeight), seed_.at(i * 3 + j)); + } + } + + p.drawPixmap(QRectF(1775 * relWidth, 1970 * relHeight, 790 * relWidth, 790 * relHeight), + qr_, qr_.rect()); +} diff --git a/GUI/QtQuick/PaperBackupWriter.h b/GUI/QtQuick/PaperBackupWriter.h new file mode 100644 index 000000000..e153a506c --- /dev/null +++ b/GUI/QtQuick/PaperBackupWriter.h @@ -0,0 +1,48 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +#ifndef PAPERBACKUPWRITER_H_INCLUDED +#define PAPERBACKUPWRITER_H_INCLUDED + +#include +#include + +QT_BEGIN_NAMESPACE +class QPrinter; +QT_END_NAMESPACE + +class WalletBackupPdfWriter final +{ +public: + static constexpr int kResolution = 1200; + static constexpr double kTotalWidthInches = 8.27; + static constexpr double kTotalHeightInches = 11.69; + static constexpr double kMarginInches = 0.0; + +public: + WalletBackupPdfWriter(const QString &walletId + , const QStringList& seed + , const QPixmap &qr); + + bool write(const QString &fileName); + + QPixmap getPreview(int width, double marginScale); + + void print(QPrinter *printer); + void draw(QPainter &p, qreal width, qreal height); + +private: + QString walletId_; + QStringList seed_; + QPixmap qr_; +}; + +#endif // PAPERBACKUPWRITER_H_INCLUDED diff --git a/GUI/QtQuick/PendingTransactionFilterModel.cpp b/GUI/QtQuick/PendingTransactionFilterModel.cpp new file mode 100644 index 000000000..93f476211 --- /dev/null +++ b/GUI/QtQuick/PendingTransactionFilterModel.cpp @@ -0,0 +1,37 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "PendingTransactionFilterModel.h" +#include "TxListModel.h" + +PendingTransactionFilterModel::PendingTransactionFilterModel(QObject* parent) + : QSortFilterProxyModel(parent) +{ + setDynamicSortFilter(true); + sort(0, Qt::AscendingOrder); +} + +bool PendingTransactionFilterModel::filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const +{ + const auto confirmationCountIndex = sourceModel()->index(source_row, 5); + if (sourceModel()->data(confirmationCountIndex, TxListModel::TableRoles::TableDataRole) >= 6) + { + return false; + } + + return true; +} + +bool PendingTransactionFilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + return sourceModel()->data(sourceModel()->index(left.row(), 5), TxListModel::TableRoles::TableDataRole) < + sourceModel()->data(sourceModel()->index(right.row(), 5), TxListModel::TableRoles::TableDataRole); +} diff --git a/GUI/QtQuick/PendingTransactionFilterModel.h b/GUI/QtQuick/PendingTransactionFilterModel.h new file mode 100644 index 000000000..7e109403a --- /dev/null +++ b/GUI/QtQuick/PendingTransactionFilterModel.h @@ -0,0 +1,29 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef PENDING_TRANSACTION_FILTER_MODEL_H +#define PENDING_TRANSACTION_FILTER_MODEL_H + +#include + +class PendingTransactionFilterModel: public QSortFilterProxyModel +{ + Q_OBJECT + +public: + PendingTransactionFilterModel(QObject* parent = nullptr); + +protected: + bool filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex& left, const QModelIndex& right) const override; +}; + +#endif // PENDING_TRANSACTION_FILTER_MODEL_H diff --git a/GUI/QtQuick/Plugin.cpp b/GUI/QtQuick/Plugin.cpp new file mode 100644 index 000000000..b5583c314 --- /dev/null +++ b/GUI/QtQuick/Plugin.cpp @@ -0,0 +1,15 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "Plugin.h" + +Plugin::Plugin(QObject* parent) + : QObject(parent) +{} diff --git a/GUI/QtQuick/Plugin.h b/GUI/QtQuick/Plugin.h new file mode 100644 index 000000000..958ebf6e1 --- /dev/null +++ b/GUI/QtQuick/Plugin.h @@ -0,0 +1,28 @@ +/* +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** +*/ +#pragma once + +#include +#include "Message/Adapter.h" + +class Plugin: public QObject +{ + Q_OBJECT +public: + Plugin(QObject *parent); + + virtual QString name() const = 0; + virtual QString description() const = 0; + virtual QString icon() const = 0; + virtual QString path() const = 0; + + Q_INVOKABLE virtual void init() = 0; + +private: +}; diff --git a/GUI/QtQuick/QTXSignRequest.cpp b/GUI/QtQuick/QTXSignRequest.cpp new file mode 100644 index 000000000..82c02be0a --- /dev/null +++ b/GUI/QtQuick/QTXSignRequest.cpp @@ -0,0 +1,204 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "QTXSignRequest.h" +#include +#include "Address.h" +#include "BTCNumericTypes.h" + + +QTXSignRequest::QTXSignRequest(const std::shared_ptr& logger, QObject* parent) + : QObject(parent), logger_(logger) +{} + +void QTXSignRequest::setTxSignReq(const bs::core::wallet::TXSignRequest& txReq + , const std::vector& utxos) +{ + txReq_ = txReq; + logger_->debug("[{}] {} outputs", __func__, txReq_.armorySigner_.getTxOutCount()); + if (outputsModel_) { + outputsModel_->clearOutputs(); + } + else { + outputsModel_ = new TxOutputsModel(logger_, this, true); + } + for (const auto& recip : txReq_.getRecipients([](const bs::Address&) { return true; })) { + try { + const auto& addr = bs::Address::fromRecipient(recip); + outputsModel_->addOutput(QString::fromStdString(addr.display()) + , recip->getValue() / BTCNumericTypes::BalanceDivider + , addr == txReq_.change.address); + } + catch (const std::exception& ) {} + } + emit txSignReqChanged(); + + if (utxos.empty()) { + std::vector inputs; + inputs.reserve(txReq.armorySigner_.getTxInCount()); + for (unsigned int i = 0; i < txReq.armorySigner_.getTxInCount(); ++i) { + const auto& spender = txReq.armorySigner_.getSpender(i); + inputs.push_back(spender->getUtxo()); + } + setInputs(inputs); + } + else { + setInputs(utxos); + } +} + +void QTXSignRequest::setError(const QString& err) +{ + error_ = err; + emit errorSet(); +} + +void QTXSignRequest::setInputs(const std::vector& utxos) +{ + logger_->debug("[{}] {} UTXO[s]", __func__, utxos.size()); + if (inputsModel_) { + inputsModel_->clear(); + inputsModel_->addUTXOs(utxos); + emit txSignReqChanged(); + } + else { + inputsModel_ = new TxInputsModel(logger_, nullptr, this); + inputsModel_->addUTXOs(utxos); + emit txSignReqChanged(); + } +} + +QStringList QTXSignRequest::outputAddresses() const +{ + if (!txReq_.isValid()) { + return {}; + } + QStringList result; + for (const auto& recip : txReq_.getRecipients([changeAddr = txReq_.change.address](const bs::Address& addr) + { return addr != changeAddr; })) { + try { + const auto& addr = bs::Address::fromRecipient(recip); + result.append(QString::fromStdString(addr.display())); + } + catch (const std::exception& e) { + result.append(QLatin1String("error: ") + QLatin1String(e.what())); + } + } + return result; +} + +double QTXSignRequest::outputAmountValue() const +{ + logger_->debug("[{}] {} outputs", __func__, txReq_.armorySigner_.getTxOutCount()); + return txReq_.amountReceived([changeAddr = txReq_.change.address] + (const bs::Address& addr) { return (addr != changeAddr); }) / BTCNumericTypes::BalanceDivider; +} + +QString QTXSignRequest::outputAmount() const +{ + return QString::number(outputAmountValue(), 'f', 8); +} + +QStringList QTXSignRequest::outputAmounts() const +{ + if (!txReq_.isValid()) { + return {}; + } + QStringList result; + for (const auto& recip : txReq_.getRecipients([changeAddr = txReq_.change.address](const bs::Address& addr) { return addr != changeAddr; })) { + try { + const auto& recipAddr = bs::Address::fromRecipient(recip); + result.append( + QString::number(txReq_.amountReceived([recipAddr] + (const bs::Address& addr) { return (addr == recipAddr); }) / BTCNumericTypes::BalanceDivider, 'f', 8)); + } + catch (const std::exception& e) { + result.append(QLatin1String("error: ") + QLatin1String(e.what())); + } + } + return result; +} + +QString QTXSignRequest::inputAmount() const +{ + if (!txReq_.isValid()) { + return {}; + } + return QString::number(txReq_.armorySigner_.getTotalInputsValue() / BTCNumericTypes::BalanceDivider + , 'f', 8); +} + +QString QTXSignRequest::returnAmount() const +{ + if (!txReq_.isValid()) { + return {}; + } + return QString::number(txReq_.changeAmount() / BTCNumericTypes::BalanceDivider + , 'f', 8); +} + +QString QTXSignRequest::fee() const +{ + if (!txReq_.isValid()) { + return {}; + } + return QString::number(txReq_.getFee() / BTCNumericTypes::BalanceDivider + , 'f', 8); +} + +quint32 QTXSignRequest::txSize() const +{ + if (!txReq_.isValid()) { + return 0; + } + return txReq_.estimateTxVirtSize(); +} + +QString QTXSignRequest::feePerByte() const +{ + if (!txReq_.isValid()) { + return {}; + } + return QString::number(txReq_.getFee() / (double)txReq_.estimateTxVirtSize(), 'f', 1); +} + +bool QTXSignRequest::isWatchingOnly() const +{ + return isWatchingOnly_; +} + +bool QTXSignRequest::isValid() const +{ + if (!error_.isEmpty() || !txReq_.isValid()) { + return false; + } + const int64_t inAmount = txReq_.armorySigner_.getTotalInputsValue(); + int64_t outAmount = 0; + for (const auto& recip : txReq_.getRecipients([](const bs::Address&) { return true; })) { + outAmount += recip->getValue(); + } + const int64_t fee = txReq_.getFee(); + logger_->debug("[QTXSignRequest::isValid] in={}, out={}, fee={}", inAmount, outAmount, fee); + return (std::abs(inAmount - outAmount - fee) < 100); +} + +void QTXSignRequest::setWatchingOnly(bool watchingOnly) +{ + isWatchingOnly_ = watchingOnly; + emit txSignReqChanged(); +} + +QString QTXSignRequest::comment() const +{ + if (!txReq_.isValid()) { + return {}; + } + return QString::fromStdString(txReq_.comment); +} diff --git a/GUI/QtQuick/QTXSignRequest.h b/GUI/QtQuick/QTXSignRequest.h new file mode 100644 index 000000000..d44e73eb7 --- /dev/null +++ b/GUI/QtQuick/QTXSignRequest.h @@ -0,0 +1,114 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef Q_TX_SIGN_REQUEST_H +#define Q_TX_SIGN_REQUEST_H + +#include +#include +#include +#include "BinaryData.h" +#include "CoreWallet.h" +#include "TxInputsModel.h" +#include "TxOutputsModel.h" + +namespace spdlog { + class logger; +} + +class QTXSignRequest : public QObject +{ + Q_OBJECT +public: + struct Recipient { + const bs::Address address; + double amount; + }; + + QTXSignRequest(const std::shared_ptr&, QObject* parent = nullptr); + bs::core::wallet::TXSignRequest txReq() const { return txReq_; } + void setTxSignReq(const bs::core::wallet::TXSignRequest&, const std::vector & = {}); + void setError(const QString&); + void addInput(const QUTXO::Input& input) { inputs_.push_back(input); } + std::vector inputs() const { return inputs_; } + + void setHWW(bool hww) + { + if (isHWW_ != hww) { + isHWW_ = hww; + emit hwwChanged(); + } + } + void setHWWready() + { + isHWWready_ = true; + emit hwwReady(); + } + + Q_PROPERTY(QStringList outputAddresses READ outputAddresses NOTIFY txSignReqChanged) + QStringList outputAddresses() const; + Q_PROPERTY(QString outputAmount READ outputAmount NOTIFY txSignReqChanged) + double outputAmountValue() const; + QString outputAmount() const; + Q_PROPERTY(QStringList outputAmounts READ outputAmounts NOTIFY txSignReqChanged) + QStringList outputAmounts() const; + Q_PROPERTY(QString inputAmount READ inputAmount NOTIFY txSignReqChanged) + QString inputAmount() const; + Q_PROPERTY(QString returnAmount READ returnAmount NOTIFY txSignReqChanged) + QString returnAmount() const; + Q_PROPERTY(QString fee READ fee NOTIFY txSignReqChanged) + QString fee() const; + Q_PROPERTY(quint32 txSize READ txSize NOTIFY txSignReqChanged) + quint32 txSize() const; + Q_PROPERTY(QString feePerByte READ feePerByte NOTIFY txSignReqChanged) + QString feePerByte() const; + Q_PROPERTY(QString errorText READ errorText NOTIFY errorSet) + QString errorText() const { return error_; } + Q_PROPERTY(bool isValid READ isValid NOTIFY txSignReqChanged) + bool isValid() const; + Q_PROPERTY(double maxAmount READ outputAmountValue NOTIFY txSignReqChanged) + Q_PROPERTY(bool isHWW READ isHWW NOTIFY hwwChanged) + bool isHWW() const { return isHWW_; } + Q_PROPERTY(bool isHWWready READ isHWWready NOTIFY hwwReady) + bool isHWWready() const { return isHWWready_; } + Q_PROPERTY(bool hasError READ hasError NOTIFY errorSet) + bool hasError() const { return !error_.isEmpty(); } + Q_PROPERTY(TxInputsModel* inputs READ inputsModel NOTIFY txSignReqChanged) + TxInputsModel* inputsModel() const { return inputsModel_; } + Q_PROPERTY(TxOutputsModel* outputs READ outputsModel NOTIFY txSignReqChanged) + TxOutputsModel* outputsModel() const { return outputsModel_; } + Q_PROPERTY(bool isWatchingOnly READ isWatchingOnly NOTIFY txSignReqChanged) + bool isWatchingOnly() const; + void setWatchingOnly(bool watchingOnly); + Q_PROPERTY(QString comment READ comment NOTIFY txSignReqChanged) + QString comment() const; + +signals: + void txSignReqChanged(); + void hwwChanged(); + void hwwReady(); + void errorSet(); + +private: + void setInputs(const std::vector&); + +private: + std::shared_ptr logger_; + bs::core::wallet::TXSignRequest txReq_{}; + QString error_; + std::vector inputs_; + TxInputsModel* inputsModel_{ nullptr }; + TxOutputsModel* outputsModel_{ nullptr }; + bool isHWW_{ false }; + bool isHWWready_{ false }; + bool isWatchingOnly_ { false }; +}; + +#endif // Q_TX_SIGN_REQUEST_H diff --git a/GUI/QtQuick/QtQuickAdapter.cpp b/GUI/QtQuick/QtQuickAdapter.cpp index b5f3e464d..8ed63834e 100644 --- a/GUI/QtQuick/QtQuickAdapter.cpp +++ b/GUI/QtQuick/QtQuickAdapter.cpp @@ -11,6 +11,7 @@ #include "CommonTypes.h" #include "QtQuickAdapter.h" #include +#include #include #include #include @@ -18,29 +19,78 @@ #include #include #include -#include #include +#include +#include #include #include #include #include #include +#include +#include +#include #include +#include "ArmoryServersModel.h" +#include "bip39/bip39.h" #include "BSMessageBox.h" #include "BSTerminalSplashScreen.h" -#include "Wallets/ProtobufHeadlessUtils.h" +#include "FeeSuggModel.h" +#include "hwdevicemanager.h" +#include "QTXSignRequest.h" +#include "TxOutputsModel.h" +#include "TxInputsSelectedModel.h" #include "SettingsAdapter.h" - -#include "common.pb.h" +#include "SystemFileUtils.h" +#include "Wallets/ProtobufHeadlessUtils.h" +#include "WalletBalancesModel.h" +#include "TransactionFilterModel.h" +#include "TransactionForAddressFilterModel.h" +#include "TxInputsModel.h" +#include "TxInputsSelectedModel.h" +#include "viewmodels/WalletPropertiesVM.h" +#include "PendingTransactionFilterModel.h" +#include "Utils.h" +#include "AddressFilterModel.h" +#include "viewmodels/plugins/PluginsListModel.h" +#include "LeverexPlugin.h" +#include "PaperBackupWriter.h" +#include "SideshiftPlugin.h" +#include "SideswapPlugin.h" +#include "StringUtils.h" + +#include "hardware_wallet.pb.h" #include "terminal.pb.h" +using namespace BlockSettle; using namespace BlockSettle::Common; using namespace BlockSettle::Terminal; using namespace bs::message; +using json = nlohmann::json; namespace { + const int kMinPasswordLength = 6; + std::shared_ptr staticLogger; + + static inline QString encTypeToString(bs::wallet::EncryptionType enc) + { + switch (enc) { + case bs::wallet::EncryptionType::Unencrypted : + return QObject::tr("Unencrypted"); + + case bs::wallet::EncryptionType::Password : + return QObject::tr("Password"); + + case bs::wallet::EncryptionType::Auth : + return QObject::tr("Auth eID"); + + case bs::wallet::EncryptionType::Hardware : + return QObject::tr("Hardware"); + }; + return QObject::tr("Unknown"); + } } // redirect qDebug() to the log // stdout redirected to parent process @@ -88,14 +138,70 @@ static void checkStyleSheet(QApplication& app) app.setStyleSheet(QString::fromLatin1(stylesheetFile.readAll())); } +class QRImageProvider : public QQuickImageProvider +{ +public: + QRImageProvider() : QQuickImageProvider(QQuickImageProvider::Pixmap) + {} + + QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override + { + const int sz = std::max(requestedSize.width(), requestedSize.height()); + if (size) { + *size = QSize(sz, sz); + } + return UiUtils::getQRCode(id, sz); + } +}; + + QtQuickAdapter::QtQuickAdapter(const std::shared_ptr &logger) : QObject(nullptr), logger_(logger) , userSettings_(std::make_shared(TerminalUsers::Settings)) , userWallets_(std::make_shared(TerminalUsers::Wallets)) , userBlockchain_(std::make_shared(TerminalUsers::Blockchain)) , userSigner_(std::make_shared(TerminalUsers::Signer)) + , userHWW_(bs::message::UserTerminal::create(bs::message::TerminalUsers::HWWallets)) + , txTypes_({ tr("All transactions"), tr("Received"), tr("Sent"), tr("Internal") }) + , settingsController_(std::make_shared()) + , addressFilterModel_(std::make_unique(settingsController_)) + , transactionFilterModel_(std::make_unique(settingsController_)) + , pluginsListModel_(std::make_unique()) { staticLogger = logger; + addrModel_ = new QmlAddressListModel(logger, this); + txModel_ = new TxListModel(logger, this); + expTxByAddrModel_ = new TxListForAddr(logger, this); + hwDeviceModel_ = new HwDeviceModel(logger, this); + walletBalances_ = new WalletBalancesModel(logger, this); + feeSuggModel_ = new FeeSuggestionModel(logger, this); + armoryServersModel_ = new ArmoryServersModel(logger, this); + walletPropertiesModel_ = std::make_unique(logger); + + addressFilterModel_->setSourceModel(addrModel_); + transactionFilterModel_->setSourceModel(txModel_); + + connect(settingsController_.get(), &SettingsController::changed, this, [this](ApplicationSettings::Setting key) + { + setSetting(key, settingsController_->getParam(key)); + }); + + + connect(settingsController_.get(), &SettingsController::reset, this, [this]() + { + if (settingsController_->hasParam(ApplicationSettings::Setting::SelectedWallet)) { + emit requestWalletSelection(settingsController_->getParam(ApplicationSettings::Setting::SelectedWallet).toInt()); + } + }); + + connect(walletBalances_, &WalletBalancesModel::walletSelected, this, [this](int index) + { + walletSelected(index); + }); + + + connect(armoryServersModel_, &ArmoryServersModel::changed, this, &QtQuickAdapter::onArmoryServerChanged); + connect(armoryServersModel_, &ArmoryServersModel::currentChanged, this, &QtQuickAdapter::onArmoryServerSelected); } QtQuickAdapter::~QtQuickAdapter() @@ -104,15 +210,18 @@ QtQuickAdapter::~QtQuickAdapter() void QtQuickAdapter::run(int &argc, char **argv) { logger_->debug("[QtQuickAdapter::run]"); + curl_global_init(CURL_GLOBAL_ALL); Q_INIT_RESOURCE(armory); Q_INIT_RESOURCE(qtquick); -// QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_DisableHighDpiScaling); QApplication app(argc, argv); QApplication::setOrganizationDomain(QLatin1String("blocksettle.com")); - QApplication::setWindowIcon(QIcon(QStringLiteral(":/images/bs_logo.png"))); + QApplication::setWindowIcon(QIcon(QStringLiteral(":/images/terminal.ico"))); + + scaleController_ = std::make_unique(); const QFileInfo localStyleSheetFile(QLatin1String("stylesheet.css")); QFile stylesheetFile(localStyleSheetFile.exists() @@ -152,7 +261,7 @@ void QtQuickAdapter::run(int &argc, char **argv) logoIcon = QLatin1String(":/FULL_BS_LOGO"); QPixmap splashLogo(logoIcon); - const int splashScreenWidth = 400; + const int splashScreenWidth = scaleController_->scaleRatio() * 400; { std::lock_guard lock(mutex_); splashScreen_ = new BSTerminalSplashScreen(splashLogo.scaledToWidth(splashScreenWidth @@ -167,10 +276,60 @@ void QtQuickAdapter::run(int &argc, char **argv) }); logger_->debug("[QtGuiAdapter::run] creating QML app"); + qmlRegisterInterface("QObjectList"); + qmlRegisterInterface("QTXSignRequest"); + qmlRegisterInterface("QUTXO"); + qmlRegisterInterface("QUTXOList"); + qmlRegisterInterface("QTxDetails"); + qmlRegisterInterface("ArmoryServersModel"); + qmlRegisterInterface("QTxOutputsModel"); + qmlRegisterInterface("QTxInputsModel"); + qmlRegisterInterface("QTxInputsSelectedModel"); + qmlRegisterUncreatableMetaObject(WalletBalance::staticMetaObject, "wallet.balance" + , 1, 0, "WalletBalance", tr("Error: only enums")); + qmlRegisterType("terminal.models", 1, 0, "TransactionForAddressFilterModel"); + qmlRegisterType("terminal.models", 1, 0, "PendingTransactionFilterModel"); + qmlRegisterUncreatableMetaObject(qtquick_gui::WalletPropertiesVM::staticMetaObject, "terminal.models" + , 1, 0, "WalletPropertiesVM", tr("Error: only enums")); + qmlRegisterUncreatableMetaObject(Transactions::staticMetaObject, "terminal.models" + , 1, 0, "Transactions", tr("Error: only enums")); + qmlRegisterUncreatableMetaObject(TxListModel::staticMetaObject, "terminal.models" + , 1, 0, "TxListModel", tr("Error: only enums")); + qmlRegisterUncreatableMetaObject(QmlAddressListModel::staticMetaObject, "terminal.models" + , 1, 0, "QmlAddressListModel", tr("Error: only enums")); + qmlRegisterUncreatableMetaObject(ArmoryServersModel::staticMetaObject, "terminal.models" + , 1, 0, "ArmoryServersModel", tr("Error: only enums")); + qmlRegisterUncreatableMetaObject(TxInOutModel::staticMetaObject, "terminal.models" + , 1, 0, "TxInOutModel", tr("Error: only enums")); + + //need to read files in qml + qputenv("QML_XHR_ALLOW_FILE_READ", QByteArray("1")); + QQmlApplicationEngine engine; QQuickWindow::setTextRenderType(QQuickWindow::NativeTextRendering); + rootCtxt_ = engine.rootContext(); const QFont fixedFont = QFontDatabase::systemFont(QFontDatabase::FixedFont); - engine.rootContext()->setContextProperty(QStringLiteral("fixedFont"), fixedFont); + rootCtxt_->setContextProperty(QStringLiteral("fixedFont"), fixedFont); + rootCtxt_->setContextProperty(QLatin1Literal("bsApp"), this); + rootCtxt_->setContextProperty(QLatin1Literal("addressListModel"), addrModel_); + rootCtxt_->setContextProperty(QLatin1Literal("txListModel"), txModel_); + rootCtxt_->setContextProperty(QLatin1Literal("txListByAddrModel"), expTxByAddrModel_); + rootCtxt_->setContextProperty(QLatin1Literal("hwDeviceModel"), hwDeviceModel_); + rootCtxt_->setContextProperty(QLatin1Literal("walletBalances"), walletBalances_); + rootCtxt_->setContextProperty(QLatin1Literal("feeSuggestions"), feeSuggModel_); + rootCtxt_->setContextProperty(QLatin1Literal("addressFilterModel"), addressFilterModel_.get()); + rootCtxt_->setContextProperty(QLatin1Literal("transactionFilterModel"), transactionFilterModel_.get()); + rootCtxt_->setContextProperty(QLatin1Literal("pluginsListModel"), pluginsListModel_.get()); + rootCtxt_->setContextProperty(QLatin1Literal("scaleController"), scaleController_.get()); + engine.addImageProvider(QLatin1Literal("QR"), new QRImageProvider); + + connect(&engine, &QQmlApplicationEngine::objectCreated, + [this]() { + if (nWalletsLoaded_ >= 0) { + emit walletsLoaded(nWalletsLoaded_); + } + }); + engine.load(QUrl(QStringLiteral("qrc:/qml/main.qml"))); if (engine.rootObjects().empty()) { BSMessageBox box(BSMessageBox::critical, app.tr("BlockSettle Terminal") @@ -178,6 +337,7 @@ void QtQuickAdapter::run(int &argc, char **argv) box.exec(); return; } + rootObj_ = engine.rootObjects().at(0); if (loadingDone_) { auto window = qobject_cast(rootObj_); @@ -187,14 +347,27 @@ void QtQuickAdapter::run(int &argc, char **argv) } updateStates(); + updateArmoryServers(); requestInitialSettings(); + loadPlugins(engine); + logger_->debug("[QtGuiAdapter::run] initial setup done"); app.exec(); } -bool QtQuickAdapter::process(const Envelope &env) +QStringList QtQuickAdapter::txWalletsList() const { + QStringList result = { tr("All wallets") }; + result.append(walletBalances_->walletNames()); + return result; +} + +ProcessingResult QtQuickAdapter::process(const Envelope &env) +{ + if (env.isRequest() && (env.sender->value() == user_->value())) { + return processOwnRequest(env); + } if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { case TerminalUsers::Settings: @@ -205,10 +378,12 @@ bool QtQuickAdapter::process(const Envelope &env) return processSigner(env); case TerminalUsers::Wallets: return processWallets(env); + case TerminalUsers::HWWallets: + return processHWW(env); default: break; } } - return true; + return ProcessingResult::Ignored; } bool QtQuickAdapter::processBroadcast(const bs::message::Envelope& env) @@ -216,27 +391,61 @@ bool QtQuickAdapter::processBroadcast(const bs::message::Envelope& env) if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { case TerminalUsers::System: - return processAdminMessage(env); + return (processAdminMessage(env) != ProcessingResult::Ignored); case TerminalUsers::Settings: - return processSettings(env); + return (processSettings(env) != ProcessingResult::Ignored); case TerminalUsers::Blockchain: - return processBlockchain(env); + return (processBlockchain(env) != ProcessingResult::Ignored); case TerminalUsers::Signer: - return processSigner(env); + return (processSigner(env) != ProcessingResult::Ignored); case TerminalUsers::Wallets: - return processWallets(env); + return (processWallets(env) != ProcessingResult::Ignored); + case TerminalUsers::HWWallets: + return (processHWW(env) != ProcessingResult::Ignored); default: break; } } return false; } -bool QtQuickAdapter::processSettings(const Envelope &env) +bool QtQuickAdapter::processTimeout(const bs::message::Envelope& env) +{ + if (env.receiver && (env.receiver->value() == userBlockchain_->value())) { + const auto& itTxSave = txSaveReq_.find(env.foreignId()); + if (itTxSave != txSaveReq_.end()) { + emit transactionExportFailed(tr("Failed to get supporting TXs for exporting %1") + .arg(QString::fromStdString(std::get<0>(itTxSave->second)))); + txSaveReq_.erase(itTxSave); + } + } + return false; +} + +void QtQuickAdapter::onArmoryServerSelected(int index) +{ + if (armoryServerIndex_ == index) { + return; + } + armoryServerIndex_ = index; + armoryState_ = ArmoryState::Connecting; + emit armoryStateChanged(); + + if (txModel_) { + txModel_->clear(); + } + + logger_->debug("[{}] #{}", __func__, index); + SettingsMessage msg; + msg.set_set_armory_server(index); + pushRequest(user_, userSettings_, msg.SerializeAsString()); +} + +ProcessingResult QtQuickAdapter::processSettings(const Envelope &env) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetResponse: @@ -246,13 +455,13 @@ bool QtQuickAdapter::processSettings(const Envelope &env) case SettingsMessage::kState: return processSettingsState(msg.state()); case SettingsMessage::kArmoryServers: - return processArmoryServers(msg.armory_servers()); + return processArmoryServers(env.responseId(), msg.armory_servers()); default: break; } - return true; + return ProcessingResult::Ignored; } -bool QtQuickAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) +ProcessingResult QtQuickAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) { std::map settings; for (const auto &setting : response.responses()) { @@ -282,7 +491,7 @@ bool QtQuickAdapter::processSettingsGetResponse(const SettingsMessage_SettingsRe #endif // _WIN32 //onResetSettings({}); } - break; + [[fallthrough]]; default: settings[setting.request().index()] = fromResponse(setting); @@ -290,12 +499,21 @@ bool QtQuickAdapter::processSettingsGetResponse(const SettingsMessage_SettingsRe } } if (!settings.empty()) { - //TODO: propagate settings to GUI + for (const auto& setting : settings) { + settingsCache_[static_cast(setting.first)] = setting.second; + } + emit settingChanged(); } - return true; + if (createdWalletId_.empty()) { + QMetaObject::invokeMethod(this, [this] { settingsController_->resetCache(settingsCache_); }); + } + else { + createdWalletId_.clear(); + } + return ProcessingResult::Success; } -bool QtQuickAdapter::processSettingsState(const SettingsMessage_SettingsResponse& response) +ProcessingResult QtQuickAdapter::processSettingsState(const SettingsMessage_SettingsResponse& response) { ApplicationSettings::State state; for (const auto& setting : response.responses()) { @@ -303,32 +521,35 @@ bool QtQuickAdapter::processSettingsState(const SettingsMessage_SettingsResponse fromResponse(setting); } //TODO: process setting - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processArmoryServers(const SettingsMessage_ArmoryServers& response) +ProcessingResult QtQuickAdapter::processArmoryServers(bs::message::SeqId msgId + , const SettingsMessage_ArmoryServers& response) { - QList servers; + armoryServerIndex_ = response.idx_current(); + logger_->debug("[{}] current={}, connected={}", __func__, response.idx_current() + , response.idx_connected()); + std::vector servers; + servers.reserve(response.servers_size()); for (const auto& server : response.servers()) { - servers << ArmoryServer{ QString::fromStdString(server.server_name()) + const ArmoryServer srv{ server.server_name() , static_cast(server.network_type()) - , QString::fromStdString(server.server_address()) - , std::stoi(server.server_port()), QString::fromStdString(server.server_key()) + , server.server_address(), server.server_port(), server.server_key() , SecureBinaryData::fromString(server.password()) , server.run_locally(), server.one_way_auth() }; + servers.push_back(srv); } - logger_->debug("[{}] {} servers, cur: {}, conn: {}", __func__, servers.size() - , response.idx_current(), response.idx_connected()); - //TODO - return true; + armoryServersModel_->setData(response.idx_current(), response.idx_connected(), servers); + return ProcessingResult::Success; } -bool QtQuickAdapter::processAdminMessage(const Envelope &env) +ProcessingResult QtQuickAdapter::processAdminMessage(const Envelope &env) { AdministrativeMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse admin msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case AdministrativeMessage::kComponentCreated: @@ -350,10 +571,20 @@ bool QtQuickAdapter::processAdminMessage(const Envelope &env) default: break; } updateSplashProgress(); - return true; + return ProcessingResult::Success; +} + +void QtQuickAdapter::rescanAllWallets() +{ + logger_->debug("[{}]", __func__); + WalletsMessage msg; + for (const auto& hdWallet : hdWallets_) { + msg.set_wallet_rescan(hdWallet.first); + pushRequest(user_, userWallets_, msg.SerializeAsString()); + } } -bool QtQuickAdapter::processBlockchain(const Envelope &env) +ProcessingResult QtQuickAdapter::processBlockchain(const Envelope &env) { ArmoryMessage msg; if (!msg.ParseFromString(env.message)) { @@ -362,7 +593,7 @@ bool QtQuickAdapter::processBlockchain(const Envelope &env) if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case ArmoryMessage::kLoading: @@ -370,18 +601,26 @@ bool QtQuickAdapter::processBlockchain(const Envelope &env) updateSplashProgress(); break; case ArmoryMessage::kStateChanged: - armoryState_ = msg.state_changed().state(); - blockNum_ = msg.state_changed().top_block(); - //TODO + setTopBlock(msg.state_changed().top_block()); + netType_ = static_cast(msg.state_changed().net_type()); + if ((int)armoryState_ != msg.state_changed().state()) { + armoryState_ = static_cast(msg.state_changed().state()); + emit armoryStateChanged(); + if (armoryState_ == ArmoryState::Ready) { + rescanAllWallets(); + } + } break; case ArmoryMessage::kNewBlock: - blockNum_ = msg.new_block().top_block(); - //TODO + setTopBlock(msg.new_block().top_block()); break; case ArmoryMessage::kWalletRegistered: if (msg.wallet_registered().success() && msg.wallet_registered().wallet_id().empty()) { + logger_->debug("wallets ready"); walletsReady_ = true; - //TODO + WalletsMessage msg; + msg.set_get_ledger_entries({}); + pushRequest(user_, userWallets_, msg.SerializeAsString()); } break; case ArmoryMessage::kAddressHistory: @@ -392,21 +631,32 @@ bool QtQuickAdapter::processBlockchain(const Envelope &env) return processZC(msg.zc_received()); case ArmoryMessage::kZcInvalidated: return processZCInvalidated(msg.zc_invalidated()); - default: break; + case ArmoryMessage::kTransactions: + return processTransactions(env.responseId(), msg.transactions()); + case ArmoryMessage::kTxPushResult: + if (msg.tx_push_result().result() != ArmoryMessage::PushTxSuccess) { + emit showError(tr("TX broadcast failed: %1") + .arg(QString::fromStdString(msg.tx_push_result().error_message()))); + } + break; + case ArmoryMessage::kUtxos: + return processUTXOs(env.responseId(), msg.utxos()); + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processSigner(const Envelope &env) +ProcessingResult QtQuickAdapter::processSigner(const Envelope &env) { SignerMessage msg; + if (!msg.ParseFromString(env.message)) { logger_->error("[QtGuiAdapter::processSigner] failed to parse msg #{}" , env.foreignId()); if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SignerMessage::kState: @@ -418,30 +668,63 @@ bool QtQuickAdapter::processSigner(const Envelope &env) createWallet(true); break; case SignerMessage::kSignTxResponse: - return processSignTX(msg.sign_tx_response()); + return processSignTX(env.responseId(), msg.sign_tx_response()); case SignerMessage::kWalletDeleted: - { - const auto& itWallet = hdWallets_.find(msg.wallet_deleted()); - bs::sync::WalletInfo wi; - if (itWallet == hdWallets_.end()) { - wi.ids.push_back(msg.wallet_deleted()); - } else { - wi = itWallet->second; - } - //TODO + return processWalletDeleted(msg.wallet_deleted()); + case SignerMessage::kCreatedWallet: + if (msg.created_wallet().error_msg().empty()) { + createdWalletId_ = msg.created_wallet().wallet_id(); + walletBalances_->clear(); + addrModel_->reset(createdWalletId_); + walletBalances_->setCreatedWalletId(createdWalletId_); + emit showSuccess(tr("Your wallet has successfully been created")); + } + else { + emit showError(QString::fromStdString(msg.created_wallet().error_msg())); + } + break; + case SignerMessage::kWalletPassChanged: + if (!msg.wallet_pass_changed()) { + emit showFail(tr("Incorrect password"), tr("The password you entered is incorrect")); + } + else { + emit successChangePassword(); + } + break; + case SignerMessage::kExportWoWalletResponse: + if (msg.export_wo_wallet_response().empty()) { + emit showError(tr("WO wallet export failed\nsee log for details")); + } + else { + emit successExport(QString::fromStdString(msg.export_wo_wallet_response())); } break; - default: break; + case SignerMessage::kWalletSeed: + return processWalletSeed(msg.wallet_seed()); + case SignerMessage::kWalletsReset: + hdWallets_.clear(); + if (addrModel_) { + addrModel_->reset({}); + } + if (walletBalances_) { + walletBalances_->clear(); + } + walletsReady_ = false; + break; + case SignerMessage::kWalletsListUpdated: + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processWallets(const Envelope &env) +ProcessingResult QtQuickAdapter::processWallets(const Envelope &env) { WalletsMessage msg; if (!msg.ParseFromString(env.message)) { - logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + logger_->error("[{}] failed to parse msg #{} (response#: {}, to {})\n{}", __func__ + , env.foreignId(), env.responseId(), (env.receiver ? env.receiver->name() : "") + , bs::toHex(env.message)); + return ProcessingResult::Error; } switch (msg.data_case()) { case WalletsMessage::kLoading: @@ -449,6 +732,36 @@ bool QtQuickAdapter::processWallets(const Envelope &env) updateSplashProgress(); break; + case WalletsMessage::kReady: + nWalletsLoaded_ = msg.ready(); + requestPostLoadingSettings(); + emit walletsLoaded(msg.ready()); + logger_->debug("[{}] loaded {} wallet[s]", __func__, msg.ready()); + { + if (createdWalletId_.empty()) { + if (settingsController_->hasParam(ApplicationSettings::Setting::SelectedWallet)) { + const int lastIdx = settingsController_->getParam(ApplicationSettings::Setting::SelectedWallet).toInt(); + if ((lastIdx >= 0) && (lastIdx < nWalletsLoaded_)) { + logger_->debug("[walletReady] lastIdx={}", lastIdx); + emit requestWalletSelection(lastIdx); + } + else if (nWalletsLoaded_ > 0) { + logger_->debug("[walletReady] nWallets:{}", nWalletsLoaded_); + emit requestWalletSelection(0); + } + } + } + else { + const int index = walletIndexById(createdWalletId_); + if (index != -1) { + emit requestWalletSelection(index); + } + logger_->debug("[walletReady] index={} of {}", index, createdWalletId_); + createdWalletId_.clear(); + } + } + break; + case WalletsMessage::kWalletLoaded: { const auto &wi = bs::sync::WalletInfo::fromCommonMsg(msg.wallet_loaded()); processWalletLoaded(wi); @@ -462,29 +775,28 @@ bool QtQuickAdapter::processWallets(const Envelope &env) break; case WalletsMessage::kWalletDeleted: { - const auto& wi = bs::sync::WalletInfo::fromCommonMsg(msg.wallet_deleted()); - //TODO + if (!msg.wallet_deleted().id_size()) { + showError(tr("Wallet deletion failed")); + break; + } + //const auto& wi = bs::sync::WalletInfo::fromCommonMsg(msg.wallet_deleted()); } break; case WalletsMessage::kWalletAddresses: { std::vector addresses; for (const auto &addr : msg.wallet_addresses().addresses()) { - try { - addresses.push_back({ std::move(bs::Address::fromAddressString(addr.address())) - , addr.index(), addr.wallet_id() }); + for (const auto& hdWallet : hdWallets_) { + try { + const auto& assetType = hdWallet.second.leaves.at(addr.wallet_id()); + addresses.push_back({ std::move(bs::Address::fromAddressString(addr.address())) + , addr.index(), addr.wallet_id(), assetType }); + break; + } + catch (const std::exception&) {} } - catch (const std::exception &) {} - } - const auto& walletId = msg.wallet_addresses().wallet_id(); - auto itReq = needChangeAddrReqs_.find(env.responseId()); - if (itReq != needChangeAddrReqs_.end()) { - //TODO - needChangeAddrReqs_.erase(itReq); - } - else { - //TODO } + processWalletAddresses(msg.wallet_addresses().wallet_id(), addresses); } break; @@ -502,25 +814,88 @@ bool QtQuickAdapter::processWallets(const Envelope &env) case WalletsMessage::kWalletData: return processWalletData(env.responseId(), msg.wallet_data()); case WalletsMessage::kWalletBalances: - return processWalletBalances(env, msg.wallet_balances()); + return processWalletBalances(env.responseId(), msg.wallet_balances()); case WalletsMessage::kTxDetailsResponse: return processTXDetails(env.responseId(), msg.tx_details_response()); case WalletsMessage::kWalletsListResponse: return processWalletsList(msg.wallets_list_response()); case WalletsMessage::kUtxos: - return processUTXOs(msg.utxos()); + return processUTXOs(env.responseId(), msg.utxos()); case WalletsMessage::kReservedUtxos: return processReservedUTXOs(msg.reserved_utxos()); case WalletsMessage::kWalletChanged: + if (scanningWallets_.find(msg.wallet_changed()) != scanningWallets_.end()) { + scanningWallets_.erase(msg.wallet_changed()); + emit rescanCompleted(QString::fromStdString(msg.wallet_changed())); + } + { //TODO: remove temporary code + if (!scanningWallets_.empty()) { + emit rescanCompleted(QString::fromStdString(*scanningWallets_.cbegin())); + scanningWallets_.clear(); + } + } if (walletsReady_) { - //TODO: onNeedLedgerEntries({}); + logger_->debug("ledger entries"); + WalletsMessage msg; + msg.set_get_ledger_entries({}); + pushRequest(user_, userWallets_, msg.SerializeAsString()); } break; case WalletsMessage::kLedgerEntries: return processLedgerEntries(msg.ledger_entries()); - default: break; + case WalletsMessage::kTxResponse: + return processTxResponse(env.responseId(), msg.tx_response()); + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; +} + +bs::message::ProcessingResult QtQuickAdapter::processHWW(const bs::message::Envelope& env) +{ + HW::DeviceMgrMessage msg; + if (!msg.ParseFromString(env.message)) { + logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); + return ProcessingResult::Error; + } + switch (msg.data_case()) { + case HW::DeviceMgrMessage::kAvailableDevices: + return processHWDevices(msg.available_devices()); + case HW::DeviceMgrMessage::kSignedTx: + return processHWSignedTX(msg.signed_tx()); + case HW::DeviceMgrMessage::kRequestPin: + curAuthDevice_ = bs::hww::fromMsg(msg.request_pin()); + QMetaObject::invokeMethod(this, [this] { emit invokePINentry(); }); + return bs::message::ProcessingResult::Success; + case HW::DeviceMgrMessage::kPasswordRequest: + curAuthDevice_ = bs::hww::fromMsg(msg.password_request().key()); + QMetaObject::invokeMethod(this, [this, onDevice=msg.password_request().allowed_on_device()] + { emit invokePasswordEntry(QString::fromStdString(curAuthDevice_.label), onDevice); }); + return bs::message::ProcessingResult::Success; + case HW::DeviceMgrMessage::kDeviceReady: + return processHWWready(msg.device_ready()); + default: break; + } + return bs::message::ProcessingResult::Ignored; +} + +bs::message::ProcessingResult QtQuickAdapter::processOwnRequest(const bs::message::Envelope& env) +{ + try { + const auto& msg = json::parse(env.message); + if (msg.contains("hw_wallet_timeout")) { + const auto& walletId = msg["hw_wallet_timeout"].get(); + if (readyWallets_.find(walletId) == readyWallets_.end()) { + emit showError(tr("Wallet %1 ready timeout - please connect the device") + .arg(QString::fromStdString(walletId))); + return bs::message::ProcessingResult::Success; + } + } + } + catch (const json::exception& e) { + logger_->error("[{}] failed to parse {}: {}", env.message, e.what()); + return bs::message::ProcessingResult::Error; + } + return bs::message::ProcessingResult::Ignored; } void QtQuickAdapter::updateStates() @@ -528,6 +903,25 @@ void QtQuickAdapter::updateStates() //TODO } +void QtQuickAdapter::setTopBlock(uint32_t curBlock) +{ + if (curBlock > blockNum_) { + emit topBlock(curBlock); + } + blockNum_ = curBlock; + txModel_->setCurrentBlock(blockNum_); + expTxByAddrModel_->setCurrentBlock(blockNum_); +} + +void QtQuickAdapter::loadPlugins(QQmlApplicationEngine& engine) +{ // load embedded plugins + pluginsListModel_->addPlugins({ new LeverexPlugin(this) + , new SideshiftPlugin(logger_, engine, this) + , new SideswapPlugin(this) }); + + //TODO: send broadcast to request 3rd-party plugins loading +} + //#define DEBUG_LOADING_PROGRESS void QtQuickAdapter::updateSplashProgress() { @@ -536,6 +930,9 @@ void QtQuickAdapter::updateSplashProgress() if (splashScreen_) { QMetaObject::invokeMethod(splashScreen_, [this] { QTimer::singleShot(100, [this] { + if (!splashScreen_) { + return; + } splashScreen_->hide(); splashScreen_->deleteLater(); splashScreen_ = nullptr; @@ -585,6 +982,9 @@ void QtQuickAdapter::splashProgressCompleted() logger_->error("[QtQuickAdapter::splashProgressCompleted] no main window found"); } QTimer::singleShot(100, [this] { + if (!splashScreen_) { + return; + } splashScreen_->hide(); splashScreen_->deleteLater(); splashScreen_ = nullptr; @@ -613,22 +1013,81 @@ void QtQuickAdapter::requestInitialSettings() setReq = msgReq->add_requests(); setReq->set_source(SettingSource_Local); - setReq->set_index(SetIdx_ShowInfoWidget); + setReq->set_index(SetIdx_AdvancedTXisDefault); setReq->set_type(SettingType_Bool); setReq = msgReq->add_requests(); setReq->set_source(SettingSource_Local); - setReq->set_index(SetIdx_AdvancedTXisDefault); + setReq->set_index(SetIdx_LogDefault); + setReq->set_type(SettingType_Strings); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_LogMessages); + setReq->set_type(SettingType_Strings); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_Environment); + setReq->set_type(SettingType_Int); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_ArmoryDbIP); + setReq->set_type(SettingType_String); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_ArmoryDbPort); + setReq->set_type(SettingType_String); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_ExportDir); + setReq->set_type(SettingType_String); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_AddressFilterHideUsed); setReq->set_type(SettingType_Bool); setReq = msgReq->add_requests(); setReq->set_source(SettingSource_Local); - setReq->set_index(SetIdx_CloseToTray); + setReq->set_index(SetIdx_AddressFilterHideInternal); setReq->set_type(SettingType_Bool); setReq = msgReq->add_requests(); setReq->set_source(SettingSource_Local); - setReq->set_index(SetIdx_Environment); + setReq->set_index(SetIdx_AddressFilterHideExternal); + setReq->set_type(SettingType_Bool); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_AddressFilterHideEmpty); + setReq->set_type(SettingType_Bool); + + pushRequest(user_, userSettings_, msg.SerializeAsString()); +} + +void QtQuickAdapter::requestPostLoadingSettings() +{ + SettingsMessage msg; + auto msgReq = msg.mutable_get_request(); + auto setReq = msgReq->add_requests(); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_TransactionFilterWalletName); + setReq->set_type(SettingType_String); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_TransactionFilterTransactionType); + setReq->set_type(SettingType_String); + + setReq = msgReq->add_requests(); + setReq->set_source(SettingSource_Local); + setReq->set_index(SetIdx_SelectedWallet); setReq->set_type(SettingType_Int); pushRequest(user_, userSettings_, msg.SerializeAsString()); @@ -639,52 +1098,188 @@ void QtQuickAdapter::createWallet(bool primary) logger_->debug("[{}] primary: {}", __func__, primary); } +std::string QtQuickAdapter::hdWalletIdByIndex(int index) const +{ + const auto& walletsList = walletBalances_->wallets(); + if ((index < 0) || (index >= walletsList.size())) { + return {}; + } + const auto& walletName = walletsList.at(index).walletName; + for (const auto& wallet : hdWallets_) { + if (wallet.second.name == walletName) { + return wallet.first; + } + } + return {}; +} + +int QtQuickAdapter::walletIndexById(const std::string& walletId) const +{ + const auto& walletsList = walletBalances_->wallets(); + for (int i = 0; i < walletsList.size(); ++i) { + if (walletsList.at(i).walletId == walletId) { + return i; + } + } + return -1; +} + +std::string QtQuickAdapter::generateWalletName() const +{ + int index = walletBalances_->rowCount(); + std::string name; + bool nameExists = true; + while (nameExists) { + name = "wallet" + std::to_string(++index); + nameExists = false; + for (const auto& w : walletNames_) { + if (w.second == name) { + nameExists = true; + break; + } + } + } + return name; +} + +void QtQuickAdapter::walletSelected(int index) +{ + if (index < 0 || index >= walletBalances_->wallets().size()) { + addrModel_->reset({}); + txModel_->clear(); + return; + } + + logger_->debug("[{}] {}", __func__, index); + QMetaObject::invokeMethod(this, [this, index] { + try { + const auto& walletName = walletBalances_->walletNames().at(index).toStdString(); + const auto& walletId = hdWalletIdByIndex(index); + + addrModel_->reset(walletId); + WalletsMessage msg; + msg.set_wallet_get(walletId); + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + walletInfoReq_[msgId] = walletName; + + if (hdWallets_.count(walletId) > 0) { + const auto& hdWallet = hdWallets_.at(walletId); + walletPropertiesModel_->setWalletInfo(QString::fromStdString(walletId), hdWallet); + } + settingsController_->setParam(ApplicationSettings::Setting::SelectedWallet, index); + } + catch (const std::exception&) {} + }); +} + void QtQuickAdapter::processWalletLoaded(const bs::sync::WalletInfo &wi) { - hdWallets_[*wi.ids.cbegin()] = wi; - //TODO + const bool isInitialLoad = hdWallets_.empty(); + const auto& walletId = *wi.ids.cbegin(); + hdWallets_[walletId] = wi; + logger_->debug("[QtQuickAdapter::processWalletLoaded] {} {}", wi.name, walletId); + + QMetaObject::invokeMethod(this, [this, isInitialLoad, walletId, walletName = wi.name] { + hwDeviceModel_->setLoaded(walletId); + walletBalances_->addWallet({ walletId, walletName }); + emit walletsListChanged(); + }); + + WalletsMessage msg; + msg.set_get_wallet_balances(walletId); + const auto& msgId = pushRequest(user_, userWallets_, msg.SerializeAsString() + , {}, 10, std::chrono::milliseconds{ 500 }); +#ifdef MSG_DEBUGGING + logger_->debug("[{}] sent #{} from {}", __func__, msgId, user_->name()); +#endif } -bool QtQuickAdapter::processWalletData(uint64_t msgId +static QString assetTypeToString(const bs::AssetType assetType) +{ + switch (assetType) { + case bs::AssetType::Legacy: return QObject::tr("Legacy"); + case bs::AssetType::NestedSW: return QObject::tr("Nested SegWit"); + case bs::AssetType::NativeSW: return QObject::tr("Native SegWit"); + case bs::AssetType::Unknown: + default: return QObject::tr("Unknown"); + } +} + +ProcessingResult QtQuickAdapter::processWalletData(bs::message::SeqId msgId , const WalletsMessage_WalletData& response) { - const auto& itWallet = walletGetMap_.find(msgId); - if (itWallet == walletGetMap_.end()) { - return true; + walletPropertiesModel_->setNbUsedAddrs(response.wallet_id(), response.used_addresses_size()); + + const auto& itReq = walletInfoReq_.find(msgId); + if (itReq != walletInfoReq_.end()) { + std::unordered_set walletIds; + for (const auto& addr : response.used_addresses()) { + walletIds.insert(addr.wallet_id()); + if (!addr.comment().empty()) { + try { + const auto& address = bs::Address::fromAddressString(addr.address()); + addrComments_[address] = addr.comment(); + } + catch (const std::exception&) {} + } + } + for (const auto& walletId : walletIds) { + walletNames_[walletId] = itReq->second; + } + walletInfoReq_.erase(itReq); } - const auto& walletId = itWallet->second; - const auto& walletData = bs::sync::WalletData::fromCommonMessage(response); - //TODO - { - walletGetMap_.erase(itWallet); - return true; + logger_->debug("[{}] {} used addresses", __func__, response.used_addresses_size()); + QVector> addresses; + for (const auto& addr : response.used_addresses()) { + try { + addressCache_[bs::Address::fromAddressString(addr.address())] = response.wallet_id(); + } + catch (const std::exception&) {} + addresses.append({ QString::fromStdString(addr.address()) + , QString::fromStdString(addr.comment()) + , QString::fromStdString(addr.index()) + , assetTypeToString(static_cast(addr.asset_type()))}); } - return false; + QMetaObject::invokeMethod(this, [this, response, addresses] { + addrModel_->addRows(response.wallet_id(), addresses); + }); + return ProcessingResult::Success; } -bool QtQuickAdapter::processWalletBalances(const bs::message::Envelope & +ProcessingResult QtQuickAdapter::processWalletBalances(bs::message::SeqId responseId , const WalletsMessage_WalletBalances &response) { - bs::sync::WalletBalanceData wbd; - wbd.id = response.wallet_id(); - wbd.balTotal = response.total_balance(); - wbd.balSpendable = response.spendable_balance(); - wbd.balUnconfirmed = response.unconfirmed_balance(); - wbd.nbAddresses = response.nb_addresses(); - for (const auto &addrBal : response.address_balances()) { - wbd.addrBalances.push_back({ BinaryData::fromString(addrBal.address()) - , addrBal.tx_count(), (int64_t)addrBal.total_balance(), (int64_t)addrBal.spendable_balance() - , (int64_t)addrBal.unconfirmed_balance() }); + //logger_->debug("[{}] {}", __func__, response.DebugString()); + const WalletBalancesModel::Balance bal{ response.spendable_balance() + , response.unconfirmed_balance() + , response.total_balance(), response.nb_addresses() }; + walletBalances_->setWalletBalance(response.wallet_id(), bal); + + if (!createdWalletId_.empty()) { + const int index = walletIndexById(createdWalletId_); + if (index != -1) { + emit requestWalletSelection(index); + } + createdWalletId_.clear(); } - //TODO - return true; + + QMetaObject::invokeMethod(this, [this, response]() + { + for (const auto& addrBal : response.address_balances()) { + addrModel_->updateRow(BinaryData::fromString(addrBal.address()) + , addrBal.total_balance(), addrBal.tx_count()); + } + emit walletBalanceChanged(); + }); + return ProcessingResult::Success; } -bool QtQuickAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetailsResponse &response) +ProcessingResult QtQuickAdapter::processTXDetails(bs::message::SeqId msgId + , const WalletsMessage_TXDetailsResponse &response) { - std::vector txDetails; + //logger_->debug("[{}] {}", __func__, response.DebugString()); for (const auto &resp : response.responses()) { - bs::sync::TXWalletDetails txDet{ BinaryData::fromString(resp.tx_hash()), resp.wallet_id() + bs::sync::TXWalletDetails txDet{ BinaryData::fromString(resp.tx_hash()), resp.hd_wallet_id() , resp.wallet_name(), static_cast(resp.wallet_type()) , resp.wallet_symbol(), static_cast(resp.direction()) , resp.comment(), resp.valid(), resp.amount() }; @@ -698,6 +1293,9 @@ bool QtQuickAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDet Tx tx(BinaryData::fromString(resp.tx())); if (tx.isInitialized()) { txDet.tx = std::move(tx); + txDet.tx.setTxHeight(resp.tx_height()); + /*logger_->debug("[{}] own txid: {}/{}", ownTxHash.toHexStr(true) + , txDet.tx.getThisHash().toHexStr(true));*/ } } } catch (const std::exception &e) { @@ -707,15 +1305,16 @@ bool QtQuickAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDet try { txDet.outAddresses.push_back(std::move(bs::Address::fromAddressString(addrStr))); } catch (const std::exception &e) { - logger_->warn("[QtGuiAdapter::processTXDetails] out deser error: {}", e.what()); + logger_->warn("[QtGuiAdapter::processTXDetails] out deser '{}' error: {}" + , addrStr, e.what()); } } for (const auto &inAddr : resp.input_addresses()) { try { txDet.inputAddresses.push_back({ bs::Address::fromAddressString(inAddr.address()) - , inAddr.value(), inAddr.value_string(), inAddr.wallet_name() + , inAddr.value(), inAddr.value_string(), inAddr.wallet_id(), inAddr.wallet_name() , static_cast(inAddr.script_type()) - , BinaryData::fromString(inAddr.out_hash()), inAddr.out_index() }); + , BinaryData::fromString(inAddr.out_hash()), (uint32_t)inAddr.out_index() }); } catch (const std::exception &e) { logger_->warn("[QtGuiAdapter::processTXDetails] input deser error: {}", e.what()); } @@ -723,47 +1322,67 @@ bool QtQuickAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDet for (const auto &outAddr : resp.output_addresses()) { try { txDet.outputAddresses.push_back({ bs::Address::fromAddressString(outAddr.address()) - , outAddr.value(), outAddr.value_string(), outAddr.wallet_name() + , outAddr.value(), outAddr.value_string(), outAddr.wallet_id(), outAddr.wallet_name() , static_cast(outAddr.script_type()) - , BinaryData::fromString(outAddr.out_hash()), outAddr.out_index() }); + , BinaryData::fromString(outAddr.out_hash()), (uint32_t)outAddr.out_index() }); } catch (const std::exception &) { // OP_RETURN data for valueStr txDet.outputAddresses.push_back({ bs::Address{} - , outAddr.value(), outAddr.address(), outAddr.wallet_name() + , outAddr.value(), outAddr.address(), outAddr.wallet_id(), outAddr.wallet_name() , static_cast(outAddr.script_type()), ownTxHash - , outAddr.out_index() }); + , (uint32_t)outAddr.out_index() }); } } try { txDet.changeAddress = { bs::Address::fromAddressString(resp.change_address().address()) , resp.change_address().value(), resp.change_address().value_string() - , resp.change_address().wallet_name() + , resp.change_address().wallet_id(), resp.change_address().wallet_name() , static_cast(resp.change_address().script_type()) , BinaryData::fromString(resp.change_address().out_hash()) - , resp.change_address().out_index() }; + , (uint32_t)resp.change_address().out_index() }; } catch (const std::exception &) {} - txDetails.emplace_back(std::move(txDet)); - } - if (!response.responses_size() && !response.error_msg().empty()) { - bs::sync::TXWalletDetails txDet; - txDet.comment = response.error_msg(); - txDetails.emplace_back(std::move(txDet)); + for (const auto& addr : resp.own_addresses()) { + try { + txDet.ownAddresses.push_back(bs::Address::fromAddressString(addr)); + } + catch (const std::exception&) {} + } + for (const auto& walletId : resp.wallet_ids()) { + txDet.walletIds.insert(walletId); + } + + const auto& itTxDet = txDetailReqs_.find(msgId); + if (itTxDet == txDetailReqs_.end()) { + if (txDet.direction == bs::sync::Transaction::Direction::Revoke) { + txModel_->removeTX(txDet.txHash); + } + else { + txModel_->setDetails(txDet); + } + } + else { + logger_->debug("[{}] {} inputs", __func__, txDet.inputAddresses.size()); + QMetaObject::invokeMethod(this, [this, details = itTxDet->second, txDet] { + details->setDetails(txDet); + details->onTopBlock(blockNum_); + }); // shouldn't be more than one entry + txDetailReqs_.erase(itTxDet); + } } - const auto& itZC = newZCs_.find(msgId); - if (itZC != newZCs_.end()) { - newZCs_.erase(itZC); - //TODO - } - else { - //TODO - } - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processLedgerEntries(const LedgerEntries &response) +ProcessingResult QtQuickAdapter::processLedgerEntries(const LedgerEntries &response) { + //logger_->debug("[{}] {}", __func__, response.DebugString()); + WalletsMessage msg; + auto msgReq = msg.mutable_tx_details_request(); std::vector entries; for (const auto &entry : response.entries()) { + auto txReq = msgReq->add_requests(); + txReq->set_tx_hash(entry.tx_hash()); + txReq->set_value(entry.value()); + bs::TXEntry txEntry; txEntry.txHash = BinaryData::fromString(entry.tx_hash()); txEntry.value = entry.value(); @@ -773,6 +1392,7 @@ bool QtQuickAdapter::processLedgerEntries(const LedgerEntries &response) txEntry.isChainedZC = entry.chained_zc(); txEntry.nbConf = entry.nb_conf(); for (const auto &walletId : entry.wallet_ids()) { + txReq->add_wallet_ids(walletId); txEntry.walletIds.insert(walletId); } for (const auto &addrStr : entry.addresses()) { @@ -784,12 +1404,450 @@ bool QtQuickAdapter::processLedgerEntries(const LedgerEntries &response) } entries.push_back(std::move(txEntry)); } - //TODO + txModel_->addRows(entries); + pushRequest(user_, userWallets_, msg.SerializeAsString()); + return ProcessingResult::Success; +} + +QStringList QtQuickAdapter::settingEnvironments() const +{ + return { tr("Main"), tr("Test") }; +} + +HwDeviceModel* QtQuickAdapter::devices() +{ + return nullptr; +} + +bool QtQuickAdapter::scanningDevices() const +{ + return false; +} + +QStringList QtQuickAdapter::newSeedPhrase() +{ + auto seed = CryptoPRNG::generateRandom(16); + std::vector seedData; + for (int i = 0; i < (int)seed.getSize(); ++i) { + seedData.push_back(seed.getPtr()[i]); + } + const auto& words = BIP39::create_mnemonic(seedData); + QStringList result; + for (const auto& word : words) { + result.append(QString::fromStdString(word)); + } + return result; +} + +QStringList QtQuickAdapter::completeBIP39dic(const QString& pfx) +{ + const auto& prefix = pfx.toLower().toStdString(); + if (prefix.empty()) { + return {}; + } + QStringList result; + for (int i = 0; i < BIP39::NUM_BIP39_WORDS; ++i) { + const auto& word = BIP39::get_word(i); + bool prefixMatched = true; + for (int j = 0; j < std::min(prefix.length(), word.length()); ++j) { + if (word.at(j) != prefix.at(j)) { + prefixMatched = false; + break; + } + } + if (prefixMatched) { + result.append(QString::fromStdString(word)); + } + if (result.size() >= 5) { + break; + } + } + return result; +} + +void QtQuickAdapter::copySeedToClipboard(const QStringList& seed) +{ + const auto& str = seed.join(QLatin1Char(' ')); + QGuiApplication::clipboard()->setText(str); +} + +static std::string seedFromWords(const QStringList& seed) +{ + BIP39::word_list words; + for (const auto& w : seed) { + words.add(w.toStdString()); + } + return words.to_string(); +} + +void QtQuickAdapter::createWallet(const QString& name, const QStringList& seed + , const QString& password) +{ + const auto walletName = name.isEmpty() ? generateWalletName() : name.toStdString(); + logger_->debug("[{}] {}", __func__, walletName); + SignerMessage msg; + auto msgReq = msg.mutable_create_wallet(); + msgReq->set_name(walletName); + msgReq->set_seed(seedFromWords(seed)); + msgReq->set_password(password.toStdString()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); + walletBalances_->clear(); +} + +void QtQuickAdapter::importWallet(const QString& name, const QStringList& seed + , const QString& password) +{ + const auto walletName = name.isEmpty() ? generateWalletName() : name.toStdString(); + logger_->debug("[{}] {}", __func__, walletName); + SignerMessage msg; + auto msgReq = msg.mutable_import_wallet(); + msgReq->set_name(walletName); + msgReq->set_seed(seedFromWords(seed)); + msgReq->set_password(password.toStdString()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); + walletBalances_->clear(); +} + +void QtQuickAdapter::generateNewAddress(int walletIndex, bool isNative) +{ + const auto& hdWalletId = hdWalletIdByIndex(walletIndex); + logger_->debug("[{}] #{}: {}", __func__, walletIndex, hdWalletId); + //TODO: find leaf walletId depending on isNative + WalletsMessage msg; + msg.set_create_ext_address(hdWalletId); + pushRequest(user_, userWallets_, msg.SerializeAsString()); +} + +void QtQuickAdapter::copyAddressToClipboard(const QString& addr) +{ + QGuiApplication::clipboard()->setText(addr); + if (!generatedAddress_.empty()) { + generatedAddress_.clear(); + emit addressGenerated(); + } +} + +QString QtQuickAdapter::pasteTextFromClipboard() +{ + return QGuiApplication::clipboard()->text(); +} + +bool QtQuickAdapter::validateAddress(const QString& addr) +{ + const auto& addrStr = addr.toStdString(); + try { + const auto& addr = bs::Address::fromAddressString(addrStr); + logger_->debug("[{}] type={}, format={} ({})", __func__, (int)addr.getType() + , (int)addr.format(), addrStr); + return (addr.isValid() && (addr.format() != bs::Address::Format::Binary) + && (addr.format() != bs::Address::Format::String)); + } + catch (const std::exception& e) { + logger_->warn("[{}] invalid address {}: {}", __func__, addrStr, e.what()); + return false; + } +} + +void QtQuickAdapter::updateArmoryServers() +{ + SettingsMessage msg; + msg.mutable_armory_servers_get(); + pushRequest(user_, userSettings_, msg.SerializeAsString()); +} + +bool QtQuickAdapter::addArmoryServer(const QString& name + , int netType, const QString& ipAddr, const QString& ipPort, const QString& key) +{ + for (const auto& srv : armoryServersModel_->data()) { + if (srv.name == name.toStdString()) { + logger_->debug("[{}] armory server {} already exists", __func__, name.toStdString()); + return false; + } + } + SettingsMessage msg; + auto msgReq = msg.mutable_add_armory_server(); + msgReq->set_server_name(name.toStdString()); + msgReq->set_network_type(netType); + msgReq->set_server_address(ipAddr.toStdString()); + msgReq->set_server_port(ipPort.toStdString()); + msgReq->set_server_key(key.toStdString()); + pushRequest(user_, userSettings_, msg.SerializeAsString()); + QMetaObject::invokeMethod(this, [this, name, netType, ipAddr, ipPort, key] { + armoryServersModel_->add({ name.toStdString(), static_cast(netType) + , ipAddr.toStdString(), ipPort.toStdString(), key.toStdString()}); + }); + updateArmoryServers(); return true; } +bool QtQuickAdapter::delArmoryServer(int idx) +{ + logger_->debug("[{}] #{}", __func__, idx); + SettingsMessage msg; + msg.set_del_armory_server(idx); + pushRequest(user_, userSettings_, msg.SerializeAsString()); + armoryServersModel_->del(idx); + return true; +} + +void QtQuickAdapter::onArmoryServerChanged(int index) +{ + auto srv = armoryServersModel_->data(index); + SettingsMessage msg; + auto msgReq = msg.mutable_upd_armory_server(); + msgReq->set_index(index); + auto msgSrv = msgReq->mutable_server(); + msgSrv->set_server_name(srv.name); + msgSrv->set_server_address(srv.armoryDBIp); + msgSrv->set_server_port(srv.armoryDBPort); + msgSrv->set_server_key(srv.armoryDBKey); + msgSrv->set_network_type((int)srv.netType); + pushRequest(user_, userSettings_, msg.SerializeAsString()); +} + +void QtQuickAdapter::requestFeeSuggestions() +{ + ArmoryMessage msg; + auto msgReq = msg.mutable_fee_levels_request(); + for (const auto& feeLevel : FeeSuggestionModel::feeLevels()) { + msgReq->add_levels(feeLevel.first); + } + pushRequest(user_, userBlockchain_, msg.SerializeAsString() + , {}, 10, std::chrono::milliseconds{500}); + feeSuggModel_->clear(); +} + +QTXSignRequest* QtQuickAdapter::newTXSignRequest(int walletIndex, const QStringList& recvAddrs + , const QList& recvAmounts, double fee, const QString& comment, bool isRbf + , QUTXOList* utxos, bool newChangeAddr) +{ + WalletsMessage msg; + auto msgReq = msg.mutable_tx_request(); + if (walletIndex >= 0) { + msgReq->set_hd_wallet_id(hdWalletIdByIndex(walletIndex)); + } + msgReq->set_new_change(newChangeAddr); + bool isMaxAmount = false; + if (recvAddrs.isEmpty()) { + return nullptr; + } + else { + int idx = 0; + for (const auto& recvAddr : recvAddrs) { + try { + const auto& addr = bs::Address::fromAddressString(recvAddr.toStdString()); + auto msgOut = msgReq->add_outputs(); + msgOut->set_address(addr.display()); + if (recvAmounts.size() > idx) { + msgOut->set_amount(recvAmounts.at(idx)); + } + else { + isMaxAmount = true; + } + } + catch (const std::exception& e) { + logger_->error("[{}] recvAddr {}", __func__, e.what()); + } + idx++; + } + } + msgReq->set_rbf(isRbf); + if (fee < 1.0) { + msgReq->set_fee(fee * BTCNumericTypes::BalanceDivider); + } + else { + msgReq->set_fee_per_byte(fee); + } + if (!comment.isEmpty()) { + msgReq->set_comment(comment.toStdString()); + } + auto txReq = new QTXSignRequest(logger_, this); + if (utxos) { + std::set uniqueUTXOs; + for (const auto& qUtxo : utxos->data()) { + if (qUtxo->utxo().getValue()) { + uniqueUTXOs.insert(qUtxo->utxo()); + } + else { + logger_->warn("[{}] UTXO not found, input {} @ {} is ignored", __func__ + , qUtxo->input().amount, qUtxo->input().txHash.toHexStr(true)); + } + } + logger_->debug("[{}] {} UTXOs", __func__, uniqueUTXOs.size()); + for (const auto& utxo : uniqueUTXOs) { + logger_->debug("[{}] UTXO {}@{} = {}", __func__, utxo.getTxHash().toHexStr(true) + , utxo.getTxOutIndex(), utxo.getValue()); + msgReq->add_utxos(utxo.serialize().toBinStr()); + } + } + else { + logger_->debug("[{}] no UTXOs", __func__); + } + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + txReqs_[msgId] = { txReq, isMaxAmount }; + return txReq; +} -bool QtQuickAdapter::processAddressHist(const ArmoryMessage_AddressHistory& response) +QTXSignRequest* QtQuickAdapter::createTXSignRequest(int walletIndex, QTxDetails* txDet + , double fee, const QString& comment, bool isRbf, QUTXOList* utxos) +{ + if (!txDet) { + logger_->error("[{}] TX details object cannot be null", __func__); + return nullptr; + } + if (!utxos) { + utxos = txDet->inputsModel()->getSelection(); + for (const auto& utxo : utxos->data()) { + logger_->debug("[{}] UTXO {}@{} = {}", __func__, utxo->utxo().getTxHash().toHexStr(true) + , utxo->utxo().getTxOutIndex(), utxo->utxo().getValue()); + } + } + return newTXSignRequest(walletIndex, txDet->outputsModel()->getOutputAddresses() + , txDet->outputsModel()->getOutputAmounts(), fee, comment, isRbf, utxos, true); +} + +void QtQuickAdapter::getUTXOsForWallet(int walletIndex, QTxDetails* txDet) +{ + if (txDet) { + txDet->inputsModel()->clear(); + } + logger_->debug("[{}] #{} txDet={}", __func__, walletIndex, (void*)txDet); + WalletsMessage msg; + auto msgReq = msg.mutable_get_utxos(); + msgReq->set_wallet_id(hdWalletIdByIndex(walletIndex)); + msgReq->set_confirmed_only(false); + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + + if (msgId && txDet) { + txDetailReqs_[msgId] = txDet; + } +} + +QTxDetails* QtQuickAdapter::getTXDetails(const QString& txHash, bool rbf + , bool cpfp, int selWalletIdx) +{ + auto txBinHash = BinaryData::CreateFromHex(txHash.trimmed().toStdString()); + txBinHash.swapEndian(); + logger_->debug("[{}] {} RBF:{} CPFP: {}, idx: {}", __func__, txBinHash.toHexStr(true) + , rbf, cpfp, selWalletIdx); + const auto txDet = new QTxDetails(logger_, txBinHash, this); + connect(this, &QtQuickAdapter::topBlock, txDet, &QTxDetails::onTopBlock); + + if (cpfp && (selWalletIdx >= 0)) { + const auto& hdWalletId = hdWalletIdByIndex(selWalletIdx); + if (!hdWalletId.empty()) { + txDet->addWalletFilter(hdWalletId); + const auto& hdWallet = hdWallets_.at(hdWalletId); + for (const auto& leaf : hdWallet.leaves) { + txDet->addWalletFilter(leaf.first); + } + } + } + + if (rbf || cpfp) { + ArmoryMessage msgSpendable; + auto msgWltIds = rbf ? msgSpendable.mutable_get_rbf_utxos() : msgSpendable.mutable_get_zc_utxos(); + for (const auto& wallet : hdWallets_) { + for (const auto& leaf : wallet.second.leaves) { + auto leafId = leaf.first; + msgWltIds->add_wallet_ids(leafId); + for (auto& c : leafId) { + c = std::toupper(c); + } + msgWltIds->add_wallet_ids(leafId); + } + } + const auto msgId = pushRequest(user_, userBlockchain_, msgSpendable.SerializeAsString()); + txDetailReqs_[msgId] = txDet; + } + + if (!txBinHash.empty()) { + if (txBinHash.getSize() != 32) { + logger_->warn("[{}] invalid TX hash size {}", __func__, txBinHash.getSize()); + return txDet; + } + WalletsMessage msg; + auto msgReq = msg.mutable_tx_details_request(); + auto txReq = msgReq->add_requests(); + txReq->set_tx_hash(txBinHash.toBinStr()); + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + txDetailReqs_[msgId] = txDet; + } + return txDet; +} + +void QtQuickAdapter::pollHWWallets() +{ + hwDevicesPolling_ = true; + HW::DeviceMgrMessage msg; + msg.mutable_startscan(); + pushRequest(user_, userHWW_, msg.SerializeAsString()); +} + +void QtQuickAdapter::stopHWWalletsPolling() +{ + hwDevicesPolling_ = false; +} + +void QtQuickAdapter::setHWpin(const QString& pin) +{ + if (curAuthDevice_.id.empty()) { + logger_->error("[{}] no device requested PIN", __func__); + return; + } + HW::DeviceMgrMessage msg; + auto msgReq = msg.mutable_set_pin(); + bs::hww::deviceKeyToMsg(curAuthDevice_, msgReq->mutable_key()); + msgReq->set_pin(pin.toStdString()); + pushRequest(user_, userHWW_, msg.SerializeAsString()); + curAuthDevice_ = {}; +} + +void QtQuickAdapter::setHWpassword(const QString& password) +{ + if (curAuthDevice_.id.empty()) { + logger_->error("[{}] no device requested passphrase", __func__); + return; + } + HW::DeviceMgrMessage msg; + auto msgReq = msg.mutable_set_password(); + bs::hww::deviceKeyToMsg(curAuthDevice_, msgReq->mutable_key()); + if (password.isEmpty()) { + msgReq->set_set_on_device(true); + } + else { + msgReq->set_password(password.toStdString()); + } + pushRequest(user_, userHWW_, msg.SerializeAsString()); + curAuthDevice_ = {}; +} + +void QtQuickAdapter::importWOWallet(const QString& filename) +{ + if (filename.isEmpty() || !SystemFileUtils::fileExist(filename.toStdString())) { + emit showError(tr("Invalid or non-existing wallet file %1").arg(filename)); + return; + } + //logger_->debug("[{}] {}", __func__, filename.toStdString()); + SignerMessage msg; + msg.set_import_wo_wallet(filename.toStdString()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); +} + +void QtQuickAdapter::importHWWallet(int deviceIndex) +{ + auto devKey = hwDeviceModel_->getDevice(deviceIndex); + if (devKey.id.empty() && (deviceIndex >= 0)) { + emit showError(tr("Invalid device #%1").arg(deviceIndex)); + return; + } + //logger_->debug("[{}] {}", __func__, deviceIndex); + HW::DeviceMgrMessage msg; + bs::hww::deviceKeyToMsg(devKey, msg.mutable_import_device()); + pushRequest(user_, userHWW_, msg.SerializeAsString()); +} + +ProcessingResult QtQuickAdapter::processAddressHist(const ArmoryMessage_AddressHistory& response) { bs::Address addr; try { @@ -797,10 +1855,13 @@ bool QtQuickAdapter::processAddressHist(const ArmoryMessage_AddressHistory& resp } catch (const std::exception& e) { logger_->error("[{}] invalid address: {}", __func__, e.what()); - return true; + return ProcessingResult::Error; } + ArmoryMessage msg; + auto msgReq = msg.mutable_get_txs_by_hash(); std::vector entries; for (const auto& entry : response.entries()) { + msgReq->add_tx_hashes(entry.tx_hash()); bs::TXEntry txEntry; txEntry.txHash = BinaryData::fromString(entry.tx_hash()); txEntry.value = entry.value(); @@ -821,79 +1882,255 @@ bool QtQuickAdapter::processAddressHist(const ArmoryMessage_AddressHistory& resp } entries.push_back(std::move(txEntry)); } - //TODO - return true; + expTxByAddrModel_->addRows(entries); + const auto msgId = pushRequest(user_, userBlockchain_, msg.SerializeAsString() + , {}, 3, std::chrono::milliseconds{2300} ); + expTxAddrReqs_.insert(msgId); + logger_->debug("[{}] response handling done", __func__); + return ProcessingResult::Success; } -bool QtQuickAdapter::processFeeLevels(const ArmoryMessage_FeeLevelsResponse& response) +ProcessingResult QtQuickAdapter::processFeeLevels(const ArmoryMessage_FeeLevelsResponse& response) { - std::map feeLevels; + std::map feeLevels; for (const auto& pair : response.fee_levels()) { feeLevels[pair.level()] = pair.fee(); } - //TODO - return true; + feeSuggModel_->addRows(feeLevels); + return ProcessingResult::Success; +} + +ProcessingResult QtQuickAdapter::processWalletsList(const WalletsMessage_WalletsListResponse& response) +{ + logger_->debug("[QtQuickAdapter::processWalletsList] {}", response.DebugString()); + QMetaObject::invokeMethod(this, [this, response] { + walletBalances_->clear(); + for (const auto& wallet : response.wallets()) { + const auto& hdWallet = bs::sync::HDWalletData::fromCommonMessage(wallet); + walletBalances_->addWallet({hdWallet.id, hdWallet.name}); + } + emit walletsListChanged(); + }); + return ProcessingResult::Success; } -bool QtQuickAdapter::processWalletsList(const WalletsMessage_WalletsListResponse& response) +bs::message::ProcessingResult QtQuickAdapter::processWalletDeleted(const std::string& walletId) { - std::vector wallets; - for (const auto& wallet : response.wallets()) { - wallets.push_back(bs::sync::HDWalletData::fromCommonMessage(wallet)); + if (walletId.empty()) { + emit failedDeleteWallet(); + return bs::message::ProcessingResult::Ignored; } - //TODO - return true; + logger_->debug("[{}] {}", __func__, walletId); + + walletBalances_->deleteWallet(walletId); + txModel_->clear(); + + WalletsMessage msg; + msg.set_get_ledger_entries({}); + pushRequest(user_, userWallets_, msg.SerializeAsString()); + + emit successDeleteWallet(); + + if (walletBalances_->rowCount() > 0) { + emit requestWalletSelection(0); + } + + return bs::message::ProcessingResult::Success; } -bool QtQuickAdapter::processUTXOs(const WalletsMessage_UtxoListResponse& response) +bs::message::ProcessingResult QtQuickAdapter::processWalletSeed(const BlockSettle::Common::SignerMessage_WalletSeed& response) { + if (response.bip39_seed().empty()) { + emit walletSeedAuthFailed(tr("Failed to obtain wallet seed")); + return bs::message::ProcessingResult::Error; + } + walletPropertiesModel_->setWalletSeed(response.wallet_id(), response.bip39_seed()); + emit walletSeedAuthSuccess(); + return bs::message::ProcessingResult::Success; +} + +ProcessingResult QtQuickAdapter::processUTXOs(bs::message::SeqId msgId, const WalletsMessage_UtxoListResponse& response) +{ + logger_->debug("[{}] {} UTXOs for {}", __func__, response.utxos_size(), response.wallet_id()); + const auto& itReq = txDetailReqs_.find(msgId); + if (itReq == txDetailReqs_.end()) { + walletPropertiesModel_->setNbUTXOs(response.wallet_id(), response.utxos_size()); + return ProcessingResult::Success; + } + std::vector utxos; for (const auto& serUtxo : response.utxos()) { UTXO utxo; utxo.unserialize(BinaryData::fromString(serUtxo)); utxos.push_back(std::move(utxo)); } - //TODO - return true; + itReq->second->inputsModel()->addUTXOs(utxos); + txDetailReqs_.erase(itReq); + return ProcessingResult::Success; } -bool QtQuickAdapter::processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse& response) +static bool save(const BlockSettle::Common::SignerMessage_SignTxResponse& tx, const std::string& pathName) { - //TODO + auto f = fopen(pathName.c_str(), "wb"); + if (!f) { + return false; + } + const auto& txSer = tx.SerializeAsString(); + if (fwrite(txSer.data(), 1, txSer.size(), f) != txSer.size()) { + fclose(f); + SystemFileUtils::rmFile(pathName); + return false; + } + fclose(f); return true; } -bool QtQuickAdapter::processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived& zcs) +ProcessingResult QtQuickAdapter::processSignTX(bs::message::SeqId msgId, const BlockSettle::Common::SignerMessage_SignTxResponse& response) { + if (!response.signed_tx().empty()) { + const auto& itExport = exportTxReqs_.find(msgId); + if (itExport != exportTxReqs_.end()) { + if (!save(response, itExport->second)) { + logger_->error("[{}] failed to save to {}", __func__, itExport->second); + emit failedTx(tr("Signed TX exporting to %1 failed").arg(QString::fromStdString(itExport->second))); + } + exportTxReqs_.erase(itExport); + return ProcessingResult::Success; + } + const auto& signedTX = BinaryData::fromString(response.signed_tx()); + logger_->debug("[{}] signed TX size: {}", __func__, signedTX.getSize()); + ArmoryMessage msg; + auto msgReq = msg.mutable_tx_push(); + //msgReq->set_push_id(id); + auto msgTx = msgReq->add_txs_to_push(); + msgTx->set_tx(response.signed_tx()); + //not adding TX hashes atm + pushRequest(user_, userBlockchain_, msg.SerializeAsString()); + emit successTx(); + } + else { + emit failedTx(tr("TX sign failed\nerror %1: %2").arg(response.error_code()) + .arg(QString::fromStdString(response.error_text()))); + } + return ProcessingResult::Success; +} + +ProcessingResult QtQuickAdapter::processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived& zcs) +{ + logger_->debug("[{}] {}", __func__, zcs.DebugString()); WalletsMessage msg; + std::unordered_set impactedWalletIds; auto msgReq = msg.mutable_tx_details_request(); for (const auto& zcEntry : zcs.tx_entries()) { auto txReq = msgReq->add_requests(); txReq->set_tx_hash(zcEntry.tx_hash()); - if (zcEntry.wallet_ids_size() > 0) { - txReq->set_wallet_id(zcEntry.wallet_ids(0)); + for (const auto& walletId : zcEntry.wallet_ids()) { + txReq->add_wallet_ids(walletId); + if (hdWallets_.find(walletId) != hdWallets_.end()) { + impactedWalletIds.insert(walletId); + } + else { + for (const auto& hdWallet : hdWallets_) { + for (const auto& leaf : hdWallet.second.leaves) { + if (leaf.first == walletId) { + impactedWalletIds.insert(hdWallet.first); + break; + } + } + } + } } txReq->set_value(zcEntry.value()); + + bs::TXEntry txEntry; + txEntry.txHash = BinaryData::fromString(zcEntry.tx_hash()); + txEntry.value = zcEntry.value(); + txEntry.blockNum = blockNum_; + txEntry.txTime = zcEntry.tx_time(); + txEntry.isRBF = zcEntry.rbf(); + txEntry.isChainedZC = zcEntry.chained_zc(); + txEntry.nbConf = zcEntry.nb_conf(); + for (const auto& walletId : zcEntry.wallet_ids()) { + txEntry.walletIds.insert(walletId); + } + for (const auto& addrStr : zcEntry.addresses()) { + try { + const auto& addr = bs::Address::fromAddressString(addrStr); + txEntry.addresses.push_back(addr); + } + catch (const std::exception&) {} + } + QMetaObject::invokeMethod(this, [this, txEntry] { notifyNewTransaction(txEntry); }); + } - const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); - if (!msgId) { - return false; + pushRequest(user_, userWallets_, msg.SerializeAsString()); + + for (const auto& walletId : impactedWalletIds) { + msg.set_get_wallet_balances(walletId); + pushRequest(user_, userWallets_, msg.SerializeAsString()); } - newZCs_.insert(msgId); - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processZCInvalidated(const ArmoryMessage_ZCInvalidated& zcInv) +ProcessingResult QtQuickAdapter::processZCInvalidated(const ArmoryMessage_ZCInvalidated& zcInv) { - std::vector txHashes; for (const auto& hashStr : zcInv.tx_hashes()) { - txHashes.push_back(BinaryData::fromString(hashStr)); + const auto& txHash = BinaryData::fromString(hashStr); + WalletsMessage msg; + auto msgReq = msg.mutable_tx_details_request(); + auto txReq = msgReq->add_requests(); + txReq->set_tx_hash(txHash.toBinStr()); + pushRequest(user_, userWallets_, msg.SerializeAsString()); } - //TODO - return true; + return ProcessingResult::Success; } -bool QtQuickAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& response) +bs::message::ProcessingResult QtQuickAdapter::processTransactions(bs::message::SeqId msgId + , const ArmoryMessage_Transactions& response) +{ + std::vector result; + std::set inHashes; + for (const auto& txData : response.transactions()) { + Tx tx(BinaryData::fromString(txData.tx())); + tx.setTxHeight(txData.height()); + for (int i = 0; i < tx.getNumTxIn(); ++i) { + const auto& in = tx.getTxInCopy(i); + const OutPoint op = in.getOutPoint(); + inHashes.insert(op.getTxHash()); + } + result.emplace_back(std::move(tx)); + } + const auto& itExpTxAddr = expTxAddrReqs_.find(msgId); + if (itExpTxAddr != expTxAddrReqs_.end()) { + ArmoryMessage msg; + auto msgReq = msg.mutable_get_txs_by_hash(); + for (const auto& inHash : inHashes) { + msgReq->add_tx_hashes(inHash.toBinStr()); + } + expTxAddrReqs_.erase(itExpTxAddr); + expTxByAddrModel_->setDetails(result); + const auto msgIdReq = pushRequest(user_, userBlockchain_, msg.SerializeAsString() + , {}, 3, std::chrono::milliseconds{ 2300 }); + expTxAddrInReqs_.insert(msgIdReq); + } + const auto& itExpTxAddrIn = expTxAddrInReqs_.find(msgId); + if (itExpTxAddrIn != expTxAddrInReqs_.end()) { + expTxAddrInReqs_.erase(itExpTxAddrIn); + expTxByAddrModel_->setInputs(result); + } + const auto& itTxSave = txSaveReq_.find(msgId); + if (itTxSave != txSaveReq_.end()) { + auto txReq = std::get<1>(itTxSave->second); + for (const auto& tx : result) { + txReq.armorySigner_.addSupportingTx(tx); + } + saveTransaction(std::get<0>(itTxSave->second), txReq, std::get<2>(itTxSave->second)); + txSaveReq_.erase(itTxSave); + } + return bs::message::ProcessingResult::Success; +} + +ProcessingResult QtQuickAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& response) { std::vector utxos; for (const auto& utxoSer : response.utxos()) { @@ -902,5 +2139,689 @@ bool QtQuickAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& re utxos.push_back(std::move(utxo)); } //TODO + return ProcessingResult::Success; +} + +void QtQuickAdapter::processWalletAddresses(const std::string& walletId + , const std::vector& addresses) +{ + if (addresses.empty()) { + logger_->debug("[{}] {} no addresses", __func__, walletId); + return; + } + auto hdWalletId = walletId; + for (const auto& hdWallet : hdWallets_) { + if (hdWallet.second.hasLeaf(walletId)) { + hdWalletId = hdWallet.first; + break; + } + } + const auto lastAddr = addresses.at(addresses.size() - 1); + logger_->debug("[{}] {} last address: {}", __func__, hdWalletId, lastAddr.address.display()); + addressCache_[lastAddr.address] = hdWalletId; + addrModel_->addRow(hdWalletId, { QString::fromStdString(lastAddr.address.display()) + , QString(), QString::fromStdString(lastAddr.index), assetTypeToString(lastAddr.assetType)}); + generatedAddress_ = lastAddr.address; + emit addressGenerated(); + walletBalances_->incNbAddresses(hdWalletId); + walletPropertiesModel_->incNbUsedAddrs(hdWalletId); +} + +bs::message::ProcessingResult QtQuickAdapter::processTxResponse(bs::message::SeqId msgId + , const WalletsMessage_TxResponse& response) +{ + //logger_->debug("[{}] {}", __func__, response.DebugString()); + auto txReq = bs::signer::pbTxRequestToCore(response.tx_sign_request(), logger_); + const auto& itReq = txReqs_.find(msgId); + if (itReq == txReqs_.end()) { + if (txSaveReqs_.empty()) { + //logger_->error("[{}] unknown request #{}", __func__, msgId); + return bs::message::ProcessingResult::Error; + } + const auto exportPath = *txSaveReqs_.rbegin(); + txSaveReqs_.pop_back(); + std::vector utxos; + std::set txHashes; + utxos.reserve(response.utxos_size()); + for (const auto& u : response.utxos()) { + try { + UTXO utxo; + const auto utxoSer = BinaryData::fromString(u); + utxo.unserialize(utxoSer); + if (!utxo.isInitialized()) { + throw std::runtime_error("invalid UTXO in wallets response"); + } + utxos.emplace_back(std::move(utxo)); + txHashes.insert(utxo.getTxHash()); + } + catch (const std::exception&) {} + } + + ArmoryMessage msg; + auto msgReq = msg.mutable_get_txs_by_hash(); + for (const auto& txHash : txHashes) { + msgReq->add_tx_hashes(txHash.toBinStr()); + } + const auto msgReqId = pushRequest(user_, userBlockchain_, msg.SerializeAsString() + , {}, 3, std::chrono::milliseconds{ 1000 }); + txSaveReq_[msgReqId] = {exportPath, txReq, utxos}; + return bs::message::ProcessingResult::Success; + } + auto qReq = itReq->second.txReq; + const bool noReqAmount = itReq->second.isMaxAmount; + txReqs_.erase(itReq); + if (!response.error_text().empty()) { + logger_->error("[{}] {}", __func__, response.error_text()); + qReq->setError(QString::fromStdString(response.error_text())); + return bs::message::ProcessingResult::Success; + } + + std::unordered_set hdWalletIds; + for (const auto& walletId : txReq.walletIds) { + for (const auto& hdWallet : hdWallets_) { + if (hdWallet.second.hasLeaf(walletId)) { + hdWalletIds.insert(hdWallet.first); + } + } + } + logger_->debug("[{}] {} HD walletId[s]", __func__, hdWalletIds.size()); + txReq.walletIds.clear(); + for (const auto& walletId : hdWalletIds) { + txReq.walletIds.push_back(walletId); + try { + const auto wallet = hdWallets_.at(walletId); + if (wallet.isHardware) { + qReq->setHWW(true); + logger_->debug("[{}] noReqAmt: {} for {}", __func__, noReqAmount, walletId); + if (!noReqAmount) { + readyWallets_.erase(walletId); + HW::DeviceMgrMessage msg; + msg.set_prepare_wallet_for_tx_sign(walletId); + pushRequest(user_, userHWW_, msg.SerializeAsString()); + hwwReady_[walletId] = qReq; + + const json msgTO{ {"hw_wallet_timeout", walletId} }; + pushRequest(user_, user_, msgTO.dump() + , bs::message::bus_clock::now() + std::chrono::seconds{15}); + } + } + else if (wallet.watchOnly) { + qReq->setWatchingOnly(true); + } + } + catch (const std::exception&) { + logger_->error("[{}] unknown walletId {}", __func__, walletId); + } + } + QMetaObject::invokeMethod(this, [qReq, txReq] { + qReq->setTxSignReq(txReq); + }); + return bs::message::ProcessingResult::Success; +} + +std::string QtQuickAdapter::hdWalletIdByLeafId(const std::string& walletId) const +{ + for (const auto& hdWallet : hdWallets_) { + for (const auto& leaf : hdWallet.second.leaves) { + if (walletId == leaf.first) { + return hdWallet.first; + } + } + } + return {}; +} + +bs::message::ProcessingResult QtQuickAdapter::processUTXOs(bs::message::SeqId msgId + , const ArmoryMessage_UTXOs& response) +{ + const auto& itReq = txDetailReqs_.find(msgId); + if (itReq == txDetailReqs_.end()) { + logger_->error("[{}] unknown request #{}", __func__, msgId); + return bs::message::ProcessingResult::Error; + } + std::vector utxos; + utxos.reserve(response.utxos_size()); + try { + for (const auto& serUtxo : response.utxos()) { + UTXO utxo; + utxo.unserialize(BinaryData::fromString(serUtxo)); + logger_->debug("[{}] {}@{}", __func__, utxo.getTxHash().toHexStr(true), utxo.getTxOutIndex()); + utxos.emplace_back(std::move(utxo)); + } + } + catch (const std::exception& e) { + logger_->error("[{}] failed to deser UTXO: {}", __func__, e.what()); + } + QMetaObject::invokeMethod(this, [txDet = itReq->second, utxos] { + txDet->setImmutableUTXOs(utxos); + }); + txDetailReqs_.erase(itReq); + return bs::message::ProcessingResult::Success; +} + +bs::message::ProcessingResult QtQuickAdapter::processHWDevices(const HW::DeviceMgrMessage_Devices& response) +{ + std::vector devices; + for (const auto& key : response.device_keys()) { + devices.push_back(bs::hww::fromMsg(key)); + } + hwDeviceModel_->setDevices(devices); + if (devices.empty() && hwDevicesPolling_) { + HW::DeviceMgrMessage msg; + msg.mutable_startscan(); + pushRequest(user_, userHWW_, msg.SerializeAsString() + , bs::message::bus_clock::now() + std::chrono::seconds{ 3 }); + } + else { + hwDevicesPolling_ = false; + for (const auto& hdWallet : hdWallets_) { + hwDeviceModel_->setLoaded(hdWallet.first); + } + hwDeviceModel_->findNewDevice(); + } + return bs::message::ProcessingResult::Success; +} + +bs::message::ProcessingResult QtQuickAdapter::processHWWready(const std::string& walletId) +{ + const auto& it = hwwReady_.find(walletId); + if (it == hwwReady_.end()) { + logger_->debug("[{}] wallet {} - ignored", __func__, walletId); + return bs::message::ProcessingResult::Ignored; + } + readyWallets_.insert(walletId); + it->second->setHWWready(); + auto txReq = it->second->txReq(); + txReq.armorySigner_.setLockTime(blockNum_); + { + HW::DeviceMgrMessage msg; + auto msgReq = msg.mutable_sign_tx(); + *msgReq = bs::signer::coreTxRequestToPb(txReq); + logger_->debug("[{}] {} TX request valid: {}, unsigned state: {}", __func__ + , walletId, txReq.isValid(), msgReq->unsigned_state().size()); + if (!msgReq->walletid_size()) { + msgReq->add_walletid(walletId); + } + pushRequest(user_, userHWW_, msg.SerializeAsString()); + } + hwwReady_.erase(it); + return bs::message::ProcessingResult::Success; +} + +QVariant QtQuickAdapter::getSetting(ApplicationSettings::Setting s) const +{ + try { + return settingsCache_.at(s); + } + catch (const std::exception&) {} + return {}; +} + +QString QtQuickAdapter::getSettingStringAt(ApplicationSettings::Setting s, int idx) +{ + const auto& list = getSetting(s).toStringList(); + if ((idx >= 0) && (idx < list.size())) { + return list.at(idx); + } + return {}; +} + +bs::message::ProcessingResult QtQuickAdapter::processHWSignedTX(const HW::DeviceMgrMessage_SignTxResponse& response) +{ + if (!response.signed_tx().empty()) { + ArmoryMessage msg; + auto msgReq = msg.mutable_tx_push(); + auto msgTx = msgReq->add_txs_to_push(); + msgTx->set_tx(response.signed_tx()); + pushRequest(user_, userBlockchain_, msg.SerializeAsString()); + } + else { + emit showError(QString::fromStdString(response.error_msg())); + } + return ProcessingResult::Success; +} + +void QtQuickAdapter::setSetting(ApplicationSettings::Setting s, const QVariant& val) +{ + if (settingsCache_.empty()) { + return; + } + try { + if (settingsCache_.at(s) == val) { + return; + } + } + catch (const std::exception&) {} + logger_->debug("[{}] {} = {}", __func__, (int)s, val.toString().toStdString()); + settingsCache_[s] = val; + SettingsMessage msg; + auto msgReq = msg.mutable_put_request(); + auto setResp = msgReq->add_responses(); + auto setReq = setResp->mutable_request(); + setReq->set_source(SettingSource_Local); + setReq->set_index(static_cast(s)); + setFromQVariant(val, setReq, setResp); + + pushRequest(user_, userSettings_, msg.SerializeAsString()); +} + +void QtQuickAdapter::signAndBroadcast(QTXSignRequest* txReq, const QString& password) +{ + if (!txReq) { + logger_->error("[{}] no TX request passed", __func__); + return; + } + auto txSignReq = txReq->txReq(); + logger_->debug("[{}] HW sign: {}", __func__, txReq->isHWW()); + if (txReq->isHWW()) { +/* HW::DeviceMgrMessage msg; //FIXME + *msg.mutable_sign_tx() = bs::signer::coreTxRequestToPb(txSignReq); + pushRequest(user_, userHWW_, msg.SerializeAsString());*/ + } + else { + SignerMessage msg; + auto msgReq = msg.mutable_sign_tx_request(); + //msgReq->set_id(id); + *msgReq->mutable_tx_request() = bs::signer::coreTxRequestToPb(txSignReq); + msgReq->set_sign_mode((int)SignContainer::TXSignMode::Full); + //msgReq->set_keep_dup_recips(keepDupRecips); + msgReq->set_passphrase(password.toStdString()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); + } +} + +int QtQuickAdapter::getSearchInputType(const QString& s) +{ + const auto& trimmed = s.trimmed().toStdString(); + if (trimmed.length() == 64) { // potential TX hash in hex + const auto& txId = BinaryData::CreateFromHex(trimmed); + if (txId.getSize() == 32) { // valid TXid + return 2; + } + } + if (validateAddress(s)) { + return 1; + } + return 0; +} + +void QtQuickAdapter::startAddressSearch(const QString& s) +{ + expTxByAddrModel_->clear(); + ArmoryMessage msg; + msg.set_get_address_history(s.trimmed().toStdString()); + const auto msgId = pushRequest(user_, userBlockchain_, msg.SerializeAsString() + , {}, 1, std::chrono::seconds{ 15 }); + logger_->debug("[{}] #{}", __func__, msgId); +} + +qtquick_gui::WalletPropertiesVM* QtQuickAdapter::walletProperitesVM() const +{ + return walletPropertiesModel_.get(); +} + +int QtQuickAdapter::rescanWallet(const QString& walletId) +{ + logger_->debug("[{}] {}", __func__, walletId.toStdString()); + scanningWallets_.insert(walletId.toStdString()); + WalletsMessage msg; + msg.set_wallet_rescan(walletId.toStdString()); + const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); + return (msgId == 0) ? -1 : 0; +} + +int QtQuickAdapter::renameWallet(const QString& walletId, const QString& newName) +{ + logger_->debug("[{}] {} -> {}", __func__, walletId.toStdString(), newName.toStdString()); + const auto& itWallet = hdWallets_.find(walletId.toStdString()); + if (itWallet == hdWallets_.end()) { + showError(tr("Wallet %1 not found").arg(walletId)); + return -1; + } + itWallet->second.name = newName.toStdString(); + SignerMessage msg; + auto msgReq = msg.mutable_set_wallet_name(); + msgReq->mutable_wallet()->set_wallet_id(walletId.toStdString()); + msgReq->set_new_name(newName.toStdString()); + pushRequest(user_, userSigner_, msg.SerializeAsString()); + walletBalances_->rename(walletId.toStdString(), newName.toStdString()); + walletPropertiesModel_->rename(walletId.toStdString(), newName.toStdString()); + return 0; +} + +int QtQuickAdapter::changePassword(const QString& walletId, const QString& oldPassword, const QString& newPassword) +{ + SignerMessage msg; + auto msgReq = msg.mutable_change_wallet_pass(); + auto msgWallet = msgReq->mutable_wallet(); + msgWallet->set_wallet_id(walletId.toStdString()); + msgWallet->set_password(oldPassword.toStdString()); + msgReq->set_new_password(newPassword.toStdString()); + const auto msgId = pushRequest(user_, userSigner_, msg.SerializeAsString()); + return (msgId == 0) ? -1 : 0; +} + +bool QtQuickAdapter::isWalletPasswordValid(const QString& walletId, const QString& Password) +{ + if (Password.isEmpty()) { + return false; + } + + return true; +} + +bool QtQuickAdapter::isWalletNameExist(const QString& walletName) +{ + return walletBalances_->nameExist(walletName.toStdString()); +} + +bool QtQuickAdapter::verifyPasswordIntegrity(const QString& password) +{ + return password.length() >= kMinPasswordLength; +} + +int QtQuickAdapter::exportWallet(const QString& walletId, const QString& exportDir) +{ + SignerMessage msg; + auto msgReq = msg.mutable_export_wo_wallet_request(); + msgReq->mutable_wallet()->set_wallet_id(walletId.toStdString()); + msgReq->set_output_dir(exportDir.toStdString()); + const auto msgId = pushRequest(user_, userSigner_, msg.SerializeAsString()); + return (msgId == 0) ? -1 : 0; +} + +int QtQuickAdapter::viewWalletSeedAuth(const QString& walletId, const QString& password) +{ + SignerMessage msg; + auto msgReq = msg.mutable_get_wallet_seed(); + msgReq->set_wallet_id(walletId.toStdString()); + msgReq->set_password(password.toStdString()); + const auto msgId = pushRequest(user_, userSigner_, msg.SerializeAsString()); + return (msgId == 0) ? -1 : 0; +} + +int QtQuickAdapter::deleteWallet(const QString& walletId, const QString& password) +{ + SignerMessage msg; + auto msgReq = msg.mutable_delete_wallet(); + msgReq->set_wallet_id(walletId.toStdString()); + msgReq->set_password(password.toStdString()); + const auto msgId = pushRequest(user_, userSigner_, msg.SerializeAsString()); + + return (msgId == 0) ? -1 : 0; +} + +void QtQuickAdapter::notifyNewTransaction(const bs::TXEntry& tx) +{ + const auto txId = QString::fromStdString(tx.txHash.toHexStr(true)); + auto txDetails = getTXDetails(txId); + connect(txDetails, &QTxDetails::updated, this, [txDetails, tx, this](){ + logger_->debug("[QtQuickAdapter::notifyNewTransaction] {}: {}", txDetails->txId().toStdString(), txDetails->timestamp().toStdString()); + showNotification( + tr("BlockSettle Terminal"), + tr("%1: %2\n%3: %4\n%5: %6\n%7: %8\n").arg( + tr("Date"), QDateTime::fromSecsSinceEpoch(tx.txTime).toString(gui_utils::dateTimeFormat) + , tr("Amount"), gui_utils::satoshiToQString(std::abs(tx.value)) + , tr("Type"), gui_utils::directionToQString(txDetails->direction()) + , tr("Address"), !tx.addresses.empty() ? QString::fromStdString((*tx.addresses.cbegin()).display()) : tr("") + ) + ); + txDetails->disconnect(this); + }, Qt::QueuedConnection); +} + +QString QtQuickAdapter::makeExportTransactionFilename(QTXSignRequest* request) +{ + if (request->txReq().walletIds.empty()) { + emit transactionExportFailed(tr("TX request doesn't contain wallets")); + return QString(); + } + const auto& timestamp = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto walletId = hdWalletIdByLeafId(*request->txReq().walletIds.cbegin()); + if (walletId.empty()) { + walletId = *request->txReq().walletIds.cbegin(); + } + const std::string filename = "BlockSettle_" + walletId + "_" + std::to_string(timestamp) + "_unsigned.bin"; + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + QString::fromLatin1("/") + QString::fromStdString(filename); +} + +void QtQuickAdapter::exportTransaction(const QUrl& path, QTXSignRequest* request) +{ + txSaveReqs_.push_back(path.toLocalFile().toStdString()); +} + +void QtQuickAdapter::saveTransaction(const std::string& exportPath, const bs::core::wallet::TXSignRequest& txReq + , const std::vector& utxos) +{ + //const auto& txSer = bs::signer::coreTxRequestToPb(txReq, false).SerializeAsString(); + WalletsMessage_TxOfflineExport msg; + for (const auto& walletId : txReq.walletIds) { + msg.add_wallet_id(walletId); + } + msg.set_tx(txReq.armorySigner_.serializeState().SerializeAsString()); + msg.set_psbt(txReq.armorySigner_.toPSBT().toBinStr()); + for (const auto& utxo : utxos) { + msg.add_utxos(utxo.serialize().toBinStr()); + } + const auto& txSer = msg.SerializeAsString(); + auto f = fopen(exportPath.c_str(), "wb"); + if (!f) { + emit transactionExportFailed(tr("Failed to open %1 for writing").arg(QString::fromStdString(exportPath))); + return; + } + if (fwrite(txSer.data(), 1, txSer.size(), f) != txSer.size()) { + logger_->error("[{}] failed to write {} bytes to {}", __func__, txSer.size(), exportPath); + emit transactionExportFailed(tr("Failed to write %1 bytes to %2. Disk full?") + .arg(txSer.size()).arg(QString::fromStdString(exportPath))); + return; + } + fclose(f); + logger_->debug("[{}] exporting {} done", __func__, exportPath); + emit transactionExported(QString::fromStdString(exportPath)); +} + +QTXSignRequest* QtQuickAdapter::importTransaction(const QUrl& path) +{ + const auto& pathName = path.toLocalFile().toStdString(); + auto f = fopen(pathName.c_str(), "rb"); + if (!f) { + emit transactionImportFailed(tr("Failed to open %1 for reading") + .arg(QString::fromStdString(pathName))); + return nullptr; + } + std::string txSer; + char buf[512]; + size_t rc = 0; + while ((rc = fread(buf, 1, sizeof(buf), f)) > 0) { + txSer.append(std::string(buf, rc)); + } + if (txSer.empty()) { + emit transactionImportFailed(tr("Failed to read from %1") + .arg(QString::fromStdString(pathName))); + return nullptr; + } + //Blocksettle::Communication::headless::SignTxRequest msg; + WalletsMessage_TxOfflineExport msg; + if (!msg.ParseFromString(txSer)) { + emit transactionImportFailed(tr("Failed to parse %1 bytes from %2") + .arg(txSer.size()).arg(QString::fromStdString(pathName))); + return nullptr; + } + //const auto& txReq = bs::signer::pbTxRequestToCore(msg); + bs::core::wallet::TXSignRequest txReq; + for (const auto& walletId : msg.wallet_id()) { + bool walletLoaded = false; + for (const auto& hdWallet : hdWallets_) { + if (hdWallet.first == walletId) { + walletLoaded = true; + break; + } + else { + for (const auto& leaf : hdWallet.second.leaves) { + if (leaf.first == walletId) { + walletLoaded = true; + break; + } + } + } + } + if (!walletLoaded) { + emit transactionImportFailed(tr("Wallet %1 is not loaded - signing is not possible") + .arg(QString::fromStdString(walletId))); + return nullptr; + } + txReq.walletIds.push_back(walletId); + } +/* try { // PSBT doesn't seem to be properly implemented in Armory atm + txReq.armorySigner_ = Armory::Signer::Signer::fromPSBT(BinaryData::fromString(msg.psbt())); + } + catch (const std::exception& e) { + logger_->error("[{}] invalid PSBT: {}", __func__, e.what());*/ + { + Codec_SignerState::SignerState state; + if (state.ParseFromString(msg.tx())) { + txReq.armorySigner_.deserializeState(state); + } + } + if (!txReq.isValid()) { + emit transactionImportFailed(tr("Failed to obtain valid data from %1") + .arg(QString::fromStdString(pathName))); + return nullptr; + } + std::vector utxos; + for (const auto& u : msg.utxos()) { + try { + UTXO utxo; + utxo.unserialize(BinaryData::fromString(u)); + if (!utxo.isInitialized()) { + throw std::runtime_error("not inited"); + } + utxos.emplace_back(std::move(utxo)); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to deser UTXO: {}", __func__, e.what()); + } + } + auto txSignRequest = new QTXSignRequest(logger_, this); + txSignRequest->setTxSignReq(txReq, utxos); + return txSignRequest; +} + +void QtQuickAdapter::exportSignedTX(const QUrl& path, QTXSignRequest* request + , const QString& password) +{ + const auto& txSignReq = request->txReq(); + const auto& timestamp = std::chrono::duration_cast(std::chrono::system_clock::now().time_since_epoch()).count(); + auto walletId = hdWalletIdByLeafId(*txSignReq.walletIds.cbegin()); + if (walletId.empty()) { + walletId = *txSignReq.walletIds.cbegin(); + } + const std::string filename = "BlockSettle_" + walletId + "_" + std::to_string(timestamp) + "_signed.bin"; + std::string pathName = path.toLocalFile().toStdString() + "/" + filename; + + SignerMessage msg; + auto msgReq = msg.mutable_sign_tx_request(); + //msgReq->set_id(id); + *msgReq->mutable_tx_request() = bs::signer::coreTxRequestToPb(txSignReq); + msgReq->set_sign_mode((int)SignContainer::TXSignMode::Full); + //msgReq->set_keep_dup_recips(keepDupRecips); + msgReq->set_passphrase(password.toStdString()); + const auto msgId = pushRequest(user_, userSigner_, msg.SerializeAsString()); + exportTxReqs_[msgId] = pathName; +} + +bool QtQuickAdapter::broadcastSignedTX(const QUrl& path) +{ + const auto& pathName = path.toLocalFile().toStdString(); + auto f = fopen(pathName.c_str(), "rb"); + if (!f) { + emit transactionImportFailed(tr("Failed to open %1 for reading") + .arg(QString::fromStdString(pathName))); + return false; + } + std::string txSer; + char buf[512]; + size_t rc = 0; + while ((rc = fread(buf, 1, sizeof(buf), f)) > 0) { + txSer.append(std::string(buf, rc)); + } + if (txSer.empty()) { + emit transactionImportFailed(tr("Failed to read from %1") + .arg(QString::fromStdString(pathName))); + return false; + } + BlockSettle::Common::SignerMessage_SignTxResponse msgF; + if (!msgF.ParseFromString(txSer)) { + emit transactionImportFailed(tr("Failed to parse %1 bytes from %2") + .arg(txSer.size()).arg(QString::fromStdString(pathName))); + return false; + } + const auto& signedTX = BinaryData::fromString(msgF.signed_tx()); + logger_->debug("[{}] signed TX size: {}", __func__, signedTX.getSize()); + if (signedTX.empty()) { + emit transactionImportFailed(tr("Invalid TX data from %1") + .arg(QString::fromStdString(pathName))); + return false; + } + ArmoryMessage msg; + auto msgReq = msg.mutable_tx_push(); + //msgReq->set_push_id(id); + auto msgTx = msgReq->add_txs_to_push(); + msgTx->set_tx(signedTX.toBinStr()); + //not adding TX hashes atm + pushRequest(user_, userBlockchain_, msg.SerializeAsString()); + return true; +} + +bool QtQuickAdapter::isRequestReadyToSend(QTXSignRequest* request) +{ + if (request == nullptr) { + return false; + } + + const auto& walletIds = request->txReq().walletIds; + for (const auto& id : walletIds) { + bool isInWallets = false; + for (const auto [walletId, wallet] : hdWallets_) { + if ((id == walletId || wallet.hasLeaf(id)) && !wallet.watchOnly) { + isInWallets = true; + break; + } + } + if (!isInWallets) { + return false; + } + } return true; } + +QString QtQuickAdapter::makeExportWalletToPdfPath(const QStringList& seed) +{ + const auto params = walletInfoFromSeed(seed); + const auto& walletId = params.first; + + return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + + QString::fromLatin1("/") + + QString::fromLatin1("BlockSettle_%1.pdf").arg(walletId); +} + +void QtQuickAdapter::exportWalletToPdf(const QUrl& path, const QStringList& seed) +{ + const auto params = walletInfoFromSeed(seed); + const auto& walletId = params.first; + const auto& walletPrivateRootKey = params.second; + + WalletBackupPdfWriter writer( + walletId, + seed, + QRImageProvider().requestPixmap(walletPrivateRootKey, nullptr, QSize(200, 200))); + writer.write(path.toLocalFile()); +} + +std::pair QtQuickAdapter::walletInfoFromSeed(const QStringList& bip39Seed) +{ + const bs::core::wallet::Seed seed(seedFromWords(bip39Seed), netType_); + const auto& rootNode = seed.getNode(); + return { QString::fromStdString(seed.getWalletId()) + , QString::fromStdString(rootNode.getBase58().toBinStr()) }; +} diff --git a/GUI/QtQuick/QtQuickAdapter.h b/GUI/QtQuick/QtQuickAdapter.h index 03312cd93..3e638afdf 100644 --- a/GUI/QtQuick/QtQuickAdapter.h +++ b/GUI/QtQuick/QtQuickAdapter.h @@ -12,12 +12,23 @@ #define QT_QUICK_ADAPTER_H #include +#include #include #include "Address.h" +#include "ArmoryServersModel.h" +#include "AddressListModel.h" #include "ApiAdapter.h" -#include "Wallets/SignContainer.h" +#include "ApplicationSettings.h" +#include "hwdevicemodel.h" #include "ThreadSafeClasses.h" +#include "TxListModel.h" #include "UiUtils.h" +#include "Wallets/SignContainer.h" +#include "viewmodels/WalletPropertiesVM.h" +#include "SettingsController.h" +#include "ScaleController.h" + +#include "common.pb.h" namespace bs { namespace gui { @@ -27,22 +38,9 @@ namespace bs { } } namespace BlockSettle { - namespace Common { - class ArmoryMessage_AddressHistory; - class ArmoryMessage_FeeLevelsResponse; - class ArmoryMessage_ZCInvalidated; - class ArmoryMessage_ZCReceived; - class LedgerEntries; - class OnChainTrackMessage_AuthAddresses; - class OnChainTrackMessage_AuthState; - class SignerMessage_SignTxResponse; - class WalletsMessage_AuthKey; - class WalletsMessage_ReservedUTXOs; - class WalletsMessage_TXDetailsResponse; - class WalletsMessage_UtxoListResponse; - class WalletsMessage_WalletBalances; - class WalletsMessage_WalletData; - class WalletsMessage_WalletsListResponse; + namespace HW { + class DeviceMgrMessage_Devices; + class DeviceMgrMessage_SignTxResponse; } namespace Terminal { class AssetsMessage_Balance; @@ -63,10 +61,22 @@ namespace BlockSettle { class SettingsMessage_ArmoryServers; class SettingsMessage_SettingsResponse; class SettingsMessage_SignerServers; + class SignerMessage_WalletSeed; } } - +class ArmoryServersModel; class BSTerminalSplashScreen; +class FeeSuggestionModel; +class HwDeviceModel; +class QQmlContext; +class QmlWalletsList; +class QTxDetails; +class QTXSignRequest; +class WalletBalancesModel; +class AddressFilterModel; +class TransactionFilterModel; +class PendingTransactionFilterModel; +class PluginsListModel; class QtQuickAdapter : public QObject, public ApiBusAdapter, public bs::MainLoopRuner { @@ -76,71 +86,284 @@ class QtQuickAdapter : public QObject, public ApiBusAdapter, public bs::MainLoop QtQuickAdapter(const std::shared_ptr &); ~QtQuickAdapter() override; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; + bool processTimeout(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } std::string name() const override { return "QtQuick"; } void run(int &argc, char **argv) override; + Q_PROPERTY(QStringList txWalletsList READ txWalletsList NOTIFY walletsListChanged) + QStringList txWalletsList() const; + Q_PROPERTY(QStringList txTypesList READ txTypesList CONSTANT) + QStringList txTypesList() const { return txTypes_; } + + Q_PROPERTY(QString generatedAddress READ generatedAddress NOTIFY addressGenerated) + QString generatedAddress() const { return QString::fromStdString(generatedAddress_.display()); } + + // Settings properties + Q_PROPERTY(QString settingLogFile READ settingLogFile WRITE setLogFile NOTIFY settingChanged) + QString settingLogFile() { return getSettingStringAt(ApplicationSettings::Setting::logDefault, 0); } + void setLogFile(const QString& str) { setSetting(ApplicationSettings::Setting::logDefault, str); } + + Q_PROPERTY(QString settingMsgLogFile READ settingMsgLogFile WRITE setMsgLogFile NOTIFY settingChanged) + QString settingMsgLogFile() { return getSettingStringAt(ApplicationSettings::Setting::logMessages, 0); } + void setMsgLogFile(const QString& str) { setSetting(ApplicationSettings::Setting::logMessages, str); } + + Q_PROPERTY(bool settingAdvancedTX READ settingAdvancedTX WRITE setAdvancedTX NOTIFY settingChanged) + bool settingAdvancedTX() { return getSetting(ApplicationSettings::Setting::AdvancedTxDialogByDefault).toBool(); } + void setAdvancedTX(bool b) { setSetting(ApplicationSettings::Setting::AdvancedTxDialogByDefault, b); } + + Q_PROPERTY(int settingEnvironment READ settingEnvironment WRITE setEnvironment NOTIFY settingChanged) + int settingEnvironment() { return getSetting(ApplicationSettings::Setting::envConfiguration).toInt(); } + void setEnvironment(int i) { setSetting(ApplicationSettings::Setting::envConfiguration, i); } + + Q_PROPERTY(QStringList settingEnvironments READ settingEnvironments) + QStringList settingEnvironments() const; + + Q_PROPERTY(bool settingActivated READ settingActivated WRITE setActivated NOTIFY settingChanged) + bool settingActivated() const { return getSetting(ApplicationSettings::Setting::initialized).toBool(); } + void setActivated(bool b) { setSetting(ApplicationSettings::Setting::initialized, b); } + + Q_PROPERTY(QString settingExportDir READ settingExportDir WRITE setExportDir NOTIFY settingChanged) + QString settingExportDir() const { return getSetting(ApplicationSettings::Setting::ExportDir).toString(); } + void setExportDir(const QString& str) {setSetting(ApplicationSettings::Setting::ExportDir, str); } + + Q_PROPERTY(int armoryState READ armoryState NOTIFY armoryStateChanged) + int armoryState() const { return (int)armoryState_; } + + Q_PROPERTY(int networkType READ networkType NOTIFY networkTypeChanged) + int networkType() const { return (int)netType_; } + + Q_PROPERTY(HwDeviceModel* devices READ devices NOTIFY devicesChanged) + HwDeviceModel* devices(); + Q_PROPERTY(bool scanningDevices READ scanningDevices NOTIFY scanningChanged) + bool scanningDevices() const; + + Q_PROPERTY(qtquick_gui::WalletPropertiesVM* walletProperitesVM READ walletProperitesVM CONSTANT) + qtquick_gui::WalletPropertiesVM* walletProperitesVM() const; + + Q_PROPERTY(ArmoryServersModel* armoryServersModel READ armoryServersModel CONSTANT) + ArmoryServersModel* armoryServersModel() const { return armoryServersModel_; } + + // QML-invokable methods + Q_INVOKABLE QStringList newSeedPhrase(); + Q_INVOKABLE QStringList completeBIP39dic(const QString& prefix); + Q_INVOKABLE void copySeedToClipboard(const QStringList&); + Q_INVOKABLE void createWallet(const QString& name, const QStringList& seed + , const QString& password); + Q_INVOKABLE void importWallet(const QString& name, const QStringList& seed + , const QString& password); + Q_INVOKABLE void pollHWWallets(); + Q_INVOKABLE void stopHWWalletsPolling(); + Q_INVOKABLE void setHWpin(const QString&); + Q_INVOKABLE void setHWpassword(const QString&); + Q_INVOKABLE void importWOWallet(const QString& filename); + Q_INVOKABLE void importHWWallet(int deviceIndex); + Q_INVOKABLE void generateNewAddress(int walletIndex, bool isNative); + Q_INVOKABLE void copyAddressToClipboard(const QString& addr); + Q_INVOKABLE QString pasteTextFromClipboard(); + Q_INVOKABLE bool validateAddress(const QString& addr); + Q_INVOKABLE void updateArmoryServers(); + Q_INVOKABLE bool addArmoryServer(const QString& name + , int netType, const QString& ipAddr, const QString& ipPort, const QString& key = {}); + Q_INVOKABLE bool delArmoryServer(int idx); + + Q_INVOKABLE void requestFeeSuggestions(); + Q_INVOKABLE QTXSignRequest* newTXSignRequest(int walletIndex, const QStringList& recvAddrs + , const QList& recvAmounts, double fee, const QString& comment = {} + , bool isRbf = true, QUTXOList* utxos = nullptr, bool newChangeAddr = false); + Q_INVOKABLE QTXSignRequest* createTXSignRequest(int walletIndex, QTxDetails* + , double fee, const QString& comment = {}, bool isRbf = true, QUTXOList* utxos = nullptr); + Q_INVOKABLE void getUTXOsForWallet(int walletIndex, QTxDetails*); + Q_INVOKABLE void signAndBroadcast(QTXSignRequest*, const QString& password); + Q_INVOKABLE int getSearchInputType(const QString&); + Q_INVOKABLE void startAddressSearch(const QString&); + Q_INVOKABLE QTxDetails* getTXDetails(const QString& txHash, bool rbf = false + , bool cpfp = false, int selWalletIdx = -1); + Q_INVOKABLE int changePassword(const QString& walletId, const QString& oldPassword, const QString& newPassword); + Q_INVOKABLE bool isWalletNameExist(const QString& walletName); + Q_INVOKABLE bool isWalletPasswordValid(const QString& walletId, const QString& password); + Q_INVOKABLE bool verifyPasswordIntegrity(const QString& password); + Q_INVOKABLE int exportWallet(const QString& walletId, const QString & exportDir); + Q_INVOKABLE int viewWalletSeedAuth(const QString& walletId, const QString& password); + Q_INVOKABLE int deleteWallet(const QString& walletId, const QString& password); + Q_INVOKABLE int rescanWallet(const QString& walletId); + Q_INVOKABLE int renameWallet(const QString& walletId, const QString& newName); + Q_INVOKABLE void walletSelected(int); + Q_INVOKABLE QString makeExportTransactionFilename(QTXSignRequest* request); + Q_INVOKABLE void exportTransaction(const QUrl& path, QTXSignRequest* request); + Q_INVOKABLE QTXSignRequest* importTransaction(const QUrl& path); + Q_INVOKABLE void exportSignedTX(const QUrl& path, QTXSignRequest* request, const QString& password); + Q_INVOKABLE bool broadcastSignedTX(const QUrl& path); + Q_INVOKABLE bool isRequestReadyToSend(QTXSignRequest* request); + Q_INVOKABLE QString makeExportWalletToPdfPath(const QStringList& seed); + Q_INVOKABLE void exportWalletToPdf(const QUrl& path, const QStringList& seed); + std::pair walletInfoFromSeed(const QStringList& seed); + +signals: + void walletsListChanged(); + void walletsLoaded(quint32 nb); + void walletBalanceChanged(); + void addressGenerated(); + void settingChanged(); + void armoryStateChanged(); + void networkTypeChanged(); + void devicesChanged(); + void scanningChanged(); + void invokePINentry(); + void invokePasswordEntry(const QString& devName, bool acceptOnDevice); + void showError(const QString&); + void showFail(const QString&, const QString&); + void showNotification(QString, QString); + void successExport(const QString& nameExport); + void requestWalletSelection(quint32 index); + void successChangePassword(); + void failedDeleteWallet(); + void successDeleteWallet(); + void walletSeedAuthFailed(const QString&); + void walletSeedAuthSuccess(); + void transactionExported(const QString& destFilename); + void transactionExportFailed(const QString& errorMessage); + void transactionImportFailed(const QString& errorMessage); + void successTx(); + void failedTx(const QString&); + void showSuccess(const QString&); + void rescanCompleted(const QString& walletId); + void topBlock(quint32); + +private slots: + void onArmoryServerChanged(int index); + void onArmoryServerSelected(int index); + private: - bool processSettings(const bs::message::Envelope &); - bool processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - bool processSettingsState(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - bool processArmoryServers(const BlockSettle::Terminal::SettingsMessage_ArmoryServers&); - bool processAdminMessage(const bs::message::Envelope &); - bool processBlockchain(const bs::message::Envelope &); - bool processSigner(const bs::message::Envelope &); - bool processWallets(const bs::message::Envelope &); + bs::message::ProcessingResult processSettings(const bs::message::Envelope &); + bs::message::ProcessingResult processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + bs::message::ProcessingResult processSettingsState(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + bs::message::ProcessingResult processArmoryServers(bs::message::SeqId + , const BlockSettle::Terminal::SettingsMessage_ArmoryServers&); + bs::message::ProcessingResult processAdminMessage(const bs::message::Envelope &); + bs::message::ProcessingResult processBlockchain(const bs::message::Envelope &); + bs::message::ProcessingResult processSigner(const bs::message::Envelope &); + bs::message::ProcessingResult processWallets(const bs::message::Envelope &); + bs::message::ProcessingResult processHWW(const bs::message::Envelope&); + bs::message::ProcessingResult processOwnRequest(const bs::message::Envelope&); void requestInitialSettings(); + void requestPostLoadingSettings(); void updateSplashProgress(); void splashProgressCompleted(); void updateStates(); + void setTopBlock(uint32_t); + void loadPlugins(QQmlApplicationEngine&); + void saveTransaction(const std::string& pathName, const bs::core::wallet::TXSignRequest& + , const std::vector&); + void notifyNewTransaction(const bs::TXEntry& tx); void createWallet(bool primary); + std::string hdWalletIdByIndex(int) const; + int walletIndexById(const std::string&) const; + std::string generateWalletName() const; + std::string hdWalletIdByLeafId(const std::string&) const; void processWalletLoaded(const bs::sync::WalletInfo &); - bool processWalletData(const uint64_t msgId + bs::message::ProcessingResult processWalletData(const bs::message::SeqId , const BlockSettle::Common::WalletsMessage_WalletData&); - bool processWalletBalances(const bs::message::Envelope & - , const BlockSettle::Common::WalletsMessage_WalletBalances &); - bool processTXDetails(uint64_t msgId, const BlockSettle::Common::WalletsMessage_TXDetailsResponse &); - bool processLedgerEntries(const BlockSettle::Common::LedgerEntries &); - bool processAddressHist(const BlockSettle::Common::ArmoryMessage_AddressHistory&); - bool processFeeLevels(const BlockSettle::Common::ArmoryMessage_FeeLevelsResponse&); - bool processWalletsList(const BlockSettle::Common::WalletsMessage_WalletsListResponse&); - bool processUTXOs(const BlockSettle::Common::WalletsMessage_UtxoListResponse&); - bool processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse&); - bool processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived&); - bool processZCInvalidated(const BlockSettle::Common::ArmoryMessage_ZCInvalidated&); - bool processReservedUTXOs(const BlockSettle::Common::WalletsMessage_ReservedUTXOs&); + bs::message::ProcessingResult processWalletBalances(bs::message::SeqId, const BlockSettle::Common::WalletsMessage_WalletBalances &); + bs::message::ProcessingResult processTXDetails(bs::message::SeqId, const BlockSettle::Common::WalletsMessage_TXDetailsResponse &); + bs::message::ProcessingResult processLedgerEntries(const BlockSettle::Common::LedgerEntries &); + bs::message::ProcessingResult processAddressHist(const BlockSettle::Common::ArmoryMessage_AddressHistory&); + bs::message::ProcessingResult processFeeLevels(const BlockSettle::Common::ArmoryMessage_FeeLevelsResponse&); + bs::message::ProcessingResult processWalletsList(const BlockSettle::Common::WalletsMessage_WalletsListResponse&); + bs::message::ProcessingResult processWalletDeleted(const std::string& walletId); + bs::message::ProcessingResult processWalletSeed(const BlockSettle::Common::SignerMessage_WalletSeed&); + bs::message::ProcessingResult processUTXOs(bs::message::SeqId, const BlockSettle::Common::WalletsMessage_UtxoListResponse&); + bs::message::ProcessingResult processSignTX(bs::message::SeqId, const BlockSettle::Common::SignerMessage_SignTxResponse&); + bs::message::ProcessingResult processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived&); + bs::message::ProcessingResult processZCInvalidated(const BlockSettle::Common::ArmoryMessage_ZCInvalidated&); + bs::message::ProcessingResult processTransactions(bs::message::SeqId, const BlockSettle::Common::ArmoryMessage_Transactions&); + bs::message::ProcessingResult processReservedUTXOs(const BlockSettle::Common::WalletsMessage_ReservedUTXOs&); + void processWalletAddresses(const std::string& walletId, const std::vector&); + bs::message::ProcessingResult processTxResponse(bs::message::SeqId + , const BlockSettle::Common::WalletsMessage_TxResponse&); + bs::message::ProcessingResult processUTXOs(bs::message::SeqId + , const BlockSettle::Common::ArmoryMessage_UTXOs&); + + bs::message::ProcessingResult processHWDevices(const BlockSettle::HW::DeviceMgrMessage_Devices&); + bs::message::ProcessingResult processHWWready(const std::string& walletId); + bs::message::ProcessingResult processHWSignedTX(const BlockSettle::HW::DeviceMgrMessage_SignTxResponse&); + + QVariant getSetting(ApplicationSettings::Setting) const; + QString getSettingStringAt(ApplicationSettings::Setting, int idx); + void setSetting(ApplicationSettings::Setting, const QVariant&); + void rescanAllWallets(); private: std::shared_ptr logger_; BSTerminalSplashScreen* splashScreen_{ nullptr }; QObject* rootObj_{ nullptr }; + QQmlContext* rootCtxt_{nullptr}; std::shared_ptr userSettings_, userWallets_; std::shared_ptr userBlockchain_, userSigner_; + std::shared_ptr userHWW_; bool loadingDone_{ false }; std::recursive_mutex mutex_; std::set createdComponents_; std::set loadingComponents_; - int armoryState_{ -1 }; + ArmoryState armoryState_{ ArmoryState::Offline }; + int armoryServerIndex_{ -1 }; + NetworkType netType_{ NetworkType::Invalid }; uint32_t blockNum_{ 0 }; int signerState_{ -1 }; std::string signerDetails_; bool walletsReady_{ false }; + std::unordered_set readyWallets_; + std::unordered_set scanningWallets_; + std::string createdWalletId_; - std::map walletGetMap_; std::unordered_map hdWallets_; - std::set newZCs_; + std::unordered_map walletNames_; + std::map walletInfoReq_; + std::map addrComments_; - std::unordered_map assetTypes_; - std::set needChangeAddrReqs_; -}; + const QStringList txTypes_; + QmlAddressListModel* addrModel_{ nullptr }; + TxListModel* txModel_{ nullptr }; + TxListForAddr* expTxByAddrModel_{ nullptr }; + HwDeviceModel* hwDeviceModel_{ nullptr }; + WalletBalancesModel* walletBalances_{ nullptr }; + FeeSuggestionModel* feeSuggModel_{ nullptr }; + ArmoryServersModel* armoryServersModel_{ nullptr }; + std::unique_ptr walletPropertiesModel_; + bs::Address generatedAddress_; + bool hwDevicesPolling_{ false }; + bs::hww::DeviceKey curAuthDevice_{}; + + struct TXReq { + QTXSignRequest* txReq; + bool isMaxAmount{ false }; + BlockSettle::Common::WalletsMessage msg{}; + }; + std::map txReqs_; + std::vector txSaveReqs_; + std::map>> txSaveReq_; + std::map exportTxReqs_; + std::unordered_map hwwReady_; + std::map txDetailReqs_; + std::map settingsCache_; + std::set expTxAddrReqs_, expTxAddrInReqs_; + std::map addressCache_; + std::set rmTxOnInvalidation_; + int nWalletsLoaded_ {-1}; + std::shared_ptr settingsController_; + std::unique_ptr addressFilterModel_; + std::unique_ptr transactionFilterModel_; + std::unique_ptr pluginsListModel_; + std::unique_ptr scaleController_; +}; #endif // QT_QUICK_ADAPTER_H diff --git a/GUI/QtQuick/ScaleController.cpp b/GUI/QtQuick/ScaleController.cpp new file mode 100644 index 000000000..68e1bf93d --- /dev/null +++ b/GUI/QtQuick/ScaleController.cpp @@ -0,0 +1,41 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "ScaleController.h" +#include +#include +#include + +namespace { + const qreal defaultDpi = 96; +} + +ScaleController::ScaleController(QObject* parent) + : QObject(parent) +{ + update(); +} + +void ScaleController::update() +{ + disconnect(this); + + const auto screen = QGuiApplication::screens()[QApplication::desktop()->screenNumber(QApplication::activeWindow())]; + connect(screen, &QScreen::logicalDotsPerInchChanged, this, &ScaleController::update); + + QRect rect = screen->geometry(); + qreal scaleRatio = screen->logicalDotsPerInch() / defaultDpi; + if (scaleRatio_ != scaleRatio || screenWidth_ != rect.width() || screenHeight_ != rect.height()) { + scaleRatio_ = scaleRatio; + screenWidth_ = rect.width(); + screenHeight_ = rect.height(); + emit changed(); + } +} diff --git a/GUI/QtQuick/ScaleController.h b/GUI/QtQuick/ScaleController.h new file mode 100644 index 000000000..3b227c3fc --- /dev/null +++ b/GUI/QtQuick/ScaleController.h @@ -0,0 +1,35 @@ +/* +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** +*/ +#pragma once + +#include + +class ScaleController : public QObject +{ + Q_OBJECT + Q_PROPERTY(qreal scaleRatio READ scaleRatio NOTIFY changed) + Q_PROPERTY(int screenWidth READ screenWidth NOTIFY changed) + Q_PROPERTY(int screenHeight READ screenHeight NOTIFY changed) +public: + ScaleController(QObject* parent = nullptr); + + qreal scaleRatio() const { return scaleRatio_; } + int screenWidth() const { return screenWidth_; } + int screenHeight() const { return screenHeight_; } + + Q_INVOKABLE void update(); + +signals: + void changed(); + +private: + int screenWidth_; + int screenHeight_; + qreal scaleRatio_{ 1.0 }; +}; diff --git a/GUI/QtQuick/SettingsController.cpp b/GUI/QtQuick/SettingsController.cpp new file mode 100644 index 000000000..0f559f16e --- /dev/null +++ b/GUI/QtQuick/SettingsController.cpp @@ -0,0 +1,59 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "SettingsController.h" + +SettingsController::SettingsController() + : QObject() +{ +} + +SettingsController::SettingsController(const SettingsController& settingsController) + : settingsCache_(settingsController.settingsCache_) +{ +} + +SettingsController& SettingsController::operator=(const SettingsController& other) +{ + settingsCache_ = other.settingsCache_; + return *this; +} + +void SettingsController::resetCache(const SettingsController::SettingsCache& cache) +{ + settingsCache_ = cache; + emit reset(); +} + +const SettingsController::SettingsCache& SettingsController::getCache() const +{ + return settingsCache_; +} + +void SettingsController::setParam(ApplicationSettings::Setting key, const QVariant& value) +{ + if (settingsCache_.count(key) == 0 || settingsCache_.at(key) != value) { + settingsCache_[key] = value; + emit changed(key); + } +} + +const QVariant& SettingsController::getParam(ApplicationSettings::Setting key) const +{ + if (settingsCache_.count(key) > 0) { + return settingsCache_.at(key); + } + return QVariant(); +} + +bool SettingsController::hasParam(ApplicationSettings::Setting key) const +{ + return settingsCache_.count(key) > 0; +} diff --git a/GUI/QtQuick/SettingsController.h b/GUI/QtQuick/SettingsController.h new file mode 100644 index 000000000..6a9e3d9b7 --- /dev/null +++ b/GUI/QtQuick/SettingsController.h @@ -0,0 +1,40 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include +#include +#include "ApplicationSettings.h" + +class SettingsController: public QObject +{ + Q_OBJECT + using SettingsCache = std::map; + +public: + SettingsController(); + SettingsController(const SettingsController& settingsController); + SettingsController& operator=(const SettingsController& settingsController); + + bool hasParam(ApplicationSettings::Setting key) const; + const QVariant& getParam(ApplicationSettings::Setting key) const; + void setParam(ApplicationSettings::Setting key, const QVariant& value); + + const SettingsCache& getCache() const; + void resetCache(const SettingsCache& cache); + +signals: + void changed(ApplicationSettings::Setting); + void reset(); + +private: + SettingsCache settingsCache_; +}; diff --git a/GUI/QtQuick/SideshiftPlugin.cpp b/GUI/QtQuick/SideshiftPlugin.cpp new file mode 100644 index 000000000..ee0b6ce05 --- /dev/null +++ b/GUI/QtQuick/SideshiftPlugin.cpp @@ -0,0 +1,533 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "SideshiftPlugin.h" +#include +#include +#include +#include +#include + +namespace { + const QHash kCurrencyListRoles { + {CurrencyListModel::CurrencyRoles::NameRole, "name"}, + {CurrencyListModel::CurrencyRoles::CoinRole, "coin"}, + {CurrencyListModel::CurrencyRoles::NetworkRole, "network"} + }; +} + +using json = nlohmann::json; + +class CoinImageProvider : public QQuickImageProvider +{ +public: + CoinImageProvider(SideshiftPlugin* parent) + : QQuickImageProvider(QQuickImageProvider::Pixmap), parent_(parent) + {} + QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; + +private: + SideshiftPlugin* parent_; +}; + +CurrencyListModel::CurrencyListModel(QObject* parent) + : QAbstractListModel(parent) +{ +} + +int CurrencyListModel::rowCount(const QModelIndex&) const +{ + return currencies_.size(); +} + +QVariant CurrencyListModel::data(const QModelIndex& index, int role) const +{ + if (index.row() < 0 || index.row() >= currencies_.size()) { + return QVariant(); + } + switch(role) { + case CurrencyListModel::CurrencyRoles::NameRole: return currencies_.at(index.row()).name; + case CurrencyListModel::CurrencyRoles::CoinRole: return currencies_.at(index.row()).coin; + case CurrencyListModel::CurrencyRoles::NetworkRole: return currencies_.at(index.row()).network; + default: return QVariant(); + } + return QVariant(); +} + +QHash CurrencyListModel::roleNames() const +{ + return kCurrencyListRoles; +} + +void CurrencyListModel::reset(const QList& currencies) +{ + beginResetModel(); + currencies_ = currencies; + endResetModel(); +} + +CurrencyFilterModel::CurrencyFilterModel(QObject* parent) + : QSortFilterProxyModel(parent) +{ + setDynamicSortFilter(true); + sort(0, Qt::AscendingOrder); + connect(this, &CurrencyFilterModel::changed, this, &CurrencyFilterModel::invalidate); +} + +const QString& CurrencyFilterModel::filter() const +{ + return filter_; +} + +void CurrencyFilterModel::setFilter(const QString& filter) +{ + if (filter_ != filter) { + filter_ = filter; + emit changed(); + } +} + +bool CurrencyFilterModel::filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const +{ + if (filter_.length() == 0) { + return true; + } + + return (sourceModel()->data(sourceModel()->index(source_row, 0), + CurrencyListModel::CurrencyRoles::NameRole).toString().toLower().contains(filter_.toLower()) + || sourceModel()->data(sourceModel()->index(source_row, 0), + CurrencyListModel::CurrencyRoles::CoinRole).toString().toLower().contains(filter_.toLower()) + || sourceModel()->data(sourceModel()->index(source_row, 0), + CurrencyListModel::CurrencyRoles::NetworkRole).toString().toLower().contains(filter_.toLower())); +} + +struct PostIn : public bs::InData +{ + ~PostIn() override = default; + std::string path; + std::string data; +}; +struct PostOut : public bs::OutData +{ + ~PostOut() override = default; + std::string response; + std::string error; +}; + +class SideshiftPlugin::PostHandler : public bs::HandlerImpl +{ +public: + PostHandler(SideshiftPlugin* parent) + : parent_(parent) + {} + ~PostHandler() override = default; + +protected: + std::shared_ptr processData(const std::shared_ptr& in) override + { + std::unique_lock lock(parent_->curlMtx_); + if (!parent_->curl_) { + return nullptr; + } + auto out = std::make_shared(); + curl_easy_setopt(parent_->curl_, CURLOPT_POST, 1); + const auto url = parent_->baseURL_ + in->path; + curl_easy_setopt(parent_->curl_, CURLOPT_URL, url.c_str()); + curl_easy_setopt(parent_->curl_, CURLOPT_POSTFIELDS, in->data.data()); + std::string response; + curl_easy_setopt(parent_->curl_, CURLOPT_WRITEDATA, &response); + + const auto res = curl_easy_perform(parent_->curl_); + if (res != CURLE_OK) { + out->error = fmt::format("{} failed: {}", url, res); + parent_->logger_->error("[{}] {}", __func__, out->error); + return out; + } + out->response = std::move(response); + parent_->logger_->debug("[{}] {} response: {} [{}]", __func__, url, response, response.size()); + return out; + } + +private: + SideshiftPlugin* parent_{ nullptr }; +}; + +struct GetIn : public bs::InData +{ + ~GetIn() override = default; + std::string path; +}; +struct GetOut : public bs::OutData +{ + ~GetOut() override = default; + std::string response; +}; + +class SideshiftPlugin::GetHandler : public bs::HandlerImpl +{ +public: + GetHandler(SideshiftPlugin* parent) : parent_(parent) {} + ~GetHandler() override = default; + +protected: + std::shared_ptr processData(const std::shared_ptr& in) override + { + if (!parent_->curl_) { + return nullptr; + } + auto out = std::make_shared(); + out->response = parent_->get(in->path); + return out; + } + +private: + SideshiftPlugin* parent_{ nullptr }; +}; + + +SideshiftPlugin::SideshiftPlugin(const std::shared_ptr& logger + , QQmlApplicationEngine& engine, QObject* parent) + : Plugin(parent), bs::WorkerPool(1, 1), logger_(logger) +{ + inputListModel_ = new CurrencyListModel(this); + inputFilterModel_ = new CurrencyFilterModel(this); + inputFilterModel_->setSourceModel(inputListModel_); + + outputListModel_ = new CurrencyListModel(this); + outputFilterModel_ = new CurrencyFilterModel(this); + outputFilterModel_->setSourceModel(outputListModel_); + + qmlRegisterInterface("SideshiftPlugin"); + qmlRegisterInterface("CurrencyListModel"); + qmlRegisterInterface("CurrencyFilterModel"); + engine.addImageProvider(QLatin1Literal("coin"), new CoinImageProvider(this)); +} + +SideshiftPlugin::~SideshiftPlugin() +{ + deinit(); +} + +static size_t writeToString(void* ptr, size_t size, size_t count, std::string* stream) +{ + const size_t resSize = size * count; + stream->append((char*)ptr, resSize); + return resSize; +} + +static int dumpFunc(CURL* handle, curl_infotype type, + char* data, size_t size, void* clientp) +{ + if (!data || !size) { + return 0; + } + const auto log = static_cast(clientp); + if (log) { + log->debug("[dump] {}", std::string(data, size)); + } + return 0; +}; + +void SideshiftPlugin::init() +{ + deinit(); + { + std::unique_lock lock(curlMtx_); + curl_ = curl_easy_init(); + curl_easy_setopt(curl_, CURLOPT_WRITEFUNCTION, writeToString); + curl_easy_setopt(curl_, CURLOPT_USERAGENT, std::string("BlockSettle " + name().toStdString() + " plugin").c_str()); + curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt(curl_, CURLOPT_SSL_VERIFYHOST, 0L); + curl_easy_setopt(curl_, CURLOPT_FOLLOWLOCATION, 1L); + + //curl_easy_setopt(curl_, CURLOPT_DEBUGFUNCTION, dumpFunc); + //curl_easy_setopt(curl_, CURLOPT_DEBUGDATA, logger_.get()); + //curl_easy_setopt(curl_, CURLOPT_VERBOSE, 1L); + + //curlHeaders_ = curl_slist_append(curlHeaders_, "accept: */*"); + curlHeaders_ = curl_slist_append(curlHeaders_, "Content-Type: application/json"); + curl_easy_setopt(curl_, CURLOPT_HTTPHEADER, curlHeaders_); + } + + auto in = std::make_shared(); + in->path = "/coins"; + const auto& getResult = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || reply->response.empty()) { + logger_->error("[SideshiftPlugin::init] network error"); + return; + } + const auto& response = reply->response; + QList currencies; + try { + const auto& msg = json::parse(response); + for (const auto& coin : msg) { + for (const auto& network : coin["networks"]) { + currencies.append({ + QString::fromStdString(coin["name"].get()), + QString::fromStdString(coin["coin"].get()), + QString::fromStdString(network.get()) + }); + } + } + QMetaObject::invokeMethod(this, [this, currencies] { + inputListModel_->reset(currencies); + outputListModel_->reset({ {tr("Bitcoin"), tr("BTC"), tr("bitcoin")} }); + emit inited(); + }); + logger_->debug("[SideshiftPlugin::init] {} input currencies", currencies.size()); + } + catch (const json::exception&) { + logger_->error("[SideshiftPlugin::init] failed to parse {}", response); + } + }; + processQueued(in, getResult); +} + +void SideshiftPlugin::deinit() +{ + cancel(); + inputNetwork_.clear(); + inputCurrency_.clear(); + convRate_.clear(); + orderId_.clear(); + + std::unique_lock lock(curlMtx_); + if (curlHeaders_) { + curl_slist_free_all(curlHeaders_); + curlHeaders_ = nullptr; + } + if (curl_) { + curl_easy_cleanup(curl_); + curl_ = NULL; + } +} + +static QString getConvRate(const json& msg, const QString& inputCur) +{ + double rate = 0; + if (msg["rate"].is_string()) { + rate = std::stod(msg["rate"].get()); + } + else if (msg["rate"].is_number_float()) { + rate = msg["rate"].get(); + } + if (rate > 0) { + if (rate < 0.0001) { + return QObject::tr("1 BTC = %1 %2").arg(QString::number(1.0 / rate, 'f', 2)) + .arg(inputCur); + } + else { + return QObject::tr("1 %1 = %2 BTC").arg(inputCur) + .arg(QString::number(rate, 'f', 6)); + } + } + return {}; +} + +void SideshiftPlugin::getPair() +{ + auto in = std::make_shared(); + in->path = "/pair/" + inputCurrency_.toStdString() + "-" + + inputNetwork_.toStdString() + "/btc-bitcoin"; + const auto& getResult = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || reply->response.empty()) { + logger_->error("[SideshiftPlugin::getPair] network error"); + return; + } + const auto& response = reply->response; + try { + const auto& respJson = json::parse(response); + if (respJson.contains("error")) { + const auto& errorMsg = respJson["error"]["message"].get(); + logger_->error("[{}] {}", __func__, errorMsg); + convRate_ = tr("Error: %1").arg(QString::fromStdString(errorMsg)); + emit pairUpdated(); + return; + } + convRate_ = getConvRate(respJson, inputCurrency_.toUpper()); + minAmount_ = QString::fromStdString(respJson["min"].get()); + maxAmount_ = QString::fromStdString(respJson["max"].get()); + } + catch (const json::exception& e) { + logger_->error("[{}] failed to parse {}: {}", __func__, response, e.what()); + } + emit pairUpdated(); + }; + processQueued(in, getResult); +} + +void SideshiftPlugin::setInputNetwork(const QString& network) +{ + logger_->debug("[{}] {} '{}' -> '{}'", __func__, inputCurrency_.toStdString() + , inputNetwork_.toStdString(), network.toStdString()); + if (inputCurrency_.isEmpty() || (inputNetwork_ == network)) { + return; + } + inputNetwork_ = network; + getPair(); +} + +std::string SideshiftPlugin::get(const std::string& request) +{ + std::unique_lock lock(curlMtx_); + if (!curl_) { + logger_->error("[{}] curl not inited", __func__); + return {}; + } + curl_easy_setopt(curl_, CURLOPT_POST, 0); + const auto url = baseURL_ + request; + curl_easy_setopt(curl_, CURLOPT_URL, url.c_str()); + std::string response; + curl_easy_setopt(curl_, CURLOPT_WRITEDATA, &response); + + const auto res = curl_easy_perform(curl_); + if (res != CURLE_OK) { + logger_->error("[{}] {} failed: {}", __func__, url, res); + return {}; + } + //logger_->debug("[{}] {} response: {} [{}]", __func__, url, response, response.size()); + return response; +} + +void SideshiftPlugin::inputCurrencySelected(const QString& cur) +{ + logger_->debug("{{}] '{}' -> '{}'", __func__, inputCurrency_.toStdString() + , cur.toStdString()); + if (inputCurrency_ == cur) { + return; + } + inputCurrency_ = cur; + emit inputCurSelected(); +} + +QString SideshiftPlugin::statusToQString(const std::string& s) const +{ + if (s == "waiting") { + return tr("WAITING FOR YOU TO SEND %1").arg(inputCurrency_.toUpper()); + } + return QString::fromStdString(s); +} + +bool SideshiftPlugin::sendShift(const QString& recvAddr) +{ + if (inputCurrency_.isEmpty() || inputNetwork_.isEmpty() || recvAddr.isEmpty()) { + logger_->error("[{}] invalid input data: inCur: '{}', inNet: '{}', recvAddr: '{}'" + , __func__, inputCurrency_.toStdString(), inputNetwork_.toStdString(), recvAddr.toStdString()); + return false; + } + shiftStatus_ = tr("Sending request..."); + const json msgReq{ {"settleAddress", recvAddr.toStdString()}, {"affiliateId", affiliateId_} + , {"settleCoin", "btc"}, {"depositCoin", inputCurrency_.toLower().toStdString()} + , {"depositNetwork", inputNetwork_.toStdString()} }; + auto in = std::make_shared(); + in->path = "/shifts/variable"; + in->data = msgReq.dump(); + const auto& postResult = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || reply->response.empty()) { + logger_->error("[SideshiftPlugin::sendShift] network error {}" + , (reply == nullptr) ? "" : reply->error); + return; + } + const auto& response = reply->response; + try { + const auto& msgResp = json::parse(response); + if (msgResp.contains("error")) { + const auto& errMsg = msgResp["error"]["message"].get(); + logger_->error("[SideshiftPlugin::sendShift] failed: {}", errMsg); + shiftStatus_ = QString::fromStdString(errMsg); + } + else { + orderId_ = QString::fromStdString(msgResp["id"].get()); + creationDate_ = QString::fromStdString(msgResp["createdAt"].get()); + expireDate_ = QString::fromStdString(msgResp["expiresAt"].get()); + depositAddr_ = QString::fromStdString(msgResp["depositAddress"].get()); + shiftStatus_ = statusToQString(msgResp["status"].get()); + } + emit orderSent(); + } + catch (const std::exception& e) { + logger_->error("[SideshiftPlugin::sendShift] failed to parse {}: {}" + , response, e.what()); + } + }; + processQueued(in, postResult); + return true; +} + +void SideshiftPlugin::updateShiftStatus() +{ + if (orderId_.isEmpty()) { + return; + } + auto in = std::make_shared(); + in->path = "/shifts/" + orderId_.toStdString(); + const auto& getResult = [this](const std::shared_ptr& data) + { + const auto& reply = std::static_pointer_cast(data); + if (!reply || reply->response.empty()) { + logger_->error("[SideshiftPlugin::updateShiftStatus] network error"); + return; + } + const auto& response = reply->response; + try { + logger_->debug("[SideshiftPlugin::updateShiftStatus] {}", response); + const auto& msgResp = json::parse(response); + if (msgResp.contains("error")) { + return; + } + shiftStatus_ = statusToQString(msgResp["status"].get()); + emit orderSent(); + } + catch (const std::exception& e) { + logger_->error("[{}] failed to parse {}: {}", __func__, response, e.what()); + } + }; + processQueued(in, getResult); + getPair(); +} + +std::shared_ptr SideshiftPlugin::worker(const std::shared_ptr&) +{ + const std::vector> handlers { + std::make_shared(this), std::make_shared(this) }; + return std::make_shared(handlers); +} + + +#include +#include +QPixmap CoinImageProvider::requestPixmap(const QString& id, QSize* size + , const QSize& requestedSize) +{ + const std::string request = "/coins/icon/" + id.toStdString(); + const auto& response = parent_->get(request); + if (response.empty()) { + return {}; + } + if (response.at(0) == '{') { //likely an error in json + return {}; + } + QSvgRenderer r(QByteArray::fromStdString(response)); + QImage img(requestedSize, QImage::Format_ARGB32); + img.fill(Qt::GlobalColor::transparent); + QPainter p(&img); + r.render(&p); + if (size) { + *size = requestedSize; + } + return QPixmap::fromImage(img); +} diff --git a/GUI/QtQuick/SideshiftPlugin.h b/GUI/QtQuick/SideshiftPlugin.h new file mode 100644 index 000000000..9ad722bf0 --- /dev/null +++ b/GUI/QtQuick/SideshiftPlugin.h @@ -0,0 +1,163 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include "Plugin.h" +#include +#include +#include +#include +#include "Message/Worker.h" + +namespace spdlog { + class logger; +} +class CoinImageProvider; + +class CurrencyListModel : public QAbstractListModel +{ + Q_OBJECT +public: + enum CurrencyRoles { + NameRole = Qt::UserRole + 1, + CoinRole, + NetworkRole + }; + struct Currency + { + QString name; + QString coin; + QString network; + }; + + CurrencyListModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + void reset(const QList& currencies); + +private: + QList currencies_; +}; + +class CurrencyFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY changed) + +public: + CurrencyFilterModel(QObject* parent = nullptr); + + const QString& filter() const; + void setFilter(const QString& filter); + +protected: + bool filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const override; + +signals: + void changed(); + +private: + QString filter_; +}; + +class SideshiftPlugin: public Plugin, protected bs::WorkerPool +{ + class GetHandler; + class PostHandler; + + friend class CoinImageProvider; + friend class GetHandler; + friend class PostHandler; + Q_OBJECT +public: + SideshiftPlugin(const std::shared_ptr&, QQmlApplicationEngine & + , QObject *parent); + ~SideshiftPlugin() override; + + QString name() const override { return QLatin1Literal("SideShift.ai"); } + QString description() const override { return tr("Shift between BTC, ETH, BCH, XMR, USDT and 90+ other cryptocurrencies"); } + QString icon() const override { return QLatin1Literal("qrc:/images/sideshift_plugin.png"); } + QString path() const override { return QLatin1Literal("qrc:/qml/Plugins/SideShift/SideShiftPopup.qml"); } + + Q_PROPERTY(QAbstractItemModel* inputCurrenciesModel READ inputCurrenciesModel NOTIFY inited) + QAbstractItemModel* inputCurrenciesModel() { return inputFilterModel_; } + Q_PROPERTY(QAbstractItemModel* outputCurrenciesModel READ outputCurrenciesModel NOTIFY inited) + QAbstractItemModel* outputCurrenciesModel() { return outputFilterModel_; } + + Q_PROPERTY(QString conversionRate READ conversionRate NOTIFY pairUpdated) + QString conversionRate() const { return convRate_; } + Q_PROPERTY(QString minAmount READ minAmount NOTIFY pairUpdated) + QString minAmount() const { return minAmount_; } + Q_PROPERTY(QString maxAmount READ maxAmount NOTIFY pairUpdated) + QString maxAmount() const { return maxAmount_; } + Q_PROPERTY(QString inputNetwork READ inputNetwork WRITE setInputNetwork NOTIFY pairUpdated) + QString inputNetwork() const { return inputNetwork_; } + void setInputNetwork(const QString&); + + Q_PROPERTY(QString networkFee READ networkFee NOTIFY orderSent) + QString networkFee() const { return networkFee_; } + Q_PROPERTY(QString depositAddress READ depositAddress NOTIFY orderSent) + QString depositAddress() const { return depositAddr_; } + Q_PROPERTY(QString orderId READ orderId NOTIFY orderSent) + QString orderId() const { return orderId_; } + Q_PROPERTY(QString creationDate READ creationDate NOTIFY orderSent) + QString creationDate() const { return creationDate_; } + Q_PROPERTY(QString status READ status NOTIFY orderSent) + QString status() const { return shiftStatus_; } + + Q_INVOKABLE void init() override; + Q_INVOKABLE void inputCurrencySelected(const QString& cur); + Q_INVOKABLE bool sendShift(const QString& recvAddr); + Q_INVOKABLE void updateShiftStatus(); + +signals: + void inited(); + void inputCurSelected(); + void inputSelected(); + void orderSent(); + void pairUpdated(); + +protected: + std::shared_ptr worker(const std::shared_ptr&) override final; + +private: + void deinit(); + std::string get(const std::string& request); + QString statusToQString(const std::string& status) const; + void getPair(); + +private: + std::shared_ptr logger_; + const std::string baseURL_{"https://sideshift.ai/api/v2"}; + const std::string affiliateId_{"a9KgPNzTn"}; + struct curl_slist* curlHeaders_{ NULL }; + void* curl_{ nullptr }; + std::mutex curlMtx_; + + QString inputNetwork_; + QString inputCurrency_; + QString convRate_; + QString depositAddr_; + QString networkFee_; + QString minAmount_, maxAmount_; + QString creationDate_, expireDate_; + QString orderId_; + QString shiftStatus_; + + CurrencyListModel* inputListModel_; + CurrencyFilterModel* inputFilterModel_; + CurrencyListModel* outputListModel_; + CurrencyFilterModel* outputFilterModel_; +}; diff --git a/GUI/QtQuick/SideswapPlugin.cpp b/GUI/QtQuick/SideswapPlugin.cpp new file mode 100644 index 000000000..20f48e08a --- /dev/null +++ b/GUI/QtQuick/SideswapPlugin.cpp @@ -0,0 +1,18 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "SideswapPlugin.h" +#include + +SideswapPlugin::SideswapPlugin(QObject* parent) + : Plugin(parent) +{ + qmlRegisterInterface("SideswapPlugin"); +} diff --git a/GUI/QtQuick/SideswapPlugin.h b/GUI/QtQuick/SideswapPlugin.h new file mode 100644 index 000000000..edef556e1 --- /dev/null +++ b/GUI/QtQuick/SideswapPlugin.h @@ -0,0 +1,29 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include "Plugin.h" + +class SideswapPlugin: public Plugin +{ + Q_OBJECT +public: + SideswapPlugin(QObject *parent); + + QString name() const override { return QLatin1Literal("SideSwap.io"); } + QString description() const override { return tr("Easiest way to get started on the Liquid Network"); } + QString icon() const override { return QLatin1Literal("qrc:/images/sideswap_plugin.png"); } + QString path() const override { return QLatin1Literal("qrc:/qml/Plugins/SideSwap/SideSwapPopup.qml"); } + + Q_INVOKABLE void init() override {} + +private: +}; diff --git a/GUI/QtQuick/TermsAndConditions.txt b/GUI/QtQuick/TermsAndConditions.txt new file mode 100644 index 000000000..27482c55b --- /dev/null +++ b/GUI/QtQuick/TermsAndConditions.txt @@ -0,0 +1,225 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based on the Program. + + To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. + + A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. + + The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. + + When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". + + c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. + + A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: + + a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. + + If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). + + The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. + + All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). + + However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. + + Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. + + If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. + + A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + + Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. + + Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. + + 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 Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + + You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . + diff --git a/GUI/QtQuick/TransactionFilterModel.cpp b/GUI/QtQuick/TransactionFilterModel.cpp new file mode 100644 index 000000000..0455ca5b0 --- /dev/null +++ b/GUI/QtQuick/TransactionFilterModel.cpp @@ -0,0 +1,92 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "TransactionFilterModel.h" +#include "TxListModel.h" + +TransactionFilterModel::TransactionFilterModel(std::shared_ptr settings) + : QSortFilterProxyModel() + , settings_(settings) +{ + setDynamicSortFilter(true); + sort(0, Qt::AscendingOrder); + connect(this, &TransactionFilterModel::changed, this, &TransactionFilterModel::invalidate); + + if (settings_ != nullptr) { + connect(settings_.get(), &SettingsController::reset, this, [this]() + { + if (settings_->hasParam(ApplicationSettings::Setting::TransactionFilterWalletName)) { + walletName_ = settings_->getParam(ApplicationSettings::Setting::TransactionFilterWalletName).toString(); + } + if (settings_->hasParam(ApplicationSettings::Setting::TransactionFilterTransactionType)) { + transactionType_ = settings_->getParam(ApplicationSettings::Setting::TransactionFilterTransactionType).toString(); + } + emit changed(); + }); + } +} + +bool TransactionFilterModel::filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const +{ + const auto walletNameIndex = sourceModel()->index(source_row, 1); + const auto transactionTypeIndex = sourceModel()->index(source_row, 2); + + if (!walletName_.isEmpty()) { + if (sourceModel()->data(walletNameIndex, TxListModel::TableRoles::TableDataRole) + != walletName_) { + return false; + } + } + + if (!transactionType_.isEmpty()) { + if (sourceModel()->data(transactionTypeIndex, TxListModel::TableRoles::TableDataRole) + != transactionType_) { + return false; + } + } + + return true; +} + +const QString& TransactionFilterModel::walletName() const +{ + return walletName_; +} + +void TransactionFilterModel::setWalletName(const QString& name) +{ + if (walletName_ != name) { + walletName_ = name; + settings_->setParam(ApplicationSettings::Setting::TransactionFilterWalletName, walletName_); + emit changed(); + } +} + +const QString& TransactionFilterModel::transactionType() const +{ + return transactionType_; +} + +void TransactionFilterModel::setTransactionType(const QString& type) +{ + if (transactionType_ != type) { + transactionType_ = type; + settings_->setParam(ApplicationSettings::Setting::TransactionFilterTransactionType + , transactionType_); + emit changed(); + } +} + +bool TransactionFilterModel::lessThan(const QModelIndex& left, const QModelIndex& right) const +{ + return sourceModel()->data(sourceModel()->index(left.row(), 5), TxListModel::TableRoles::TableDataRole) < + sourceModel()->data(sourceModel()->index(right.row(), 5), TxListModel::TableRoles::TableDataRole); +} diff --git a/GUI/QtQuick/TransactionFilterModel.h b/GUI/QtQuick/TransactionFilterModel.h new file mode 100644 index 000000000..000f0c123 --- /dev/null +++ b/GUI/QtQuick/TransactionFilterModel.h @@ -0,0 +1,45 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TRANSACTION_FILTER_MODEL_H +#define TRANSACTION_FILTER_MODEL_H + +#include +#include "SettingsController.h" + +class TransactionFilterModel: public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString walletName READ walletName WRITE setWalletName NOTIFY changed) + Q_PROPERTY(QString transactionType READ transactionType WRITE setTransactionType NOTIFY changed) + +public: + TransactionFilterModel(std::shared_ptr settings); + + const QString& walletName() const; + void setWalletName(const QString&); + const QString& transactionType() const; + void setTransactionType(const QString&); + +signals: + void changed(); + +protected: + bool filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const override; + bool lessThan(const QModelIndex & left, const QModelIndex & right) const override; + +private: + QString walletName_; + QString transactionType_; + std::shared_ptr settings_; +}; + +#endif // TRANSACTION_FILTER_MODEL_H diff --git a/GUI/QtQuick/TransactionForAddressFilterModel.cpp b/GUI/QtQuick/TransactionForAddressFilterModel.cpp new file mode 100644 index 000000000..74b740a12 --- /dev/null +++ b/GUI/QtQuick/TransactionForAddressFilterModel.cpp @@ -0,0 +1,49 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "TransactionForAddressFilterModel.h" +#include "TxListModel.h" +#include + +TransactionForAddressFilterModel::TransactionForAddressFilterModel(QObject* parent) + : QSortFilterProxyModel(parent) +{ + connect(this, &TransactionForAddressFilterModel::changed, this, &TransactionForAddressFilterModel::invalidate); +} + +bool TransactionForAddressFilterModel::filterAcceptsRow(int source_row, const QModelIndex& source_parent) const +{ + const auto index = sourceModel()->index(source_row, 5); + const auto transaction_value = sourceModel()->data(index, TxListForAddr::TableRoles::TableDataRole); + if ((positive_ && transaction_value < 0) || (!positive_ && transaction_value > 0)) { + return false; + } + + return true; +} + +bool TransactionForAddressFilterModel::filterAcceptsColumn(int source_column, const QModelIndex& source_parent) const +{ + if (source_column == 0 || source_column == 1 || source_column == 2 || source_column == 5) { + return true; + } + return false; +} + +bool TransactionForAddressFilterModel::positive() const +{ + return positive_; +} + +void TransactionForAddressFilterModel::set_positive(bool value) +{ + positive_ = value; + emit changed(); +} diff --git a/GUI/QtQuick/TransactionForAddressFilterModel.h b/GUI/QtQuick/TransactionForAddressFilterModel.h new file mode 100644 index 000000000..169593233 --- /dev/null +++ b/GUI/QtQuick/TransactionForAddressFilterModel.h @@ -0,0 +1,40 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TRANSACTION_FOR_ADDRESS_FILTER_MODEL_H +#define TRANSACTION_FOR_ADDRESS_FILTER_MODEL_H + +#include + +class TransactionForAddressFilterModel: public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(bool positive READ positive WRITE set_positive NOTIFY changed) + +public: + TransactionForAddressFilterModel(QObject* parent = nullptr); + + bool positive() const; + void set_positive(bool value); + +signals: + void changed(); + +protected: + bool filterAcceptsRow(int source_row, + const QModelIndex& source_parent) const override; + bool filterAcceptsColumn(int source_column, + const QModelIndex& source_parent) const override; + +private: + bool positive_{ false }; +}; + +#endif // TRANSACTION_FOR_ADDRESS_FILTER_MODEL_H diff --git a/GUI/QtQuick/TxInputsModel.cpp b/GUI/QtQuick/TxInputsModel.cpp new file mode 100644 index 000000000..dea7ac8f9 --- /dev/null +++ b/GUI/QtQuick/TxInputsModel.cpp @@ -0,0 +1,597 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "TxInputsModel.h" +#include +#include "Address.h" +#include "BTCNumericTypes.h" +#include "CoinSelection.h" +#include "TxOutputsModel.h" +#include "ColorScheme.h" +#include "Utils.h" + +namespace { + static const QHash kRoles{ + {TxInputsModel::TableDataRole, "tableData"}, + {TxInputsModel::HeadingRole, "heading"}, + {TxInputsModel::ColorRole, "dataColor"}, + {TxInputsModel::SelectedRole, "selected"}, + {TxInputsModel::ExpandedRole, "expanded"}, + {TxInputsModel::CanBeExpandedRole, "is_expandable"}, + {TxInputsModel::EditableRole, "is_editable"} + }; +} + +TxInputsModel::TxInputsModel(const std::shared_ptr& logger + , TxOutputsModel* outs, QObject* parent) + : QAbstractTableModel(parent), logger_(logger), outsModel_(outs) + , header_{{ColumnAddress, tr("Address/Hash")}, {ColumnTx, tr("#Tx")}, + {ColumnComment, tr("Comment")}, {ColumnBalance, tr("Balance (BTC)")}} +{} + +int TxInputsModel::rowCount(const QModelIndex &) const +{ + return data_.size() + 1; +} + +int TxInputsModel::columnCount(const QModelIndex &) const +{ + return header_.size(); +} + +QVariant TxInputsModel::data(const QModelIndex& index, int role) const +{ + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case HeadingRole: + return (index.row() == 0); + case SelectedRole: + if (index.column() != 0) { + return false; + } + else if (index.row() == 0) { + return selectionRoot_; + } + else if (data_[index.row() - 1].txId.empty()) { + return (selectionAddresses_.find(data_[index.row() - 1].address) + != selectionAddresses_.end()); + } + else { + return (selectionUtxos_.find({data_[index.row() - 1].txId, data_[index.row() - 1].txOutIndex}) + != selectionUtxos_.end()); + } + case ExpandedRole: + return (index.row() > 0 && index.column() == 0) ? data_[index.row() - 1].expanded : false; + case CanBeExpandedRole: + return (index.row() > 0 && index.column() == 0) ? data_[index.row() - 1].txId.empty() : false; + case ColorRole: + return dataColor(index.row(), index.column()); + case EditableRole: + return isInputSelectable(index.row()); + default: break; + } + return QVariant(); +} + +QVariant TxInputsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_[section]; + } + return QVariant(); +} + +QColor TxInputsModel::dataColor(int row, int col) const +{ + if (row == 0) { + return ColorScheme::tableHeaderColor; + } + return ColorScheme::tableTextColor; +} + +QHash TxInputsModel::roleNames() const +{ + return kRoles; +} + +void TxInputsModel::clear() +{ + decltype(fixedEntries_) fixedEntries; + fixedEntries.swap(fixedEntries_); + beginResetModel(); + utxos_.clear(); + data_.clear(); + selectionRoot_ = false; + preSelected_.clear(); + endResetModel(); + setFixedInputs(fixedEntries); + clearSelection(); + emit rowCountChanged(); +} + +void TxInputsModel::addUTXOs(const std::vector& utxos) +{ + QMetaObject::invokeMethod(this, [this, utxos] { + for (const auto& utxo : utxos) { + try { + const auto& addr = bs::Address::fromUTXO(utxo); + + if (std::find(utxos_[addr].begin(), utxos_[addr].end(), utxo) != utxos_[addr].end()) { + continue; + } + + utxos_[addr].push_back(utxo); + int addrIndex = -1; + for (int i = fixedEntries_.size(); i < data_.size(); ++i) { + if (data_.at(i).address == addr) { + addrIndex = i; + break; + } + } + if (addrIndex < 0) { + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + data_.push_back({ addr }); + endInsertRows(); + emit rowCountChanged(); + } + else { + if (data_.at(addrIndex).expanded) { + beginInsertRows(QModelIndex(), addrIndex + 2, addrIndex + 2); + data_.insert(data_.cbegin() + addrIndex + 1, { {}, utxo.getTxHash(), utxo.getTxOutIndex() }); + endInsertRows(); + emit rowCountChanged(); + } + } + } + catch (const std::exception&) { + continue; + } + } + if (collectUTXOsForAmount_) { + collectUTXOsFor(); + collectUTXOsForAmount_ = false; + } + }); +} + +int TxInputsModel::setFixedUTXOs(const std::vector& utxos) +{ + fixedUTXOs_ = utxos; + nbTx_ = 0; + selectedBalance_ = 0; + selectionUtxos_.clear(); + for (const auto& utxo : utxos) { + for (const auto& entry : fixedEntries_) { + if ((entry.txId == utxo.getTxHash()) && (entry.txOutIndex == utxo.getTxOutIndex())) { + logger_->debug("[{}] UTXO selection: {}@{}", __func__, utxo.getTxHash().toHexStr(true), utxo.getTxOutIndex()); + selectionUtxos_.insert({ utxo.getTxHash(), utxo.getTxOutIndex() }); + selectedBalance_ += utxo.getValue(); + nbTx_++; + const auto& addr = bs::Address::fromUTXO(utxo); + utxos_[addr].push_back(utxo); + break; + } + } + } + emit selectionChanged(); + emit dataChanged(createIndex(0, 0), createIndex(fixedEntries_.size() - 1, 0), {SelectedRole}); + logger_->debug("[{}] nbTX: {}, balance: {}", __func__, nbTx_, selectedBalance_); + return nbTx_; +} + +void TxInputsModel::addEntries(const std::vector& entries) +{ + if (entries.empty()) { + return; + } + beginInsertRows(QModelIndex(), rowCount(), rowCount() + entries.size() - 1); + data_.insert(data_.cend(), entries.cbegin(), entries.cend()); + endInsertRows(); + emit rowCountChanged(); +} + +void TxInputsModel::setFixedInputs(const std::vector& entries) +{ + if (entries.empty()) { + return; + } + logger_->debug("[{}] {} entries", __func__, entries.size()); + if (!fixedEntries_.empty() && (data_.size() >= fixedEntries_.size())) { + logger_->debug("[{}] removing {} fixed entries", __func__, fixedEntries_.size()); + beginRemoveRows(QModelIndex(), 0, fixedEntries_.size() - 1); + data_.erase(data_.cbegin(), data_.cbegin() + fixedEntries_.size()); + endRemoveRows(); + } + std::vector treeEntries; + for (const auto& entry : entries) { + auto it = std::find_if(treeEntries.cbegin(), treeEntries.cend(), [entry](const Entry& e) + { return e.address == entry.address; }); + if (it == treeEntries.end()) { + auto addrEntry = entry; + addrEntry.txId.clear(); + addrEntry.expanded = true; + treeEntries.push_back(addrEntry); + } + auto txEntry = entry; + txEntry.address.clear(); + treeEntries.push_back(txEntry); + } + fixedEntries_ = treeEntries; + beginInsertRows(QModelIndex(), 0, treeEntries.size() - 1); + data_.insert(data_.cbegin(), treeEntries.cbegin(), treeEntries.cend()); + endInsertRows(); + emit rowCountChanged(); +} + +void TxInputsModel::toggle(int row) +{ + --row; + if (row < fixedEntries_.size()) { + return; + } + auto& entry = data_[row]; + if (!entry.txId.empty()) { + return; + } + const auto& it = utxos_.find(entry.address); + if (it == utxos_.end()) { + return; + } + + std::map selChanges; + if (entry.expanded) { + entry.expanded = false; + beginRemoveRows(QModelIndex(), row + 2, row + it->second.size() + 1); + data_.erase(data_.cbegin() + row + 1, data_.cbegin() + row + it->second.size() + 1); + endRemoveRows(); + + emit rowCountChanged(); + } + else { + entry.expanded = true; + std::vector entries; + for (const auto& utxo : it->second) { + entries.push_back({ {}, utxo.getTxHash(), utxo.getTxOutIndex()}); + } + beginInsertRows(QModelIndex(), row + 2, row + it->second.size() + 1); + data_.insert(data_.cbegin() + row + 1, entries.cbegin(), entries.cend()); + endInsertRows(); + emit rowCountChanged(); + emit dataChanged(createIndex(row + 1, 0), createIndex(rowCount() - 1, 0), { SelectedRole }); + } +} + +void TxInputsModel::toggleSelection(int row) +{ + if (row == 0) { + selectionRoot_ = !selectionRoot_; + + if (!selectionRoot_) { + clearSelection(); + } + else { + for (int iRow = fixedEntries_.size() + 1; iRow < rowCount(); iRow ++) { + const auto& entry = data_.at(iRow-1); + if (entry.txId.empty()) { + selectionAddresses_.insert(entry.address); + } + const auto& itAddr = utxos_.find(entry.address); + if (itAddr != utxos_.end()) { + for (const auto& u : itAddr->second) { + selectionUtxos_.insert({u.getTxHash(), u.getTxOutIndex()}); + selectedBalance_ += u.getValue(); + nbTx_++; + } + } + } + } + emit selectionChanged(); + emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0), {SelectedRole}); + return; + } + + --row; + if (row < fixedEntries_.size()) { + return; + } + const auto& entry = data_.at(row); + const auto& itAddr = utxos_.find(entry.address); + UTXO utxo{}; + if (itAddr == utxos_.end()) { + bool found = false; + for (const auto& byAddr : utxos_) { + for (const auto& u : byAddr.second) { + if ((u.getTxHash() == entry.txId) && (u.getTxOutIndex() == entry.txOutIndex)) { + utxo = u; + found = true; + break; + } + } + if (found) { + break; + } + } + } + int selStart = row + 1; + int selEnd = row + 1; + + const bool wasSelectedAddr = (selectionAddresses_.find(data_[row].address)) + != selectionAddresses_.end(); + const bool wasSelectedUtxo = (selectionUtxos_.find({data_[row].txId, data_[row].txOutIndex})) + != selectionUtxos_.end(); + + if (!wasSelectedAddr && !wasSelectedUtxo) { + if (!entry.txId.empty()) { + nbTx_++; + selectedBalance_ += utxo.getValue(); + selectionUtxos_.insert({utxo.getTxHash(), utxo.getTxOutIndex()}); + } + else { + selectionAddresses_.insert(entry.address); + if (itAddr != utxos_.end()) { + for (const auto& utxo_addr : itAddr->second) { + if ((selectionUtxos_.find({utxo_addr.getTxHash(), utxo_addr.getTxOutIndex()})) + == selectionUtxos_.end()) { + selectedBalance_ += utxo_addr.getValue(); + selectionUtxos_.insert({utxo_addr.getTxHash(), utxo_addr.getTxOutIndex()}); + nbTx_++; + } + } + selEnd = row + 1 + itAddr->second.size(); + } + } + } + else if (wasSelectedUtxo) { + nbTx_--; + selectedBalance_ -= utxo.getValue(); + int rowParent = row - 1; + selectionUtxos_.erase({utxo.getTxHash(), utxo.getTxOutIndex()}); + while (rowParent >= 0) { + if (data_.at(rowParent).expanded) { + selectionAddresses_.erase(data_.at(rowParent).address); + break; + } + rowParent--; + } + if (rowParent >= 0) + selStart = rowParent + 1; + } + else if (wasSelectedAddr){ + selectionAddresses_.erase(entry.address); + if (itAddr != utxos_.end()) { + for (const auto& utxo_addr : itAddr->second) { + if ((selectionUtxos_.find({utxo_addr.getTxHash(), utxo_addr.getTxOutIndex()})) + != selectionUtxos_.end()) { + selectedBalance_ -= utxo_addr.getValue(); + selectionUtxos_.erase({utxo_addr.getTxHash(), utxo_addr.getTxOutIndex()}); + nbTx_--; + } + } + selEnd = row + 1 + itAddr->second.size(); + } + } + emit selectionChanged(); + emit dataChanged(createIndex(selStart, 0), createIndex(selEnd, 0), {SelectedRole}); +} + +QUTXOList* TxInputsModel::getSelection(const QString& address, double amount) +{ + QList result; + if (!address.isEmpty() && (amount > 0) && fixedEntries_.empty() && // auto selection + selectionUtxos_.empty() && selectionAddresses_.empty()) { + //(static_cast(std::floor(amount * BTCNumericTypes::BalanceDivider)) > selectedBalance_)) { + const auto& it = preSelected_.find(static_cast(std::floor(amount * BTCNumericTypes::BalanceDivider))); + if (it != preSelected_.end()) { + return new QUTXOList(it->second, (QObject*)this); + } + if (utxos_.empty()) { + collectUTXOsForAmount_ = amount; + return nullptr; + } + result = collectUTXOsFor(bs::Address::fromAddressString(address.toStdString()), amount); + logger_->debug("[{}] auto-selected {} inputs", __func__, result.size()); + preSelected_[static_cast(std::floor(amount * BTCNumericTypes::BalanceDivider))] = result; + } + else { + selectedBalance_ = 0; + logger_->debug("[{}] {} UTXOs, {} addresses", __func__, selectionUtxos_.size(), selectionAddresses_.size()); + for (const auto& sel : selectionUtxos_) { + for (const auto& byAddr : utxos_) { + bool added = false; + for (const auto& utxo : byAddr.second) { + if ((sel.first == utxo.getTxHash()) && (sel.second == utxo.getTxOutIndex())) { + logger_->debug("[{}] adding {} {}", __func__, sel.first.toHexStr(true), sel.second); + selectedBalance_ += utxo.getValue(); + result.push_back(new QUTXO(utxo, (QObject*)this)); + added = true; + break; + } + } + if (added) { + break; + } + } + } + for (const auto& sel : selectionAddresses_) { + const auto& itUTXO = utxos_.find(sel); + if (itUTXO != utxos_.end()) { + for (const auto& utxo : itUTXO->second) { + if (selectionUtxos_.find({utxo.getTxHash(), utxo.getTxOutIndex()}) + != selectionUtxos_.end()) { + continue; + } + logger_->debug("[{}] adding {} {}", __func__, utxo.getTxHash().toHexStr(true), utxo.getTxOutIndex()); + selectedBalance_ += utxo.getValue(); + result.push_back(new QUTXO(utxo, (QObject*)this)); + } + } + } + nbTx_ = result.size(); + logger_->debug("[{}] {} UTXOs selected", __func__, nbTx_); + } + return new QUTXOList(result, (QObject*)this); +} + +void TxInputsModel::updateAutoselection() +{ + //if (static_cast(std::floor(amount * BTCNumericTypes::BalanceDivider)) <= selectedBalance_) { + //return; + //} + if (!selectionUtxos_.empty() || !selectionAddresses_.empty()) { + return; + } + for (int i = data_.size() - 1; i >= 0; --i) { + const auto& entry = data_[i]; + if (!entry.expanded) { + toggle(i + 1); + } + } + + if (utxos_.empty()) { + collectUTXOsForAmount_ = true; + return; + } + auto result = collectUTXOsFor(); + selectionUtxos_.clear(); + selectedBalance_ = 0; + nbTx_ = 0; + for (const auto utxo : result) { + selectionUtxos_.insert({ utxo->utxo().getTxHash(), utxo->utxo().getTxOutIndex() }); + logger_->debug("[{}] UTXO selection: {}@{}", __func__, utxo->utxo().getTxHash().toHexStr(true), utxo->utxo().getTxOutIndex()); + selectedBalance_ += utxo->utxo().getValue(); + nbTx_++; + } + + emit selectionChanged(); + emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0), { SelectedRole }); +} + +QUTXOList* TxInputsModel::zcInputs() const +{ + QList result; + for (const auto& entry : data_) { + logger_->debug("[{}] {}:{} {}", __func__, entry.txId.toHexStr(true), entry.txOutIndex, entry.amount); + result.push_back(new QUTXO(QUTXO::Input{ entry.txId, entry.amount, entry.txOutIndex } + , (QObject*)this)); + } + return new QUTXOList(result, (QObject*)this); +} + +QList TxInputsModel::collectUTXOsFor(const bs::Address& addr, double amount) +{ + QList result; + std::vector allUTXOs; + for (const auto& byAddr : utxos_) { + allUTXOs.insert(allUTXOs.cend(), byAddr.second.cbegin(), byAddr.second.cend()); + } + bs::Address::decorateUTXOs(allUTXOs); + const auto& recipients = outsModel_ ? outsModel_->recipients() : decltype(outsModel_->recipients()){}; + std::map>> recipientsMap; + for (unsigned i = 0; i < recipients.size(); ++i) { + recipientsMap[i] = { recipients.at(i) }; + } + if (!addr.empty() && (amount > 0)) { + recipientsMap[recipients.size()] = { addr.getRecipient(bs::XBTAmount{amount}) }; + } + if (recipientsMap.empty()) { + logger_->error("[TxInputsModel::collectUTXOsFor] no recipients found"); + return result; + } + float feePerByte = fee_.isEmpty() ? 1.0 : std::stof(fee_.toStdString()); + auto payment = Armory::CoinSelection::PaymentStruct(recipientsMap, 0, feePerByte, 0); + Armory::CoinSelection::CoinSelection coinSelection([allUTXOs](uint64_t) { return allUTXOs; } + , std::vector{}, UINT64_MAX, topBlock_); + const auto selection = coinSelection.getUtxoSelectionForRecipients(payment, allUTXOs); + + selectedBalance_ = 0; + for (const auto& utxo : selection.utxoVec_) { + selectedBalance_ += utxo.getValue(); + result.push_back(new QUTXO(utxo, (QObject*)this)); + } + nbTx_ = result.size(); + emit selectionChanged(); + return result; +} + +QVariant TxInputsModel::getData(int row, int col) const +{ + if (row == 0) { + return header_[col]; + } + const auto& entry = data_.at(row - 1); + const auto& itUTXOs = entry.address.empty() ? utxos_.end() : utxos_.find(entry.address); + switch (col) { + case ColumnAddress: + if (!entry.txId.empty()) { + const auto& txId = entry.txId.toHexStr(true); + std::string str = txId; + if (txId.size() > 40) + str = txId.substr(0, 20) + "..." + txId.substr(txId.size() - 21, 20); + + return QString::fromStdString(str); + } + else { + return QString::fromStdString(entry.address.display()); + } + case ColumnTx: + if (itUTXOs == utxos_.end()) { + return QString::number(entry.txOutIndex); + } + else { + return QString::number(itUTXOs->second.size()); + } + break; + case ColumnBalance: + if (itUTXOs != utxos_.end()) { + uint64_t balance = 0; + for (const auto& utxo : itUTXOs->second) { + balance += utxo.getValue(); + } + return gui_utils::satoshiToQString(balance); + } + else { + if (utxos_.empty()) { + return gui_utils::satoshiToQString(entry.amount); + } + for (const auto& byAddr : utxos_) { + for (const auto& utxo : byAddr.second) { + if ((entry.txId == utxo.getTxHash()) && (entry.txOutIndex == utxo.getTxOutIndex())) { + return gui_utils::satoshiToQString(utxo.getValue()); + } + } + } + } + break; + default: break; + } + return {}; +} + +void TxInputsModel::clearSelection() +{ + selectionUtxos_.clear(); + selectionAddresses_.clear(); + selectedBalance_ = 0.0f; + nbTx_ = 0; + setFixedUTXOs(fixedUTXOs_); + emit dataChanged(createIndex(0, 0), createIndex(rowCount() - 1, 0), { SelectedRole }); +} + +bool TxInputsModel::isInputSelectable(int row) const +{ + if (row == 0) { + return true; + } + if (row > 0 && row <= fixedEntries_.size()) { + return false; + } + return true; +} diff --git a/GUI/QtQuick/TxInputsModel.h b/GUI/QtQuick/TxInputsModel.h new file mode 100644 index 000000000..f1cd555ed --- /dev/null +++ b/GUI/QtQuick/TxInputsModel.h @@ -0,0 +1,154 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TX_INPUTS_MODEL_H +#define TX_INPUTS_MODEL_H + +#include +#include +#include +#include +#include +#include +#include + +#include "Address.h" +#include "BinaryData.h" +#include "TxClasses.h" + +namespace spdlog { + class logger; +} +class TxOutputsModel; + + +class QUTXO : public QObject +{ + Q_OBJECT +public: + QUTXO(const UTXO& utxo, QObject* parent = nullptr) + : QObject(parent), utxo_(utxo) {} + + struct Input { + BinaryData txHash; + uint64_t amount; + uint32_t txOutIndex; + }; + QUTXO(const Input& input, QObject* parent = nullptr) + : QObject(parent), input_(input) {} + + UTXO utxo() const { return utxo_; } + Input input() const { return input_; } + +private: + UTXO utxo_{}; + Input input_{}; +}; + +class QUTXOList : public QObject +{ + Q_OBJECT +public: + QUTXOList(const QList& data, QObject* parent = nullptr) + : QObject(parent), data_(data) + {} + QList data() const { return data_; } + + Q_PROPERTY(int rowCount READ rowCount CONSTANT) + int rowCount() const { return data_.size(); } + +private: + QList data_; +}; + +class TxInputsModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + +public: + enum TableRoles { TableDataRole = Qt::UserRole + 1, HeadingRole, ColorRole, + SelectedRole, ExpandedRole, CanBeExpandedRole, EditableRole }; + TxInputsModel(const std::shared_ptr&, TxOutputsModel* + , QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void clear(); + void addUTXOs(const std::vector&); + int setFixedUTXOs(const std::vector&); + void setTopBlock(uint32_t topBlock) { topBlock_ = topBlock; } + + struct Entry { + bs::Address address; + BinaryData txId{}; + uint32_t txOutIndex{ UINT32_MAX }; + uint64_t amount{ 0 }; + bool expanded{ false }; + }; + void addEntries(const std::vector&); + void setFixedInputs(const std::vector&); + + Q_PROPERTY(int nbTx READ nbTx NOTIFY selectionChanged) + int nbTx() const { return nbTx_; } + Q_PROPERTY(QString balance READ balance NOTIFY selectionChanged) + double balanceValue() const { return selectedBalance_ / BTCNumericTypes::BalanceDivider; } + QString balance() const { return QString::number(balanceValue(), 'f', 8); } + + Q_PROPERTY(QString fee READ fee WRITE setFee NOTIFY feeChanged) + QString fee() const { return fee_; } + void setFee(const QString& fee) { fee_ = fee; emit feeChanged(); } + + Q_INVOKABLE void toggle(int row); + Q_INVOKABLE void toggleSelection(int row); + Q_INVOKABLE QUTXOList* getSelection(const QString& address = {}, double amt = 0); + Q_INVOKABLE QUTXOList* zcInputs() const; + Q_INVOKABLE void updateAutoselection(); + Q_INVOKABLE void clearSelection(); + +signals: + void selectionChanged() const; + void feeChanged() const; + void rowCountChanged (); + +private: + QVariant getData(int row, int col) const; + QColor dataColor(int row, int col) const; + QList collectUTXOsFor(const bs::Address& = {}, double amount = 0); + bool isInputSelectable(int row) const; + +private: + enum Columns {ColumnAddress, ColumnTx, ColumnComment, ColumnBalance}; + + std::shared_ptr logger_; + TxOutputsModel* outsModel_{ nullptr }; + const QMap header_; + std::map> utxos_; + + std::vector data_; + std::vector fixedEntries_; + std::vector fixedUTXOs_; + std::set> selectionUtxos_; + std::set selectionAddresses_; + bool selectionRoot_ {false}; + + std::map> preSelected_; + int nbTx_{ 0 }; + uint64_t selectedBalance_{ 0 }; + QString fee_; + uint32_t topBlock_{ 0 }; + bool collectUTXOsForAmount_{ false }; +}; + +#endif // TX_INPUTS_MODEL_H diff --git a/GUI/QtQuick/TxInputsSelectedModel.cpp b/GUI/QtQuick/TxInputsSelectedModel.cpp new file mode 100644 index 000000000..d2a976211 --- /dev/null +++ b/GUI/QtQuick/TxInputsSelectedModel.cpp @@ -0,0 +1,103 @@ +#include "TxInputsSelectedModel.h" +#include "ColorScheme.h" +#include "Utils.h" + +namespace { + static const QHash kRoles{ + { TxInputsSelectedModel::TableDataRole, "tableData"}, + { TxInputsSelectedModel::HeadingRole, "heading" }, + { TxInputsSelectedModel::ColorRole, "dataColor" } + }; +} + +TxInputsSelectedModel::TxInputsSelectedModel(TxInputsModel* source) + : QAbstractTableModel(source), source_(source) + , header_{ {ColumnTxId, tr("TX Hash")}, {ColumnTxOut, tr("OutNdx")}, + {ColumnBalance, tr("Balance (BTC)")} } +{ + selection_ = source_->getSelection(); + connect(source_, &TxInputsModel::selectionChanged, this, &TxInputsSelectedModel::onSelectionChanged); +} + +void TxInputsSelectedModel::onSelectionChanged() +{ + beginResetModel(); + selection_ = source_->getSelection(); + endResetModel(); + emit rowCountChanged(); +} + +int TxInputsSelectedModel::rowCount(const QModelIndex&) const +{ + return (selection_ == nullptr) ? 1 : selection_->rowCount() + 1; +} + +int TxInputsSelectedModel::columnCount(const QModelIndex&) const +{ + return header_.size(); +} + +QVariant TxInputsSelectedModel::data(const QModelIndex& index, int role) const +{ + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case HeadingRole: + return (index.row() == 0); + case ColorRole: + return dataColor(index.row(), index.column()); + default: break; + } + return {}; +} + +QHash TxInputsSelectedModel::roleNames() const +{ + return kRoles; +} + +QVariant TxInputsSelectedModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + return header_[section]; +} + +QVariant TxInputsSelectedModel::getData(int row, int col) const +{ + if (row == 0) { + return header_[col]; + } + if (!selection_) { + return {}; + } + if (selection_->rowCount() < row) { + return {}; + } + const auto& entry = selection_->data().at(row - 1); + switch (col) { + case ColumnTxId: { + const auto& txId = entry->utxo().getTxHash().toHexStr(true); + std::string str; + if (txId.size() > 40) { + str = txId.substr(0, 20) + "..." + txId.substr(txId.size() - 21, 20); + } + else { + str = txId; + } + return QString::fromStdString(str); + } + case ColumnTxOut: + return QString::number(entry->utxo().getTxOutIndex()); + case ColumnBalance: + return gui_utils::satoshiToQString(entry->utxo().getValue()); + default: break; + } + return {}; +} + +QColor TxInputsSelectedModel::dataColor(int row, int col) const +{ + if (row == 0) { + return ColorScheme::tableHeaderColor; + } + return ColorScheme::tableTextColor; +} diff --git a/GUI/QtQuick/TxInputsSelectedModel.h b/GUI/QtQuick/TxInputsSelectedModel.h new file mode 100644 index 000000000..215b92e85 --- /dev/null +++ b/GUI/QtQuick/TxInputsSelectedModel.h @@ -0,0 +1,52 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TX_INPUTS_SELECTED_MODEL_H +#define TX_INPUTS_SELECTED_MODEL_H + +#include "TxInputsModel.h" + +class TxInputsSelectedModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + +public: + enum TableRoles { + TableDataRole = Qt::UserRole + 1, HeadingRole, ColorRole + }; + explicit TxInputsSelectedModel(TxInputsModel* source); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + +signals: + void rowCountChanged(); + +private slots: + void onSelectionChanged(); + +private: + QVariant getData(int row, int col) const; + QColor dataColor(int row, int col) const; + +private: + enum Columns { ColumnTxId, ColumnTxOut, ColumnBalance }; + TxInputsModel* source_{ nullptr }; + const QMap header_; + QUTXOList* selection_{ nullptr }; +}; + + + +#endif // TX_INPUTS_SELECTED_MODEL_H diff --git a/GUI/QtQuick/TxListModel.cpp b/GUI/QtQuick/TxListModel.cpp new file mode 100644 index 000000000..d65bcda8f --- /dev/null +++ b/GUI/QtQuick/TxListModel.cpp @@ -0,0 +1,990 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "TxListModel.h" +#include +#include +#include +#include "ColorScheme.h" +#include "StringUtils.h" +#include "TxInputsSelectedModel.h" +#include "TxOutputsModel.h" +#include "Utils.h" + +namespace { + static const QHash kTxListRoles{ + {TxListModel::TableDataRole, "tableData"}, + {TxListModel::ColorRole, "dataColor"}, + {TxListModel::TxIdRole, "txId"}, + }; + static const QHash kTxListForAddrRoles{ + {TxListForAddr::TableDataRole, "tableData"}, + {TxListForAddr::ColorRole, "dataColor"}, + }; + static const QHash kTxInOutRoles{ + {TxInOutModel::TableDataRole, "tableData"}, + {TxInOutModel::ColorRole, "dataColor"}, + {TxInOutModel::TxHashRole, "txHash"}, + }; + + QString getTime2String(std::time_t& t) + { + std::tm* tm = std::localtime(&t); + char buffer[50]; + + std::strftime(buffer, sizeof(buffer), "%y%m%d", tm); + return QString::fromStdString(std::string(buffer)); + } +} + +TxListModel::TxListModel(const std::shared_ptr& logger, QObject* parent) + : QAbstractTableModel(parent), logger_(logger) + , header_{ tr("Date"), tr("Wallet"), tr("Type"), tr("Address"), tr("Amount") + , tr("#Conf"), tr("Flag"), tr("Comment") } +{} + +int TxListModel::rowCount(const QModelIndex &) const +{ + return data_.size(); +} + +int TxListModel::columnCount(const QModelIndex &) const +{ + return header_.size(); +} + +QVariant TxListModel::getData(int row, int col) const +{ + if (row > data_.size()) { + return {}; + } + const auto& entry = data_.at(row); + switch (col) { + case 0: return QDateTime::fromSecsSinceEpoch(entry.txTime).toString(gui_utils::dateTimeFormat); + case 1: return walletName(row); + case 2: return txType(row); + case 3: { + bs::Address address; + if (!entry.addresses.empty()) { + address = *entry.addresses.cbegin(); + } + return QString::fromStdString(address.display()); + } + case 4: return gui_utils::satoshiToQString(std::abs(entry.value)); + case 5: return entry.nbConf; + case 6: return txFlag(row); + case 7: return txComment(row); + default: break; + } + return {}; +} + +QColor TxListModel::dataColor(int row, int col) const +{ + const auto& entry = data_.at(row); + if (col == 5) { + switch (entry.nbConf) { + case 0: return ColorScheme::transactionConfirmationZero; + case 1: + case 2: + case 3: + case 4: + case 5: return QColorConstants::Yellow; + default: return ColorScheme::transactionConfirmationHigh; + } + } + else if (col == 2) { + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + switch (itTxDet->second.direction) { + case bs::sync::Transaction::Direction::Received: return ColorScheme::transactionConfirmationHigh; + case bs::sync::Transaction::Direction::Sent: return ColorScheme::transactionConfirmationZero; + case bs::sync::Transaction::Direction::Internal: return QColorConstants::Cyan; + default: break; + } + } + } + return QColorConstants::White; +} + +QString TxListModel::walletName(int row) const +{ + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + return QString::fromStdString(itTxDet->second.walletName); + } + return {}; +} + +bs::sync::Transaction::Direction TxListModel::txDirection(int row) const +{ + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + return itTxDet->second.direction; + } + return bs::sync::Transaction::Unknown; +} + +QString TxListModel::txType(int row) const +{ + return gui_utils::directionToQString(txDirection(row)); +} + +QString TxListModel::txComment(int row) const +{ + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + return QString::fromStdString(itTxDet->second.comment); + } + return {}; +} + +QString TxListModel::txFlag(int row) const +{ + if (isRBF(row)) { + return tr("RBF"); + } + return {}; +} + +QString TxListModel::txId(int row) const +{ + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + return QString::fromStdString(itTxDet->second.txHash.toHexStr(true)); + } + return {}; +} + +bool TxListModel::isRBF(int row) const +{ + std::unique_lock lock{ dataMtx_ }; + const auto& itTxDet = txDetails_.find(row); + if (itTxDet != txDetails_.end()) { + return itTxDet->second.tx.isRBF(); + } + return false; +} + +QVariant TxListModel::data(const QModelIndex& index, int role) const +{ + if (index.column() >= header_.size()) { + return {}; + } + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case ColorRole: + return dataColor(index.row(), index.column()); + case TxIdRole: + return txId(index.row()); + case RBFRole: + return isRBF(index.row()); + case NbConfRole: + return data_.at(index.row()).nbConf; + case DirectionRole: + return txDirection(index.row()); + default: break; + } + return QVariant(); +} + +QVariant TxListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_.at(section); + } + return QVariant(); +} + +QHash TxListModel::roleNames() const +{ + return kTxListRoles; +} + +void TxListModel::addRows(const std::vector& entries) +{ + logger_->debug("[TxListModel::addRows] {} entries", entries.size()); + std::vector newEntries; + for (const auto& entry : entries) { + int row = -1; + { + std::unique_lock lock{ dataMtx_ }; + for (int i = 0; i < data_.size(); ++i) { + auto& de = data_.at(i); + if ((entry.txHash == de.txHash) && (de.walletIds == entry.walletIds)) { + de.txTime = entry.txTime; + de.recvTime = entry.recvTime; + row = i; + break; + } + } + } + if (row != -1) { + //logger_->debug("[{}] updating entry #{} {}", __func__, row, entry.txHash.toHexStr(true)); + emit dataChanged(createIndex(row, 0), createIndex(row, 0)); + } + else { + //logger_->debug("[{}::{}] adding entry {}", (void*)this, __func__, entry.txHash.toHexStr(true)); + newEntries.push_back(entry); + } + } + if (!newEntries.empty()) { + if (addingEntries_ == (int)newEntries.size()) { + logger_->warn("[TxListModel::addRows] already adding {} entries", addingEntries_); + return; + } + addingEntries_ = (int)newEntries.size(); + + QMetaObject::invokeMethod(this, [this, newEntries] { + logger_->debug("[TxListModel::addRows] {} new entries", newEntries.size()); + { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + newEntries.size() - 1); + std::unique_lock lock{ dataMtx_ }; + data_.insert(data_.end(), newEntries.cbegin(), newEntries.cend()); + endInsertRows(); + } + emit nbTxChanged(); + + if (!pendingDetails_.empty()) { + logger_->debug("[TxListModel::addRows] {} pending details", pendingDetails_.size()); + while (!pendingDetails_.empty()) { + std::unique_lock lock{ dataMtx_ }; + auto txDet = *pendingDetails_.rbegin(); + pendingDetails_.pop_back(); + lock.unlock(); + setDetails(txDet, false); + } + } + addingEntries_ = 0; + }); + } +} + +void TxListModel::clear() +{ + beginResetModel(); + std::unique_lock lock{ dataMtx_ }; + data_.clear(); + txDetails_.clear(); + endResetModel(); + emit nbTxChanged(); +} + +void TxListModel::setDetails(const bs::sync::TXWalletDetails& txDet, bool usePending) +{ + //logger_->debug("[TxListModel::setDetails] {}", txDet.txHash.toHexStr(true)); + int rowStart = -1, rowEnd = -1; + std::unique_lock lock{ dataMtx_ }; + for (int i = 0; i < data_.size(); ++i) { + const auto& entry = data_.at(i); + if ((entry.txHash == txDet.txHash) && (txDet.walletIds == entry.walletIds)) { + txDetails_[i] = txDet; + data_[i].addresses = txDet.ownAddresses; + if (rowStart < 0) { + rowStart = i; + } + rowEnd = i; + } + } + if (rowStart != -1) { + emit dataChanged(createIndex(rowStart, 1), createIndex(rowEnd, 7)); + //logger_->debug("[TxListModel::setDetails] {} {} found at rows {}-{}" + // , txDet.txHash.toHexStr(), txDet.hdWalletId, rowStart, rowEnd); + } + else { + logger_->warn("[TxListModel::setDetails] {} {} not found", txDet.txHash.toHexStr(), txDet.hdWalletId); + if (usePending) { + pendingDetails_.push_back(txDet); + } + } +} + +void TxListModel::removeTX(const BinaryData& txHash) +{ + int rowStart = -1, rowEnd = -1; + for (int i = 0; i < data_.size(); ++i) { + const auto& entry = data_.at(i); + if (entry.txHash == txHash) { + if (rowStart < 0) { + rowStart = i; + } + rowEnd = i; + } + } + if ((rowEnd < 0) || (rowStart < 0)) { + logger_->warn("[{}] TX {} not found", __func__, txHash.toHexStr(true)); + return; + } + logger_->debug("[{}] {}: start: {}, end: {}", __func__, txHash.toHexStr(true), rowStart, rowEnd); + QMetaObject::invokeMethod(this, [this, rowStart, rowEnd] { + if (rowStart >= rowCount()) { + return; + } + beginRemoveRows(QModelIndex(), rowStart, rowEnd); + std::unique_lock lock{ dataMtx_ }; + data_.erase(data_.cbegin() + rowStart, data_.cbegin() + rowEnd + 1); + for (int i = rowEnd + 1; i < txDetails_.size(); i++) { + txDetails_[i - (rowEnd - rowStart) - 1] = std::move(txDetails_[i]); + } + endRemoveRows(); + }); +} + +void TxListModel::setCurrentBlock(uint32_t nbBlock) +{ + if (!nbBlock) { + return; + } + if (!curBlock_) { + curBlock_ = nbBlock; + return; + } + const int diff = nbBlock - curBlock_; + std::unique_lock writeLock{ dataMtx_ }; + curBlock_ = nbBlock; + for (auto& entry : data_) { + entry.nbConf += diff; + } + emit dataChanged(createIndex(0, 5), createIndex(data_.size() - 1, 5)); +} + +bool TxListModel::exportCSVto(const QString& filename) +{ + const std::string& prefix = "file:///"; + std::string fileName = filename.toStdString(); + const auto itPrefix = fileName.find(prefix); + if (itPrefix != std::string::npos) { + fileName.replace(itPrefix, prefix.size(), ""); + } + logger_->debug("[{}] {}", __func__, fileName); + std::ofstream fstrm(fileName); + if (!fstrm.is_open()) { + return false; + } + fstrm << "sep=;\nTimestamp;Wallet;Type;Address;TxId;Amount;Comment\n"; + for (int i = 0; i < rowCount(); ++i) { + const auto& entry = data_.at(i); + std::time_t txTime = entry.txTime; + fstrm << "\"" << std::put_time(std::localtime(&txTime), "%Y-%m-%d %X") << "\";" + << "\"" << getData(i, 1).toString().toUtf8().toStdString() << "\";" + << getData(i, 2).toString().toStdString() << ";" + << getData(i, 3).toString().toStdString() << ";" + << txId(i).toStdString() << ";" + << fmt::format("{:.8f}", entry.value / BTCNumericTypes::BalanceDivider) << ";" + << "\"" << getData(i, 7).toString().toStdString() << "\"\n"; + } + return true; +} + +QString TxListModel::getBegDate() const +{ + std::time_t minTime = std::time(nullptr); + + for (int i = 0; i < rowCount(); ++i) { + const auto& entry = data_.at(i); + + if (minTime > entry.txTime) + minTime = entry.txTime; + } + + return getTime2String(minTime); +} + +QString TxListModel::getEndDate() const +{ + std::time_t maxTime = std::time(nullptr); + + return getTime2String(maxTime); +} + +TxListForAddr::TxListForAddr(const std::shared_ptr& logger, QObject* parent) + : QAbstractTableModel(parent), logger_(logger) + , header_{ tr("Date"), tr("Transaction ID"), tr("#Conf"), tr("#Ins"), tr("#Outs"), tr("Amount (BTC)") + , tr("Fees (BTC)"), tr("fpb"), tr("VSize (B)") } +{} + +int TxListForAddr::rowCount(const QModelIndex&) const +{ + return data_.size(); +} + +int TxListForAddr::columnCount(const QModelIndex&) const +{ + return header_.size(); +} + +static QString displayNb(int nb) +{ + if (nb < 0) { + return QObject::tr("..."); + } + return QString::number(nb); +} + +static QString displayBTC(double btc, int precision = 8) +{ + if (btc < 0) { + return QObject::tr("..."); + } + return QString::number(btc, 'f', precision); +} + +QString TxListForAddr::getData(int row, int col) const +{ + if (row > data_.size()) { + return {}; + } + const auto& entry = data_.at(row); + const auto totFees = totalFees(row); + switch (col) { + case 0: return QDateTime::fromSecsSinceEpoch(entry.txTime).toString(gui_utils::dateTimeFormat); + case 1: return txId(row); + case 2: return QString::number(entry.nbConf); + case 3: return displayNb(nbInputs(row)); + case 4: return displayNb(nbOutputs(row)); + case 5: return gui_utils::satoshiToQString(entry.value); + case 6: return displayBTC(totFees / BTCNumericTypes::BalanceDivider); + case 7: return (totFees < 0) ? tr("...") : displayBTC(totFees / (double)txSize(row), 1); + case 8: return displayNb(txSize(row)); + default: break; + } + return {}; +} + +int TxListForAddr::nbInputs(int row) const +{ + const auto& itTxDet = txs_.find(row); + if (itTxDet != txs_.end()) { + return itTxDet->second.getNumTxIn(); + } + return -1; +} + +int TxListForAddr::nbOutputs(int row) const +{ + const auto& itTxDet = txs_.find(row); + if (itTxDet != txs_.end()) { + return itTxDet->second.getNumTxOut(); + } + return -1; +} + +int TxListForAddr::txSize(int row) const +{ + const auto& itTxDet = txs_.find(row); + if (itTxDet != txs_.end()) { + return itTxDet->second.getTxWeight(); + } + return -1; +} + +int64_t TxListForAddr::totalFees(int row) const +{ + const auto& itTxDet = txs_.find(row); + if (itTxDet != txs_.end()) { + int64_t txValue = 0; + for (int i = 0; i < itTxDet->second.getNumTxIn(); ++i) { + const auto& in = itTxDet->second.getTxInCopy(i); + const OutPoint op = in.getOutPoint(); + const auto& itInput = inputs_.find(op.getTxHash()); + if (itInput == inputs_.end()) { + return -1; + } + const auto& prevOut = itInput->second.getTxOutCopy(op.getTxOutIndex()); + txValue += prevOut.getValue(); + } + for (int i = 0; i < itTxDet->second.getNumTxOut(); ++i) { + const auto& out = itTxDet->second.getTxOutCopy(i); + txValue -= out.getValue(); + } + return txValue; + } + return -1; +} + +QColor TxListForAddr::dataColor(int row, int col) const +{ + const auto& entry = data_.at(row); + if (col == 2) { + switch (entry.nbConf) { + case 0: return ColorScheme::transactionConfirmationZero; + case 1: + case 2: + case 3: + case 4: + case 5: return QColorConstants::Yellow; + default: return ColorScheme::transactionConfirmationHigh; + } + } + return QColorConstants::White; +} + +QString TxListForAddr::txId(int row) const +{ + try { + return QString::fromStdString(data_.at(row).txHash.toHexStr(true)); + } + catch (const std::exception&) {} + return {}; +} + +QVariant TxListForAddr::data(const QModelIndex& index, int role) const +{ + if (index.column() >= header_.size()) { + return {}; + } + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case ColorRole: + return dataColor(index.row(), index.column()); + default: break; + } + return QVariant(); +} + +QVariant TxListForAddr::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_.at(section); + } + return QVariant(); +} + +QHash TxListForAddr::roleNames() const +{ + return kTxListForAddrRoles; +} + +void TxListForAddr::addRows(const std::vector& entries) +{ + if (!entries.empty()) { + QMetaObject::invokeMethod(this, [this, entries] { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + entries.size() - 1); + data_.insert(data_.end(), entries.crbegin(), entries.crend()); + endInsertRows(); + emit changed(); + }); + } +} + +void TxListForAddr::clear() +{ + QMetaObject::invokeMethod(this, [this] { + beginResetModel(); + data_.clear(); + txs_.clear(); + endResetModel(); + emit changed(); + }); +} + +void TxListForAddr::setDetails(const std::vector& txs) +{ + int rowStart = -1, rowEnd = -1; + for (const auto& tx : txs) { + for (int i = 0; i < data_.size(); ++i) { + const auto& entry = data_.at(i); + if (entry.txHash == tx.getThisHash()) { + txs_[i] = tx; + if (!rowStart) { + rowStart = i; + } + if (rowEnd <= i) { + rowEnd = i; + } + } + } + } + if (rowStart != -1 && rowEnd != -1) { + emit dataChanged(createIndex(rowStart, 3), createIndex(rowEnd, 8)); + } +} + +void TxListForAddr::setInputs(const std::vector& txs) +{ + for (const auto& tx : txs) { + inputs_[tx.getThisHash()] = tx; + } + emit dataChanged(createIndex(0, 6), createIndex(data_.size() - 1, 7)); +} + +void TxListForAddr::setCurrentBlock(uint32_t nbBlock) +{ + if (!nbBlock) { + return; + } + if (!curBlock_) { + curBlock_ = nbBlock; + return; + } + const int diff = nbBlock - curBlock_; + curBlock_ = nbBlock; + for (auto& entry : data_) { + entry.nbConf += diff; + } + emit dataChanged(createIndex(0, 2), createIndex(data_.size() - 1, 2)); +} + +QString TxListForAddr::totalReceived() const +{ + int64_t result = 0; + for (const auto& entry : data_) { + if (entry.value > 0) { + result += entry.value; + } + } + return displayBTC(result / BTCNumericTypes::BalanceDivider); +} + +QString TxListForAddr::totalSent() const +{ + int64_t result = 0; + for (const auto& entry : data_) { + if (entry.value < 0) { + result += entry.value; + } + } + return displayBTC(std::abs(result) / BTCNumericTypes::BalanceDivider); +} + +QString TxListForAddr::balance() const +{ + int64_t result = 0; + for (const auto& entry : data_) { + result += entry.value; + } + return displayBTC(result / BTCNumericTypes::BalanceDivider); +} + + +QTxDetails::QTxDetails(const std::shared_ptr& logger, const BinaryData& txHash, QObject* parent) + : QObject(parent), logger_(logger), txHash_(txHash) +{ + outputsModel_ = new TxOutputsModel(logger_, this); + inputsModel_ = new TxInputsModel(logger_, outputsModel_, this); + selInputsModel_ = new TxInputsSelectedModel(inputsModel_); + inputs_ = new TxInOutModel(tr("Input"), this); + outputs_ = new TxInOutModel(tr("Output"), this); +} + +void QTxDetails::setDetails(const bs::sync::TXWalletDetails& details) +{ + details_ = details; + if (details.changeAddress.address.isValid()) { + details_.outputAddresses.push_back(details.changeAddress); + } + inputs_->setData(details_.inputAddresses); + outputs_->setData(details_.outputAddresses); + outputsModel_->clearOutputs(); + inputsModel_->clear(); + if (!needInputsFromOutputs_) { + for (const auto& out : details.outputAddresses) { + outputsModel_->addOutput(QString::fromStdString(out.address.display()) + , out.value / BTCNumericTypes::BalanceDivider); + } + ins_.clear(); + ins_.reserve(details.inputAddresses.size()); + for (const auto& in : details.inputAddresses) { + logger_->debug("[{}] in: {} {}@{} = {}", __func__, in.address.display() + , in.outHash.toHexStr(true), in.outIndex, in.value); + if (!walletIdFilter_.empty() && (walletIdFilter_.find(in.walletId) == walletIdFilter_.end())) { + continue; + } + ins_.push_back({ in.address, in.outHash, in.outIndex, in.value }); + } + if (!fixedInputs_.empty()) { + setImmutableUTXOs(fixedInputs_); + fixedInputs_.clear(); + } + inputsModel_->addEntries(ins_); + } + + outs_.clear(); + outs_.reserve(details.outputAddresses.size()); + const auto& txId = details_.tx.isInitialized() ? details_.tx.getThisHash() : BinaryData{}; + for (const auto& out : details.outputAddresses) { + outs_.push_back({ out.address, txId, out.outIndex, out.value }); + } + if (needInputsFromOutputs_) { + setInputsFromOutputs(); + } + emit updated(); +} + +void QTxDetails::setImmutableUTXOs(const std::vector& utxos) +{ + if (ins_.empty()) { + fixedInputs_ = utxos; + return; + } + logger_->debug("[{}] {}", __func__, utxos.size()); + inputsModel_->setFixedInputs(ins_); + if (inputsModel_->setFixedUTXOs(utxos) <= 0) { + logger_->debug("[{}] {} UTXOs discarded, saving for later user", __func__, utxos.size()); + fixedInputs_ = utxos; + } +} + +void QTxDetails::addWalletFilter(const std::string& walletId) +{ + walletIdFilter_.insert(walletId); +} + +void QTxDetails::setInputsFromOutputs() +{ + if (outs_.empty()) { + logger_->info("[{}] no outputs received [yet]", __func__); + needInputsFromOutputs_ = true; + return; + } + decltype(outs_) outs; + outs.reserve(details_.outputAddresses.size()); + const auto& txId = details_.tx.isInitialized() ? details_.tx.getThisHash() : BinaryData{}; + for (const auto& out : details_.outputAddresses) { + if (!walletIdFilter_.empty() && (walletIdFilter_.find(out.walletId) == walletIdFilter_.end())) { + logger_->debug("[{}] {} from {} filtered out", __func__, out.address.display(), out.walletId); + continue; + } + outs.push_back({ out.address, txId, out.outIndex, out.value }); + } + + logger_->debug("[{}] {} outs, {} fixed UTXOs saved", __func__, outs.size(), fixedInputs_.size()); + for (const auto& out : outs) { + logger_->debug("[{}] {} {}@{} = {}", __func__, out.address.display() + , out.txId.toHexStr(true), out.txOutIndex, out.amount); + } + inputsModel_->setFixedInputs(outs); + if (!fixedInputs_.empty()) { + inputsModel_->setFixedUTXOs(fixedInputs_); + fixedInputs_.clear(); + } +} + +std::vector> QTxDetails::outputData() const +{ + std::vector> result; + for (const auto& out : outs_) { + result.push_back({out.address, out.amount / BTCNumericTypes::BalanceDivider}); + } + return result; +} + +bool QTxDetails::amountsMatch(float fpb) const +{ + if (outputsModel_->rowCount() <= 1) { + return false; + } + const auto inBalance = inputsModel_->balanceValue(); + if (inBalance < 0.0000001) { + return true; + } + const float fpbCur = feePerByteValue(); + logger_->debug("[{}] fpb={}/{}, feeVal={}", __func__, fpb, fpbCur, feeValue()); + const auto outBalance = outputsModel_->totalAmount(); + const float fee = fpb ? (feeValue() * fpb / (fpbCur ? fpbCur : 1)) : feeValue(); + double diff = inBalance - outBalance - fee; + logger_->debug("[{}] in:{:.8f}, out:{:.8f}, fee:{:.8f}, diff={} (ok:{}) fpb={}", __func__, inBalance + , outBalance, fee, diff, (std::abs(diff) < 0.00001), fpb); + return ((diff > 0) || (std::abs(diff) < 0.00001)); +} + +void QTxDetails::onTopBlock(quint32 curBlock) +{ + if (curBlock_ != curBlock) { + curBlock_ = curBlock; + if (inputsModel_) { + inputsModel_->setTopBlock(curBlock); + } + QMetaObject::invokeMethod(this, [this] { + emit newBlock(); + }); + } +} + +QString QTxDetails::virtSize() const +{ + return displayNb(details_.tx.getTxWeight()); +} + +QString QTxDetails::nbConf() const +{ + const auto txHeight = details_.tx.getTxHeight(); + return (txHeight != UINT32_MAX) ? displayNb(curBlock_ - txHeight + 1) : displayNb(0); +} + +QString QTxDetails::nbInputs() const +{ + return displayNb(details_.tx.getNumTxIn()); +} + +QString QTxDetails::nbOutputs() const +{ + return displayNb(details_.tx.getNumTxOut()); +} + +QString QTxDetails::inputAmount() const +{ + uint64_t amount = 0; + for (const auto& in : details_.inputAddresses) { + amount += in.value; + } + return displayBTC(amount / BTCNumericTypes::BalanceDivider); +} + +QString QTxDetails::outputAmount() const +{ + uint64_t amount = 0; + for (const auto& out : details_.outputAddresses) { + amount += out.value; + } + return displayBTC(amount / BTCNumericTypes::BalanceDivider); +} + +float QTxDetails::feeValue() const +{ + int64_t amount = 0; + for (const auto& in : details_.inputAddresses) { + amount += in.value; + } + for (const auto& out : details_.outputAddresses) { + amount -= out.value; + } + return amount / BTCNumericTypes::BalanceDivider; +} + +QString QTxDetails::fee() const +{ + return displayBTC(feeValue()); +} + +float QTxDetails::feePerByteValue() const +{ + int64_t amount = 0; + for (const auto& in : details_.inputAddresses) { + amount += in.value; + } + for (const auto& out : details_.outputAddresses) { + amount -= out.value; + } + int txWeight = details_.tx.getTxWeight(); + if (!txWeight) { + txWeight = -1; + } + return amount / txWeight; +} +QString QTxDetails::feePerByte() const +{ + return displayBTC(feePerByteValue(), 1); +} + +QString QTxDetails::timestamp() const +{ + return QDateTime::fromSecsSinceEpoch(details_.tx.getTxTime()) + .toString(gui_utils::dateTimeFormat); +} + +QString QTxDetails::height() const +{ + return (details_.tx.getTxHeight() != UINT32_MAX) ? displayNb(details_.tx.getTxHeight()) + : tr("-"); +} + +bs::sync::Transaction::Direction QTxDetails::direction() const +{ + return details_.direction; +} + +TxInOutModel::TxInOutModel(const QString& type, QObject* parent) + : QAbstractTableModel(parent) + , type_(type) + , header_{ tr("Type"), tr("Address"), tr("Amount"), tr("Wallet") } +{} + +void TxInOutModel::setData(const std::vector& data) +{ + beginResetModel(); + data_ = data; + endResetModel(); +} + +int TxInOutModel::rowCount(const QModelIndex&) const +{ + return data_.size(); +} + +int TxInOutModel::columnCount(const QModelIndex&) const +{ + return header_.size(); +} + +QVariant TxInOutModel::data(const QModelIndex& index, int role) const +{ + if (index.column() >= header_.size()) { + return {}; + } + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case ColorRole: + return dataColor(index.row(), index.column()); + case TxHashRole: + try { + return QString::fromStdString(data_.at(index.row()).outHash.toHexStr(true)); + } + catch (const std::exception&) { return {}; } + default: break; + } + return {}; +} + +QVariant TxInOutModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_.at(section); + } + return QVariant(); +} + +QHash TxInOutModel::roleNames() const +{ + return kTxInOutRoles; +} + +QString TxInOutModel::getData(int row, int col) const +{ + if (row > data_.size()) { + return {}; + } + try { + switch (col) { + case 0: return type_; + case 1: return QString::fromStdString(data_.at(row).address.display()); + case 2: return QString::fromStdString(data_.at(row).valueStr); + case 3: return QString::fromStdString(data_.at(row).walletName); + default: break; + } + } + catch (const std::exception&) { + return {}; + } + return {}; +} + +QColor TxInOutModel::dataColor(int row, int col) const +{ + return QColorConstants::White; +} diff --git a/GUI/QtQuick/TxListModel.h b/GUI/QtQuick/TxListModel.h new file mode 100644 index 000000000..e9a1b5f46 --- /dev/null +++ b/GUI/QtQuick/TxListModel.h @@ -0,0 +1,254 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TX_LIST_MODEL_H +#define TX_LIST_MODEL_H + +#include +#include +#include +#include +#include +#include +#include "ArmoryConnection.h" +#include "TxInputsModel.h" +#include "Wallets/SignerDefs.h" + +namespace spdlog { + class logger; +} + +class TxListModel : public QAbstractTableModel +{ + Q_OBJECT +public: + enum TableRoles { TableDataRole = Qt::UserRole + 1, ColorRole, TxIdRole + , RBFRole, NbConfRole, DirectionRole }; + Q_ENUM(TableRoles) + + TxListModel(const std::shared_ptr&, QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + Q_PROPERTY(int nbTx READ nbTx NOTIFY nbTxChanged); + int nbTx() const { return data_.size(); } + + Q_INVOKABLE bool exportCSVto(const QString&); + + Q_INVOKABLE QString getBegDate() const; + Q_INVOKABLE QString getEndDate() const; + + void addRows(const std::vector&); + void clear(); + void setDetails(const bs::sync::TXWalletDetails&, bool usePending = true); + void removeTX(const BinaryData& txHash); + void setCurrentBlock(uint32_t); + +signals: + void nbTxChanged(); + +private: + QVariant getData(int row, int col) const; + QColor dataColor(int row, int col) const; + QString walletName(int row) const; + QString txType(int row) const; + QString txFlag(int row) const; + QString txId(int row) const; + QString txComment(int row) const; + bool isRBF(int row) const; + bs::sync::Transaction::Direction txDirection(int row) const; + +private: + std::shared_ptr logger_; + const QStringList header_; + std::vector data_; + std::map txDetails_; + std::vector pendingDetails_; + uint32_t curBlock_; + mutable std::mutex dataMtx_; + std::atomic_int addingEntries_{ 0 }; +}; + +class TxListForAddr : public QAbstractTableModel +{ + Q_OBJECT +public: + enum TableRoles { + TableDataRole = Qt::UserRole + 1, ColorRole + }; + TxListForAddr(const std::shared_ptr&, QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + void addRows(const std::vector&); + void clear(); + void setDetails(const std::vector&); + void setInputs(const std::vector&); + void setCurrentBlock(uint32_t); + + Q_PROPERTY(QString totalReceived READ totalReceived NOTIFY changed) + QString totalReceived() const; + Q_PROPERTY(QString totalSent READ totalSent NOTIFY changed) + QString totalSent() const; + Q_PROPERTY(QString balance READ balance NOTIFY changed) + QString balance() const; + Q_PROPERTY(int nbTx READ nbTx NOTIFY changed) + int nbTx() const { return data_.size(); } + +signals: + void changed(); + +private: + QString getData(int row, int col) const; + QColor dataColor(int row, int col) const; + QString txId(int row) const; + int nbInputs(int row) const; + int nbOutputs(int row) const; + int txSize(int row) const; + int64_t totalFees(int row) const; + +private: + std::shared_ptr logger_; + const QStringList header_; + std::vector data_; + std::map txs_; + std::map inputs_; + uint32_t curBlock_; +}; + + +class TxInOutModel : public QAbstractTableModel +{ + Q_OBJECT +public: + enum TableRoles { + TableDataRole = Qt::UserRole + 1, ColorRole, TxHashRole + }; + Q_ENUM(TableRoles) + TxInOutModel(const QString& type, QObject* parent = nullptr); + + void setData(const std::vector& data); + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + +private: + QString getData(int row, int col) const; + QColor dataColor(int row, int col) const; + +private: + const QString type_; + const QStringList header_; + std::vector data_; +}; + +namespace Transactions { + Q_NAMESPACE + enum Direction { + Received = 0, + Sent, + Internal, + Unknown + }; + Q_ENUM_NS(Direction) +} + +class TxInputsSelectedModel; +class TxOutputsModel; + +class QTxDetails : public QObject +{ + Q_OBJECT +public: + QTxDetails(const std::shared_ptr& logger, const BinaryData& txHash + , QObject* parent = nullptr); + ~QTxDetails() override = default; + + void setDetails(const bs::sync::TXWalletDetails&); + void setImmutableUTXOs(const std::vector&); + void addWalletFilter(const std::string& walletId); + Q_INVOKABLE void setInputsFromOutputs(); + + Q_PROPERTY(QString txId READ txId NOTIFY updated) + QString txId() const { return QString::fromStdString(txHash_.toHexStr(true)); } + Q_PROPERTY(QString virtSize READ virtSize NOTIFY updated) + QString virtSize() const; + Q_PROPERTY(QString nbConf READ nbConf NOTIFY newBlock) + QString nbConf() const; + Q_PROPERTY(QString nbInputs READ nbInputs NOTIFY updated) + QString nbInputs() const; + Q_PROPERTY(QString nbOutputs READ nbOutputs NOTIFY updated) + QString nbOutputs() const; + Q_PROPERTY(QString inputAmount READ inputAmount NOTIFY updated) + QString inputAmount() const; + Q_PROPERTY(QString outputAmount READ outputAmount NOTIFY updated) + QString outputAmount() const; + Q_PROPERTY(QString fee READ fee NOTIFY updated) + QString fee() const; + Q_PROPERTY(QString feePerByte READ feePerByte NOTIFY updated) + float feePerByteValue() const; + QString feePerByte() const; + Q_PROPERTY(QString height READ height NOTIFY updated) + QString height() const; + Q_PROPERTY(QString timestamp READ timestamp NOTIFY updated) + QString timestamp() const; + + Q_PROPERTY(TxInOutModel* inputs READ inputs CONSTANT) + TxInOutModel* inputs() const { return inputs_; } + Q_PROPERTY(TxInOutModel* outputs READ outputs CONSTANT) + TxInOutModel* outputs() const { return outputs_; } + Q_PROPERTY(TxInputsModel* inputsModel READ inputsModel CONSTANT) + TxInputsModel* inputsModel() const { return inputsModel_; } + Q_PROPERTY(TxInputsSelectedModel* selectedInputsModel READ selInputsModel CONSTANT) + TxInputsSelectedModel* selInputsModel() const { return selInputsModel_; } + Q_PROPERTY(TxOutputsModel* outputsModel READ outputsModel CONSTANT) + TxOutputsModel* outputsModel() const { return outputsModel_; } + std::vector> outputData() const; + Q_INVOKABLE bool amountsMatch(float fpb) const; + + bs::sync::Transaction::Direction direction() const; + +signals: + void updated(); + void newBlock(); + +public slots: + void onTopBlock(quint32); + +private: + float feeValue() const; + +private: + std::shared_ptr logger_; + const BinaryData txHash_; + bs::sync::TXWalletDetails details_; + TxInOutModel* inputs_{ nullptr }; + TxInOutModel* outputs_{ nullptr }; + TxInputsModel* inputsModel_{ nullptr }; + TxInputsSelectedModel* selInputsModel_{ nullptr }; + TxOutputsModel* outputsModel_{ nullptr }; + std::vector ins_, outs_; + uint32_t curBlock_{ 0 }; + bool needInputsFromOutputs_{ false }; + std::vector fixedInputs_; + std::unordered_set walletIdFilter_; +}; + +#endif // TX_LIST_MODEL_H diff --git a/GUI/QtQuick/TxOutputsModel.cpp b/GUI/QtQuick/TxOutputsModel.cpp new file mode 100644 index 000000000..f4aa07520 --- /dev/null +++ b/GUI/QtQuick/TxOutputsModel.cpp @@ -0,0 +1,189 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "TxOutputsModel.h" +#include +#include "Address.h" +#include "BTCNumericTypes.h" +#include "ColorScheme.h" +#include "TxListModel.h" +#include "Utils.h" + +namespace { + static const QHash kRoles{ + {TxOutputsModel::TableDataRole, "tableData"}, + {TxOutputsModel::HeadingRole, "heading"}, + {TxOutputsModel::ColorRole, "dataColor"} + }; +} + +TxOutputsModel::TxOutputsModel(const std::shared_ptr& logger + , QObject* parent, bool readOnly) + : QAbstractTableModel(parent), logger_(logger), readOnly_(readOnly) + , header_{ tr("Output Address"), {}, tr("Amount (BTC)"), {} } +{ + connect(this, &TxOutputsModel::modelReset, + this, &TxOutputsModel::rowCountChanged); + connect(this, &TxOutputsModel::rowsInserted, + this, &TxOutputsModel::rowCountChanged); + connect(this, &TxOutputsModel::rowsRemoved, + this, &TxOutputsModel::rowCountChanged); +} + +int TxOutputsModel::rowCount(const QModelIndex &) const +{ + return data_.size() + 1; +} + +int TxOutputsModel::columnCount(const QModelIndex &) const +{ + return readOnly_ ? header_.size() - 1 : header_.size(); +} + +QVariant TxOutputsModel::data(const QModelIndex& index, int role) const +{ + switch (role) { + case TableDataRole: + return getData(index.row(), index.column()); + case HeadingRole: + return (index.row() == 0); + case ColorRole: + return dataColor(index.row()); + default: break; + } + return QVariant(); +} + +QHash TxOutputsModel::roleNames() const +{ + return kRoles; +} + +QVariant TxOutputsModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation == Qt::Orientation::Horizontal) { + return header_.at(section); + } + return QVariant(); +} + +double TxOutputsModel::totalAmount() const +{ + double result = 0.0; + for (const auto& entry : data_) { + result += entry.amount; + } + return result; +} + +std::vector> TxOutputsModel::recipients() const +{ + std::vector> result; + for (const auto& entry : data_) { + result.emplace_back(entry.address.getRecipient(bs::XBTAmount(entry.amount))); + } + return result; +} + +void TxOutputsModel::clearOutputs() +{ + beginResetModel(); + data_.clear(); + endResetModel(); +} + +QStringList TxOutputsModel::getOutputAddresses() const +{ + QStringList res; + for (int row = 1; row < rowCount(); row++) { + res.append(getData(row, 0).toString()); + } + return res; +} + +QList TxOutputsModel::getOutputAmounts() const +{ + QList res; + for (int row = 1; row < rowCount(); row++) { + res.append(data_.at(row - 1).amount); + } + return res; +} + +void TxOutputsModel::setOutputsFrom(QTxDetails* tx) +{ + beginResetModel(); + data_.clear(); + for (const auto& out : tx->outputData()) { + data_.push_back({out.first, out.second}); + } + endResetModel(); + logger_->debug("[{}] {} entries", __func__, data_.size()); +} + +void TxOutputsModel::addOutput(const QString& address, double amount, bool isChange) +{ + bs::Address addr; + try { + addr = bs::Address::fromAddressString(address.toStdString()); + } + catch (const std::exception&) { + return; + } + QMetaObject::invokeMethod(this, [this, addr, amount, isChange] { + Entry entry{ addr, amount, isChange }; + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + data_.emplace_back(std::move(entry)); + endInsertRows(); + }); +} + +void TxOutputsModel::delOutput(int row) +{ + if (readOnly_) { + return; + } + if (row == 0) { + beginResetModel(); + data_.clear(); + endResetModel(); + } + else { + beginRemoveRows(QModelIndex(), row, row); + data_.erase(data_.cbegin() + row - 1); + endRemoveRows(); + } +} + +QVariant TxOutputsModel::getData(int row, int col) const +{ + if (row == 0) { + return header_.at(col); + } + const auto& entry = data_.at(row - 1); + switch (col) { + case 0: + return QString::fromStdString(entry.address.display()); + case 1: + return entry.isChange ? tr("(change)") : QString{}; + case 2: + return gui_utils::xbtToQString(entry.amount); + default: break; + } + return {}; +} + +QColor TxOutputsModel::dataColor(int row) const +{ + if (row == 0) { + return ColorScheme::tableHeaderColor; + } + return ColorScheme::tableTextColor; +} diff --git a/GUI/QtQuick/TxOutputsModel.h b/GUI/QtQuick/TxOutputsModel.h new file mode 100644 index 000000000..26a97802d --- /dev/null +++ b/GUI/QtQuick/TxOutputsModel.h @@ -0,0 +1,78 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef TX_OUTPUTS_MODEL_H +#define TX_OUTPUTS_MODEL_H + +#include +#include +#include +#include +#include +#include "Address.h" +#include "BinaryData.h" +#include "ScriptRecipient.h" +#include "TxClasses.h" + +namespace spdlog { + class logger; +} +class QTxDetails; + +class TxOutputsModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + Q_PROPERTY(double totalAmount READ totalAmount NOTIFY rowCountChanged) + +public: + enum TableRoles { TableDataRole = Qt::UserRole + 1, HeadingRole, WidthRole + , ColorRole }; + TxOutputsModel(const std::shared_ptr&, QObject* parent = nullptr + , bool readOnly = false); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + + double totalAmount() const; + std::vector> recipients() const; + + Q_INVOKABLE void addOutput(const QString& address, double amount, bool isChange = false); + Q_INVOKABLE void delOutput(int row); + Q_INVOKABLE void clearOutputs(); + Q_INVOKABLE QStringList getOutputAddresses() const; + Q_INVOKABLE QList getOutputAmounts() const; + Q_INVOKABLE void setOutputsFrom(QTxDetails*); + +signals: + void selectionChanged() const; + void rowCountChanged(); + +private: + QVariant getData(int row, int col) const; + QColor dataColor(int row) const; + +private: + std::shared_ptr logger_; + const bool readOnly_; + const QStringList header_; + + struct Entry { + bs::Address address; + double amount; + bool isChange{ false }; + }; + std::vector data_; +}; + +#endif // TX_OUTPUTS_MODEL_H diff --git a/GUI/QtQuick/Utils.cpp b/GUI/QtQuick/Utils.cpp new file mode 100644 index 000000000..bf2b7bda0 --- /dev/null +++ b/GUI/QtQuick/Utils.cpp @@ -0,0 +1,26 @@ +#include "Utils.h" +#include "BTCNumericTypes.h" +#include + +using namespace gui_utils; + +QString gui_utils::satoshiToQString(int64_t balance) +{ + return xbtToQString(balance / BTCNumericTypes::BalanceDivider); +} + +QString gui_utils::xbtToQString(double balance) +{ + return QString::number(balance, 'f', 8); +} + +QString gui_utils::directionToQString(bs::sync::Transaction::Direction direction) +{ + switch (direction) { + case bs::sync::Transaction::Direction::Received: return QObject::tr("Received"); + case bs::sync::Transaction::Direction::Sent: return QObject::tr("Sent"); + case bs::sync::Transaction::Direction::Internal: return QObject::tr("Internal"); + case bs::sync::Transaction::Direction::Unknown: return QObject::tr("Unknown"); + default: return QString::number(static_cast(direction)); + } +} diff --git a/GUI/QtQuick/Utils.h b/GUI/QtQuick/Utils.h new file mode 100644 index 000000000..c5d958913 --- /dev/null +++ b/GUI/QtQuick/Utils.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "Wallets/SignerDefs.h" + +namespace gui_utils { + QString satoshiToQString(int64_t balance); + QString xbtToQString(double balance); + QString directionToQString(bs::sync::Transaction::Direction direction); + + static const QString dateTimeFormat = QString::fromStdString("yyyy-MM-dd hh:mm:ss"); +} diff --git a/GUI/QtQuick/WalletBalancesModel.cpp b/GUI/QtQuick/WalletBalancesModel.cpp new file mode 100644 index 000000000..a3c6e79dc --- /dev/null +++ b/GUI/QtQuick/WalletBalancesModel.cpp @@ -0,0 +1,251 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "WalletBalancesModel.h" +#include +#include +#include +#include "StringUtils.h" +#include "Utils.h" + +namespace { + static const QHash kWalletBalanceRoles{ + {WalletBalance::NameRole, "name"}, + {WalletBalance::IdRole, "id"}, + {WalletBalance::TotalRole, "total"}, + {WalletBalance::ConfirmedRole, "confirmed"}, + {WalletBalance::UnconfirmedRole, "unconfirmed"}, + {WalletBalance::NbAddrRole, "nb_used_addrs"} + }; +} + +WalletBalancesModel::WalletBalancesModel(const std::shared_ptr& logger, QObject* parent) + : QAbstractTableModel(parent), logger_(logger) +{} + +int WalletBalancesModel::rowCount(const QModelIndex &) const +{ + return wallets_.size(); +} + +QVariant WalletBalancesModel::data(const QModelIndex& index, int role) const +{ + if (index.row() < 0) { + return {}; + } + FieldFunc ff{ nullptr }; + switch (role) { + case WalletBalance::NameRole: + return QString::fromStdString(wallets_.at(index.row()).walletName); + case WalletBalance::IdRole: + return QString::fromStdString(wallets_.at(index.row()).walletId); + case WalletBalance::TotalRole: + ff = [](const Balance& bal) { return gui_utils::xbtToQString(bal.total); }; + break; + case WalletBalance::ConfirmedRole: + ff = [](const Balance& bal) { return gui_utils::xbtToQString(bal.confirmed); }; + break; + case WalletBalance::UnconfirmedRole: + ff = [](const Balance& bal) { return gui_utils::xbtToQString(bal.unconfirmed); }; + break; + case WalletBalance::NbAddrRole: + ff = [](const Balance& bal) { return QString::number(bal.nbAddresses); }; + break; + default: break; + } + if (ff != nullptr) { + return getBalance(wallets_.at(index.row()).walletId, ff); + } + return QVariant(); +} + +QString WalletBalancesModel::getBalance(const std::string& walletId + , const FieldFunc& ff, const QString defaultValue) const +{ + const auto& itBal = balances_.find(walletId); + if (itBal == balances_.end()) { + return defaultValue; + } + return ff(itBal->second); +} + +QHash WalletBalancesModel::roleNames() const +{ + return kWalletBalanceRoles; +} + +void WalletBalancesModel::addWallet(const Wallet& wallet) +{ + const auto idx = getWalletIndex(wallet.walletId); + if (idx >= 0) { + logger_->warn("[{}] wallet {} already exists (#{})", __func__, wallet.walletId, idx); + return; + } + //logger_->debug("[WalletBalancesModel::addWallet] adding #{}: {}", rowCount(), wallet.walletName); + beginInsertRows(QModelIndex(), rowCount(), rowCount()); + wallets_.push_back(wallet); + endInsertRows(); + + //find index of new created wallet + if (!createdWalletId_.empty() && wallet.walletId == createdWalletId_) { + createdWalletId_.clear(); + int findIndex = wallets_.size() - 1; + emit walletSelected(findIndex); + } + emit rowCountChanged(); + emit changed(); +} + +int WalletBalancesModel::getWalletIndex(const std::string& walletId) const +{ + int idx = -1; + for (int i = 0; i < wallets_.size(); ++i) { + if (wallets_.at(i).walletId == walletId) { + idx = i; + break; + } + } + return idx; +} + +void WalletBalancesModel::deleteWallet(const std::string& walletId) +{ + const auto idx = getWalletIndex(walletId); + if (idx < 0) { + logger_->warn("[{}] wallet {} is not in the list", __func__, walletId); + return; + } + QMetaObject::invokeMethod(this, [this, idx] { + beginRemoveRows(QModelIndex(), idx, idx); + wallets_.erase(wallets_.cbegin() + idx); + endRemoveRows(); + emit rowCountChanged(); + emit changed(); + if (idx >= wallets_.size()) + emit walletSelected(wallets_.size() - 1); + }); +} + +void WalletBalancesModel::rename(const std::string& walletId, const std::string& newName) +{ + const auto idx = getWalletIndex(walletId); + if (idx < 0) { + logger_->warn("[{}] wallet {} is not in the list", __func__, walletId); + return; + } + wallets_.at(idx).walletName = newName; + emit dataChanged(createIndex(idx, 0), createIndex(idx, 0), { WalletBalance::NameRole }); +} + +QStringList WalletBalancesModel::walletNames() const +{ + QStringList result; + for (const auto& w : wallets_) { + result.append(QString::fromStdString(w.walletName)); + } + return result; +} + +void WalletBalancesModel::clear() +{ + beginResetModel(); + wallets_.clear(); + balances_.clear(); + endResetModel(); + + emit changed(); + emit rowCountChanged(); +} + +void WalletBalancesModel::setWalletBalance(const std::string& walletId, const Balance& bal) +{ + balances_[walletId] = bal; + const int row = getWalletIndex(walletId); + if (row >= 0) { + //logger_->debug("[{}] {} {} found at row {}", __func__, txDet.txHash.toHexStr(), txDet.hdWalletId, row); + emit dataChanged(createIndex(row, 0), createIndex(row, 0), { WalletBalance::TotalRole + , WalletBalance::ConfirmedRole, WalletBalance::UnconfirmedRole, WalletBalance::NbAddrRole }); + } + else { + logger_->warn("[{}] {} not found", __func__, walletId); + } + emit changed(); +} + +void WalletBalancesModel::setSelectedWallet(int index) +{ + selectedWallet_ = index; + emit changed(); +} + +int WalletBalancesModel::selectedWallet() const +{ + return selectedWallet_; +} + +QString WalletBalancesModel::confirmedBalance() const +{ + if (selectedWallet_ >= 0 && selectedWallet_ < wallets_.size()) { + return getBalance(wallets_.at(selectedWallet_).walletId + , [](const Balance& bal) { return gui_utils::xbtToQString(bal.confirmed); }); + } + return tr("-"); +} + +QString WalletBalancesModel::unconfirmedBalance() const +{ + if (selectedWallet_ >= 0 && selectedWallet_ < wallets_.size()) { + return getBalance(wallets_.at(selectedWallet_).walletId + , [](const Balance& bal) { return gui_utils::xbtToQString(bal.unconfirmed); }); + } + return tr("-"); +} + +QString WalletBalancesModel::totalBalance() const +{ + if (selectedWallet_ >= 0 && selectedWallet_ < wallets_.size()) { + return getBalance(wallets_.at(selectedWallet_).walletId + , [](const Balance& bal) { return gui_utils::xbtToQString(bal.total); }); + } + return tr("-"); +} + +QString WalletBalancesModel::numberAddresses() const +{ + if (selectedWallet_ >= 0 && selectedWallet_ < wallets_.size()) { + return getBalance(wallets_.at(selectedWallet_).walletId + , [](const Balance& bal) { return QString::number(bal.nbAddresses); }, QString::fromLatin1("0")); + } + return tr("-"); +} + +void WalletBalancesModel::incNbAddresses(const std::string& walletId, int nb) +{ + balances_[walletId].nbAddresses += nb; + if (wallets_.at(selectedWallet_).walletId == walletId) { + emit changed(); + } +} + +bool WalletBalancesModel::nameExist(const std::string& walletName) +{ + for (int i = 0; i < wallets_.size(); ++i) { + const auto& w = wallets_.at(i); + if (w.walletName == walletName) { + return true; + } + } + return false; +} + +void WalletBalancesModel::setCreatedWalletId(const std::string& walletId) +{ + createdWalletId_ = walletId; +} diff --git a/GUI/QtQuick/WalletBalancesModel.h b/GUI/QtQuick/WalletBalancesModel.h new file mode 100644 index 000000000..005de2488 --- /dev/null +++ b/GUI/QtQuick/WalletBalancesModel.h @@ -0,0 +1,104 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef WALLET_BALANCES_MODEL_H +#define WALLET_BALANCES_MODEL_H + +#include +#include +#include +#include +#include +#include + +namespace spdlog { + class logger; +} + +namespace WalletBalance { + Q_NAMESPACE + enum WalletBalancesRoles { + NameRole = Qt::DisplayRole, + IdRole = Qt::UserRole, + TotalRole, + ConfirmedRole, + UnconfirmedRole, + NbAddrRole + }; + Q_ENUM_NS(WalletBalancesRoles) +} + +class WalletBalancesModel : public QAbstractTableModel +{ + Q_OBJECT + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) + Q_PROPERTY(int selectedWallet READ selectedWallet WRITE setSelectedWallet NOTIFY changed) + Q_PROPERTY(QString confirmedBalance READ confirmedBalance NOTIFY changed) + Q_PROPERTY(QString unconfirmedBalance READ unconfirmedBalance NOTIFY changed) + Q_PROPERTY(QString totalBalance READ totalBalance NOTIFY changed) + Q_PROPERTY(QString numberAddresses READ numberAddresses NOTIFY changed) + +public: + WalletBalancesModel(const std::shared_ptr&, QObject* parent = nullptr); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + int columnCount(const QModelIndex & = QModelIndex()) const override { return 1; } // only wallet names + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + QStringList walletNames() const; + void clear(); + struct Wallet { + std::string walletId; + std::string walletName; + }; + void addWallet(const Wallet&); + std::vector wallets() const { return wallets_; } + void deleteWallet(const std::string& walletId); + + void rename(const std::string& walletId, const std::string& newName); + + struct Balance { + double confirmed{ 0 }; + double unconfirmed{ 0 }; + double total{ 0 }; + uint32_t nbAddresses{ 0 }; + }; + void setWalletBalance(const std::string& walletId, const Balance&); + + void setSelectedWallet(int index); + int selectedWallet() const; + QString confirmedBalance() const; + QString unconfirmedBalance() const; + QString totalBalance() const; + QString numberAddresses() const; + void incNbAddresses(const std::string& walletId, int nb = 1); + bool nameExist(const std::string& walletName); + void setCreatedWalletId(const std::string& walletId); + +signals: + void changed(); + void rowCountChanged(); + void walletSelected(int index); + +private: + using FieldFunc = std::function; + QString getBalance(const std::string& walletId, const FieldFunc&, const QString defaultValue = QLatin1String("0.00000000")) const; + int getWalletIndex(const std::string& walletId) const; + +private: + int selectedWallet_{ -1 }; + std::shared_ptr logger_; + std::vector wallets_; + std::unordered_map balances_; //key: walletId + std::string createdWalletId_; +}; + +#endif // WALLET_BALANCES_MODEL_H diff --git a/GUI/QtQuick/fonts/Roboto-Regular.ttf b/GUI/QtQuick/fonts/Roboto-Regular.ttf new file mode 100644 index 000000000..ddf4bfacb Binary files /dev/null and b/GUI/QtQuick/fonts/Roboto-Regular.ttf differ diff --git a/GUI/QtQuick/hwcommonstructure.h b/GUI/QtQuick/hwcommonstructure.h new file mode 100644 index 000000000..421a28366 --- /dev/null +++ b/GUI/QtQuick/hwcommonstructure.h @@ -0,0 +1,30 @@ +/* + +*********************************************************************************** +* Copyright (C) 2020 - 2021, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#ifndef HWCOMMONSTRUCTURE_H +#define HWCOMMONSTRUCTURE_H + +#include +#include +#include +#include +#include +#include "CoreWallet.h" + + +namespace HWInfoStatus { + const QString kPressButton = QObject::tr("Confirm transaction output(s) on your device"); + const QString kTransaction = QObject::tr("Loading transaction to your device...."); + const QString kReceiveSignedTx = QObject::tr("Receiving signed transaction from device...."); + const QString kTransactionFinished = QObject::tr("Transaction signing finished with success"); + const QString kCancelledByUser = QObject::tr("Cancelled by user"); +} + +#endif // HWCOMMONSTRUCTURE_H diff --git a/BlockSettleHW/hwdevicemodel.cpp b/GUI/QtQuick/hwdevicemodel.cpp similarity index 51% rename from BlockSettleHW/hwdevicemodel.cpp rename to GUI/QtQuick/hwdevicemodel.cpp index f465caad0..98d8e40f4 100644 --- a/BlockSettleHW/hwdevicemodel.cpp +++ b/GUI/QtQuick/hwdevicemodel.cpp @@ -9,20 +9,20 @@ */ #include "hwdevicemodel.h" +#include -HwDeviceModel::HwDeviceModel(QObject *parent /*= nullptr*/) - : QAbstractItemModel(parent) -{ -} +HwDeviceModel::HwDeviceModel(const std::shared_ptr& logger + , QObject *parent /*= nullptr*/) + : QAbstractItemModel(parent), logger_(logger) +{} QVariant HwDeviceModel::data(const QModelIndex& index, int role /*= Qt::DisplayRole*/) const { if (!index.isValid()) { return {}; } - const int row = index.row(); - if (row < 0 || row > devices_.size()) { + if (row < 0 || row >= devices_.size()) { assert(false); return {}; } @@ -30,19 +30,18 @@ QVariant HwDeviceModel::data(const QModelIndex& index, int role /*= Qt::DisplayR switch (static_cast(role)) { case HwDeviceRoles::DeviceId: - return devices_[row].deviceId_; + return QString::fromStdString(devices_.at(row).id); case HwDeviceRoles::Label: - return devices_[row].deviceLabel_; + return QString::fromStdString(devices_.at(row).label); case HwDeviceRoles::Vendor: - return devices_[row].vendor_; + return QString::fromStdString(devices_.at(row).vendor); case HwDeviceRoles::PairedWallet: - return devices_[row].walletId_; + return QString::fromStdString(devices_.at(row).walletId); case HwDeviceRoles::Status: - return devices_[row].status_; + //return QString::fromStdString(devices_.at(row).status); default: break; } - return {}; } @@ -64,59 +63,79 @@ QModelIndex HwDeviceModel::parent(const QModelIndex& index) const return {}; } -int HwDeviceModel::rowCount(const QModelIndex& parent /*= QModelIndex()*/) const +int HwDeviceModel::rowCount(const QModelIndex&) const { return devices_.size(); } -int HwDeviceModel::columnCount(const QModelIndex& parent /*= QModelIndex()*/) const +int HwDeviceModel::columnCount(const QModelIndex&) const { return 1; } -void HwDeviceModel::resetModel(QVector&& deviceKeys) +void HwDeviceModel::setDevices(const std::vector& deviceKeys) { beginResetModel(); - devices_ = std::move(deviceKeys); + loaded_.clear(); + loaded_.resize(deviceKeys.size(), false); + devices_ = deviceKeys; endResetModel(); - emit toppestImportChanged(); + emit dataSet(); } -DeviceKey HwDeviceModel::getDevice(int index) +bs::hww::DeviceKey HwDeviceModel::getDevice(int index) { - if (index < 0 || index > devices_.size()) { + if (index < 0 || index >= devices_.size()) { return {}; } - - return devices_[index]; + return devices_.at(index); } -int HwDeviceModel::getDeviceIndex(DeviceKey key) +int HwDeviceModel::getDeviceIndex(bs::hww::DeviceKey key) { for (int i = 0; i < devices_.size(); ++i) { - if (devices_[i].deviceId_ == key.deviceId_) { + if (devices_.at(i).id == key.id) { return i; } } - return -1; } -int HwDeviceModel::toppestImport() const +void HwDeviceModel::setLoaded(const std::string& walletId) { - if (devices_.empty()) { - return -1; + logger_->debug("[{}] {}", __func__, walletId); + for (int i = 0; i < devices_.size(); ++i) { + const auto& device = devices_.at(i); + if (device.walletId == walletId) { + logger_->debug("[{}] index={}", __func__, i); + loaded_[i] = true; + } } + emit dataSet(); +} +int HwDeviceModel::selDevice() const +{ + logger_->debug("[{}] {} devices, {} loaded", __func__, devices_.size(), loaded_.size()); + if (loaded_.empty() && !devices_.empty()) { + return 0; + } for (int i = 0; i < devices_.size(); ++i) { - if (devices_[i].status_.isEmpty() && devices_[i].walletId_.isEmpty()) { + if (!loaded_.at(i)) { + logger_->debug("[{}] selected {}", __func__, i); return i; } } - return -1; } +void HwDeviceModel::findNewDevice() +{ + if (selDevice() >= 0) { + emit selected(); + } +} + QHash HwDeviceModel::roleNames() const { return { diff --git a/BlockSettleHW/hwdevicemodel.h b/GUI/QtQuick/hwdevicemodel.h similarity index 61% rename from BlockSettleHW/hwdevicemodel.h rename to GUI/QtQuick/hwdevicemodel.h index d33510d9a..4938d35bb 100644 --- a/BlockSettleHW/hwdevicemodel.h +++ b/GUI/QtQuick/hwdevicemodel.h @@ -12,8 +12,13 @@ #define HWDEVICEMODEL_H #include +#include "hwdeviceinterface.h" #include "hwcommonstructure.h" +namespace spdlog { + class logger; +} + enum HwDeviceRoles { DeviceId = Qt::UserRole + 1, Label, @@ -25,9 +30,12 @@ enum HwDeviceRoles { class HwDeviceModel : public QAbstractItemModel { Q_OBJECT - Q_PROPERTY(int toppestImport READ toppestImport NOTIFY toppestImportChanged) + Q_PROPERTY(int selDevice READ selDevice NOTIFY selected) + Q_PROPERTY(bool empty READ empty NOTIFY dataSet) + bool empty() const { return devices_.empty(); } + public: - HwDeviceModel(QObject *parent = nullptr); + HwDeviceModel(const std::shared_ptr&, QObject *parent = nullptr); ~HwDeviceModel() override = default; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; @@ -38,17 +46,23 @@ class HwDeviceModel : public QAbstractItemModel QHash roleNames() const override; - void resetModel(QVector&& deviceKey); - DeviceKey getDevice(int index); - int getDeviceIndex(DeviceKey key); - - Q_INVOKABLE int toppestImport() const; + void setDevices(const std::vector&); + void setLoaded(const std::string& walletId); + bs::hww::DeviceKey getDevice(int index); + int getDeviceIndex(bs::hww::DeviceKey key); + void findNewDevice(); signals: - void toppestImportChanged(); + void selected(); + void dataSet(); + +private: + int selDevice() const; private: - QVector devices_; + std::shared_ptr logger_; + std::vector devices_; + std::vector loaded_; }; Q_DECLARE_METATYPE(HwDeviceModel*) diff --git a/GUI/QtQuick/images/Eye_icon _unvisible.png b/GUI/QtQuick/images/Eye_icon _unvisible.png new file mode 100755 index 000000000..420af037a Binary files /dev/null and b/GUI/QtQuick/images/Eye_icon _unvisible.png differ diff --git a/GUI/QtQuick/images/Eye_icon _visible.png b/GUI/QtQuick/images/Eye_icon _visible.png new file mode 100755 index 000000000..420af037a Binary files /dev/null and b/GUI/QtQuick/images/Eye_icon _visible.png differ diff --git a/GUI/QtQuick/images/File.png b/GUI/QtQuick/images/File.png new file mode 100755 index 000000000..a80ca66fc Binary files /dev/null and b/GUI/QtQuick/images/File.png differ diff --git a/GUI/QtQuick/images/RPK12.png b/GUI/QtQuick/images/RPK12.png new file mode 100644 index 000000000..2e2e725f9 Binary files /dev/null and b/GUI/QtQuick/images/RPK12.png differ diff --git a/GUI/QtQuick/images/RPK24.png b/GUI/QtQuick/images/RPK24.png new file mode 100644 index 000000000..48cc2e84e Binary files /dev/null and b/GUI/QtQuick/images/RPK24.png differ diff --git a/GUI/QtQuick/images/USB_icon_conn.png b/GUI/QtQuick/images/USB_icon_conn.png new file mode 100755 index 000000000..6b096010e Binary files /dev/null and b/GUI/QtQuick/images/USB_icon_conn.png differ diff --git a/GUI/QtQuick/images/USB_icon_disconn.png b/GUI/QtQuick/images/USB_icon_disconn.png new file mode 100755 index 000000000..945307493 Binary files /dev/null and b/GUI/QtQuick/images/USB_icon_disconn.png differ diff --git a/GUI/QtQuick/images/about.png b/GUI/QtQuick/images/about.png new file mode 100644 index 000000000..c1e25bf05 Binary files /dev/null and b/GUI/QtQuick/images/about.png differ diff --git a/GUI/QtQuick/images/about_github.svg b/GUI/QtQuick/images/about_github.svg new file mode 100644 index 000000000..5ab02a019 --- /dev/null +++ b/GUI/QtQuick/images/about_github.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/GUI/QtQuick/images/about_hello.svg b/GUI/QtQuick/images/about_hello.svg new file mode 100644 index 000000000..9b6dbf497 --- /dev/null +++ b/GUI/QtQuick/images/about_hello.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/about_telegram.svg b/GUI/QtQuick/images/about_telegram.svg new file mode 100644 index 000000000..dfa1fc974 --- /dev/null +++ b/GUI/QtQuick/images/about_telegram.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/about_terminal.svg b/GUI/QtQuick/images/about_terminal.svg new file mode 100644 index 000000000..d7fee6f7c --- /dev/null +++ b/GUI/QtQuick/images/about_terminal.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/about_twitter.svg b/GUI/QtQuick/images/about_twitter.svg new file mode 100644 index 000000000..0dface869 --- /dev/null +++ b/GUI/QtQuick/images/about_twitter.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/advanced_icon.png b/GUI/QtQuick/images/advanced_icon.png new file mode 100644 index 000000000..08223fc50 Binary files /dev/null and b/GUI/QtQuick/images/advanced_icon.png differ diff --git a/GUI/QtQuick/images/arrow.png b/GUI/QtQuick/images/arrow.png new file mode 100644 index 000000000..da8dc11b8 Binary files /dev/null and b/GUI/QtQuick/images/arrow.png differ diff --git a/GUI/QtQuick/images/back_arrow.png b/GUI/QtQuick/images/back_arrow.png new file mode 100755 index 000000000..b83e845a6 Binary files /dev/null and b/GUI/QtQuick/images/back_arrow.png differ diff --git a/GUI/QtQuick/images/bitcoin-disabled.png b/GUI/QtQuick/images/bitcoin-disabled.png new file mode 100644 index 000000000..a6a75e5dd Binary files /dev/null and b/GUI/QtQuick/images/bitcoin-disabled.png differ diff --git a/GUI/QtQuick/images/bitcoin-main-net.png b/GUI/QtQuick/images/bitcoin-main-net.png new file mode 100644 index 000000000..2645a46fa Binary files /dev/null and b/GUI/QtQuick/images/bitcoin-main-net.png differ diff --git a/GUI/QtQuick/images/bitcoin-test-net.png b/GUI/QtQuick/images/bitcoin-test-net.png new file mode 100644 index 000000000..737dbc01a Binary files /dev/null and b/GUI/QtQuick/images/bitcoin-test-net.png differ diff --git a/GUI/QtQuick/images/bs_logo.png b/GUI/QtQuick/images/bs_logo.png deleted file mode 100644 index 640473792..000000000 Binary files a/GUI/QtQuick/images/bs_logo.png and /dev/null differ diff --git a/GUI/QtQuick/images/calendar_icon.svg b/GUI/QtQuick/images/calendar_icon.svg new file mode 100644 index 000000000..04aaa6f51 --- /dev/null +++ b/GUI/QtQuick/images/calendar_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/check.svg b/GUI/QtQuick/images/check.svg new file mode 100644 index 000000000..95abec65e --- /dev/null +++ b/GUI/QtQuick/images/check.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/close_button.svg b/GUI/QtQuick/images/close_button.svg new file mode 100644 index 000000000..d72431cdc --- /dev/null +++ b/GUI/QtQuick/images/close_button.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/collapsed.svg b/GUI/QtQuick/images/collapsed.svg new file mode 100644 index 000000000..64f2a472f --- /dev/null +++ b/GUI/QtQuick/images/collapsed.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/combobox_open_button.svg b/GUI/QtQuick/images/combobox_open_button.svg new file mode 100644 index 000000000..e987229de --- /dev/null +++ b/GUI/QtQuick/images/combobox_open_button.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/copy_icon.svg b/GUI/QtQuick/images/copy_icon.svg new file mode 100644 index 000000000..f00ca0002 --- /dev/null +++ b/GUI/QtQuick/images/copy_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/delete.png b/GUI/QtQuick/images/delete.png new file mode 100644 index 000000000..6ed324a4f Binary files /dev/null and b/GUI/QtQuick/images/delete.png differ diff --git a/GUI/QtQuick/images/delete.svg b/GUI/QtQuick/images/delete.svg new file mode 100644 index 000000000..7b9a91b7d --- /dev/null +++ b/GUI/QtQuick/images/delete.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/GUI/QtQuick/images/delete_custom_server.svg b/GUI/QtQuick/images/delete_custom_server.svg new file mode 100644 index 000000000..bd0571a2f --- /dev/null +++ b/GUI/QtQuick/images/delete_custom_server.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/GUI/QtQuick/images/delete_icon.svg b/GUI/QtQuick/images/delete_icon.svg new file mode 100644 index 000000000..97e80d653 --- /dev/null +++ b/GUI/QtQuick/images/delete_icon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/GUI/QtQuick/images/down_arrow.svg b/GUI/QtQuick/images/down_arrow.svg new file mode 100644 index 000000000..491182880 --- /dev/null +++ b/GUI/QtQuick/images/down_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/download_icon.svg b/GUI/QtQuick/images/download_icon.svg new file mode 100644 index 000000000..36c08f22f --- /dev/null +++ b/GUI/QtQuick/images/download_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/GUI/QtQuick/images/edit_wallet_name.png b/GUI/QtQuick/images/edit_wallet_name.png new file mode 100644 index 000000000..23a4bead4 Binary files /dev/null and b/GUI/QtQuick/images/edit_wallet_name.png differ diff --git a/GUI/QtQuick/images/expanded.svg b/GUI/QtQuick/images/expanded.svg new file mode 100644 index 000000000..cc6d8406d --- /dev/null +++ b/GUI/QtQuick/images/expanded.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/explorer_icon.png b/GUI/QtQuick/images/explorer_icon.png new file mode 100755 index 000000000..2b4579dc4 Binary files /dev/null and b/GUI/QtQuick/images/explorer_icon.png differ diff --git a/GUI/QtQuick/images/explorer_icon_unchoosed.png b/GUI/QtQuick/images/explorer_icon_unchoosed.png new file mode 100755 index 000000000..dbd3e9c13 Binary files /dev/null and b/GUI/QtQuick/images/explorer_icon_unchoosed.png differ diff --git a/GUI/QtQuick/images/eye_icon.svg b/GUI/QtQuick/images/eye_icon.svg new file mode 100644 index 000000000..95d0d6da7 --- /dev/null +++ b/GUI/QtQuick/images/eye_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/GUI/QtQuick/images/file_drop.png b/GUI/QtQuick/images/file_drop.png new file mode 100755 index 000000000..f90d6c3c9 Binary files /dev/null and b/GUI/QtQuick/images/file_drop.png differ diff --git a/GUI/QtQuick/images/folder_icon.png b/GUI/QtQuick/images/folder_icon.png new file mode 100755 index 000000000..607559723 Binary files /dev/null and b/GUI/QtQuick/images/folder_icon.png differ diff --git a/GUI/QtQuick/images/full_logo.png b/GUI/QtQuick/images/full_logo.png index bb74f3693..dbb901bdc 100644 Binary files a/GUI/QtQuick/images/full_logo.png and b/GUI/QtQuick/images/full_logo.png differ diff --git a/GUI/QtQuick/images/general.png b/GUI/QtQuick/images/general.png new file mode 100644 index 000000000..b44685500 Binary files /dev/null and b/GUI/QtQuick/images/general.png differ diff --git a/GUI/QtQuick/images/import_icon.svg b/GUI/QtQuick/images/import_icon.svg new file mode 100644 index 000000000..5d1edbff7 --- /dev/null +++ b/GUI/QtQuick/images/import_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/GUI/QtQuick/images/leverex_plugin.png b/GUI/QtQuick/images/leverex_plugin.png new file mode 100644 index 000000000..b8c5fa312 Binary files /dev/null and b/GUI/QtQuick/images/leverex_plugin.png differ diff --git a/GUI/QtQuick/images/lock_icon.svg b/GUI/QtQuick/images/lock_icon.svg new file mode 100644 index 000000000..b4026057d --- /dev/null +++ b/GUI/QtQuick/images/lock_icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/GUI/QtQuick/images/logo.png b/GUI/QtQuick/images/logo.png new file mode 100755 index 000000000..88449d0a4 Binary files /dev/null and b/GUI/QtQuick/images/logo.png differ diff --git a/GUI/QtQuick/images/logo_no_text.svg b/GUI/QtQuick/images/logo_no_text.svg new file mode 100644 index 000000000..4946473e5 --- /dev/null +++ b/GUI/QtQuick/images/logo_no_text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/GUI/QtQuick/images/network.png b/GUI/QtQuick/images/network.png new file mode 100644 index 000000000..71f70be9b Binary files /dev/null and b/GUI/QtQuick/images/network.png differ diff --git a/GUI/QtQuick/images/overview_icon.png b/GUI/QtQuick/images/overview_icon.png new file mode 100755 index 000000000..438232a25 Binary files /dev/null and b/GUI/QtQuick/images/overview_icon.png differ diff --git a/GUI/QtQuick/images/overview_icon_not_choosed.png b/GUI/QtQuick/images/overview_icon_not_choosed.png new file mode 100755 index 000000000..f4ce1baea Binary files /dev/null and b/GUI/QtQuick/images/overview_icon_not_choosed.png differ diff --git a/GUI/QtQuick/images/paste_icon.png b/GUI/QtQuick/images/paste_icon.png new file mode 100644 index 000000000..015a12ebc Binary files /dev/null and b/GUI/QtQuick/images/paste_icon.png differ diff --git a/GUI/QtQuick/images/plugins_icon.png b/GUI/QtQuick/images/plugins_icon.png new file mode 100755 index 000000000..ebb50a781 Binary files /dev/null and b/GUI/QtQuick/images/plugins_icon.png differ diff --git a/GUI/QtQuick/images/plugins_icon_unchoosed.png b/GUI/QtQuick/images/plugins_icon_unchoosed.png new file mode 100755 index 000000000..0e405f3a1 Binary files /dev/null and b/GUI/QtQuick/images/plugins_icon_unchoosed.png differ diff --git a/GUI/QtQuick/images/plus.png b/GUI/QtQuick/images/plus.png new file mode 100644 index 000000000..311b160a1 Binary files /dev/null and b/GUI/QtQuick/images/plus.png differ diff --git a/GUI/QtQuick/images/plus.svg b/GUI/QtQuick/images/plus.svg new file mode 100644 index 000000000..9218f2010 --- /dev/null +++ b/GUI/QtQuick/images/plus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/GUI/QtQuick/images/receive_icon.png b/GUI/QtQuick/images/receive_icon.png new file mode 100755 index 000000000..a3f38c4e6 Binary files /dev/null and b/GUI/QtQuick/images/receive_icon.png differ diff --git a/GUI/QtQuick/images/scan_icon.svg b/GUI/QtQuick/images/scan_icon.svg new file mode 100644 index 000000000..6fbea33cd --- /dev/null +++ b/GUI/QtQuick/images/scan_icon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/GUI/QtQuick/images/search_icon.svg b/GUI/QtQuick/images/search_icon.svg new file mode 100644 index 000000000..bebca294e --- /dev/null +++ b/GUI/QtQuick/images/search_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/send_icon.png b/GUI/QtQuick/images/send_icon.png new file mode 100755 index 000000000..9ffddafc1 Binary files /dev/null and b/GUI/QtQuick/images/send_icon.png differ diff --git a/GUI/QtQuick/images/settings_icon.png b/GUI/QtQuick/images/settings_icon.png new file mode 100755 index 000000000..e2d98c2b8 Binary files /dev/null and b/GUI/QtQuick/images/settings_icon.png differ diff --git a/GUI/QtQuick/images/shield_icon.svg b/GUI/QtQuick/images/shield_icon.svg new file mode 100644 index 000000000..97a745843 --- /dev/null +++ b/GUI/QtQuick/images/shield_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/sideshift_plugin.png b/GUI/QtQuick/images/sideshift_plugin.png new file mode 100644 index 000000000..b27f3f7e1 Binary files /dev/null and b/GUI/QtQuick/images/sideshift_plugin.png differ diff --git a/GUI/QtQuick/images/sideshift_right_arrow.svg b/GUI/QtQuick/images/sideshift_right_arrow.svg new file mode 100644 index 000000000..391d85bd0 --- /dev/null +++ b/GUI/QtQuick/images/sideshift_right_arrow.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/GUI/QtQuick/images/sideswap/btc_icon.svg b/GUI/QtQuick/images/sideswap/btc_icon.svg new file mode 100644 index 000000000..02138e6ba --- /dev/null +++ b/GUI/QtQuick/images/sideswap/btc_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/GUI/QtQuick/images/sideswap/lbtc_icon.svg b/GUI/QtQuick/images/sideswap/lbtc_icon.svg new file mode 100644 index 000000000..eb5408309 --- /dev/null +++ b/GUI/QtQuick/images/sideswap/lbtc_icon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/GUI/QtQuick/images/sideswap_plugin.png b/GUI/QtQuick/images/sideswap_plugin.png new file mode 100644 index 000000000..377c792b6 Binary files /dev/null and b/GUI/QtQuick/images/sideswap_plugin.png differ diff --git a/GUI/QtQuick/images/success.png b/GUI/QtQuick/images/success.png new file mode 100644 index 000000000..0d83d2c5d Binary files /dev/null and b/GUI/QtQuick/images/success.png differ diff --git a/GUI/QtQuick/images/terminal.ico b/GUI/QtQuick/images/terminal.ico new file mode 100644 index 000000000..3b9b2b731 Binary files /dev/null and b/GUI/QtQuick/images/terminal.ico differ diff --git a/GUI/QtQuick/images/transactions_icon.png b/GUI/QtQuick/images/transactions_icon.png new file mode 100755 index 000000000..56cd5b3d5 Binary files /dev/null and b/GUI/QtQuick/images/transactions_icon.png differ diff --git a/GUI/QtQuick/images/transactions_icon_unchoosed.png b/GUI/QtQuick/images/transactions_icon_unchoosed.png new file mode 100755 index 000000000..56e13b315 Binary files /dev/null and b/GUI/QtQuick/images/transactions_icon_unchoosed.png differ diff --git a/GUI/QtQuick/images/transfer_icon.png b/GUI/QtQuick/images/transfer_icon.png new file mode 100644 index 000000000..1ba69cfa4 Binary files /dev/null and b/GUI/QtQuick/images/transfer_icon.png differ diff --git a/GUI/QtQuick/images/try_icon.png b/GUI/QtQuick/images/try_icon.png new file mode 100644 index 000000000..d99226088 Binary files /dev/null and b/GUI/QtQuick/images/try_icon.png differ diff --git a/GUI/QtQuick/images/up_arrow.svg b/GUI/QtQuick/images/up_arrow.svg new file mode 100644 index 000000000..b6766bb96 --- /dev/null +++ b/GUI/QtQuick/images/up_arrow.svg @@ -0,0 +1,3 @@ + + + diff --git a/GUI/QtQuick/images/wallet icon.png b/GUI/QtQuick/images/wallet icon.png new file mode 100755 index 000000000..2aeae9cb4 Binary files /dev/null and b/GUI/QtQuick/images/wallet icon.png differ diff --git a/GUI/QtQuick/images/wallet_file.png b/GUI/QtQuick/images/wallet_file.png new file mode 100755 index 000000000..5d786be90 Binary files /dev/null and b/GUI/QtQuick/images/wallet_file.png differ diff --git a/GUI/QtQuick/images/wallet_icon_warn.svg b/GUI/QtQuick/images/wallet_icon_warn.svg new file mode 100644 index 000000000..07e382b16 --- /dev/null +++ b/GUI/QtQuick/images/wallet_icon_warn.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/GUI/QtQuick/images/warning_icon.png b/GUI/QtQuick/images/warning_icon.png new file mode 100755 index 000000000..efb6fa708 Binary files /dev/null and b/GUI/QtQuick/images/warning_icon.png differ diff --git a/GUI/QtQuick/qml/AddressDetails.qml b/GUI/QtQuick/qml/AddressDetails.qml new file mode 100644 index 000000000..a97327687 --- /dev/null +++ b/GUI/QtQuick/qml/AddressDetails.qml @@ -0,0 +1,288 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.3 +import terminal.models 1.0 + +import "StyledControls" +import "BsStyles" + +Popup { + id: address_details + property string address: "" + property string transactions: "" + property string balance: "" + property string comment: "" + property string asset_type: "" + property string type: "" + property string wallet: "" + + width: BSSizes.applyWindowWidthScale(916) + height: BSSizes.applyWindowHeightScale(718) + anchors.centerIn: Overlay.overlay + + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: BSStyle.popupBackgroundColor + border.width: BSSizes.applyScale(1) + border.color: BSStyle.popupBorderColor + radius: BSSizes.applyScale(14) + } + + contentItem: Rectangle { + color: "transparent" + + Column { + anchors.fill: parent + anchors.topMargin: BSSizes.applyScale(12) + anchors.leftMargin: BSSizes.applyScale(12) + anchors.rightMargin: BSSizes.applyScale(12) + anchors.bottomMargin: BSSizes.applyScale(12) + spacing: BSSizes.applyScale(20) + + Text { + text: qsTr("Address") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + } + + Row { + spacing: BSSizes.applyScale(20) + + Rectangle { + width: BSSizes.applyScale(128) + height: BSSizes.applyScale(128) + color: "white" + radius: BSSizes.applyScale(10) + anchors.verticalCenter: parent.verticalCenter + + Image { + source: address !== "" ? ("image://QR/" + address_details.address) : "" + sourceSize.width: parent.width - parent.radius + sourceSize.height: parent.width - parent.radius + anchors.centerIn: parent + } + } + + Grid { + columns: 2 + rowSpacing: 8 + + Text { + text: qsTr("Transactions") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Text { + text: txListByAddrModel.nbTx > 0 ? txListByAddrModel.nbTx : '-' + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Wallet") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Text { + text: address_details.wallet + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Address") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Row { + Text { + text: address_details.address + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + anchors.verticalCenter: parent.verticalCenter + } + CopyIconButton { + anchors.verticalCenter: parent.verticalCenter + onCopy: bsApp.copyAddressToClipboard(address_details.address) + } + } + + Text { + text: qsTr("Address Type/ID") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Row { + spacing: BSSizes.applyScale(14) + Text { + text: address_details.asset_type + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + Text { + text: address_details.type + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + } + + Text { + text: qsTr("Comment") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Text { + text: address_details.comment !== '' ? address_details.comment : '-' + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Balance") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(140) + } + Label { + text: address_details.balance !== '' ? address_details.balance : '-' + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + } + } + + Row { + spacing: BSSizes.applyScale(8) + + Label { + text: qsTr("Incoming transactions") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(16) + font.weight: Font.DemiBold + font.family: "Roboto" + } + + Image { + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(12) + source: "qrc:/images/down_arrow.svg" + anchors.leftMargin: BSSizes.applyScale(20) + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(170) + color: "transparent" + radius: BSSizes.applyScale(14) + border.color: BSStyle.popupBorderColor + border.width: BSSizes.applyScale(1) + + CustomTableView { + width: parent.width - BSSizes.applyScale(20) + height: parent.height + anchors.centerIn: parent + + model: TransactionForAddressFilterModel { + id: incoming_transaction_model + positive: true + sourceModel: txListByAddrModel + } + copy_button_column_index: 1 + columnWidths: [0.17, 0.6, 0.08, 0.15] + onCopyRequested: bsApp.copyAddressToClipboard(id) + } + } + + Row { + spacing: BSSizes.applyScale(8) + + Label { + text: qsTr("Outgoing transactions") + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(16) + font.weight: Font.DemiBold + } + + Image { + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(12) + source: "qrc:/images/up_arrow.svg" + anchors.leftMargin: BSSizes.applyScale(20) + anchors.verticalCenter: parent.verticalCenter + } + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(170) + color: "transparent" + radius: BSSizes.applyScale(14) + border.color: BSStyle.popupBorderColor + border.width: BSSizes.applyScale(1) + + CustomTableView { + width: parent.width - BSSizes.applyScale(20) + height: parent.height + anchors.centerIn: parent + + + model: TransactionForAddressFilterModel { + id: outgoing_transaction_model + positive: false + sourceModel: txListByAddrModel + } + copy_button_column_index: 1 + columnWidths: [0.17, 0.6, 0.08, 0.15] + onCopyRequested: bsApp.copyAddressToClipboard(id) + } + } + } + } + + CloseIconButton { + anchors.topMargin: BSSizes.applyScale(5) + anchors.rightMargin: BSSizes.applyScale(5) + anchors.right: parent.right + anchors.top: parent.top + + onClose: address_details.close() + } +} diff --git a/GUI/QtQuick/qml/BsStyles/BSSizes.qml b/GUI/QtQuick/qml/BsStyles/BSSizes.qml new file mode 100644 index 000000000..d848e9612 --- /dev/null +++ b/GUI/QtQuick/qml/BsStyles/BSSizes.qml @@ -0,0 +1,40 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +pragma Singleton +import QtQuick 2.0 + + +Item { + property var applyScale + property var applyWindowWidthScale + property var applyWindowHeightScale + + function setupScaleFunctions() { + applyScale = function (size) { + return size * scaleController.scaleRatio; + } + applyWindowWidthScale = function (size) { + return Math.min(applyScale(size), scaleController.screenWidth) + } + applyWindowHeightScale = function (size) { + return Math.min(applyScale(size), (scaleController.screenHeight - applyScale(100))) + } + } + + Connections { + target: scaleController + function onChanged() { + setupScaleFunctions() + } + } + + Component.onCompleted: setupScaleFunctions() +} diff --git a/GUI/QtQuick/qml/BsStyles/BSStyle.qml b/GUI/QtQuick/qml/BsStyles/BSStyle.qml index 5fcf78014..157cf55da 100644 --- a/GUI/QtQuick/qml/BsStyles/BSStyle.qml +++ b/GUI/QtQuick/qml/BsStyles/BSStyle.qml @@ -19,13 +19,14 @@ QtObject { readonly property color backgroundModalColor: "#737373" readonly property color backgroundModeLessColor: "#939393" - readonly property color disabledColor: "#41484f" + readonly property color disabledTextColor: "#71787f" readonly property color disabledBgColor: "#31383f" readonly property color textColor: "white" readonly property color textPressedColor: "#3a8ab4" readonly property color disabledHeaderColor: "#909090" + readonly property color titleTextColor: "#7A88B0" readonly property color labelsTextColor: "#939393" readonly property color labelsTextDisabledColor: "#454E53" @@ -54,7 +55,7 @@ QtObject { readonly property color switchCheckedColor: "#247dac" readonly property color switchOrangeColor: "#f6a724" readonly property color switchUncheckedColor: "#b1b8bf" - readonly property color switchDisabledBgColor: disabledColor + //readonly property color switchDisabledBgColor: disabledColor readonly property color switchDisabledColor: disabledTextColor readonly property color dialogHeaderColor: "#0A1619" @@ -66,11 +67,85 @@ QtObject { readonly property color comboBoxBgColor: "transparent" readonly property color comboBoxItemBgColor: "#17262b" readonly property color comboBoxItemBgHighlightedColor: "#27363b" - readonly property color comboBoxItemTextColor: textColor - readonly property color comboBoxItemTextHighlightedColor: textColor + readonly property color mainnetColor: "#fe9727" readonly property color testnetColor: "#22c064" readonly property color mainnetTextColor: "white" readonly property color testnetTextColor: "black" + + readonly property color selectedColor: "white" + + + + //new properties + readonly property color defaultGreyColor: "#3C435A" + readonly property color wildBlueColor: "#7A88B0" + readonly property color titanWhiteColor: "#E2E7FF" + + readonly property color buttonsStandardColor: defaultGreyColor + readonly property color buttonsStandardPressedColor: "#232734" + readonly property color buttonsStandardHoveredColor: "#2E3343" + readonly property color buttonsStandardBorderColor: "#FFFFFF" + + readonly property color buttonsPreferredColor: "#45A6FF" + readonly property color buttonsPreferredPressedColor: "#0077E4" + readonly property color buttonsPreferredHoveredColor: "#0085FF" + readonly property color buttonsPreferredBorderColor: "#FFFFFF" + + readonly property color buttonsTextColor: "#FFFFFF" + readonly property color buttonsHeaderTextColor: "#7A88B0" + readonly property color buttonsDisabledTextColor: "#1C2130" + + readonly property color buttonsDisabledColor: "#32394F" + + readonly property color comboBoxItemTextColor: "#020817" + readonly property color comboBoxItemTextHighlightedColor: "#45A6FF" + readonly property color comboBoxItemTextCurrentColor: wildBlueColor + readonly property color comboBoxItemHighlightedColor: "#45A6FF" + + readonly property color comboBoxBorderColor: defaultGreyColor + readonly property color comboBoxHoveredBorderColor: wildBlueColor + readonly property color comboBoxFocusedBorderColor: wildBlueColor + readonly property color comboBoxPopupedBorderColor: "#45A6FF" + + readonly property color comboBoxIndicatorColor: "#DCE2FF" + readonly property color comboBoxPopupedIndicatorColor: "#45A6FF" + + readonly property color tableSeparatorColor: defaultGreyColor + readonly property color tableCellBackgroundColor: "transparent" + readonly property color tableCellSelectedBackgroundColor: "#22293B" + + readonly property color balanceValueTextColor: titanWhiteColor + readonly property color addressesPanelBackgroundColor: "#333C435A" + + readonly property color listItemBorderColor: defaultGreyColor + readonly property color listItemHoveredBorderColor: wildBlueColor + + readonly property color popupBackgroundColor: "#191E2A" + readonly property color popupBorderColor: defaultGreyColor + + readonly property color transactionConfirmationZero: "#EB6060" + readonly property color transactionConfirmationLow: "yellow" + readonly property color transactionConfirmationHigh: "#67D2A3" + + readonly property color defaultBorderColor: defaultGreyColor + + readonly property color menuItemTextColor: wildBlueColor + readonly property color menuItemHoveredColor: "#2E3343" + readonly property color menuItemColor: "transparent" + + //not colors + readonly property int defaultPrecision: 8 + + readonly property color transactionTypeSent: "#EB6060" + readonly property color transactionTypeInternal: "cyan" + readonly property color transactionTypeReceived: "#67D2A3" + + readonly property color exportWalletLabelBackground: "#32394F" + readonly property color exportWalletLabelNameColor: "#45A6FF" + + readonly property color smallButtonBackgroundColor: "#020817" + + readonly property color loadingPanelBackgroundColor: "#AA191E2A" } diff --git a/GUI/QtQuick/qml/BsStyles/qmldir b/GUI/QtQuick/qml/BsStyles/qmldir index ad71c3261..b4aea31a3 100644 --- a/GUI/QtQuick/qml/BsStyles/qmldir +++ b/GUI/QtQuick/qml/BsStyles/qmldir @@ -1,3 +1,4 @@ module BlockSettleStyle singleton BSStyle 1.0 BSStyle.qml +singleton BSSizes 1.0 BSSizes.qml diff --git a/GUI/QtQuick/qml/CreateWallet/ConfirmPassword.qml b/GUI/QtQuick/qml/CreateWallet/ConfirmPassword.qml new file mode 100644 index 000000000..5e99392cd --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/ConfirmPassword.qml @@ -0,0 +1,201 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property string wallet_name + signal sig_confirm() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Set password") + } + + CustomMessageDialog { + id: error_dialog + error: qsTr("Password strength is insufficient,\nplease use at least 6 characters") + visible: false + } + + CustomTextInput { + id: password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + activeFocusOnTab: true + + isPassword: true + isHiddenText: true + + title_text: qsTr("Password") + + onEnterPressed: { + click_enter() + } + onReturnPressed: { + click_enter() + } + onTabNavigated: { + if(checkPasswordLength()) { + confirm_password.setActiveFocus() + } + else { + password.setActiveFocus() + } + } + onBackTabNavigated: { + if(checkPasswordLength()) { + if (confirm_but.enabled) { + confirm_but.setActiveFocus() + } + else { + confirm_password.setActiveFocus() + } + } + else { + password.setActiveFocus() + } + } + + function click_enter() { + if (confirm_but.enabled) { + confirm_but.click_enter() + } + else { + if (checkPasswordLength()) { + confirm_password.setActiveFocus() + } + else { + password.setActiveFocus() + } + } + } + } + + + CustomTextInput { + id: confirm_password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + isPassword: true + isHiddenText: true + + title_text: qsTr("Confirm Password") + + onEnterPressed: { + confirm_but.click_enter() + } + onReturnPressed: { + confirm_but.click_enter() + } + + onTabNavigated: { + if (confirm_but.enabled) { + confirm_but.setActiveFocus() + } + else { + password.setActiveFocus() + } + } + onBackTabNavigated: { + password.setActiveFocus() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + text: qsTr("Confirm") + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + enabled: (password.input_text !== "") + && (confirm_password.input_text !== "") + && (password.input_text === confirm_password.input_text) + preferred: true + + function click_enter() { + if (!confirm_but.enabled) return + + if(password.input_text === confirm_password.input_text && checkPasswordLength()) + { + bsApp.createWallet(layout.wallet_name, phrase, password.input_text) + layout.sig_confirm() + clear() + } + else + { + password.isValid = false + confirm_password.isValid = false + } + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } + + function init() + { + clear() + password.setActiveFocus() + } + + function clear() + { + password.isValid = true + confirm_password.isValid = true + password.input_text = "" + confirm_password.input_text = "" + } + + function checkPasswordLength() + { + if (!bsApp.verifyPasswordIntegrity(password.input_text)) { + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + return false + } + return true + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/CreateWallet.qml b/GUI/QtQuick/qml/CreateWallet/CreateWallet.qml new file mode 100644 index 000000000..ad31a6ca5 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/CreateWallet.qml @@ -0,0 +1,154 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +CustomPopup { + id: root + + property var phrase + + objectName: "create_wallet" + + _stack_view.initialItem: terms_conditions + _arrow_but_visibility: !(terms_conditions.visible || start_create.visible ) + + TermsAndConditions { + id: terms_conditions + visible: false + onSig_continue: { + _stack_view.replace(start_create) + start_create.init() + } + } + + StartCreateWallet { + id: start_create + visible: false + onSig_create_new: { + root.phrase = bsApp.newSeedPhrase() + _stack_view.push(wallet_seed) + wallet_seed.init() + } + onSig_import_wallet: { + _stack_view.push(import_wallet) + import_wallet.init() + } + onSig_hardware_wallet: { + //bsApp.importHWWallet(-1) //debugging wallet import + bsApp.pollHWWallets() + _stack_view.push(import_hardware) + } + } + + WalletSeed { + id: wallet_seed + visible: false + phrase: root.phrase + onSig_continue: { + _stack_view.push(wallet_seed_verify) + wallet_seed_verify.init() + } + } + + WalletSeedVerify { + id: wallet_seed_verify + visible: false + phrase: root.phrase + onSig_verified: { + _stack_view.push(wallet_name) + wallet_name.init() + } + onSig_skipped: { + _stack_view.push(wallet_seed_accept) + wallet_seed_accept.init() + } + } + + WalletSeedSkipAccept { + id: wallet_seed_accept + visible: false + onSig_skip: { + _stack_view.replace(wallet_name) + wallet_name.init() + } + onSig_not_skip: { + _stack_view.pop() + } + } + + WalletName { + id: wallet_name + visible: false + onSig_confirm: { + _stack_view.push(confirm_password) + confirm_password.wallet_name = wallet_name.wallet_name + confirm_password.init() + } + } + + ConfirmPassword { + id: confirm_password + visible: false + onSig_confirm: { + root.close() + _stack_view.pop(null) + } + } + + ImportHardware { + id: import_hardware + visible: false + onSig_import: { + bsApp.importHWWallet(hwDeviceModel.selDevice) + root.close() + _stack_view.pop(null) + } + onVisibleChanged: { + if (!visible) { + bsApp.stopHWWalletsPolling() + } + } + } + + ImportWatchingWallet { + id: import_watching_wallet + visible: false + onSig_import: { + root.close() + _stack_view.pop(null) + } + onSig_full: { + _stack_view.replace(import_wallet) + import_wallet.init() + } + } + + ImportWallet { + id: import_wallet + visible: false + onSig_import: { + _stack_view.push(wallet_name) + wallet_name.init() + root.phrase = import_wallet.phrase + } + onSig_only_watching: { + _stack_view.replace(import_watching_wallet) + } + } + + function init() { + if (bsApp.settingActivated === true) + { + _stack_view.pop() + _stack_view.replace(start_create, StackView.Immediate) + start_create.init() + + import_watching_wallet.init() + } + } +} + diff --git a/GUI/QtQuick/qml/CreateWallet/ImportHardware.qml b/GUI/QtQuick/qml/CreateWallet/ImportHardware.qml new file mode 100644 index 000000000..e074bb443 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/ImportHardware.qml @@ -0,0 +1,119 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property bool isConnected: false + + signal sig_import() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Import hardware") + } + + Image { + id: usb_icon + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.preferredHeight : BSSizes.applyScale(44) + Layout.preferredWidth : BSSizes.applyScale(44) + + width: BSSizes.applyScale(44) + height: BSSizes.applyScale(44) + + source: layout.isConnected ? "qrc:/images/USB_icon_conn.png" : "qrc:/images/USB_icon_disconn.png" + } + + + Label { + id: subtitle + visible: hwDeviceModel.empty + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight : BSSizes.applyScale(16) + text: qsTr("Connect your wallet") + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + TableView { + visible: !hwDeviceModel.empty + width: BSSizes.applyScale(800) + height: BSSizes.applyScale(200) + columnSpacing: BSSizes.applyScale(1) + rowSpacing: BSSizes.applyScale(1) + clip: true + ScrollIndicator.horizontal: ScrollIndicator { } + ScrollIndicator.vertical: ScrollIndicator { } + model: hwDeviceModel + delegate: Rectangle { + implicitWidth: BSSizes.applyScale(750) + implicitHeight: BSSizes.applyScale(20) + border.color: "black" + border.width: 1 + color: 'darkslategrey' + Text { + text: label + font.pointSize: BSSizes.applyScale(12) + color: 'lightgrey' + anchors.centerIn: parent + } + MouseArea { + anchors.fill: parent + onClicked: { + //TODO: select device + } + } + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + text: qsTr("Import") + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + enabled: /*layout.isConnected &&*/ (hwDeviceModel.selDevice >= 0) + preferred: true + + function click_enter() { + if (!confirm_but.enabled) return + + sig_import() + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/ImportWallet.qml b/GUI/QtQuick/qml/CreateWallet/ImportWallet.qml new file mode 100644 index 000000000..f65be0c5e --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/ImportWallet.qml @@ -0,0 +1,395 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + + +ColumnLayout { + + id: layout + + signal sig_import() + signal sig_only_watching() + + property bool isFileChoosen: false + property var phrase: [] + + property var grid_model_12: ["1", "2", "3", "4", + "5", "6", "7", "8", + "9", "10", "11", "12"] + property var grid_model_24: ["1", "2", "3", "4", + "5", "6", "7", "8", + "9", "10", "11", "12", + "13", "14", "15", "16", + "17", "18", "19", "20", + "21", "22", "23", "24"] + + height: BSSizes.applyScale(radbut_12.checked ? 555 : 670) + width: BSSizes.applyScale(radbut_12.checked? 580: 760) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Import wallet") + } + + CustomTextSwitch { + id: type_switch + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(24) + + isFullChoosed: true + + onSig_full_changed: (isFull) => { + if (isFull === false) + { + type_switch.isFullChoosed = true + layout.sig_only_watching() + } + } + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(12) + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(32) + Layout.preferredHeight: BSSizes.applyScale(19) + + Label { + Layout.fillWidth: true + } + + Label { + id: radbut_text + + text: qsTr("Seed phrase type:") + + Layout.leftMargin: BSSizes.applyScale(25) + + width: BSSizes.applyScale(126) + height: BSSizes.applyScale(19) + + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + CustomRadioButton { + id: radbut_12 + + text: "12 words" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: true + } + + CustomRadioButton { + id: radbut_24 + + text: "24 words" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: false + } + + Label { + Layout.fillWidth: true + } + } + + Rectangle { + id: hor_line + + Layout.topMargin: BSSizes.applyScale(16) + Layout.fillWidth: true + + height: 1 + + color: BSStyle.defaultGreyColor + } + + GridView { + id: grid + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: BSSizes.applyScale(25) + Layout.topMargin: BSSizes.applyScale(24) + + cellHeight : BSSizes.applyScale(56) + cellWidth : BSSizes.applyScale(180) + + property bool isValid: true + property bool isEmpty: true + property bool hasEmptyWords: true + + model: radbut_12.checked ? layout.grid_model_12 : layout.grid_model_24 + delegate: CustomSeedTextInput { + id: _delega + + property bool isAccepted: false + + width: BSSizes.applyScale(170) + title_text: modelData + onTextEdited : { + show_fill_in_completer() + } + + onActiveFocusChanged: { + if(_delega.activeFocus) + show_fill_in_completer() + } + + onEditingFinished: { + completer_accepted() + check_input() + grid.validate() + } + + Keys.onDownPressed: comp_popup.current_increment() + + Keys.onUpPressed: comp_popup.current_decrement() + + function show_fill_in_completer() + { + if(input_text.length <= 1) + { + comp_popup.close() + _delega.isAccepted = false + } + else + { + if (!comp_popup.visible) + { + comp_popup.x = _delega.x + comp_popup.y = _delega.y + _delega.height + comp_popup.width = _delega.width + comp_popup.index = index + + comp_popup.open() + } + + var _comp_vars = bsApp.completeBIP39dic(input_text) + + _delega.isValid = true + comp_popup.not_valid_word = false + + if (_comp_vars.length === 1) + { + comp_popup.comp_vars = _comp_vars + if (!_delega.isAccepted) + { + completer_accepted() + change_focus() + } + } + else + { + _delega.isAccepted = false + if (_comp_vars.length === 0) + { + _delega.isValid = false + comp_popup.not_valid_word = true + _comp_vars = ["Not a valid word"] + } + + comp_popup.comp_vars = _comp_vars + } + } + + grid.validate() + grid.check_empty_words() + } + + function completer_accepted() + { + if (comp_popup.visible && comp_popup.index === index) + { + if (_delega.isValid) + { + input_text = comp_popup.comp_vars[comp_popup.current_index] + _delega.isAccepted = true + _delega.isValid = true + + grid.validate() + } + comp_popup.close() + comp_popup.comp_vars = [] + comp_popup.not_valid_word = false + } + } + + function change_focus() + { + if(index < grid.count - 1) + grid.itemAtIndex(index+1).setActiveFocus() + else + import_but.forceActiveFocus() + } + + function check_input() + { + _delega.isValid = false + + var _comp_vars = bsApp.completeBIP39dic(input_text) + for(var i=0; i<_comp_vars.length; i++) + { + if (input_text === _comp_vars[i]) + { + _delega.isValid = true + break + } + } + + return _delega.isValid + } + + Keys.onEnterPressed: { + change_focus() + } + + Keys.onReturnPressed: { + change_focus() + } + } + + CustomCompleterPopup { + id: comp_popup + + visible: false + + onCompChoosed: { + grid.itemAtIndex(comp_popup.index).completer_accepted() + } + } + + function validate() + { + grid.isValid = true + grid.isEmpty = true + for (var i = 0; i < grid.count; i++) { + if(!grid.itemAtIndex(i).isValid) { + grid.isValid = false + break + } + if (!grid.itemAtIndex(i).input_text.length > 0) { + grid.isEmpty = false + break + } + } + } + + function check_empty_words() + { + grid.hasEmptyWords = false + for (var i = 0; i < grid.count; i++) + { + var text = grid.itemAtIndex(i).input_text + if(!text.length) + { + grid.hasEmptyWords = true + break + } + } + } + } + + + Label { + id: error_description + + visible: !grid.isValid && !grid.isEmpty + + text: qsTr("Invalid seed") + + Layout.bottomMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(16) + + height: BSSizes.applyScale(16) + width: BSSizes.applyScale(136) + + color: "#EB6060" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + CustomButton { + id: import_but + text: qsTr("Import") + Layout.leftMargin: BSSizes.applyScale(25) + Layout.bottomMargin: BSSizes.applyScale(40) + + width: BSSizes.applyScale(530) + enabled: !grid.hasEmptyWords + preferred: true + Layout.alignment: Qt.AlignCenter + + function click_enter() { + if (!import_but.enabled) return + + for (var i = 0; i < grid.count; i++) + { + grid.itemAtIndex(i).check_input() + } + + if (!grid.isValid) + return + + //Success!!! + for (var i=0; i { + if (isFull === true) + { + type_switch.isFullChoosed = false + layout.sig_full() + } + } + } + + Image { + id: dashed_border + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.leftMargin: BSSizes.applyScale(25) + Layout.rightMargin: BSSizes.applyScale(25) + Layout.preferredHeight: BSSizes.applyScale(282) + height: BSSizes.applyScale(282) + + source: "qrc:/images/file_drop.png" + + Image { + id: file_icon + + visible: layout.chosenFilename.length > 0 + + width: BSSizes.applyScale(12) + height: BSSizes.applyScale(16) + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(203) + anchors.verticalCenter: parent.verticalCenter + + source: "qrc:/images/File.png" + } + + Image { + id: folder_icon + + visible: layout.chosenFilename === "" + + width: BSSizes.applyScale(20) + height: BSSizes.applyScale(16) + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(200) + anchors.verticalCenter: parent.verticalCenter + + source: "qrc:/images/folder_icon.png" + } + + Label { + id: label_file + + visible: layout.chosenFilename.length > 0 + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(230) + anchors.verticalCenter: parent.verticalCenter + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + id: label_folder + + visible: layout.chosenFilename === "" + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(225) + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Select the file") + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + states: [ + State { + name: "clicked" + AnchorChanges { target: file_icon; anchors { left: undefined; } } + AnchorChanges { target: label_file; anchors { left: undefined; horizontalCenter: parent.horizontalCenter} } + + PropertyChanges { target: file_icon; anchors.leftMargin: 0; x: 0; } + PropertyChanges { target: label_file; anchors.horizontalCenterMargin: BSSizes.applyScale(13) } + PropertyChanges { target: file_icon; x: label_file.x - BSSizes.applyScale(27); } + } + ] + + FileDialog { + id: fileDialog + visible: false + folder: shortcuts.documents + onAccepted: { + dashed_border.state = "clicked" + + label_file.text = basename(fileDialog.fileUrl.toString()) + var pathname = fileDialog.fileUrl.toString() + pathname = pathname.replace(/^(file:\/{3})/, "") + layout.chosenFilename = decodeURIComponent(pathname) + dashed_border.source = "qrc:/images/wallet_file.png" + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + fileDialog.visible = true + } + } + + } + + CustomButton { + id: import_but + text: qsTr("Import") + Layout.leftMargin:BSSizes.applyScale( 25) + Layout.topMargin: BSSizes.applyScale(32) + width: BSSizes.applyScale(530) + enabled: layout.chosenFilename.length > 0 + preferred: true + + function click_enter() { + if (!import_but.enabled) return + console.log("import: ", layout.chosenFilename) + bsApp.importWOWallet(layout.chosenFilename) + layout.sig_import() + } + } + + + Keys.onEnterPressed: { + import_but.click_enter() + } + + Keys.onReturnPressed: { + import_but.click_enter() + } + + Label { + id: spacer + Layout.fillHeight: true + Layout.fillWidth: true + } + + function basename(str) + { + return (str.slice(str.lastIndexOf("/")+1)) + } + + function init() { + chosenFilename = "" + } + +} diff --git a/GUI/QtQuick/qml/CreateWallet/StartCreateWallet.qml b/GUI/QtQuick/qml/CreateWallet/StartCreateWallet.qml new file mode 100644 index 000000000..b948c7106 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/StartCreateWallet.qml @@ -0,0 +1,112 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + + +ColumnLayout { + + id: layout + + signal sig_create_new() + signal sig_import_wallet() + signal sig_hardware_wallet() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Create new wallet") + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(24) + } + + Image { + id: wallet_icon + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/wallet icon.png" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(16) + } + + Text { + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(16) + text: "

Need help? Please consult our Getting Started Guides

" + color: "#7A88B0" + onLinkActivated: Qt.openUrlExternally(link) + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + CustomButton { + id: hardware_but + text: qsTr("Hardware Wallet") + width: BSSizes.applyScale(170) + + preferred: false + + function click_enter() { + sig_hardware_wallet() + } + } + + CustomButton { + id: import_but + text: qsTr("Import Wallet") + width: BSSizes.applyScale(170) + + preferred: false + + function click_enter() { + sig_import_wallet() + } + } + + CustomButton { + id: create_but + text: qsTr("Create new") + width: BSSizes.applyScale(170) + + preferred: true + + function click_enter() { + sig_create_new() + } + } + } + + function init() + { + create_but.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/TermsAndConditions.qml b/GUI/QtQuick/qml/CreateWallet/TermsAndConditions.qml new file mode 100644 index 000000000..878f5d2c8 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/TermsAndConditions.qml @@ -0,0 +1,116 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + signal sig_continue() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + Component.onCompleted: { + var xhr = new XMLHttpRequest; + xhr.open("GET", "qrc:/TermsAndConditions.txt"); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + edit.text = xhr.responseText; + } + }; + xhr.send(); + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: BSSizes.applyScale(24) + Layout.preferredHeight : title.height + text: qsTr("Terms and conditions") + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(24) + } + + ScrollView { + id: scroll + Layout.alignment: Qt.AlignLeft + Layout.leftMargin: BSSizes.applyScale(24) + implicitWidth: BSSizes.applyScale(532) + implicitHeight: BSSizes.applyScale(340) + + ScrollBar.vertical.policy: ScrollBar.AlwaysOn + + clip: true + + TextArea { + id: edit + + width: parent.width + height: parent.height + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + leftPadding: 0 + rightPadding: BSSizes.applyScale(10) + color: BSStyle.titanWhiteColor + selectByMouse: true + wrapMode: TextEdit.WordWrap + readOnly: true + + background: Rectangle { + color: "transparent" + radius: BSSizes.applyScale(4) + } + + } + } + + Rectangle { + Layout.fillWidth: true + height: 1 + color: BSStyle.defaultGreyColor + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(24) + } + + CustomButton { + id: continue_but + text: qsTr("Continue") + Layout.leftMargin: BSSizes.applyScale(24) + width: BSSizes.applyScale(532) + preferred: true + + function click_enter() { + bsApp.settingActivated = true + sig_continue() + } + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(24) + } + + + Keys.onEnterPressed: { + continue_but.click_enter() + } + + Keys.onReturnPressed: { + continue_but.click_enter() + } +} + diff --git a/GUI/QtQuick/qml/CreateWallet/WalletName.qml b/GUI/QtQuick/qml/CreateWallet/WalletName.qml new file mode 100644 index 000000000..f6e20a08e --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/WalletName.qml @@ -0,0 +1,110 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property alias wallet_name: input.input_text + + signal sig_confirm() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Name your wallet") + } + + CustomMessageDialog { + id: error_dialog + error: qsTr("Wallet name already exist") + visible: false + } + + CustomTextInput { + id: input + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + activeFocusOnTab: true + hide_placeholder_when_activefocus: false + + title_text: qsTr("Wallet Name") + placeholder_text: qsTr("Primary wallet") + + onEnterPressed: { + confirm_but.click_enter() + } + onReturnPressed: { + confirm_but.click_enter() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + text: qsTr("Confirm") + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + enabled: (input.input_text !== "") + preferred: true + + function click_enter() { + if (!confirm_but.enabled) return + + if (bsApp.isWalletNameExist(input.input_text)) { + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + init() + } + else { + layout.sig_confirm() + } + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } + + function init() + { + clear() + input.setActiveFocus() + } + + function clear() + { + input.input_text = "" + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/WalletSeed.qml b/GUI/QtQuick/qml/CreateWallet/WalletSeed.qml new file mode 100644 index 000000000..4e7b32cda --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/WalletSeed.qml @@ -0,0 +1,110 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 as QLP + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + signal sig_continue() + + property var phrase + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Create new wallet") + } + + Label { + id: subtitle + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight : BSSizes.applyScale(16) + text: qsTr("Write down and store your 12 word seed someplace safe and offline") + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + GridView { + id: grid + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: BSSizes.applyScale(25) + Layout.topMargin: BSSizes.applyScale(32) + + cellHeight : BSSizes.applyScale(56) + cellWidth : BSSizes.applyScale(180) + + model: phrase + delegate: CustomSeedLabel { + seed_text: modelData + serial_num: index + 1 + } + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + //Layout.leftMargin: 24 + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + + QLP.FileDialog { + id: exportFileDialog + title: qsTr("Please choose folder to export pdf") + folder: QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation) + defaultSuffix: "pdf" + fileMode: QLP.FileDialog.SaveFile + onAccepted: { + bsApp.exportWalletToPdf(exportFileDialog.currentFile, phrase) + Qt.openUrlExternally(exportFileDialog.currentFile); + } + } + + CustomButton { + id: copy_seed_but + text: qsTr("Export PDF") + width: BSSizes.applyScale(261) + + preferred: false + + function click_enter() { + exportFileDialog.currentFile = "file:///" + bsApp.makeExportWalletToPdfPath(phrase) + exportFileDialog.open() + } + } + + CustomButton { + id: continue_but + text: qsTr("Continue") + width: BSSizes.applyScale(261) + + preferred: true + + function click_enter() { + layout.sig_continue() + } + } + } + + function init() + { + continue_but.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/WalletSeedSkipAccept.qml b/GUI/QtQuick/qml/CreateWallet/WalletSeedSkipAccept.qml new file mode 100644 index 000000000..9ed039652 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/WalletSeedSkipAccept.qml @@ -0,0 +1,101 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + signal sig_skip() + signal sig_not_skip() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Verify your seed") + } + + Image { + id: warning_icon + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.topMargin: BSSizes.applyScale(24) + Layout.preferredHeight : BSSizes.applyScale(44) + Layout.preferredWidth : BSSizes.applyScale(44) + + source: "qrc:/images/warning_icon.png" + width: BSSizes.applyScale(44) + height: BSSizes.applyScale(44) + } + + Label { + id: warning_description + + text: qsTr("Are you sure you do not want to verify your seed?") + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight : BSSizes.applyScale(16) + + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + //Layout.leftMargin: 24 + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + CustomButton { + id: no_but + text: qsTr("No") + width: BSSizes.applyScale(261) + + preferred: false + + function click_enter() { + layout.sig_not_skip() + } + + } + + CustomButton { + id: skip_but + text: qsTr("Yes, Skip") + width: BSSizes.applyScale(261) + + preferred: true + + function click_enter() { + layout.sig_skip() + } + } + } + + function init() + { + skip_but.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/CreateWallet/WalletSeedVerify.qml b/GUI/QtQuick/qml/CreateWallet/WalletSeedVerify.qml new file mode 100644 index 000000000..890c0f692 --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/WalletSeedVerify.qml @@ -0,0 +1,171 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property var phrase + signal sig_verified() + signal sig_skipped() + + property var indexes: [] + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Verify your seed") + } + + ListView { + id: list + + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: BSSizes.applyScale(25) + Layout.rightMargin: BSSizes.applyScale(25) + Layout.topMargin: BSSizes.applyScale(32) + + spacing: BSSizes.applyScale(10) + property var isValid: true + property var isComplete: false + + delegate: CustomSeedTextInput { + id: _delegate + + width: parent.width + title_text: layout.indexes[index] + isValid: list.isValid + onTextEdited : { + list.isComplete = true + for (var i = 0; i < list.count; i++) + { + if(list.itemAtIndex(i).input_text === "") + { + list.isComplete = false + break + } + } + } + onEnterPressed: { + continue_but.click_enter() + } + onReturnPressed: { + continue_but.click_enter() + } + } + + } + + Label { + id: error_description + + visible: !list.isValid + + text: qsTr("Your words are wrong") + + Layout.leftMargin: BSSizes.applyScale(222) + Layout.bottomMargin: BSSizes.applyScale(114) + Layout.preferredHeight: BSSizes.applyScale(16) + + height: BSSizes.applyScale(16) + width: BSSizes.applyScale(136) + + color: "#EB6060" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + //Layout.leftMargin: 24 + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + CustomButton { + id: skip_but + text: qsTr("Skip") + width: BSSizes.applyScale(261) + + preferred: false + + function click_enter() { + layout.sig_skipped() + } + } + + CustomButton { + id: continue_but + text: qsTr("Continue") + width: BSSizes.applyScale(261) + + enabled: list.isComplete + + preferred: true + + function click_enter() { + if (!continue_but.enabled) return + + list.isValid = true + for (var i = 0; i < list.count; i++) + { + if(list.itemAtIndex(i).input_text !== layout.phrase[layout.indexes[i] - 1]) + { + list.isValid = false + break + } + } + if (list.isValid) + { + layout.sig_verified() + } + } + + } + + } + + function createRandomIndexes() { + var idx = [] + while(idx.length < 4) + { + var r = Math.floor(Math.random() * 12) + 1; + if(idx.indexOf(r) === -1) idx.push(r); + } + for(var i_ord = 0; i_ord < 4; i_ord++) + { + for(var i = 0; i < 3 - i_ord; i++) + { + if(idx[i] > idx[i+1]) + { + var temp = idx[i] + idx[i] = idx[i+1] + idx[i+1] = temp + } + } + } + layout.indexes = idx + list.model = idx + } + + function init() + { + createRandomIndexes() + list.itemAtIndex(0).setActiveFocus() + } +} + diff --git a/GUI/QtQuick/qml/CreateWallet/qmldir b/GUI/QtQuick/qml/CreateWallet/qmldir new file mode 100644 index 000000000..d12ffe5ab --- /dev/null +++ b/GUI/QtQuick/qml/CreateWallet/qmldir @@ -0,0 +1,13 @@ +module CreateWalletControls + +ConfirmPassword 1.0 ConfirmPassword.qml +CreateWallet 1.0 CreateWallet.qml +ImportHardware 1.0 ImportHardware.qml +ImportWallet 1.0 ImportWallet.qml +ImportWatchingWallet 1.0 ImportWatchingWallet.qml +StartCreateWallet 1.0 StartCreateWallet.qml +SuccessNewWallet 1.0 SuccessNewWallet.qml +TermsAndConditions 1.0 TermsAndConditions.qml +WalletSeed 1.0 WalletSeed.qml +WalletSeedSkipAccept 1.0 WalletSeedSkipAccept.qml +WalletSeedVerify 1.0 WalletSeedVerify.qml \ No newline at end of file diff --git a/GUI/QtQuick/qml/ExplorerAddress.qml b/GUI/QtQuick/qml/ExplorerAddress.qml new file mode 100644 index 000000000..aade6bf40 --- /dev/null +++ b/GUI/QtQuick/qml/ExplorerAddress.qml @@ -0,0 +1,157 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 + +import "StyledControls" +import "BsStyles" +import "Overview" +//import "BsControls" +//import "BsDialogs" +//import "js/helper.js" as JsHelper + +Item { + property string address + + signal requestPageChange(var text) + + Column { + spacing: BSSizes.applyScale(20) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + anchors.bottomMargin: BSSizes.applyScale(18) + anchors.fill: parent + + Row { + spacing: BSSizes.applyScale(16) + height: BSSizes.applyScale(20) + + Label { + text: qsTr("Address") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + height: parent.height + verticalAlignment: Text.AlignBottom + } + Label { + id: address_label + text: address !== null ? address : "" + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(14) + height: parent.height + verticalAlignment: Text.AlignBottom + + CopyIconButton { + anchors.left: address_label.right + onCopy: bsApp.copyAddressToClipboard(address) + } + } + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(60) + anchors.bottomMargin: BSSizes.applyScale(24) + anchors.topMargin: BSSizes.applyScale(18) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + + radius: BSSizes.applyScale(14) + color: BSStyle.addressesPanelBackgroundColor + + border.width: BSSizes.applyScale(1) + border.color: BSStyle.comboBoxBorderColor + + Row { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + + BaseBalanceLabel { + width: BSSizes.applyScale(130) + label_text: qsTr("Transaction count") + label_value: txListByAddrModel.nbTx + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(130) + label_text: qsTr("Balance (BTC)") + label_value: txListByAddrModel.balance + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(150) + label_text: qsTr("Total Received (BTC)") + label_value: txListByAddrModel.totalReceived + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(130) + label_text: qsTr("Total Sent (BTC)") + label_value: txListByAddrModel.totalSent + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Label { + text: qsTr("Transactions") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + } + + CustomTableView { + width: parent.width + height: parent.height - BSSizes.applyScale(150) + model: txListByAddrModel + + copy_button_column_index: 1 + columnWidths: [0.12, 0.46, 0.05, 0.04, 0.04, 0.08, 0.08, 0.07, 0.06] + onCopyRequested: bsApp.copyAddressToClipboard(id) + + // TODO: change constant 257 with C++ defined enum + onCellClicked: (row, column, data) => { + var tx_id = column === 1 ? data : model.data(model.index(row, 1), 257) + requestPageChange(tx_id) + } + } + } +} diff --git a/GUI/QtQuick/qml/ExplorerEmpty.qml b/GUI/QtQuick/qml/ExplorerEmpty.qml new file mode 100644 index 000000000..9dbf8ce3b --- /dev/null +++ b/GUI/QtQuick/qml/ExplorerEmpty.qml @@ -0,0 +1,39 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 + +import "BsStyles" + +Item { + anchors.fill: parent + + Column { + spacing: BSSizes.applyScale(24) + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter + + Image { + width: BSSizes.applyScale(57) + height: BSSizes.applyScale(72) + source: "qrc:/images/logo_no_text.svg" + anchors.horizontalCenter: parent.horizontalCenter + } + Label { + text: qsTr("Provides you with a convenient, powerful, yet simple tool to read\n transaction and address data from the bitcoin network") + font.pixelSize: BSSizes.applyScale(16) + color: BSStyle.titleTextColor + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/GUI/QtQuick/qml/ExplorerPage.qml b/GUI/QtQuick/qml/ExplorerPage.qml new file mode 100644 index 000000000..9acf4a77a --- /dev/null +++ b/GUI/QtQuick/qml/ExplorerPage.qml @@ -0,0 +1,238 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 + +import "StyledControls" +import "BsStyles" +//import "BsControls" +//import "BsDialogs" +//import "js/helper.js" as JsHelper + +Rectangle { + color: BSStyle.backgroundColor + property int historyIndex: -1 + property var searchHist: [] + + id: explorer + + function openTransaction(txId) { + expSearchBox.requestSearchText(txId) + } + + function setFocus() + { + expSearchBox.forceActiveFocus() + } + + ExplorerEmpty { + id: explorerEmpty + visible: false + } + ExplorerAddress { + id: explorerAddress + visible: false + onRequestPageChange: (text) => { expSearchBox.requestSearchText(text) } + } + ExplorerTX { + id: explorerTX + visible: false + onRequestPageChange: (text) => { expSearchBox.requestSearchText(text) } + } + + Column { + anchors.fill: parent + spacing: 0 + + Rectangle { + id: explorer_menu_row + height: BSSizes.applyScale(100) + width: parent.width + + color: "transparent" + + Rectangle { + anchors.bottomMargin: BSSizes.applyScale(24) + anchors.topMargin: BSSizes.applyScale(24) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + anchors.fill: parent + anchors.centerIn: parent + + radius: 14 + color: "#020817" + + border.width: 1 + border.color: BSStyle.comboBoxBorderColor + + Row { + spacing: BSSizes.applyScale(20) + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(18) + + Image { + id: search_icon + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + source: "qrc:/images/search_icon.svg" + anchors.verticalCenter: parent.verticalCenter + } + + TextInput { + id: expSearchBox + anchors.verticalCenter: parent.verticalCenter + width: BSSizes.applyScale(900) + clip: true + color: 'lightgrey' + font.pixelSize: BSSizes.applyScale(16) + focus: true + + function resetSearch() { + searchHist = [] + historyIndex = -1 + expSearchBox.clear() + explorerStack.replace(explorerEmpty) + } + + function prev() { + if (historyIndex >= 0) { + historyIndex--; + if (historyIndex >= 0) { + expSearchBox.text = searchHist[historyIndex] + openSearchResult() + } + else { + expSearchBox.clear() + explorerStack.replace(explorerEmpty) + } + } + } + + function next() { + if (historyIndex < (searchHist.length - 1)) { + historyIndex++; + expSearchBox.text = searchHist[historyIndex] + openSearchResult() + } + } + + function requestSearch() { + if (historyIndex >= 0) + searchHist = searchHist.slice(0, historyIndex + 1) + searchHist.push(expSearchBox.text) + historyIndex++; + openSearchResult() + } + + function requestSearchText(newText) { + expSearchBox.text = newText + requestSearch() + } + + function openSearchResult() { + if (expSearchBox.text.length === 0) { + return; + } + + var rc = bsApp.getSearchInputType(expSearchBox.text) + if (rc === 0) { + ibFailure.displayMessage(qsTr("Unknown type of search key")) + } + else if (rc === 1) { // address entered + explorerAddress.address = expSearchBox.text + bsApp.startAddressSearch(explorerAddress.address) + explorerStack.replace(explorerAddress) + expSearchBox.text = "" + } + else if (rc === 2) { // TXid entered + explorerTX.tx = bsApp.getTXDetails(expSearchBox.text) + explorerStack.replace(explorerTX) + expSearchBox.text = "" + } + } + + onAccepted: requestSearch() + onTextEdited: { + if (bsApp.getSearchInputType(expSearchBox.text) != 0) { + requestSearch() + } + } + + Text { + text: qsTr("Search for transaction or address") + font.pixelSize: BSSizes.applyScale(16) + color: BSStyle.titleTextColor + anchors.fill: parent + visible: !expSearchBox.text + verticalAlignment: Text.AlignVCenter + } + } + } + + Row { + id: right_buttons_menu + spacing: BSSizes.applyScale(8) + anchors.rightMargin: BSSizes.applyScale(11) + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Image { + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + source: "qrc:/images/paste_icon.png" + anchors.verticalCenter: parent.verticalCenter + + MouseArea { + anchors.fill: parent + onClicked: { + expSearchBox.clear() + expSearchBox.paste() + expSearchBox.requestSearch() + } + } + } + + CustomSmallButton { + text: qsTr("<") + width: BSSizes.applyScale(29) + height: BSSizes.applyScale(29) + enabled: historyIndex >= 0 + onClicked: expSearchBox.prev() + } + + CustomSmallButton { + text: qsTr(">") + width: BSSizes.applyScale(29) + height: BSSizes.applyScale(29) + enabled: historyIndex < (searchHist.length - 1) + onClicked: expSearchBox.next() + } + + CustomSmallButton { + text: qsTr("Reset") + width: BSSizes.applyScale(68) + height: BSSizes.applyScale(29) + onClicked: expSearchBox.resetSearch() + } + } + } + } + + StackView { + id: explorerStack + width: parent.width + height: parent.height - explorer_menu_row.height + initialItem: explorerEmpty + } + } +} diff --git a/GUI/QtQuick/qml/ExplorerTX.qml b/GUI/QtQuick/qml/ExplorerTX.qml new file mode 100644 index 000000000..2f2191d43 --- /dev/null +++ b/GUI/QtQuick/qml/ExplorerTX.qml @@ -0,0 +1,294 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 +import terminal.models 1.0 + +import "StyledControls" +import "BsStyles" +import "Overview" +//import "BsControls" +//import "BsDialogs" +//import "js/helper.js" as JsHelper + +Item { + property var tx: null + + signal requestPageChange(var text) + + Column { + spacing: BSSizes.applyScale(23) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + anchors.bottomMargin: BSSizes.applyScale(18) + anchors.fill: parent + + Row { + spacing: BSSizes.applyScale(16) + height: BSSizes.applyScale(20) + width: parent.width + + Label { + text: qsTr("Transaction ID") + height: parent.height + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + verticalAlignment: Text.AlignBottom + } + Label { + id: transactionIdLabel + height: parent.height + text: tx ? tx.txId : qsTr("Unknown") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + verticalAlignment: Text.AlignBottom + + CopyIconButton { + anchors.left: transactionIdLabel.right + onCopy: bsApp.copyAddressToClipboard(tx.txId) + } + } + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(60) + anchors.bottomMargin: BSSizes.applyScale(24) + anchors.topMargin: BSSizes.applyScale(24) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + + radius: BSSizes.applyScale(14) + color: BSStyle.addressesPanelBackgroundColor + + border.width: 1 + border.color: BSStyle.comboBoxBorderColor + + Row { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + + BaseBalanceLabel { + width: BSSizes.applyScale(110) + label_text: qsTr("Confirmations") + label_value: tx !== null ? tx.nbConf : "" + anchors.verticalCenter: parent.verticalCenter + label_value_color: "green" + } + + Rectangle { + width: 1 + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(80) + label_text: qsTr("Inputs") + label_value: tx !== null ? tx.nbInputs : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(90) + label_text: qsTr("Outputs") + label_value: tx !== null ? tx.nbOutputs : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(150) + label_text: qsTr("Input Amount (BTC)") + label_value: tx !== null ? tx.inputAmount : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(150) + label_text: qsTr("Output Amount (BTC)") + label_value: tx !== null ? tx.outputAmount : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(130) + label_text: qsTr("Fees (BTC)") + label_value: tx !== null ? tx.fee : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(150) + label_text: qsTr("Fee per byte (s/b)") + label_value: tx !== null ? tx.feePerByte : "" + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(36) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + BaseBalanceLabel { + width: BSSizes.applyScale(150) + label_text: qsTr("Size (virtual bytes)") + label_value: tx !== null ? tx.virtSize : "" + anchors.verticalCenter: parent.verticalCenter + } + } + } + + Row { + spacing: BSSizes.applyScale(24) + width: parent.width + height: parent.height - BSSizes.applyScale(120) + + Rectangle { + height: parent.height + width: parent.width / 2 - BSSizes.applyScale(12) + + color: "transparent" + radius: BSSizes.applyScale(14) + + border.width: 1 + border.color: BSStyle.tableSeparatorColor + + Column { + anchors.fill: parent + anchors.margins: BSSizes.applyScale(20) + + Row { + spacing: BSSizes.applyScale(11) + Label { + text: qsTr("Input") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + } + Image { + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(12) + source: "qrc:/images/down_arrow.svg" + anchors.leftMargin: BSSizes.applyScale(20) + anchors.verticalCenter: parent.verticalCenter + } + } + + InputOutputTableView { + width: parent.width + height: parent.height - BSSizes.applyScale(20) + model: tx !== null ? tx.inputs : [] + copy_button_column_index: -1 + columnWidths: [0.0, 0.7, 0.2, 0.1] + onCopyRequested: bsApp.copyAddressToClipboard(id) + + onCellClicked: (row, column, data, mouse) => { + var address = model.data(model.index(row, 1), TxInOutModel.TableDataRole) + requestPageChange(address) + } + } + } + } + + Rectangle { + height: parent.height + width: parent.width / 2 - BSSizes.applyScale(12) + + color: "transparent" + radius: BSSizes.applyScale(14) + + border.width: BSSizes.applyScale(1) + border.color: BSStyle.tableSeparatorColor + + Column { + anchors.fill: parent + anchors.margins: BSSizes.applyScale(20) + + Row { + spacing: BSSizes.applyScale(11) + Label { + text: qsTr("Output") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + } + Image { + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(12) + source: "qrc:/images/up_arrow.svg" + anchors.verticalCenter: parent.verticalCenter + } + } + + InputOutputTableView { + width: parent.width + height: parent.height - BSSizes.applyScale(20) + model: tx !== null ? tx.outputs : [] + copy_button_column_index: -1 + columnWidths: [0.0, 0.7, 0.2, 0.1] + onCopyRequested: bsApp.copyAddressToClipboard(id) + + onCellClicked: (row, column, data, mouse) => { + var address = model.data(model.index(row, 1), TxInOutModel.TableDataRole) + requestPageChange(address) + } + } + } + } + } + } +} diff --git a/GUI/QtQuick/qml/InfoBanner.qml b/GUI/QtQuick/qml/InfoBanner.qml index 7cedd5c37..b7dc0ad4d 100644 --- a/GUI/QtQuick/qml/InfoBanner.qml +++ b/GUI/QtQuick/qml/InfoBanner.qml @@ -19,6 +19,7 @@ Loader { messages.source = Qt.resolvedUrl("InfoBannerComponent.qml"); messages.item.message = message; messages.item.bgColor = bgColor + messages.visible = true } width: parent.width @@ -33,9 +34,10 @@ Loader { Timer { id: timer - interval: 7000 + interval: 5000 onTriggered: { messages.state = "" + messages.visible = false } } diff --git a/GUI/QtQuick/qml/InfoBannerComponent.qml b/GUI/QtQuick/qml/InfoBannerComponent.qml index b43bad00d..63832c537 100644 --- a/GUI/QtQuick/qml/InfoBannerComponent.qml +++ b/GUI/QtQuick/qml/InfoBannerComponent.qml @@ -10,13 +10,15 @@ */ import QtQuick 2.9 +import "BsStyles" + Item { id: banner property alias message: messageText.text property alias bgColor: background.color - height: 70 + height: BSSizes.applyScale(70) Rectangle { id: background @@ -27,10 +29,10 @@ Item { } Text { - font.pixelSize: 20 + font.pixelSize: BSSizes.applyScale(20) renderType: Text.QtRendering - width: 150 - height: 40 + width: BSSizes.applyScale(150) + height: BSSizes.applyScale(40) id: messageText @@ -43,7 +45,7 @@ Item { states: State { name: "portrait" - PropertyChanges { target: banner; height: 100 } + PropertyChanges { target: banner; height: BSSizes.applyScale(100) } } MouseArea { diff --git a/GUI/QtQuick/qml/InfoBar.qml b/GUI/QtQuick/qml/InfoBar.qml index 1a96c8cfe..f09a989a7 100644 --- a/GUI/QtQuick/qml/InfoBar.qml +++ b/GUI/QtQuick/qml/InfoBar.qml @@ -15,13 +15,13 @@ import "BsStyles" Item { id: infoBarRoot - height: 30 + height: BSSizes.applyScale(30) property bool showChangeApplyMessage: false RowLayout { anchors.fill: parent - spacing: 10 + spacing: BSSizes.applyScale(10) Item { Layout.fillWidth: true @@ -31,7 +31,7 @@ Item { visible: infoBarRoot.showChangeApplyMessage anchors { fill: parent - leftMargin: 10 + leftMargin: BSSizes.applyScale(10) } horizontalAlignment: Text.AlignLeft verticalAlignment: Text.AlignVCenter @@ -45,10 +45,10 @@ Item { id: netLabel property bool bInitAsTestNet: signerSettings.testNet - radius: 5 + radius: BSSizes.applyScale(5) color: bInitAsTestNet ? BSStyle.testnetColor : BSStyle.mainnetColor - width: 100 - height: 20 + width: BSSizes.applyScale(100) + height: BSSizes.applyScale(20) Layout.alignment: Qt.AlignVCenter Text { diff --git a/GUI/QtQuick/qml/Overview/BalanceBar.qml b/GUI/QtQuick/qml/Overview/BalanceBar.qml new file mode 100644 index 000000000..66c35f308 --- /dev/null +++ b/GUI/QtQuick/qml/Overview/BalanceBar.qml @@ -0,0 +1,86 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 + +import "../BsStyles" +import "." as OverviewControls + +Rectangle { + id: control + + width: BSSizes.applyScale(520) + height: BSSizes.applyScale(100) + color: "transparent" + + property alias confirmed_balance_value: confirmed_balance.label_value + property alias uncorfirmed_balance_value: unconfirmed_balance.label_value + property alias total_balance_value: total_balance.label_value + property alias used_addresses_value: used_addresses.label_value + + property int spacer_height: BSSizes.applyScale(36) + + Row { + anchors.fill: parent + spacing: BSSizes.applyScale(10) + + OverviewControls.BaseBalanceLabel { + id: confirmed_balance + width: BSSizes.applyScale(130) + label_text: qsTr("Confirmed balance") + value_suffix: qsTr('BTC') + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 1 + height: control.spacer_height + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + OverviewControls.BaseBalanceLabel { + id: unconfirmed_balance + width: BSSizes.applyScale(130) + label_text: qsTr("Unconfirmed balance") + value_suffix: qsTr('BTC') + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 1 + height: control.spacer_height + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + OverviewControls.BaseBalanceLabel { + id: total_balance + width: BSSizes.applyScale(130) + label_text: qsTr("Total balance") + value_suffix: qsTr('BTC') + anchors.verticalCenter: parent.verticalCenter + } + + Rectangle { + width: 1 + height: control.spacer_height + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + OverviewControls.BaseBalanceLabel { + id: used_addresses + label_text: qsTr("#Used addresses") + value_suffix: "" + anchors.verticalCenter: parent.verticalCenter + } + } +} diff --git a/GUI/QtQuick/qml/Overview/BaseBalanceLabel.qml b/GUI/QtQuick/qml/Overview/BaseBalanceLabel.qml new file mode 100644 index 000000000..c13f32b3d --- /dev/null +++ b/GUI/QtQuick/qml/Overview/BaseBalanceLabel.qml @@ -0,0 +1,54 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 + +import "../BsStyles" + +Rectangle { + id: control + + property string label_text + property string label_value + property color label_value_color: BSStyle.balanceValueTextColor + + property string value_suffix + property int left_text_padding: BSSizes.applyScale(10) + + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(53) + color: "transparent" + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 5 + + Text { + text: control.label_text + leftPadding: control.left_text_padding + + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(12) + font.letterSpacing: -0.2 + } + + Text { + text: control.label_value + " " + control.value_suffix + leftPadding: control.left_text_padding + + color: control.label_value_color + font.family: "Roboto" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(14) + //font.letterSpacing: 0.2 + } + } +} diff --git a/GUI/QtQuick/qml/Overview/OverviewPanel.qml b/GUI/QtQuick/qml/Overview/OverviewPanel.qml new file mode 100644 index 000000000..31a3f073a --- /dev/null +++ b/GUI/QtQuick/qml/Overview/OverviewPanel.qml @@ -0,0 +1,194 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "." as OverviewControls +import ".." +import "../BsStyles" +import "../StyledControls" +import terminal.models 1.0 + +Rectangle { + id: control + + signal openSend (string txId, bool isRBF, bool isCPFP) + signal openExplorer (string txId) + color: "transparent" + + signal requestWalletProperties() + signal createNewWallet() + signal walletIndexChanged(index : int) + signal openAddressDetails(var address, var transactions, var balance, var comment, var asset_type, var type, var wallet) + + Column { + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + anchors.fill: parent + spacing: 0 + + OverviewControls.OverviewWalletBar { + id: overview_panel + width: parent.width + height: BSSizes.applyScale(100) + + onRequestWalletProperties: control.requestWalletProperties() + onCreateNewWallet: control.createNewWallet() + onWalletIndexChanged: control.walletIndexChanged(index) + } + + Rectangle { + height: (parent.height - overview_panel.height) * 0.6 + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + + radius: BSSizes.applyScale(16) + color: BSStyle.addressesPanelBackgroundColor + border.width: 1 + border.color: BSStyle.tableSeparatorColor + + Column { + anchors.fill: parent + anchors.margins: BSSizes.applyScale(20) + spacing: BSSizes.applyScale(10) + + Rectangle { + id: tableMenu + color: "transparent" + width: parent.width + height: BSSizes.applyScale(30) + + Text { + text: qsTr("Addresses") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: BSSizes.applyScale(0.35) + } + + Row { + anchors.right: parent.right + spacing: 6 + + CustomSmallButton { + width: BSSizes.applyScale(85) + text: qsTr("Hide used") + onClicked: { + addressFilterModel.hideUsed = !addressFilterModel.hideUsed + tablewView.update() + } + backgroundColor: addressFilterModel.hideUsed ? BSStyle.smallButtonBackgroundColor : 'transparent' + } + + CustomSmallButton { + width: BSSizes.applyScale(90) + text: qsTr("Hide internal") + onClicked: { + addressFilterModel.hideInternal = !addressFilterModel.hideInternal + tablewView.update() + } + backgroundColor: addressFilterModel.hideInternal ? BSStyle.smallButtonBackgroundColor : 'transparent' + } + + CustomSmallButton { + width: BSSizes.applyScale(90) + text: qsTr("Hide external") + onClicked: { + addressFilterModel.hideExternal = !addressFilterModel.hideExternal + tablewView.update() + } + backgroundColor: addressFilterModel.hideExternal ? BSStyle.smallButtonBackgroundColor : 'transparent' + } + + CustomSmallButton { + width: BSSizes.applyScale(85) + text: qsTr("Hide empty") + onClicked: { + addressFilterModel.hideEmpty = !addressFilterModel.hideEmpty + tablewView.update() + } + backgroundColor: addressFilterModel.hideEmpty ? BSStyle.smallButtonBackgroundColor : 'transparent' + } + } + } + + CustomTableView { + id: tablewView + width: parent.width + height: parent.height - tableMenu.height + + model: addressFilterModel + copy_button_column_index: 0 + text_header_size: BSSizes.applyScale(12) + cell_text_size: BSSizes.applyScale(13) + + columnWidths: [0.35, 0.15, 0.1, 0.4] + onCopyRequested: bsApp.copyAddressToClipboard(id) + onCellClicked: (row, column, data) => { + const address = (column === 0) ? data : model.data(model.index(row, 0), QmlAddressListModel.TableDataRole) + const transactions = model.data(model.index(row, 1), QmlAddressListModel.TableDataRole) + const balance = model.data(model.index(row, 2), QmlAddressListModel.TableDataRole) + const comment = model.data(model.index(row, 3), QmlAddressListModel.TableDataRole) + const type = model.data(model.index(row, 0), QmlAddressListModel.AddressTypeRole) + const asset_type = model.data(model.index(row, 0), QmlAddressListModel.AssetTypeRole) + + openAddressDetails(address, transactions, balance, comment, asset_type, type, overview_panel.currentWallet) + } + + Connections { + target: addressFilterModel + function onModelReset() + { + tablewView.update() + } + } + } + } + } + + Rectangle { + color: "transparent" + width: parent.width + height: (parent.height - overview_panel.height) * 0.4 + + Column { + anchors.fill: parent + anchors.topMargin: BSSizes.applyScale(20) + spacing: BSSizes.applyScale(10) + + Text { + text: qsTr("Non-settled Transactions") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + } + + CustomTransactionsTableView { + width: parent.width + height: parent.height - BSSizes.applyScale(40) + + model: PendingTransactionFilterModel { + id: pendingTransactionModel + + sourceModel: txListModel + dynamicSortFilter: true + } + + onOpenSend: (txId, isRBF, isCPFP) => control.openSend(txId, isRBF, isCPFP) + onOpenExplorer: (txId) => control.openExplorer(txId) + } + } + } + } +} diff --git a/GUI/QtQuick/qml/Overview/OverviewWalletBar.qml b/GUI/QtQuick/qml/Overview/OverviewWalletBar.qml new file mode 100644 index 000000000..176bb85dd --- /dev/null +++ b/GUI/QtQuick/qml/Overview/OverviewWalletBar.qml @@ -0,0 +1,113 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import wallet.balance 1.0 + +import "../BsStyles" +import "." as OverviewControls +import "../StyledControls" as Controls + +Rectangle { + id: control + color: "transparent" + + property alias currentWallet: wallet_selection_combobox.currentValue + + signal requestWalletProperties() + signal createNewWallet() + signal walletIndexChanged(index : int) + + Row { + anchors.fill:parent + spacing: BSSizes.applyScale(20) + + Controls.CustomComboBox { + id: wallet_selection_combobox + anchors.verticalCenter: parent.verticalCenter + objectName: "walletsComboBox" + + width: BSSizes.applyScale(263) + height: BSSizes.applyScale(53) + + model: walletBalances.rowCount > 0 ? walletBalances : [{ "name": qsTr("Create wallet")}] + textRole: "name" + valueRole: "name" + + fontSize: BSSizes.applyScale(16) + + onCurrentIndexChanged: { + if (walletBalances.rowCount !== 0) { + bsApp.walletSelected(wallet_selection_combobox.currentIndex) + control.walletIndexChanged(wallet_selection_combobox.currentIndex) + walletBalances.selectedWallet = wallet_selection_combobox.currentIndex + } + } + + onActivated: { + if (walletBalances.rowCount === 0) { + control.createNewWallet() + } + else { + bsApp.walletSelected(wallet_selection_combobox.currentIndex) + control.walletIndexChanged(wallet_selection_combobox.currentIndex) + walletBalances.selectedWallet = wallet_selection_combobox.currentIndex + } + } + + onModelChanged: { + if (model === walletBalances) { + wallet_selection_combobox.currentIndex = walletBalances.selectedWallet + } + } + + Connections { + target: bsApp + function onRequestWalletSelection(index) { + var modelSize = (wallet_selection_combobox.model === walletBalances) + ? wallet_selection_combobox.model.rowCount + : wallet_selection_combobox.model.length + if (index >= 0 && index < modelSize) { + bsApp.walletSelected(index) + control.walletIndexChanged(index) + wallet_selection_combobox.currentIndex = index + } + } + } + } + + OverviewControls.BalanceBar { + id: balance_bar + anchors.verticalCenter: parent.verticalCenter + + confirmed_balance_value: walletBalances.confirmedBalance + uncorfirmed_balance_value: walletBalances.unconfirmedBalance + total_balance_value: walletBalances.totalBalance + used_addresses_value: walletBalances.numberAddresses + } + } + + Row { + spacing: BSSizes.applyScale(10) + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + + Controls.CustomMediumButton { + enabled: wallet_selection_combobox.model == walletBalances && walletBalances.selectedWallet >= 0 + text: qsTr("Wallet Properties") + onClicked: control.requestWalletProperties() + } + + Controls.CustomMediumButton { + text: qsTr("Create new wallet") + onClicked: control.createNewWallet() + } + } +} diff --git a/GUI/QtQuick/qml/OverviewPage.qml b/GUI/QtQuick/qml/OverviewPage.qml new file mode 100644 index 000000000..d0fd8b520 --- /dev/null +++ b/GUI/QtQuick/qml/OverviewPage.qml @@ -0,0 +1,80 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 + +import "Overview" as Overview +import "StyledControls" +import "BsStyles" +import "WalletProperties" +//import "BsControls" +//import "BsDialogs" +//import "js/helper.js" as JsHelper +import wallet.balance 1.0 + +Rectangle { + id: overview + property int walletIndex: 0 + color: BSStyle.backgroundColor + + signal newWalletClicked(); + signal curWalletIndexChanged(index : int) + signal openSend (string txId, bool isRBF, bool isCPFP, int selWallet) + signal openExplorer (string txId) + + MouseArea { + anchors.fill: parent + } + + AddressDetails { + id: addressDetails + visible: false + } + + WalletPropertiesPopup { + id: walletProperties + visible: false + + wallet_properties_vm: bsApp.walletProperitesVM + } + + Overview.OverviewPanel { + anchors.fill: parent + + onRequestWalletProperties: { + bsApp.getUTXOsForWallet(walletIndex, null) + walletProperties.show() + walletProperties.raise() + walletProperties.requestActivate() + } + onCreateNewWallet: overview.newWalletClicked() + onWalletIndexChanged: { + walletIndex = index + overview.curWalletIndexChanged(index) + } + onOpenAddressDetails: (address, transactions, balance, comment, asset_type, type, wallet) => { + addressDetails.address = address + addressDetails.transactions = transactions + addressDetails.balance = balance + addressDetails.comment = comment + addressDetails.asset_type = asset_type + addressDetails.type = type + addressDetails.wallet = wallet + bsApp.startAddressSearch(address) + addressDetails.open() + } + + onOpenSend: (txId, isRBF, isCPFP) => overview.openSend(txId, isRBF, isCPFP, walletIndex) + onOpenExplorer: (txId) => overview.openExplorer(txId) + } +} diff --git a/GUI/QtQuick/qml/Pin/PasswordEntryPopup.qml b/GUI/QtQuick/qml/Pin/PasswordEntryPopup.qml new file mode 100644 index 000000000..1cb6e61af --- /dev/null +++ b/GUI/QtQuick/qml/Pin/PasswordEntryPopup.qml @@ -0,0 +1,193 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +Window { + id: root + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + maximumHeight: rect.height + maximumWidth: rect.width + + minimumHeight: rect.height + minimumWidth: rect.width + + height: rect.height + width: rect.width + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width) / 2 + y: mainWindow.y + 28 + + property string device_name + property bool accept_on_device + + Rectangle { + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + height: BSSizes.applyScale(510) + width: BSSizes.applyScale(430) + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + Image { + id: close_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(24) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(24) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: { + root.clean() + root.close() + } + } + } + + CustomTitleLabel { + id: title + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(36) + + text: qsTr("Enter Password") + } + + Label { + + id: device_name_title_lbl + + anchors.top: title.bottom + anchors.topMargin: BSSizes.applyScale(48) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Device name:") + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: device_name_lbl + + anchors.top: title.bottom + anchors.topMargin: BSSizes.applyScale(48) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + horizontalAlignment: Text.AlignRight + + text: device_name + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + CustomTextInput { + id: password + + anchors.top: device_name_lbl.bottom + anchors.topMargin: BSSizes.applyScale(48) + anchors.horizontalCenter: parent.horizontalCenter + + //visible: !root.accept_on_device //always visible now - password can be empty in all cases + + height : BSSizes.applyScale(70) + width: BSSizes.applyScale(390) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Password") + + isPassword: true + isHiddenText: true + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + anchors.bottom: parent.bottom + anchors.bottomMargin: BSSizes.applyScale(40) + anchors.horizontalCenter: parent.horizontalCenter + + CustomButton { + id: cancel_but + text: qsTr("Cancel") + width: BSSizes.applyScale(190) + + preferred: false + function click_enter() { + root.clean() + root.close() + } + } + + CustomButton { + id: accept_but + text: qsTr("Accept") + width: BSSizes.applyScale(190) + + //enabled: accept_on_device || password.input_text.length //always accept now + + preferred: true + + function click_enter() { + bsApp.setHWpassword(password.input_text) + root.clean() + root.close() + } + + } + } + + + Keys.onEnterPressed: { + accept_but.click_enter() + } + + Keys.onReturnPressed: { + accept_but.click_enter() + } + + } + + function init() { + clean() + } + + function clean() { + password.input_text = "" + device_name = "" + accept_on_device = false + } + +} diff --git a/GUI/QtQuick/qml/Pin/PinEntriesPopup.qml b/GUI/QtQuick/qml/Pin/PinEntriesPopup.qml new file mode 100644 index 000000000..dc5e4e695 --- /dev/null +++ b/GUI/QtQuick/qml/Pin/PinEntriesPopup.qml @@ -0,0 +1,219 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +Window { + id: root + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + maximumHeight: rect.height + maximumWidth: rect.width + + minimumHeight: rect.height + minimumWidth: rect.width + + height: rect.height + width: rect.width + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width)/2 + y: mainWindow.y + BSSizes.applyScale(28) + + property var numbers: [ "7", "8", "9", + "4", "5", "6", + "1", "2", "3"] + property string output + + Rectangle { + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + height: BSSizes.applyScale(610) + width: BSSizes.applyScale(430) + border.color : BSStyle.defaultBorderColor + border.width : 1 + + Image { + id: close_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(24) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(24) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: { + root.clean() + root.close() + } + } + } + + CustomTitleLabel { + id: title + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(36) + + text: qsTr("Enter PIN") + } + + Item { + id: grid_item + + width: BSSizes.applyScale(390) + height: BSSizes.applyScale(330) + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(25) + anchors.top: title.bottom + anchors.topMargin: BSSizes.applyScale(24) + + GridView { + id: grid + + property bool isComplete: false + + anchors.fill: parent + + cellHeight : BSSizes.applyScale(110) + cellWidth : BSSizes.applyScale(130) + + model: numbers + + interactive: false + + delegate: Button { + id: input + + height: BSSizes.applyScale(100) + width: BSSizes.applyScale(120) + + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.buttonsTextColor + + text: String.fromCodePoint(0x2022) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(100) + implicitHeight: BSSizes.applyScale(120) + + color: input.down ? BSStyle.buttonsStandardPressedColor : + (input.hovered ? BSStyle.buttonsStandardHoveredColor : BSStyle.buttonsStandardColor) + + radius: BSSizes.applyScale(14) + + border.color: BSStyle.buttonsStandardBorderColor + border.width: input.down? 1 : 0 + } + + onClicked: { + output += numbers[index] + pin_field.text += String.fromCodePoint(0x2022) + } + } + + } + } + + + TextField { + id: pin_field + + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(25) + anchors.top: grid_item.bottom + anchors.topMargin: BSSizes.applyScale(24) + + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Normal + color: BSStyle.buttonsTextColor + + leftPadding: BSSizes.applyScale(14) + rightPadding: BSSizes.applyScale(14) + + readOnly: true + + background: Rectangle { + implicitWidth: BSSizes.applyScale(380) + implicitHeight: BSSizes.applyScale(46) + color: "#020817" + radius: BSSizes.applyScale(14) + } + + Image { + id: clear_button + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(24) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: clean() + } + } + } + + CustomButton { + id: accept_but + text: qsTr("Accept") + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + anchors.bottomMargin: BSSizes.applyScale(40) + + width: BSSizes.applyScale(380) + + preferred: true + + function click_enter() { + if (!accept_but.enabled) return + + bsApp.setHWpin(output) + clean() + root.close() + } + } + + Keys.onEnterPressed: { + accept_but.click_enter() + } + + Keys.onReturnPressed: { + accept_but.click_enter() + } + + } + + function init() { + clean() + } + + function clean() { + output = "" + pin_field.text = "" + } + +} diff --git a/GUI/QtQuick/qml/Pin/qmldir b/GUI/QtQuick/qml/Pin/qmldir new file mode 100644 index 000000000..78a71adf3 --- /dev/null +++ b/GUI/QtQuick/qml/Pin/qmldir @@ -0,0 +1,3 @@ +module SendControls + +PinEntriesPopup 1.0 PinEntriesPopup.qml diff --git a/GUI/QtQuick/qml/Plugins/Common/Card.qml b/GUI/QtQuick/qml/Plugins/Common/Card.qml new file mode 100644 index 000000000..f884ca18f --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/Common/Card.qml @@ -0,0 +1,71 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../../BsStyles" + +Rectangle { + id: control + width: BSSizes.applyScale(217) + height: BSSizes.applyScale(292) + color: "transparent" + radius: BSSizes.applyScale(14) + border.width: 1 + border.color: BSStyle.defaultGreyColor + + property alias name: title_item.text + property alias description: description_item.text + property alias icon_source: card_icon.source + + signal cardClicked() + + Column { + spacing: BSSizes.applyScale(8) + anchors.fill: parent + anchors.margins: BSSizes.applyScale(12) + + Image { + id: card_icon + width: BSSizes.applyScale(193) + height: BSSizes.applyScale(122) + } + + Text { + id: title_item + topPadding: BSSizes.applyScale(10) + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(16) + font.weight: Font.DemiBold + font.letterSpacing: 0.3 + color: BSStyle.titanWhiteColor + width: parent.width + } + + Text { + id: description_item + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + font.letterSpacing: 0.3 + color: BSStyle.titleTextColor + width: parent.width + wrapMode: Text.Wrap + } + } + + MouseArea { + id: mouse_area + anchors.fill: parent + hoverEnabled: true + + onClicked: control.cardClicked() + } +} + diff --git a/GUI/QtQuick/qml/Plugins/Common/PluginPopup.qml b/GUI/QtQuick/qml/Plugins/Common/PluginPopup.qml new file mode 100644 index 000000000..2d0f7be56 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/Common/PluginPopup.qml @@ -0,0 +1,28 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "../../StyledControls" + +Popup { + id: plugin_popup + width: BSSizes.applyWindowWidthScale(580) + height: BSSizes.applyWindowHeightScale(720) + anchors.centerIn: Overlay.overlay + + property var controller: null + + modal: true + focus: true + closePolicy: Popup.CloseOnEscape +} diff --git a/GUI/QtQuick/qml/Plugins/Common/PluginsPage.qml b/GUI/QtQuick/qml/Plugins/Common/PluginsPage.qml new file mode 100644 index 000000000..5848b4548 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/Common/PluginsPage.qml @@ -0,0 +1,108 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "." +import "../SideShift" +import "../../BsStyles" +import "../../StyledControls" + +Rectangle { + id: root + color: BSStyle.backgroundColor + + Column { + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(14) + + Rectangle { + id: header + width: parent.width + height: BSSizes.applyScale(76) + color: "transparent" + + Row { + spacing: BSSizes.applyScale(6) + anchors.fill: parent + + Text { + height: parent.height + width: BSSizes.applyScale(110) + text: qsTr("Apps") + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(20) + font.weight: Font.Bold + font.letterSpacing: 0.35 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + } + } + + GridView { + width: parent.width + height: parent.height - header.height + cellWidth: BSSizes.applyScale(237) + cellHeight: BSSizes.applyScale(302) + model: pluginsListModel + clip: true + + ScrollBar.vertical: ScrollBar { + id: verticalScrollBar + policy: ScrollBar.AsNeeded + } + + delegate: Rectangle { + id: plugin_item + color: "transparent" + width: BSSizes.applyScale(237) + height: BSSizes.applyScale(302) + + property var component + property var plugin_popup + + Card { + anchors.top: parent.top + name: name_role + description: description_role + icon_source: icon_role + onCardClicked: { + plugin_popup.reset() + plugin_popup.controller = pluginsListModel.getPlugin(index) + plugin_popup.controller.init() + plugin_popup.open() + } + } + + function finishCreation() { + plugin_popup = component.createObject(plugin_item) + } + + Component.onCompleted: { + component = Qt.createComponent(path_role); + if (component !== null) { + if (component.status === Component.Ready) { + finishCreation(); + } + else if (component.status === Component.Error) { + console.log(component.errorString()) + } + else { + component.statusChanged.connect(finishCreation); + } + } + } + } + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftButton.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftButton.qml new file mode 100644 index 000000000..23ffdfcc9 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftButton.qml @@ -0,0 +1,50 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../../BsStyles" + + +Button { + id: control + + width: BSSizes.applyScale(150) + height: BSSizes.applyScale(50) + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.letterSpacing: 0.5 + + hoverEnabled: true + activeFocusOnTab: true + + background: Rectangle { + id: backgroundItem + color: control.enabled ? "#f05c44" : "black" + radius: BSSizes.applyScale(4) + + border.color: + (enabled ? "transparent" : + (control.hovered ? "white" : + (control.activeFocus ? "gray" : "gray"))) + border.width: 1 + } + + contentItem: Text { + text: control.text + font: control.font + anchors.fill: parent + color: control.enabled ? "black" : "white" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftBuyPage.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftBuyPage.qml new file mode 100644 index 000000000..0f09c6160 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftBuyPage.qml @@ -0,0 +1,330 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "../../" +import "../../BsStyles" + + +Item { + id: root + width: BSSizes.applyWindowWidthScale(620) + height: BSSizes.applyWindowHeightScale(720) + + property var controller + property string inputCurrency + property string outputCurrency + property string receivingAddress + + Rectangle { + anchors.fill: parent + radius: BSSizes.applyScale(14) + color: "black" + } + + Column { + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(60) + anchors.rightMargin: BSSizes.applyScale(60) + anchors.topMargin: BSSizes.applyScale(25) + anchors.bottomMargin: BSSizes.applyScale(25) + spacing: 0 + + Rectangle { + color: "transparent" + height: BSSizes.applyScale(40) + width: parent.width + + Row { + spacing: BSSizes.applyScale(10) + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + Text { + text: root.inputCurrency + color: "lightgray" + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + } + + Image { + id: arrowImage + width: BSSizes.applyScale(20) + height: BSSizes.applyScale(12) + sourceSize.width: BSSizes.applyScale(20) + sourceSize.height: BSSizes.applyScale(12) + source: "qrc:/images/sideshift_right_arrow.svg" + anchors.verticalCenter: parent.verticalCenter + } + + Text { + text: outputCurrency + color: "lightgray" + font.weight: Font.Bold + anchors.verticalCenter: parent.verticalCenter + } + } + + Column { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + color: "lightgray" + text: qsTr("ORDER") + anchors.right: parent.right + } + + Text { + color: "lightgray" + linkColor: "lightgray" + font.weight: Font.Bold + text: "%1".arg(controller !== null ? controller.orderId : "") + onLinkActivated: Qt.openUrlExternally("https://sideshift.ai/orders/%1".arg(controller !== null ? controller.orderId : "")) + } + } + } + + Text { + color: "white" + text: controller !== null ? controller.status : "" + topPadding: BSSizes.applyScale(40) + wrapMode: Text.Wrap + font.pixelSize: BSSizes.applyScale(24) + anchors.horizontalCenter: parent.horizontalCenter + width: parent.width + horizontalAlignment: Text.AlignHCenter + } + + Text { + topPadding: BSSizes.applyScale(15) + color: "lightgray" + text: controller !== null ? controller.conversionRate : "" + anchors.horizontalCenter: parent.horizontalCenter + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(300) + color: "transparent" + + Column { + spacing: BSSizes.applyScale(10) + width: parent.width / 2 + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + Text { + text: qsTr("PLEASE SEND") + color: "lightgray" + font.pixelSize: BSSizes.applyScale(20) + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: BSSizes.applyScale(10) + anchors.horizontalCenter: parent.horizontalCenter + Text { + text: qsTr("Min") + color: "lightgray" + anchors.verticalCenter: parent.verticalCenter + } + Text { + text: controller !== null ? controller.minAmount : "" + color: "white" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(18) + } + Text { + text: root.inputCurrency + color: "white" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(18) + } + } + + Row { + spacing: BSSizes.applyScale(10) + anchors.horizontalCenter: parent.horizontalCenter + Text { + text: qsTr("Max") + color: "lightgray" + anchors.verticalCenter: parent.verticalCenter + } + Text { + text: controller !== null ? controller.maxAmount : "" + color: "white" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(18) + } + Text { + text: root.inputCurrency + color: "white" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(18) + } + } + + Text { + topPadding: BSSizes.applyScale(20) + text: qsTr("TO ADDRESS") + color: "lightgray" + font.pixelSize: BSSizes.applyScale(20) + anchors.horizontalCenter: parent.horizontalCenter + } + + Rectangle { + height: BSSizes.applyScale(40) + width: BSSizes.applyScale(200) + color: "transparent" + radius: BSSizes.applyScale(4) + border.width: 1 + border.color: "white" + anchors.horizontalCenter: parent.horizontalCenter + + TextInput { + id: toAddress + anchors.fill: parent + text: controller !== null ? controller.depositAddress : "" + color: "white" + clip: true + font.weight: Font.Bold + verticalAlignment: Text.AlignVCenter + anchors.leftMargin: BSSizes.applyScale(20) + anchors.rightMargin: BSSizes.applyScale(20) + enabled: false + } + } + + SideShiftCopyButton { + text: timer.running ? qsTr("COPIED") : qsTr("COPY ADDRESS") + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + toAddress.selectAll() + bsApp.copyAddressToClipboard(toAddress.text) + timer.start() + } + + Timer { + id: timer + repeat: false + interval: 5000 + onTriggered: toAddress.select(0, 0) + } + } + } + + Column { + width: parent.width / 2 + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: BSSizes.applyScale(200) + height: BSSizes.applyScale(200) + color: "gray" + anchors.horizontalCenter: parent.horizontalCenter + + Image { + source: controller !== null ? ( "image://QR/" + controller.depositAddress) : "" + sourceSize.width: BSSizes.applyScale(180) + sourceSize.height: BSSizes.applyScale(180) + width: BSSizes.applyScale(180) + height: BSSizes.applyScale(180) + anchors.centerIn: parent + } + } + } + } + + Item { + width: 1 + height: 80 + } + + Text { + text: qsTr("ESTIMATED NETWORK FEES: ") + (controller !== null ? controller.networkFee : "") + color: "white" + anchors.horizontalCenter: parent.horizontalCenter + } + + Item { + width: 1 + height: 60 + } + + Rectangle { + height: 40 + width: parent.width + color: "transparent" + + Column { + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + + Text { + color: "lightgray" + text: qsTr("RECEIVING ADDRESS") + } + + Text { + width: 200 + color: "lightgray" + text: root.receivingAddress + font.weight: Font.Bold + clip: true + } + } + + Column { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + + Text { + color: "lightgray" + text: qsTr("CREATED AT") + anchors.right: parent.right + } + + Text { + color: "lightgray" + text: controller !== null ? controller.creationDate : "" + font.weight: Font.Bold + anchors.right: parent.right + } + } + } + } + + Timer { + id: updateTimer + interval: 5000 + repeat: true + onTriggered: { + controller.updateShiftStatus() + if (controller.status === "complete") { + close() + } + } + } + + onVisibleChanged: { + if (visible) { + updateTimer.start() + } + else { + updateTimer.stop() + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCombobox.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCombobox.qml new file mode 100644 index 000000000..d8a7b9978 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCombobox.qml @@ -0,0 +1,122 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../../BsStyles" + +ComboBox { + id: control + + width: BSSizes.applyScale(400) + height: BSSizes.applyScale(50) + + activeFocusOnTab: true + + contentItem: Rectangle { + id: input_rect + color: "transparent" + + Text { + anchors.fill: parent + text: control.currentText + font.pixelSize: BSSizes.applyScale(18) + font.family: "Roboto" + font.weight: Font.Bold + color: "white" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + clip: true + } + } + + indicator: Item {} + + background: Rectangle { + + color: "#181414" + opacity: 1 + radius: BSSizes.applyScale(4) + + border.color: control.popup.visible ? "white" : + (control.hovered ? "white" : + (control.activeFocus ? "white" : "gray")) + border.width: 1 + + implicitWidth: control.width + implicitHeight: control.height + } + + delegate: ItemDelegate { + + id: menuItem + + width: control.width - BSSizes.applyScale(12) + height: BSSizes.applyScale(50) + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + + contentItem: Text { + + text: control.textRole + ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) + : modelData + color: "white" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + elide: Text.ElideNone + verticalAlignment: Text.AlignVCenter + } + + highlighted: control.highlightedIndex === index + property bool currented: control.currentIndex === index + + background: Rectangle { + color: menuItem.highlighted ? "white" : "transparent" + opacity: menuItem.highlighted ? 0.2 : 1 + radius: 4 + } + } + + popup: Popup { + id: _popup + + y: control.height - 1 + width: control.width + padding: BSSizes.applyScale(6) + + contentItem: ListView { + id: popup_item + + clip: true + implicitHeight: contentHeight + model: control.popup.visible ? control.delegateModel : null + //model: control.delegateModel + currentIndex: control.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: "black" + radius: BSSizes.applyScale(4) + + border.width: 1 + border.color: "white" + } + } +} + diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftComboboxWithIcon.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftComboboxWithIcon.qml new file mode 100644 index 000000000..944c0d29c --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftComboboxWithIcon.qml @@ -0,0 +1,233 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../../BsStyles" + +ComboBox { + id: control + + width: BSSizes.applyScale(200) + height: BSSizes.applyScale(200) + + property string controlHint: "123" + property int popupWidth: BSSizes.applyScale(400) + + textRole: "coin" + valueRole: "network" + + activeFocusOnTab: true + + contentItem: Rectangle { + + id: input_rect + color: "transparent" + + Column { + spacing: BSSizes.applyScale(8) + anchors.centerIn: parent + + Image { + width: BSSizes.applyScale(80) + height: BSSizes.applyScale(80) + sourceSize.width: BSSizes.applyScale(80) + sourceSize.height: BSSizes.applyScale(80) + source: "image://coin/" + control.currentText + "-" + control.currentValue + anchors.horizontalCenter: parent.horizontalCenter + } + + Text { + text: control.controlHint + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + color: "white" + topPadding: BSSizes.applyScale(15) + anchors.horizontalCenter: parent.horizontalCenter + } + + Text { + text: control.currentText + font.pixelSize: BSSizes.applyScale(18) + font.family: "Roboto" + font.weight: Font.Bold + color: "white" + horizontalAlignment: Text.AlignHCenter + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + indicator: Item {} + + background: Rectangle { + + color: "#181414" + opacity: 1 + radius: BSSizes.applyScale(14) + + border.color: control.popup.visible ? "white" : + (control.hovered ? "white" : + (control.activeFocus ? "white" : "gray")) + border.width: 1 + + implicitWidth: control.width + implicitHeight: control.height + } + + delegate: ItemDelegate { + + id: menuItem + + width: control.popupWidth - BSSizes.applyScale(12) + height: BSSizes.applyScale(80) + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + + contentItem: Rectangle { + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(10) + anchors.rightMargin: BSSizes.applyScale(10) + color: "transparent" + + Row { + spacing: BSSizes.applyScale(10) + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(10) + + Image { + width: BSSizes.applyScale(40) + height: BSSizes.applyScale(40) + sourceSize.width: BSSizes.applyScale(40) + sourceSize.height: BSSizes.applyScale(40) + source: "image://coin/" + model["coin"] + "-" + model["network"] + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: BSSizes.applyScale(10) + width: parent.width - BSSizes.applyScale(50) + anchors.verticalCenter: parent.verticalCenter + + Text { + text: model["name"] + color: "white" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.SemiBold + } + + Row { + spacing: BSSizes.applyScale(10) + + Text { + text: model["coin"] + color: "white" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Text { + text: model["network"] + color: "white" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + } + } + } + } + + highlighted: control.highlightedIndex === index + property bool currented: control.currentIndex === index + + background: Rectangle { + anchors.fill: parent + anchors.leftMargin: BSSizes.applyScale(10) + anchors.rightMargin: BSSizes.applyScale(10) + color: menuItem.highlighted ? "white" : "transparent" + opacity: menuItem.highlighted ? 0.2 : 1 + radius: BSSizes.applyScale(4) + } + } + + popup: Popup { + id: _popup + + y: 0 + width: control.popupWidth + height: BSSizes.applyScale(400) + padding: BSSizes.applyScale(6) + + contentItem: Rectangle { + color: "transparent" + anchors.fill: parent + + Column { + anchors.fill: parent + + Rectangle { + width: parent.width + height: BSSizes.applyScale(80) + color: "transparent" + + Rectangle { + anchors.fill: parent + anchors.margins: BSSizes.applyScale(10) + color: "transparent" + border.color: "white" + radius: BSSizes.applyScale(10) + } + + TextInput { + id: search_input + anchors.fill: parent + color: "white" + leftPadding: BSSizes.applyScale(20) + verticalAlignment: Text.AlignVCenter + + onTextEdited: control.model.filter = text + } + } + + ListView { + id: popup_item + width: parent.width + height: parent.height - BSSizes.applyScale(80) + + clip: true + model: control.popup.visible ? control.delegateModel : null + currentIndex: control.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + + maximumFlickVelocity: 1000 + } + } + } + + background: Rectangle { + color: "black" + radius: BSSizes.applyScale(14) + + border.width: 1 + border.color: "white" + } + + onOpened: search_input.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCopyButton.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCopyButton.qml new file mode 100644 index 000000000..3723cab5a --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftCopyButton.qml @@ -0,0 +1,51 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 +import "../../BsStyles" + + +Button { + id: control + + width: BSSizes.applyScale(150) + height: BSSizes.applyScale(50) + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Bold + + hoverEnabled: true + + background: Rectangle { + id: backgroundItem + color: "transparent" + } + + contentItem: Row { + spacing: BSSizes.applyScale(5) + anchors.fill: parent + + Image { + width: BSSizes.applyScale(20) + height: BSSizes.applyScale(20) + source: "qrc:/images/copy_icon.svg" + } + + Text { + text: control.text + font: control.font + color: "skyblue" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftIconButton.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftIconButton.qml new file mode 100644 index 000000000..341432982 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftIconButton.qml @@ -0,0 +1,38 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 +import "../../BsStyles" + +Rectangle { + id: control + width: BSSizes.applyScale(40) + height: BSSizes.applyScale(40) + color: "transparent" + activeFocusOnTab: true + + signal buttonClicked() + + Image { + anchors.fill: parent + source: "qrc:/images/transfer_icon.png" + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + control.buttonClicked() + } + } +} \ No newline at end of file diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftMainPage.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftMainPage.qml new file mode 100644 index 000000000..3c0baba33 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftMainPage.qml @@ -0,0 +1,136 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "../../" +import "../../BsStyles" + + +Item { + id: root + width: BSSizes.applyWindowWidthScale(620) + height: BSSizes.applyWindowHeightScale(720) + + property var controller + property bool receive: true + property var receiveModel: controller !== null ? controller.inputCurrenciesModel : null + property var sendModel: controller !== null ? controller.outputCurrenciesModel : null + + property alias inputCurrency: inputCombobox.currentText + property alias outputCurrency: receivingCombobox.currentText + property alias receivingAddress: addressCombobox.currentText + + signal shift() + + Rectangle { + anchors.fill: parent + radius: BSSizes.applyScale(14) + color: "black" + } + + Column { + anchors.centerIn: parent + spacing: BSSizes.applyScale(20) + + Text { + text: controller !== null ? controller.conversionRate : "" + color: "gray" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + spacing: BSSizes.applyScale(20) + anchors.horizontalCenter: parent.horizontalCenter + + SideShiftComboboxWithIcon { + id: inputCombobox + popupWidth: BSSizes.applyScale(400) + controlHint: qsTr("YOU SEND") + model: root.receive ? root.receiveModel : root.sendModel + enabled: root.receive + + onCurrentValueChanged: { + root.controller.inputCurrencySelected(currentText) + root.controller.inputNetwork = currentValue + } + onActivated: { + root.controller.inputCurrencySelected(currentText) + root.controller.inputNetwork = currentValue + } + + onModelChanged: inputCombobox.currentIndex = 0 + } + + SideShiftIconButton { + anchors.verticalCenter: parent.verticalCenter + onButtonClicked: root.receive = !root.receive + } + + SideShiftComboboxWithIcon { + id: receivingCombobox + popupWidth: BSSizes.applyScale(400) + controlHint: qsTr("YOU RECEIVE") + model: root.receive ? root.sendModel : root.receiveModel + enabled: !root.receive + onModelChanged: receivingCombobox.currentIndex = 0 + } + } + + Item { + width: 1 + height: BSSizes.applyScale(20) + } + + Text { + text: qsTr("RECEIVING ADDRESS") + color: "white" + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + anchors.horizontalCenter: parent.horizontalCenter + } + + SideShiftTextInput { + id: addressInput + visible: !root.receive + textHint: qsTr("Your ") + receivingCombobox.currentText + qsTr(" address") + anchors.horizontalCenter: parent.horizontalCenter + } + + SideShiftCombobox { + id: addressCombobox + visible: root.receive + model: addressListModel + textRole: "address" + valueRole: "address" + anchors.horizontalCenter: parent.horizontalCenter + } + + SideShiftButton { + text: qsTr("SHIFT") + enabled: root.receive ? addressCombobox.currentIndex >= 0 : addressInput.text !== "" + anchors.horizontalCenter: parent.horizontalCenter + onClicked: { + if (root.controller.sendShift(addressCombobox.currentText)) { + root.shift() + } + } + } + } + + function reset() { + addressInput.text = "" + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftPopup.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftPopup.qml new file mode 100644 index 000000000..f4105b326 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftPopup.qml @@ -0,0 +1,82 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "../Common" +import "../../" +import "../../BsStyles" +import "../../StyledControls" + +PluginPopup { + id: root + property var controller: null + + background: Rectangle { + width: root.width + height: root.height + color: "transparent" + } + + contentItem: StackView { + id: stackView + initialItem: mainPage + anchors.fill: parent + + SideShiftMainPage { + id: mainPage + onShift: { + controller.inputCurrencySelected("USDT"); //TODO: should be a selected input currency from previous step + if (mainPage.receive) { + stackView.replace(buyPage) + } + } + controller: root.controller + visible: false + anchors.centerIn: parent + + CloseIconButton { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: BSSizes.applyScale(10) + anchors.topMargin: BSSizes.applyScale(10) + onClose: root.close() + } + } + + SideShiftBuyPage { + id: buyPage + controller: root.controller + visible: false + + inputCurrency: mainPage.inputCurrency + outputCurrency: mainPage.outputCurrency + receivingAddress: mainPage.receivingAddress + anchors.centerIn: parent + + CloseIconButton { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: BSSizes.applyScale(10) + anchors.topMargin: BSSizes.applyScale(10) + onClose: root.close() + } + } + } + + function reset() + { + mainPage.reset() + stackView.replace(mainPage) + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideShift/SideShiftTextInput.qml b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftTextInput.qml new file mode 100644 index 000000000..6e19ed7ac --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideShift/SideShiftTextInput.qml @@ -0,0 +1,59 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "../../BsStyles" + +Rectangle { + id: control + width: BSSizes.applyScale(400) + height: BSSizes.applyScale(50) + radius: BSSizes.applyScale(4) + color: "#181414" + border.width: 1 + border.color: (mouseArea.containsMouse || control.activeFocus) ? "white" : "gray" + activeFocusOnTab: true + + property string textHint + property string fontFamily + property alias text: textEdit.text + + TextInput { + id: textEdit + color: "white" + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + anchors.fill: parent + font.family: control.fontFamily + width: parent.width + clip: true + + Text { + text: control.textHint + color: "white" + anchors.centerIn: parent + font.family: control.fontFamily + visible: textEdit.text == "" && !textEdit.activeFocus + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + textEdit.forceActiveFocus() + mouse.accepted = false + } + } +} \ No newline at end of file diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CurrencyLabel.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CurrencyLabel.qml new file mode 100644 index 000000000..dcfa07ec5 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CurrencyLabel.qml @@ -0,0 +1,56 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 + +import "../Styles" +import "../../../BsStyles" + +Column { + property string header_text + property string currency + property string currency_icon + property string comment + + spacing: BSSizes.applyScale(10) + + Text { + text: header_text + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + color: SideSwapStyles.secondaryTextColor + } + + Row { + spacing: BSSizes.applyScale(10) + + Image { + source: currency_icon + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + } + + Text { + text: currency + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(18) + color: SideSwapStyles.primaryTextColor + } + } + + + Text { + text: comment + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(16) + color: SideSwapStyles.paragraphTextColor + } +} + diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomBorderedButton.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomBorderedButton.qml new file mode 100644 index 000000000..92387be0e --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomBorderedButton.qml @@ -0,0 +1,48 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../Styles" +import "../../../BsStyles" + +Button { + id: control + + width: BSSizes.applyScale(150) + height: BSSizes.applyScale(50) + focusPolicy: Qt.NoFocus + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.5 + + hoverEnabled: true + activeFocusOnTab: true + + background: Rectangle { + id: backgroundItem + color: "transparent" + radius: BSSizes.applyScale(8) + border.width: 1 + border.color: SideSwapStyles.buttonBackground + } + + contentItem: Text { + text: control.text + font: control.font + anchors.fill: parent + color: SideSwapStyles.primaryTextColor + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomButton.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomButton.qml new file mode 100644 index 000000000..2230069f2 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomButton.qml @@ -0,0 +1,47 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../Styles" +import "../../../BsStyles" + +Button { + id: control + + width: BSSizes.applyScale(150) + height: BSSizes.applyScale(50) + + focusPolicy: Qt.NoFocus + property bool active: true + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.letterSpacing: 0.5 + + hoverEnabled: true + activeFocusOnTab: true + + background: Rectangle { + id: backgroundItem + color: control.active ? SideSwapStyles.buttonBackground : SideSwapStyles.buttonSecondaryBackground + radius: BSSizes.applyScale(8) + } + + contentItem: Text { + text: control.text + font: control.font + anchors.fill: parent + color: control.active ? SideSwapStyles.primaryTextColor : SideSwapStyles.paragraphTextColor + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomCombobox.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomCombobox.qml new file mode 100644 index 000000000..46d3b0c57 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomCombobox.qml @@ -0,0 +1,136 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../Styles" +import "../../../BsStyles" + +ComboBox { + id: control + + width: BSSizes.applyScale(400) + height: BSSizes.applyScale(50) + + activeFocusOnTab: true + + property string comboboxHint + + contentItem: Rectangle { + + id: input_rect + color: "transparent" + + Column { + spacing: BSSizes.applyScale(4) + anchors.verticalCenter: parent.verticalCenter + + Text { + text: control.comboboxHint + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Bold + color: SideSwapStyles.secondaryTextColor + leftPadding: BSSizes.applyScale(10) + } + + Text { + text: control.currentText + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + color: "white" + leftPadding: BSSizes.applyScale(10) + } + } + } + + // indicator: Item {} + + background: Rectangle { + + color: SideSwapStyles.buttonSecondaryBackground + opacity: 1 + radius: BSSizes.applyScale(4) + + border.color: control.popup.visible ? SideSwapStyles.buttonBackground : + (control.hovered ? SideSwapStyles.buttonBackground : + (control.activeFocus ? SideSwapStyles.buttonBackground : SideSwapStyles.spacerColor)) + border.width: 1 + + implicitWidth: control.width + implicitHeight: control.height + } + + delegate: ItemDelegate { + + id: menuItem + + width: control.width - BSSizes.applyScale(12) + height: BSSizes.applyScale(50) + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + + contentItem: Text { + + text: control.textRole + ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) + : modelData + color: "white" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + elide: Text.ElideNone + verticalAlignment: Text.AlignVCenter + } + + highlighted: control.highlightedIndex === index + property bool currented: control.currentIndex === index + + background: Rectangle { + color: menuItem.highlighted ? "white" : "transparent" + opacity: menuItem.highlighted ? 0.2 : 1 + radius: BSSizes.applyScale(4) + } + } + + popup: Popup { + id: _popup + + y: control.height - 1 + width: control.width + padding: BSSizes.applyScale(6) + + contentItem: ListView { + id: popup_item + + clip: true + implicitHeight: contentHeight + model: control.popup.visible ? control.delegateModel : null + //model: control.delegateModel + currentIndex: control.highlightedIndex + + ScrollIndicator.vertical: ScrollIndicator { } + } + + background: Rectangle { + color: SideSwapStyles.buttonSecondaryBackground + radius: BSSizes.applyScale(4) + + border.width: 1 + border.color: SideSwapStyles.spacerColor + } + } +} + diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomSwitch.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomSwitch.qml new file mode 100644 index 000000000..f5dccddbe --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomSwitch.qml @@ -0,0 +1,41 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../Styles" +import "../../../BsStyles" + +Rectangle { + id: root + color: "transparent" + + property bool checked: true + + Row { + anchors.fill: parent + + CustomButton { + text: qsTr("PEG-IN") + width: parent.width / 2 + height: parent.height + active: root.checked + onClicked: root.checked = true + } + CustomButton { + text: qsTr("PEG-OUT") + width: parent.width / 2 + height: parent.height + active: !root.checked + onClicked: root.checked = false + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomTextEdit.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomTextEdit.qml new file mode 100644 index 000000000..4c6bb1a38 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/CustomTextEdit.qml @@ -0,0 +1,65 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "../Styles" +import "../../../BsStyles" + +Rectangle { + id: control + width: BSSizes.applyScale(400) + height: BSSizes.applyScale(50) + radius: BSSizes.applyScale(12) + color: SideSwapStyles.darkBlueBackground + border.width: 1 + border.color: textEdit.activeFocus ? SideSwapStyles.buttonBackground : SideSwapStyles.spacerColor + activeFocusOnTab: true + + property string textHint + property string fontFamily + property alias text: textEdit.text + property alias inputHints: textEdit.inputMethodHints + + TextInput { + id: textEdit + color: "white" + leftPadding: BSSizes.applyScale(10) + topPadding: BSSizes.applyScale(32) + rightPadding: BSSizes.applyScale(10) + bottomPadding: BSSizes.applyScale(10) + clip: true + anchors.fill: parent + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + + Text { + text: control.textHint + color: SideSwapStyles.secondaryTextColor + font.family: "Roboto" + font.weight: Font.Bold + font.pixelSize: BSSizes.applyScale(12) + leftPadding: BSSizes.applyScale(10) + topPadding: BSSizes.applyScale(10) + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + textEdit.forceActiveFocus() + mouse.accepted = false + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Controls/IconButton.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/IconButton.qml new file mode 100644 index 000000000..22d47af86 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Controls/IconButton.qml @@ -0,0 +1,35 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "../Styles" +import "../../../BsStyles" + +Rectangle { + id: control + width: BSSizes.applyScale(50) + height: BSSizes.applyScale(50) + color: SideSwapStyles.buttonBackground + activeFocusOnTab: true + + signal buttonClicked() + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + control.buttonClicked() + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapMainPage.qml b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapMainPage.qml new file mode 100644 index 000000000..540f44e2a --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapMainPage.qml @@ -0,0 +1,201 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "Styles" +import "Controls" +import "../../" +import "../../BsStyles" + + +Item { + id: root + width: BSSizes.applyWindowWidthScale(620) + height: BSSizes.applyWindowHeightScale(740) + + property bool peg_in: true + + signal continueClicked() + + Rectangle{ + anchors.fill: parent + color: "transparent" + + Rectangle { + id: topPanel + width: parent.width + height: BSSizes.applyScale(290) + anchors.top: parent.top + color: SideSwapStyles.darkBlueBackground + } + + Rectangle { + width: parent.width + height: parent.height - topPanel.height + anchors.bottom: parent.bottom + color: SideSwapStyles.skyBlueBackground + } + + Column { + spacing: BSSizes.applyScale(20) + anchors.fill: parent + anchors.topMargin: BSSizes.applyScale(40) + anchors.rightMargin: BSSizes.applyScale(20) + anchors.leftMargin: BSSizes.applyScale(20) + + CustomSwitch { + width: parent.width + height: BSSizes.applyScale(30) + anchors.horizontalCenter: parent.horizontalCenter + checked: root.peg_in + + onCheckedChanged: root.peg_in = checked + } + + CurrencyLabel { + header_text: qsTr("Deliver") + currency: root.peg_in ? qsTr("BTC") : qsTr("L-BTC") + currency_icon: root.peg_in + ? "qrc:/images/sideswap/btc_icon.svg" + : "qrc:/images/sideswap/lbtc_icon.svg" + comment: qsTr("Min: 0.001 ") + currency + } + + Rectangle { + height: BSSizes.applyScale(2) + width: parent.width + color: SideSwapStyles.spacerColor + } + + Text { + text: qsTr("SideSwap will generate a Peg-In address for you to deliver BTC into. Each peg-in/out URL is unique and can be re-entered to view your progress. A peg-in/out address may be re-used.") + width: parent.width - BSSizes.applyScale(20) + color: SideSwapStyles.paragraphTextColor + clip: true + wrapMode: Text.Wrap + } + + Row { + spacing: BSSizes.applyScale(20) + width: parent.width + height: BSSizes.applyScale(50) + + IconButton { + width: BSSizes.applyScale(50) + height: BSSizes.applyScale(50) + radius: BSSizes.applyScale(25) + + onButtonClicked: root.peg_in = !root.peg_in + } + + Rectangle { + width: BSSizes.applyScale(200) + height: BSSizes.applyScale(30) + radius: BSSizes.applyScale(15) + color: "lightblue" + anchors.verticalCenter: parent.verticalCenter + + Rectangle { + width: BSSizes.applyScale(15) + height: BSSizes.applyScale(15) + anchors.left: parent.left + anchors.top: parent.top + color: "lightblue" + } + + Text { + text: qsTr("Coversion rate 99.9%") + color: "black" + anchors.centerIn: parent + } + } + } + + Row { + spacing: BSSizes.applyScale(20) + height: BSSizes.applyScale(60) + width: parent.width + + CurrencyLabel { + id: receiveLabel + header_text: qsTr("Receive") + currency: root.peg_in ? qsTr("L-BTC") : qsTr("BTC") + currency_icon: root.peg_in + ? "qrc:/images/sideswap/lbtc_icon.svg" + : "qrc:/images/sideswap/btc_icon.svg" + comment: "" + width: 100 + } + + CustomCombobox { + model: ['A', 'B', 'C'] + height: BSSizes.applyScale(60) + width: parent.width - receiveLabel.width - BSSizes.applyScale(20) + anchors.verticalCenter: parent.verticalCenter + visible: !root.peg_in + comboboxHint: qsTr("Fee suggestion") + } + } + + Rectangle { + height: BSSizes.applyScale(2) + width: parent.width + color: SideSwapStyles.spacerColor + } + + Column { + width: parent.width + spacing: BSSizes.applyScale(5) + + CustomCombobox { + height: BSSizes.applyScale(60) + width: parent.width + model: walletBalances + textRole: "name" + valueRole: "name" + comboboxHint: qsTr("Wallet") + } + + CustomTextEdit { + id: amountInput + width: parent.width + height: BSSizes.applyScale(60) + textHint: qsTr("Amount") + visible: root.peg_in + inputHints: Text.ImhDigitsOnly + } + + CustomTextEdit { + id: addressInput + width: parent.width + height: BSSizes.applyScale(60) + textHint: qsTr("Your Liquid Address") + visible: root.peg_in + } + } + + CustomButton { + text: qsTr("CONTINUE") + width: parent.width + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.continueClicked() + } + } + } + + function reset() { + amountInput.text = "" + addressInput.text = "" + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegIn.qml b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegIn.qml new file mode 100644 index 000000000..5e3e138df --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegIn.qml @@ -0,0 +1,44 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "Styles" +import "Controls" +import "../../" +import "../../BsStyles" + + +Item { + id: root + width: BSSizes.applyWindowWidthScale(620) + height: BSSizes.applyWindowHeightScale(720) + + signal back() + + Rectangle { + anchors.fill: parent + color: SideSwapStyles.darkBlueBackground + } + + Column { + anchors.fill: parent + anchors.margins: BSSizes.applyScale(100) + + CustomBorderedButton { + width: parent.width + text: qsTr("BACK") + onClicked: root.back() + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegOut.qml b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegOut.qml new file mode 100644 index 000000000..6b3c32b75 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPegOut.qml @@ -0,0 +1,155 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "Styles" +import "Controls" +import "../../" +import "../../BsStyles" + + +Item { + id: root + width: BSSizes.applyWindowWidthScale(620) + height: BSSizes.applyWindowHeightScale(720) + + property string minTransferAmount: qsTr("0.001") + property string toAddress: "bc1qvrl85pygns90xut25qu0tpmawm9h03j3d9w94a" + property string fromAddress: "bc1qvrl85pygns90xut25qu0tpmawm9h03j3d9w94a" + + signal back() + + Rectangle { + anchors.fill: parent + color: SideSwapStyles.darkBlueBackground + } + + Column { + spacing: BSSizes.applyScale(30) + anchors.fill: parent + anchors.margins: BSSizes.applyScale(20) + + Text { + text: qsTr("Send L-BTC to the following address:") + color: SideSwapStyles.primaryTextColor + font.pixelSize: BSSizes.applyScale(24) + anchors.horizontalCenter: parent.horizontalCenter + topPadding: BSSizes.applyScale(20) + } + + Text { + text: qsTr("Min amount: ") + minTransferAmount + color: SideSwapStyles.primaryTextColor + font.pixelSize: BSSizes.applyScale(14) + anchors.horizontalCenter: parent.horizontalCenter + } + + Rectangle { + height: 1 + width: parent.width + color: SideSwapStyles.spacerColor + } + + Row { + spacing: BSSizes.applyScale(20) + width: parent.width + height: BSSizes.applyScale(300) + + Item { + width: parent.width / 2 - parent.spacing + height: parent.height + + Rectangle { + width: BSSizes.applyScale(240) + height: BSSizes.applyScale(240) + radius: BSSizes.applyScale(20) + color: SideSwapStyles.buttonBackground + anchors.centerIn: parent + + Rectangle { + width: BSSizes.applyScale(226) + height: BSSizes.applyScale(226) + radius: BSSizes.applyScale(16) + anchors.centerIn: parent + + Image { + source: "image://QR/" + root.toAddress + sourceSize.width: BSSizes.applyScale(220) + sourceSize.height: BSSizes.applyScale(220) + width: BSSizes.applyScale(220) + height: BSSizes.applyScale(220) + anchors.centerIn: parent + } + } + } + } + + Item { + width: parent.width / 2 - parent.spacing + height: parent.height + + Column { + spacing: BSSizes.applyScale(20) + anchors.fill: parent + anchors.margins: BSSizes.applyScale(20) + + Text { + text: root.toAddress + color: SideSwapStyles.primaryTextColor + font.pixelSize: BSSizes.applyScale(18) + width: parent.width + clip: true + wrapMode: Text.Wrap + } + + CustomButton { + text: qsTr("Copy Address") + width: parent.width + height: BSSizes.applyScale(50) + anchors.horizontalCenter: parent.horizontalCenter + onClicked: bsApp.copyAddressToClipboard(root.toAddress) + } + } + } + } + + Rectangle { + width: parent.width + height: 1 + color: SideSwapStyles.spacerColor + } + + Column { + Text { + text: qsTr("BTC payment address") + font.pixelSize: BSSizes.applyScale(14) + color: SideSwapStyles.paragraphTextColor + } + + Text { + text: root.fromAddress + font.pixelSize: BSSizes.applyScale(14) + color: SideSwapStyles.paragraphTextColor + } + } + + CustomBorderedButton { + text: qsTr("BACK") + width: parent.width + height: BSSizes.applyScale(60) + anchors.horizontalCenter: parent.horizontalCenter + onClicked: root.back() + } + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPopup.qml b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPopup.qml new file mode 100644 index 000000000..4c79e998b --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/SideSwapPopup.qml @@ -0,0 +1,94 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 + +import "." +import "../Common" +import "../../" +import "../../BsStyles" +import "../../StyledControls" + +PluginPopup { + id: root + + background: Rectangle { + anchors.fill: parent + color: "black" + radius: BSSizes.applyScale(14) + } + + contentItem: StackView { + id: stackView + initialItem: mainPage + anchors.fill: parent + + SideSwapMainPage { + id: mainPage + visible: false + anchors.centerIn: parent + + onContinueClicked: { + if (mainPage.peg_in) { + stackView.replace(pegInPage) + } + else { + stackView.replace(pegOutPage) + } + } + + CloseIconButton { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: BSSizes.applyScale(10) + anchors.topMargin: BSSizes.applyScale(10) + onClose: root.close() + } + } + + SideSwapPegOut { + id: pegOutPage + visible: false + onBack: root.reset() + anchors.centerIn: parent + + CloseIconButton { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: BSSizes.applyScale(10) + anchors.topMargin: BSSizes.applyScale(10) + onClose: root.close() + } + } + + SideSwapPegIn { + id: pegInPage + visible: false + onBack: root.reset() + anchors.centerIn: parent + + CloseIconButton { + anchors.right: parent.right + anchors.top: parent.top + anchors.rightMargin: BSSizes.applyScale(10) + anchors.topMargin: BSSizes.applyScale(10) + onClose: root.close() + } + } + } + + function reset() + { + stackView.replace(mainPage) + mainPage.reset() + } +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Styles/SideSwapStyles.qml b/GUI/QtQuick/qml/Plugins/SideSwap/Styles/SideSwapStyles.qml new file mode 100644 index 000000000..07f84d7f6 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Styles/SideSwapStyles.qml @@ -0,0 +1,26 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +pragma Singleton +import QtQuick 2.0 + +QtObject { + readonly property color darkBlueBackground: "#003150" + readonly property color skyBlueBackground: "#064066" + + readonly property color primaryTextColor: "white" + readonly property color secondaryTextColor: "#03b3f3" + readonly property color paragraphTextColor: "#6d9dab" + + readonly property color spacerColor: "#104c72" + + readonly property color buttonBackground: "#00b4e9" + readonly property color buttonSecondaryBackground: "#043857" +} diff --git a/GUI/QtQuick/qml/Plugins/SideSwap/Styles/qmldir b/GUI/QtQuick/qml/Plugins/SideSwap/Styles/qmldir new file mode 100644 index 000000000..cab2ea498 --- /dev/null +++ b/GUI/QtQuick/qml/Plugins/SideSwap/Styles/qmldir @@ -0,0 +1,3 @@ +module BlockSettleStyle + +singleton SideSwapStyles 1.0 SideSwapStyles.qml diff --git a/GUI/QtQuick/qml/Receive/ReceivePopup.qml b/GUI/QtQuick/qml/Receive/ReceivePopup.qml new file mode 100644 index 000000000..75d9ce37d --- /dev/null +++ b/GUI/QtQuick/qml/Receive/ReceivePopup.qml @@ -0,0 +1,21 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +CustomPopup { + id: root + + objectName: "receive_popup" + + _stack_view.initialItem: receive_qr_code + _arrow_but_visibility: !receive_qr_code.visible + + ReceiveQrCode { + id: receive_qr_code + visible: false + } +} diff --git a/GUI/QtQuick/qml/Receive/ReceiveQrCode.qml b/GUI/QtQuick/qml/Receive/ReceiveQrCode.qml new file mode 100644 index 000000000..70a876ece --- /dev/null +++ b/GUI/QtQuick/qml/Receive/ReceiveQrCode.qml @@ -0,0 +1,107 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +import wallet.balance 1.0 + +ColumnLayout { + + id: layout + + signal sig_finish() + + height: BSSizes.applyWindowHeightScale(549) + width: BSSizes.applyWindowWidthScale(580) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Generate address") + } + + + Label { + id: subtitle + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight : BSSizes.applyScale(16) + text: qsTr("Bitcoins sent to this address will appear in:") + color: "#7A88B0" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + + Label { + id: wallet_name + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(6) + Layout.preferredHeight : BSSizes.applyScale(16) + text: qsTr("%1 / Native SegWit").arg(getWalletData(walletBalances.selectedWallet, WalletBalance.NameRole)) + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + + Image { + id: wallet_icon + + Layout.topMargin: BSSizes.applyScale(48) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(214) + Layout.preferredWidth : BSSizes.applyScale(214) + + source: bsApp.generatedAddress !== "" ? ("image://QR/" + bsApp.generatedAddress) : "" + sourceSize.width: BSSizes.applyScale(214) + sourceSize.height: BSSizes.applyScale(214) + width: BSSizes.applyScale(214) + height: BSSizes.applyScale(214) + } + + Label { + Layout.topMargin: BSSizes.applyScale(30) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + text: bsApp.generatedAddress + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Medium + color: "#E2E7FF" + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: copy_but + + width: BSSizes.applyScale(530) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: qsTr("Copy to clipboard") + + icon.source: "qrc:/images/copy_icon.svg" + icon.width: BSSizes.applyScale(24) + icon.height: BSSizes.applyScale(24) + icon.color: "#FFFFFF" + + preferred: true + + function click_enter() { + bsApp.copySeedToClipboard(bsApp.generatedAddress) + } + + } +} diff --git a/GUI/QtQuick/qml/Receive/qmldir b/GUI/QtQuick/qml/Receive/qmldir new file mode 100644 index 000000000..880ab8d50 --- /dev/null +++ b/GUI/QtQuick/qml/Receive/qmldir @@ -0,0 +1,4 @@ +module ReceiveControls + +ReceivePopup 1.0 ReceivePopup.qml +ReceiveQrCode 1.0 ReceiveQrCode.qml \ No newline at end of file diff --git a/GUI/QtQuick/qml/Send/AdvancedDetails.qml b/GUI/QtQuick/qml/Send/AdvancedDetails.qml new file mode 100644 index 000000000..8eb1f2731 --- /dev/null +++ b/GUI/QtQuick/qml/Send/AdvancedDetails.qml @@ -0,0 +1,823 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 +import Qt.labs.platform 1.1 as QLP + +import "../BsStyles" +import "../StyledControls" + +import wallet.balance 1.0 + +ColumnLayout { + + id: layout + + signal sig_continue(signature: var) + signal sig_simple() + signal sig_select_inputs() + signal import_error() + + height: BSSizes.applyWindowHeightScale(723) + width: BSSizes.applyWindowWidthScale(1132) + spacing: 0 + + property var tempRequest: null + property var tx: null + property bool isRBF: false + property bool isCPFP: false + property bool is_ready_broadcast: (tx.outputsModel.rowCount > 1) && rec_addr_input.input_text.length === 0 && amount_input.input_text.length === 0 + property bool is_ready_output: (rec_addr_input.isValid && rec_addr_input.input_text.length + && parseFloat(amount_input.input_text) !== 0 && amount_input.input_text.length) + + Connections { + target: tx.inputsModel + onSelectionChanged: { + create_temp_request() + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(34) + Layout.leftMargin: BSSizes.applyScale(20) + Layout.topMargin: BSSizes.applyScale(10) + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlingVCenter + + text: (!isRBF && !isCPFP) ? qsTr("Send Bitcoin") + : (isRBF ? qsTr("Send Bitcoin (RBF)") : qsTr("Send Bitcoin (CPFP)")) + } + + Label { + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(34) + } + + Button { + id: import_transaction_button + + // Layout.rightMargin: BSSizes.applyScale(60) + Layout.alignment: Qt.AlingVCenter + + activeFocusOnTab: false + hoverEnabled: true + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.titleTextColor + + text: qsTr("Import transaction") + + + icon.color: "transparent" + icon.source: "qrc:/images/import_icon.svg" + icon.width: BSSizes.applyScale(16) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(156) + implicitHeight: BSSizes.applyScale(34) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: import_transaction_button.hovered ? BSStyle.comboBoxHoveredBorderColor : BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + } + + onClicked: { + importTransactionFileDialog.open() + } + + FileDialog { + id: importTransactionFileDialog + title: qsTr("Please choose transaction to import") + folder: shortcuts.documents + selectFolder: false + selectExisting: true + onAccepted: { + tempRequest = bsApp.importTransaction(importTransactionFileDialog.fileUrl) + if (bsApp.isRequestReadyToSend(tempRequest)) { + sig_continue(tempRequest) + } + else { + import_error() + } + } + } + } + + Button { + id: simple_but + + Layout.leftMargin: BSSizes.applyScale(20) + Layout.rightMargin: BSSizes.applyScale(60) + Layout.alignment: Qt.AlingVCenter + + activeFocusOnTab: false + hoverEnabled: true + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.buttonsHeaderTextColor + + text: qsTr("Simple") + + icon.color: "transparent" + icon.source: "qrc:/images/advanced_icon.png" + icon.width: BSSizes.applyScale(16) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(100) + implicitHeight: BSSizes.applyScale(34) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: simple_but.hovered ? BSStyle.comboBoxHoveredBorderColor : BSStyle.defaultBorderColor + border.width: 1 + + } + + onClicked: { + layout.sig_simple() + } + } + } + + RowLayout { + + id: rects_row + + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(580) + Layout.topMargin: BSSizes.applyScale(15) + + spacing: BSSizes.applyScale(12) + + Rectangle { + id: inputs_rect + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignLeft | Qt.AlingVCenter + + width: BSSizes.applyScale(536) + height: BSSizes.applyScale(565) + color: "transparent" + + radius: BSSizes.applyScale(16) + + border.color: BSStyle.defaultBorderColor + border.width: 1 + + Column { + id: inputs_layout + + anchors.fill: parent + + spacing: 0 + + ColumnLayout { + width: parent.width + height: parent.height * 0.7 + spacing: 0 + + RowLayout { + id: input_header_layout + Layout.fillWidth: true + Layout.fillHeight: false + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight: BSSizes.applyScale(19) + Layout.alignment: Qt.AlignTop + + Label { + id: inputs_title + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.fillHeight: false + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + text: qsTr("Inputs") + + height : BSSizes.applyScale(19) + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Medium + } + + Label { + Layout.fillWidth: true + } + + CustomCheckBox { + id: checkbox_rbf + + focusPolicy: Qt.NoFocus + activeFocusOnTab: false + + implicitHeight: BSSizes.applyScale(18) + + Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.rightMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(0) + + text: qsTr("RBF") + enabled: !isRBF + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + } + + } + + WalletsComboBox { + id: from_wallet_combo + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + visible: true //!isRBF && !isCPFP + + width: BSSizes.applyScale(504) + height: BSSizes.applyScale(70) + + onActivated: { + walletBalances.selectedWallet = currentIndex + prepareRequest() + } + + function prepareRequest() { + if (rec_addr_input.isValid) { + create_temp_request() + } + + //I dont understand why but acceptableInput dont work... + var cur_value = parseFloat(amount_input.input_text) + var bottom = 0 + //var top = tempRequest.maxAmount + //if(cur_value < bottom || cur_value > top) + //{ + // amount_input.input_text = tempRequest.maxAmount + //} + + bsApp.getUTXOsForWallet(from_wallet_combo.currentIndex, tx) + } + + Connections { + target: walletBalances + function onChanged() { + if (layout.visible) { + from_wallet_combo.prepareRequest() + } + } + } + } + + FeeSuggestComboBox { + + id: fee_suggest_combo + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(10) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + width: BSSizes.applyScale(504) + height: BSSizes.applyScale(70) + + function change_index_handler() + { + if (isRBF) { + } + else if (isCPFP) { + } + else { + tx.inputsModel.fee = parseFloat(fee_suggest_combo.edit_value()) + bsApp.getUTXOsForWallet(from_wallet_combo.currentIndex, tx) + tx.outputsModel.clearOutputs() + } + } + + function setup_fee() { + if (tx !== null && (isRBF || isCPFP)) { + fee_suggest_combo.currentIndex = feeSuggestions.rowCount - 1 + fee_suggest_combo.input_item.text = Qt.binding(function() { + var fpb = parseFloat(tx.feePerByte) + 3.0 + return Math.max(feeSuggestions.fastestFee, fpb) + }) + } + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + ColumnLayout { + width: parent.width + height: parent.height * 0.3 + spacing: 0 + + Rectangle { + id: divider + height: BSSizes.applyScale(1) + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + color: BSStyle.defaultGreyColor + } + + + CustomTableView { + id: table_sel_inputs + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: BSSizes.applyScale(16) + Layout.rightMargin: BSSizes.applyScale(16) + Layout.preferredHeight: BSSizes.applyScale(300) + + model: tx.selectedInputsModel + columnWidths: [0.7, 0.1, 0.2] + + copy_button_column_index: -1 + has_header: false + + Component + { + id: cmpnt_sel_inputs + + Row { + id: cmpnt_sel_inputs_row + + spacing: BSSizes.applyScale(12) + + Text { + id: internal_text + + visible: model_column !== delete_button_column_index + + text: model_tableData + height: parent.height + verticalAlignment: Text.AlignVCenter + clip: true + + color: get_data_color(model_row, model_column) + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: model_row === 0 ? table_sel_inputs.text_header_size : table_sel_inputs.cell_text_size + + leftPadding: table_sel_inputs.get_text_left_padding(model_row, model_column) + } + + Button { + id: sel_inputs_button + + enabled: true + + activeFocusOnTab: false + + text: qsTr("Select Inputs") + + font.family: "Roboto" + font.weight: Font.DemiBold + font.pixelSize: BSSizes.applyScale(12) + + anchors.verticalCenter: parent.verticalCenter + + contentItem: Text { + text: sel_inputs_button.text + font: sel_inputs_button.font + color: "#45A6FF" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + + background: Rectangle { + implicitWidth: BSSizes.applyScale(84) + implicitHeight: BSSizes.applyScale(25) + color: "transparent" + border.color: "#45A6FF" + border.width: BSSizes.applyScale(1) + radius: BSSizes.applyScale(8) + } + + onClicked: layout.sig_select_inputs() + } + } + } + + + CustomTableDelegateRow { + id: cmpnt_table_delegate + } + + function choose_row_source_component(row, column) + { + if(row === 0 && column === 0) + return cmpnt_sel_inputs + else + return cmpnt_table_delegate + } + + function get_text_left_padding(row, column) + { + return (row === 0 && column === 0) ? 0 : left_text_padding + } + } + } + } + } + + Rectangle { + id: outputs_rect + + Layout.rightMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignRight | Qt.AlingVCenter + + width: BSSizes.applyScale(536) + height: BSSizes.applyScale(565) + color: "transparent" + + radius: BSSizes.applyScale(16) + + border.color: BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + Column { + id: outputs_layout + + anchors.fill: parent + + spacing: 0 + + ColumnLayout { + width: parent.width + height: parent.height * 0.7 + spacing: 0 + + Label { + id: outputs_title + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter + + text: qsTr("Outputs") + + height : BSSizes.applyScale(19) + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Medium + } + + RecvAddrTextInput { + + id: rec_addr_input + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + width: BSSizes.applyScale(504) + height: BSSizes.applyScale(70) + + wallets_current_index: from_wallet_combo.currentIndex + + onTextChanged: { + if (rec_addr_input.input_text.length && bsApp.validateAddress(rec_addr_input.input_text)) { + create_temp_request() + } + } + + onEnterPressed: { + if (!processEnterKey()) { + amount_input.setActiveFocus() + } + } + + onReturnPressed: { + if (!processEnterKey()) { + amount_input.setActiveFocus() + } + } + + onFocus_next: { + amount_input.setActiveFocus() + } + } + + AmountInput { + + id: amount_input + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(10) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + width: BSSizes.applyScale(504) + height: BSSizes.applyScale(70) + + function getMax() { + var maxValue = tempRequest.maxAmount - tx.outputsModel.totalAmount + return (maxValue >= 0 ? maxValue : 0).toFixed(8) + } + + onEnterPressed: { + if (!processEnterKey()) { + comment_input.setActiveFocus() + } + } + + onReturnPressed: { + if (!processEnterKey()) { + comment_input.setActiveFocus() + } + } + } + + CustomTextEdit { + + id: comment_input + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(10) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + Layout.preferredHeight : BSSizes.applyScale(90) + Layout.preferredWidth: BSSizes.applyScale(504) + + //aliases + title_text: qsTr("Comment") + + onTabNavigated: include_output_but.forceActiveFocus() + onBackTabNavigated: fee_suggest_combo.forceActiveFocus() + } + + CustomButton { + id: include_output_but + text: qsTr("Include Output") + + Layout.leftMargin: BSSizes.applyScale(16) + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + activeFocusOnTab: include_output_but.enabled + + enabled: isRBF || is_ready_output + preferred: !isRBF && is_ready_output + + icon.source: "qrc:/images/plus.svg" + icon.color: include_output_but.enabled ? "#45A6FF" : BSStyle.buttonsDisabledTextColor + + width: BSSizes.applyScale(504) + + function click_enter() { + if (!include_output_but.enabled) return + + //txOutputsModel.addOutput(rec_addr_input.input_text, amount_input.input_text) + tx.outputsModel.addOutput(rec_addr_input.input_text, amount_input.input_text) + + rec_addr_input.input_text = "" + amount_input.input_text = "" + + if (!isRBF && !isCPFP) { + tx.inputsModel.updateAutoselection() + } + create_temp_request() + console.log("valid: " + tempRequest.isValid + ", amounts match: " + tx.amountsMatch(parseFloat(fee_suggest_combo.edit_value()))) + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + } + + ColumnLayout { + width: parent.width + height: parent.height * 0.3 + spacing: 0 + + Rectangle { + + height: 1 + + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + color: BSStyle.defaultGreyColor + } + + CustomTableView { + id: table_outputs + + Layout.fillWidth: true + Layout.fillHeight : true + Layout.leftMargin: BSSizes.applyScale(16) + Layout.rightMargin: BSSizes.applyScale(16) + + model: tx.outputsModel + columnWidths: [0.544, 0.2, 0.20, 0.056] + + copy_button_column_index: -1 + delete_button_column_index: 3 + has_header: false + + onDeleteRequested: (row) => + { + //txOutputsModel.delOutput(row) + model.delOutput(row) + if (model.rowCount <= 1) { + tx.inputsModel.clearSelection() + } + create_temp_request() + } + + function get_text_left_padding(row, column) + { + return (row === 0 && column === 0) ? 0 : left_text_padding + } + } + } + } + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight : true + } + + QLP.FileDialog { + id: exportFileDialog + title: qsTr("Please choose folder to export transaction") + defaultSuffix: "bin" + fileMode: QLP.FileDialog.SaveFile + folder: QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation) + onAccepted: { + bsApp.exportTransaction(exportFileDialog.currentFile, continue_but.prepare_transaction()) + } + } + + CustomButton { + id: continue_but + + activeFocusOnTab: continue_but.enabled + + enabled: tempRequest && tempRequest.isValid && tx.amountsMatch(parseFloat(fee_suggest_combo.edit_value())) + preferred: isRBF || is_ready_broadcast + + width: BSSizes.applyScale(1084) + + Layout.bottomMargin: BSSizes.applyScale(30) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: (tempRequest !== null && tempRequest.isWatchingOnly) ? qsTr("Export transaction") : qsTr("Continue") + + function prepare_transaction() { + var fpb = parseFloat(fee_suggest_combo.edit_value()) + if (isRBF) { + return bsApp.createTXSignRequest(-1 //special index for RBF mode + , tx, fpb, comment_input.input_text, checkbox_rbf.checked) + } + else if (isCPFP) { + return bsApp.createTXSignRequest(-2 //special index for CPFP mode + , tx, fpb, comment_input.input_text, checkbox_rbf.checked) + } + else { // normal operation + return bsApp.createTXSignRequest(from_wallet_combo.currentIndex + , tx, fpb, comment_input.input_text, checkbox_rbf.checked) + } + } + + function click_enter() { + if (!continue_but.enabled) { + return + } + + if (tempRequest && tempRequest.isWatchingOnly) + { + exportFileDialog.currentFile = "file:///" + bsApp.makeExportTransactionFilename(tempRequest) + exportFileDialog.open() + } + else { + layout.sig_continue(prepare_transaction()) + } + } + + } + + + Keys.onEnterPressed: { + click_buttons() + } + + Keys.onReturnPressed: { + click_buttons() + } + + function click_buttons() + { + if (include_output_but.enabled) + { + include_output_but.click_enter() + } + else if (continue_but.enabled) + { + continue_but.click_enter() + } + } + + function create_temp_request() + { + var fpb = parseFloat(fee_suggest_combo.edit_value()) + var outputAddresses = tx.outputsModel.getOutputAddresses() + var outputAmounts = tx.outputsModel.getOutputAmounts() + var selectedInputs = tx.inputsModel.getSelection() + + if (rec_addr_input.isValid && rec_addr_input.input_text.length) { + outputAddresses.push(rec_addr_input.input_text) + } + + if (!isRBF && !isCPFP) { + tempRequest = bsApp.newTXSignRequest(from_wallet_combo.currentIndex + , outputAddresses, outputAmounts, + (fpb > 0) ? fpb : 1.0, comment_input.input_text + , checkbox_rbf.checked + , (selectedInputs.rowCount > 0) ? selectedInputs : null) + } + else { + tempRequest = bsApp.newTXSignRequest(from_wallet_combo.currentIndex + , outputAddresses, outputAmounts, (fpb > 0) ? fpb : 1.0 + , "", true, selectedInputs) + } + } + + function processEnterKey() + { + if (isRBF) { + include_output_but.click_enter() + continue_but.click_enter() + return true + } + + if (is_ready_broadcast) { + continue_but.click_enter() + return true + } + else if (is_ready_output) { + include_output_but.click_enter() + rec_addr_input.setActiveFocus() + return true + } + return false + } + + function init() + { + rec_addr_input.setActiveFocus() + + //we need set first time currentIndex to 0 + //only after we will have signal rowchanged + if (fee_suggest_combo.currentIndex >= 0) + fee_suggest_combo.currentIndex = 0 + + amount_input.input_text = "" + comment_input.input_text = "" + rec_addr_input.input_text = "" + checkbox_rbf.checked = true + + tx.outputsModel.clearOutputs() + bsApp.getUTXOsForWallet(from_wallet_combo.currentIndex, tx) + bsApp.requestFeeSuggestions() + + if (isRBF || isCPFP) { + create_temp_request() + if (isRBF) { + tx.outputsModel.setOutputsFrom(tx) + } + else if (isCPFP) { + tx.setInputsFromOutputs() + } + } + } +} diff --git a/GUI/QtQuick/qml/Send/AmountInput.qml b/GUI/QtQuick/qml/Send/AmountInput.qml new file mode 100644 index 000000000..5dfc616bf --- /dev/null +++ b/GUI/QtQuick/qml/Send/AmountInput.qml @@ -0,0 +1,111 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "../StyledControls" + +CustomTextInput { + + id: amount_input + + //aliases + title_text: qsTr("Amount") + + function getMax() { + return tempRequest.maxAmount + } + + //app (if was launched from visual studio) crashes when there is input_validator + //and we change text inside of onTextEdited + //it is why I have realized my validator inside of onTextEdited + property string prev_text : "" + onTextEdited : { + + if (tempRequest === null || !tempRequest.isValid) { + amount_input.input_text = "0" + } + + amount_input.input_text = amount_input.input_text.replace(",", ".") + + var indexOfDot = amount_input.input_text.indexOf(".") + if (indexOfDot >= 0) + { + amount_input.input_text = amount_input.input_text.substring(0, + Math.min(indexOfDot+9, amount_input.input_text.length)) + } + if (amount_input.input_text.startsWith("0") + && !amount_input.input_text.startsWith("0.") + && amount_input.input_text.length > 1) + { + amount_input.input_text = "0." + + amount_input.input_text.substring(1, amount_input.input_text.length) + } + try { + var input_number = Number.fromLocaleString(Qt.locale("en_US"), amount_input.input_text) + } + catch (error) + { + amount_input.input_text = prev_text + return + } + + if (input_number < 0 || (input_number > amount_input.getMax())) + { + amount_input.input_text = prev_text + return + } + + prev_text = amount_input.input_text + } + + CustomButton { + + id: max_but + + z: 1 + + width: BSSizes.applyScale(55) + height: BSSizes.applyScale(28) + back_radius: BSSizes.applyScale(37) + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(23) + + text: qsTr("MAX") + font.pixelSize: BSSizes.applyScale(12) + enabled: (tempRequest != null) + + function click_enter() { + if (tempRequest != null && amount_input.getMax().length > 0) { + amount_input.input_text = amount_input.getMax() + } + } + } + + Label { + + id: currency + + anchors.verticalCenter: parent.verticalCenter + anchors.right: max_but.left + anchors.rightMargin: BSSizes.applyScale(16) + + text: "BTC" + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + color: "#7A88B0" + } + +} diff --git a/GUI/QtQuick/qml/Send/FeeSuggestComboBox.qml b/GUI/QtQuick/qml/Send/FeeSuggestComboBox.qml new file mode 100644 index 000000000..952ab626a --- /dev/null +++ b/GUI/QtQuick/qml/Send/FeeSuggestComboBox.qml @@ -0,0 +1,84 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "../StyledControls" + +CustomComboBox { + id: fee_suggest_combo + + model: feeSuggestions + editable: currentIndex == (feeSuggestions.rowCount - 1) + + //aliases + title_text: qsTr("Fee Suggestions") + + height: BSSizes.applyScale(70) + + textRole: (currentIndex == (feeSuggestions.rowCount - 1) && !popup.visible) ? "value" : "text" + valueRole: "value" + suffix_text: qsTr("s/b") + + validator: RegExpValidator {regExp: new RegExp(create_regexp())} + + Connections + { + target:feeSuggestions + function onRowCountChanged () + { + fee_suggest_combo.currentIndex = 0 + if (typeof change_index_handler === "function") + { + change_index_handler() + } + if (typeof setup_fee === "function") { + setup_fee() + } + + validator.regExp = new RegExp(create_regexp()) + } + } + + onCurrentIndexChanged: { + if (typeof change_index_handler === "function") + { + change_index_handler() + } + validator.regExp = new RegExp(create_regexp()) + + if (currentIndex == (feeSuggestions.rowCount - 1)) { + fee_suggest_combo.input_item.forceActiveFocus() + fee_suggest_combo.input_item.cursorPosition = 0 + } + } + + function create_regexp() + { + return "\\d*\\.?\\d?" + } + + function edit_value() + { + var res; + if (currentIndex != (feeSuggestions.rowCount - 1)) { + res = fee_suggest_combo.currentText + var index = res.indexOf(":") + res = res.slice(index+2) + res = res.replace(" " + fee_suggest_combo.suffix_text, "") + } + else { + res = fee_suggest_combo.input_text + } + return res + } +} diff --git a/GUI/QtQuick/qml/Send/PasswordWithTimer.qml b/GUI/QtQuick/qml/Send/PasswordWithTimer.qml new file mode 100644 index 000000000..452d70335 --- /dev/null +++ b/GUI/QtQuick/qml/Send/PasswordWithTimer.qml @@ -0,0 +1,90 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "../StyledControls" + +Item { + id: root + + property int time_progress + + property alias value: password.input_text + + signal enterPressed() + signal returnPressed() + + CustomTextInput { + id: password + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 0 + + width: root.width + height: BSSizes.applyScale(70) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + visible: txSignRequest !== null ? !txSignRequest.isHWW : false + title_text: qsTr("Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + root.enterPressed() + } + onReturnPressed: { + root.returnPressed() + } + } + + CustomProgressBar { + id: progress_bar + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: password.bottom + anchors.topMargin: BSSizes.applyScale(16) + + width: root.width + + from: 0 + to: 120 + + value: root.time_progress + } + + Label { + id: progress_label + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: progress_bar.bottom + anchors.topMargin: BSSizes.applyScale(8) + + text: qsTr("%1 seconds left").arg (Number(root.time_progress).toLocaleString()) + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + } + + function setActiveFocus() { + password.setActiveFocus() + } + +} diff --git a/GUI/QtQuick/qml/Send/RecvAddrTextInput.qml b/GUI/QtQuick/qml/Send/RecvAddrTextInput.qml new file mode 100644 index 000000000..4477d0ed2 --- /dev/null +++ b/GUI/QtQuick/qml/Send/RecvAddrTextInput.qml @@ -0,0 +1,74 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "../StyledControls" + +CustomTextInput { + + id: rec_addr_input + + property string fee_current_value + property int wallets_current_index + + signal focus_next() + + input_right_margin: BSSizes.applyScale(paste_but.anchors.rightMargin + paste_but.width + 16) + + //aliases + title_text: qsTr("Receiver address") + + Image { + id: paste_but + + z: 1 + + anchors.top: rec_addr_input.top + anchors.topMargin: BSSizes.applyScale(23) + anchors.right: rec_addr_input.right + anchors.rightMargin: BSSizes.applyScale(23) + + source: "qrc:/images/paste_icon.png" + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + + MouseArea { + anchors.fill: parent + onClicked: { + rec_addr_input.input_text = bsApp.pasteTextFromClipboard() + rec_addr_input.validate() + } + } + } + + onTextEdited : { + rec_addr_input.validate() + } + + function validate() + { + if (rec_addr_input.input_text.length) + { + rec_addr_input.isValid = bsApp.validateAddress(rec_addr_input.input_text) + if (rec_addr_input.isValid) + { + create_temp_request() + focus_next() + } + } + else + { + rec_addr_input.isValid = true + } + } +} diff --git a/GUI/QtQuick/qml/Send/SelectInputs.qml b/GUI/QtQuick/qml/Send/SelectInputs.qml new file mode 100644 index 000000000..b4ee1f7a9 --- /dev/null +++ b/GUI/QtQuick/qml/Send/SelectInputs.qml @@ -0,0 +1,275 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + property var inputsModel: null + id: layout + + property int text_header_size: BSSizes.applyScale(11) + property int cell_text_size: BSSizes.applyScale(12) + + height: BSSizes.applyScale(662) + width: BSSizes.applyScale(1132) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Select inputs") + } + + Rectangle { + id: addresses_rect + + Layout.topMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + width: BSSizes.applyScale(1084) + height: BSSizes.applyScale(434) + color: "transparent" + + radius: BSSizes.applyScale(16) + + border.color: BSStyle.defaultBorderColor + border.width: 1 + + CustomTableView { + id: inputs_table + + width: parent.width - BSSizes.applyScale(32) + height: parent.height - BSSizes.applyScale(12) + anchors.centerIn: parent + + model: inputsModel + columnWidths: [0.7, 0.1, 0.1, 0.1] + + copy_button_column_index: -1 + has_header: false + + Component + { + id: cmpnt_address_item + + Item { + id: address_item + + CustomCheckBox { + id: checkbox_address + enabled: model_is_editable + + anchors.left: parent.left + anchors.leftMargin: inputs_table.get_text_left_padding(model_row, model_column, model_is_expandable) + anchors.verticalCenter: parent.verticalCenter + + checked: model_selected + checkable: true + + onClicked: { + inputsModel.toggleSelection(model_row) + } + } + + Text { + id: internal_text + + anchors.left: checkbox_address.right + anchors.leftMargin: BSSizes.applyScale(8) + anchors.verticalCenter: parent.verticalCenter + + visible: model_column !== delete_button_column_index + + text: model_tableData + height: parent.height + verticalAlignment: Text.AlignVCenter + clip: true + + color: get_data_color(model_row, model_column) + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: model_row === 0 ? text_header_size : cell_text_size + + MouseArea { + anchors.fill: parent + + onClicked: { + if (model_row !== 0) + inputsModel.toggle(model_row) + } + } + } + + Image { + id: arrow_icon + + visible: model_is_expandable + + anchors.left: internal_text.left + anchors.leftMargin: BSSizes.applyScale(10) + internal_text.contentWidth + anchors.verticalCenter: parent.verticalCenter + + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(6) + sourceSize.width: BSSizes.applyScale(9) + sourceSize.height: BSSizes.applyScale(6) + + source: model_expanded? "qrc:/images/expanded.svg" : "qrc:/images/collapsed.svg" + + MouseArea { + anchors.fill: parent + + onClicked: { + if (model_row !== 0) + inputsModel.toggle(model_row) + } + } + } + } + } + + CustomTableDelegateRow { + id: cmpnt_table_delegate + } + + function choose_row_source_component(row, column) + { + return (column === 0) ? cmpnt_address_item : cmpnt_table_delegate + } + + function get_text_left_padding(row, column, isExpandable) + { + return (!isExpandable && column === 0 && row !== 0) ? 61 : left_text_padding + } + + function get_line_left_padding(row, column, isExpandable) + { + return (!isExpandable && column === 0 && row !== 0) ? 51 : 0 + } + } + } + + Label { + id: inputs_details_title + + Layout.leftMargin: BSSizes.applyScale(26) + Layout.topMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + text: qsTr("Inputs details") + + height : BSSizes.applyScale(19) + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Medium + } + + Rectangle { + id: total_rect + + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + width: BSSizes.applyScale(1084) + height: BSSizes.applyScale(82) + color: "#32394F" + + radius: BSSizes.applyScale(14) + + Label { + + id: trans_inputs_title + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Transaction Inputs:") + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: total_amount_title + + anchors.top: trans_inputs_title.bottom + anchors.topMargin: BSSizes.applyScale(14) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Total Amount:") + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: trans_inputs + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: inputsModel.nbTx + + color: "#FFFFFF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: total_amount + + anchors.top: trans_inputs.bottom + anchors.topMargin: BSSizes.applyScale(18) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: inputsModel.balance + + color: "#FFFFFF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + function init() + { + } +} diff --git a/GUI/QtQuick/qml/Send/SendPopup.qml b/GUI/QtQuick/qml/Send/SendPopup.qml new file mode 100644 index 000000000..08eb9e993 --- /dev/null +++ b/GUI/QtQuick/qml/Send/SendPopup.qml @@ -0,0 +1,195 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +CustomPopup { + id: root + + objectName: "send_popup" + + navig_bar_width: BSSizes.applyScale(30) + + _stack_view.initialItem: simple_details + _arrow_but_visibility: !simple_details.visible && !advanced_details.visible + + property var tx: bsApp.getTXDetails("") + property bool isRBF: false + property bool isCPFP: false + + SimpleDetails { + id: simple_details + visible: false + tx: root.tx + + onSig_continue: (signature) => { + sign_trans.txSignRequest = signature + _stack_view.push(sign_trans) + sign_trans.init() + } + + onSig_advanced: { + _stack_view.replace(advanced_details) + root.tx = bsApp.getTXDetails("") + advanced_details.init() + } + + onImport_error: { + failImportDialog.show() + failImportDialog.raise() + failImportDialog.requestActivate() + } + } + + SignTransaction { + id: sign_trans + visible: false + + onSig_broadcast: { + root.close() + _stack_view.pop(null) + } + + onSig_time_finished: { + root.close() + _stack_view.pop(null) + } + } + + AdvancedDetails { + id: advanced_details + visible: false + + tx: root.tx + isRBF: root.isRBF + isCPFP: root.isCPFP + + onSig_continue: (signature) => { + sign_trans_advanced.txSignRequest = signature + _stack_view.push(sign_trans_advanced) + sign_trans_advanced.init() + } + + onSig_simple: { + _stack_view.replace(simple_details) + simple_details.init() + } + + onSig_select_inputs: { + _stack_view.push(select_inputs) + select_inputs.init() + } + + onImport_error: { + failImportDialog.show() + failImportDialog.raise() + failImportDialog.requestActivate() + } + } + + SelectInputs { + inputsModel: tx.inputsModel + id: select_inputs + visible: false + } + + SignTransactionAdvanced { + id: sign_trans_advanced + visible: false + + isRBF: root.isRBF + isCPFP: root.isCPFP + + onSig_broadcast: { + root.close() + _stack_view.pop(null) + } + + onSig_time_finished: { + root.close() + _stack_view.pop(null) + } + } + + CustomSuccessDialog { + id: exportTransactionSuccessDailog + visible: false + + onSig_finish: { + root.close() + _stack_view.pop(null) + } + } + + CustomFailDialog { + id: exportTransactionFailDialog + visible: false + } + + CustomFailDialog { + id: failImportDialog + header: qsTr("Send failed") + fail: qsTr("There is no appropriate wallet to send the transaction") + visible: false + } + + Connections { + target: bsApp + function onTransactionExported(text) { + exportTransactionSuccessDailog.details_text = qsTr("Transaction successfully exported to %1").arg(text) + + exportTransactionSuccessDailog.show() + exportTransactionSuccessDailog.raise() + exportTransactionSuccessDailog.requestActivate() + } + function onTransactionExportFailed(text) { + exportTransactionFailDialog.header = qsTr("Export transaction failed") + exportTransactionFailDialog.fail = text + + exportTransactionFailDialog.show() + exportTransactionFailDialog.raise() + exportTransactionFailDialog.requestActivate() + } + } + + function init() { + root.tx = bsApp.getTXDetails("") + _stack_view.replace(bsApp.settingAdvancedTX ? advanced_details : simple_details) + + if (_stack_view.currentItem === simple_details) + { + simple_details.init() + } + else if (_stack_view.currentItem === advanced_details) + { + advanced_details.init() + } + root.isRBF = false + root.isCPFP = false + } + + function open(txId: string, isRBF: bool, isCPFP: bool, selWalletIdx: int) + { + _stack_view.replace(advanced_details) + root.tx = bsApp.getTXDetails(txId, isRBF, isCPFP, selWalletIdx) + root.isRBF = isRBF + root.isCPFP = isCPFP + advanced_details.init() + } + + function close_click() + { + if (select_inputs.visible) + { + _stack_view.pop() + } + else + { + root.close() + _stack_view.pop(null) + } + } +} diff --git a/GUI/QtQuick/qml/Send/SignTransaction.qml b/GUI/QtQuick/qml/Send/SignTransaction.qml new file mode 100644 index 000000000..5241369fb --- /dev/null +++ b/GUI/QtQuick/qml/Send/SignTransaction.qml @@ -0,0 +1,428 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property var txSignRequest: null + + property int time_progress + + signal sig_broadcast() + signal sig_time_finished() + + height: BSSizes.applyWindowHeightScale(554) + width: BSSizes.applyWindowWidthScale(580) + spacing: 0 + + property int k: 0 + Connections + { + target:bsApp + function onSuccessTx() + { + if (!layout.visible) { + return + } + + sig_broadcast() + } + function onFailedTx(error) + { + if (!layout.visible) { + return + } + + fail_dialog.fail = error + fail_dialog.show() + fail_dialog.raise() + fail_dialog.requestActivate() + } + } + + CustomFailDialog { + id: fail_dialog + header: "Failed to send" + visible: false; + } + + CustomTitleLabel { + id: title + Layout.topMargin: BSSizes.applyScale(6) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : title.height + text: qsTr("Sign Transaction") + } + + Label { + Layout.fillWidth: true + height: BSSizes.applyScale(24) + } + + Rectangle { + + id: output_rect + + width: BSSizes.applyScale(532) + height: BSSizes.applyScale(82) + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + radius: BSSizes.applyScale(14) + + color: "#32394F" + + Label { + + id: out_addr_title + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Output address:") + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: out_amount_title + + anchors.top: out_addr_title.bottom + anchors.topMargin: BSSizes.applyScale(14) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Output amount:") + + color: "#45A6FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: out_addr + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.outputAddresses[0] : "" + + color: "#FFFFFF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: out_amount + + anchors.top: out_addr.bottom + anchors.topMargin: BSSizes.applyScale(14) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.outputAmount : "" + + color: "#FFFFFF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + } + + Rectangle { + + id: input_rect + + width: BSSizes.applyScale(532) + height: BSSizes.applyScale(188) + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + + color: "transparent" + + Label { + + id: in_amount_title + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Input amount:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: in_amount + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(18) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.inputAmount : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: return_amount_title + + anchors.top: in_amount_title.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Return amount:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: return_amount + + anchors.top: in_amount.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.returnAmount : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_fee_title + + anchors.top: return_amount_title.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Transaction fee:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_fee + + anchors.top: return_amount.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.fee : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_size_title + + anchors.top: transaction_fee_title.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Transaction size:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_size + + anchors.top: transaction_fee.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.txSize : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: fee_per_byte_title + + anchors.top: transaction_size_title.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(20) + + text: qsTr("Fee-per-byte:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: fee_per_byte + + anchors.top: transaction_size.bottom + anchors.topMargin: BSSizes.applyScale(15) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(20) + + text: txSignRequest !== null ? txSignRequest.feePerByte : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + } + + PasswordWithTimer { + id: password + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(10) + + width: BSSizes.applyScale(532) + + time_progress: layout.time_progress + + onEnterPressed: { + broadcast_but.click_enter() + } + onReturnPressed: { + broadcast_but.click_enter() + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: broadcast_but + text: txSignRequest !== null ? (txSignRequest.hasError ? txSignRequest.errorText : qsTr("Broadcast")) : "" + width: BSSizes.applyScale(532) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + enabled: txSignRequest !== null ? + (!txSignRequest.hasError && ((txSignRequest.isHWW && txSignRequest.isHWWready) || password.value.length)) : + false + + preferred: true + + function click_enter() { + bsApp.signAndBroadcast(txSignRequest, password.value) + password.value = "" + } + } + + Keys.onEnterPressed: { + broadcast_but.click_enter() + } + + Keys.onReturnPressed: { + broadcast_but.click_enter() + } + + Timer { + id: timer + + interval: 1000 + running: true + repeat: true + onTriggered: { + if (layout.time_progress != 0) { + layout.time_progress = layout.time_progress - 1 + } + + if (time_progress === 0 && !fail_dialog.visible) + { + running = false + password.value = "" + + sig_time_finished() + } + } + } + + function init() + { + password.value = "" + time_progress = 120 + password.setActiveFocus() + timer.running = true + } + +} diff --git a/GUI/QtQuick/qml/Send/SignTransactionAdvanced.qml b/GUI/QtQuick/qml/Send/SignTransactionAdvanced.qml new file mode 100644 index 000000000..e582890d6 --- /dev/null +++ b/GUI/QtQuick/qml/Send/SignTransactionAdvanced.qml @@ -0,0 +1,465 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property var txSignRequest: null + + property int time_progress + property bool isRBF: false + property bool isCPFP: false + + signal sig_broadcast() + signal sig_time_finished() + + height: BSSizes.applyWindowHeightScale(748) + width: BSSizes.applyWindowWidthScale(1132) + spacing: 0 + + Connections + { + target:bsApp + function onSuccessTx() + { + if (!layout.visible) { + return + } + + sig_broadcast() + } + function onFailedTx(error) + { + if (!layout.visible) { + return + } + + fail_dialog.fail = error + fail_dialog.show() + fail_dialog.raise() + fail_dialog.requestActivate() + } + } + + CustomFailDialog { + id: fail_dialog + header: "Failed to send" + visible: false; + } + + CustomTitleLabel { + id: title + Layout.topMargin: 6 + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : title.height + text: qsTr("Sign Transaction") + } + + + RowLayout { + id: rects_row + + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(312) + Layout.topMargin: BSSizes.applyScale(24) + + spacing: BSSizes.applyScale(20) + + Rectangle { + id: inputs_rect + + Layout.leftMargin: BSSizes.applyScale(22) + Layout.alignment: Qt.AlignLeft | Qt.AlingVCenter + + width: BSSizes.applyScale(532) + height: BSSizes.applyScale(312) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + CustomTableView { + id: table_sel_inputs + + width: parent.width - BSSizes.applyScale(28) + height: parent.height - BSSizes.applyScale(24) + anchors.centerIn: parent + + model: txSignRequest !== null ? txSignRequest.inputs : [] + columnWidths: [0.7, 0.1, 0, 0.2] + + copy_button_column_index: -1 + has_header: false + + function get_text_left_padding(row, column) + { + return (row === 0 && column === 0) ? 0 : left_text_padding + } + + function get_data_color(row, column) + { + return row === 0 ? "#45A6FF" : null + } + } + } + + Rectangle { + id: outputs_rect + + Layout.rightMargin: BSSizes.applyScale(22) + Layout.alignment: Qt.AlignRight | Qt.AlingVCenter + + width: BSSizes.applyScale(532) + height: BSSizes.applyScale(312) + color: "#32394F" + + radius: BSSizes.applyScale(14) + + CustomTableView { + id: table_outputs + + width: parent.width - BSSizes.applyScale(28) + height: parent.height - BSSizes.applyScale(24) + anchors.centerIn: parent + + model: txSignRequest !== null ? txSignRequest.outputs : [] + columnWidths: [0.7, 0.1, 0.20] + + copy_button_column_index: -1 + delete_button_column_index: -1 + has_header: false + + function get_text_left_padding(row, column) + { + return (row === 0 && column === 0) ? 0 : left_text_padding + } + + function get_data_color(row, column) + { + return row === 0 ? "#45A6FF" : null + } + } + } + } + + Rectangle { + id: details_rect + + Layout.alignment: Qt.AlignHCenter | Qt.AlingTop + Layout.preferredHeight : BSSizes.applyScale(100) + Layout.topMargin: BSSizes.applyScale(20) + + width: BSSizes.applyScale(1084) + height: BSSizes.applyScale(100) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + Label { + + id: in_amount_title + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(16) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(16) + + text: qsTr("Input amount:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: in_amount + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(16) + anchors.right: parent.horizontalCenter + anchors.rightMargin: BSSizes.applyScale(24) + + text: txSignRequest !== null ? txSignRequest.inputAmount : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: return_amount_title + + anchors.top: in_amount_title.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(16) + + text: qsTr("Return amount:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: return_amount + + anchors.top: in_amount.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.right: parent.horizontalCenter + anchors.rightMargin: BSSizes.applyScale(24) + + text: txSignRequest !== null ? txSignRequest.returnAmount : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_fee_title + + anchors.top: return_amount_title.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(16) + + text: qsTr("Transaction fee:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_fee + + anchors.top: return_amount.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.right: parent.horizontalCenter + anchors.rightMargin: BSSizes.applyScale(24) + + text: txSignRequest !== null ? txSignRequest.fee : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_size_title + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(16) + anchors.left: parent.horizontalCenter + anchors.leftMargin: BSSizes.applyScale(24) + + text: qsTr("Transaction size:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: transaction_size + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(16) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(16) + + text: txSignRequest !== null ? txSignRequest.txSize : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: fee_per_byte_title + + anchors.top: transaction_size_title.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.left: parent.horizontalCenter + anchors.leftMargin: BSSizes.applyScale(24) + + text: qsTr("Fee-per-byte:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: fee_per_byte + + anchors.top: transaction_size.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(16) + + text: txSignRequest !== null ? txSignRequest.feePerByte : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: total_spent_title + + anchors.top: fee_per_byte_title.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.left: parent.horizontalCenter + anchors.leftMargin: BSSizes.applyScale(24) + + text: qsTr("Total spent:") + + color: "#7A88B0" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + + id: total_spent + + anchors.top: fee_per_byte.bottom + anchors.topMargin: BSSizes.applyScale(10) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(16) + + text: txSignRequest !== null ? txSignRequest.outputAmount : "" + + color: "#E2E7FF" + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + } + + PasswordWithTimer { + id: password + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(10) + + width: BSSizes.applyScale(530) + + time_progress: layout.time_progress + + onEnterPressed: { + broadcast_but.click_enter() + } + onReturnPressed: { + broadcast_but.click_enter() + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: broadcast_but + text: txSignRequest !== null ? (txSignRequest.hasError ? txSignRequest.errorText : qsTr("Broadcast")) : "" + width: BSSizes.applyScale(1084) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + enabled: txSignRequest !== null ? + (!txSignRequest.hasError && ((txSignRequest.isHWW && txSignRequest.isHWWready) || password.value.length)) : + false + + preferred: true + + function click_enter() { + bsApp.signAndBroadcast(txSignRequest, password.value) + password.value = "" + } + } + + Keys.onEnterPressed: { + broadcast_but.click_enter() + } + + Keys.onReturnPressed: { + broadcast_but.click_enter() + } + + Timer { + id: timer + + interval: 1000 + running: true + repeat: true + onTriggered: { + if (layout.time_progress != 0) { + layout.time_progress = layout.time_progress - 1 + } + + if (time_progress === 0 && !fail_dialog.visible) + { + running = false + password.value = "" + sig_time_finished() + } + } + } + + function init() + { + password.value = "" + time_progress = 120 + password.setActiveFocus() + timer.running = true + } + +} diff --git a/GUI/QtQuick/qml/Send/SimpleDetails.qml b/GUI/QtQuick/qml/Send/SimpleDetails.qml new file mode 100644 index 000000000..0e3590f81 --- /dev/null +++ b/GUI/QtQuick/qml/Send/SimpleDetails.qml @@ -0,0 +1,354 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.3 +import Qt.labs.platform 1.1 as QLP + +import "../BsStyles" +import "../StyledControls" + +import wallet.balance 1.0 + +ColumnLayout { + property var tx: null + + id: layout + + signal sig_continue(signature: var) + signal sig_advanced() + signal import_error() + + height: BSSizes.applyWindowHeightScale(554) + width: BSSizes.applyWindowWidthScale(600) + spacing: 0 + + property var tempRequest: null + + RowLayout { + + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(34) + Layout.leftMargin: BSSizes.applyScale(20) + Layout.topMargin: BSSizes.applyScale(10) + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlingVCenter + text: qsTr("Send Bitcoin") + } + + Label { + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(34) + } + + Button { + id: import_transaction_button + + // Layout.rightMargin: BSSizes.applyScale(60) + Layout.alignment: Qt.AlingVCenter + + activeFocusOnTab: false + hoverEnabled: true + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.titleTextColor + + text: qsTr("Import transaction") + + + icon.color: "transparent" + icon.source: "qrc:/images/import_icon.svg" + icon.width: BSSizes.applyScale(16) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(156) + implicitHeight: BSSizes.applyScale(34) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: import_transaction_button.hovered ? BSStyle.comboBoxHoveredBorderColor : BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + } + + onClicked: { + importTransactionFileDialog.open() + } + + FileDialog { + id: importTransactionFileDialog + title: qsTr("Please choose transaction to import") + folder: shortcuts.documents + selectFolder: false + selectExisting: true + onAccepted: { + tempRequest = bsApp.importTransaction(importTransactionFileDialog.fileUrl) + if (bsApp.isRequestReadyToSend(tempRequest)) { + sig_continue(tempRequest) + } + else { + import_error() + } + } + } + } + + Button { + id: advanced_but + + Layout.leftMargin: BSSizes.applyScale(20) + Layout.rightMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlingVCenter + + activeFocusOnTab: false + hoverEnabled: true + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.titleTextColor + + text: qsTr("Advanced") + + icon.color: "transparent" + icon.source: "qrc:/images/advanced_icon.png" + icon.width: BSSizes.applyScale(16) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(116) + implicitHeight: BSSizes.applyScale(34) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: advanced_but.hovered ? BSStyle.comboBoxHoveredBorderColor : BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + } + + onClicked: { + layout.sig_advanced(tx) + } + } + } + + RecvAddrTextInput { + + id: rec_addr_input + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(552) + Layout.topMargin: BSSizes.applyScale(23) + + wallets_current_index: from_wallet_combo.currentIndex + + onFocus_next: { + amount_input.setActiveFocus() + } + + function createTempRequest() { + create_temp_request() + } + } + + AmountInput { + + id: amount_input + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(552) + Layout.topMargin: BSSizes.applyScale(10) + + onEnterPressed: { + continue_but.click_enter() + } + onReturnPressed: { + continue_but.click_enter() + } + } + + RowLayout { + + Layout.fillWidth: true + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.topMargin: BSSizes.applyScale(10) + + WalletsComboBox { + + id: from_wallet_combo + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignLeft | Qt.AlingVCenter + + width: BSSizes.applyScale(271) + + onActivated: (index_act) => { + walletBalances.selectedWallet = currentIndex + create_temp_request() + } + + Connections { + target: walletBalances + function onChanged() { + if (layout.visible) { + create_temp_request() + } + } + } + } + + Label { + Layout.fillWidth: true + Layout.preferredHeight: BSSizes.applyScale(70) + } + + FeeSuggestComboBox { + + id: fee_suggest_combo + + Layout.rightMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignRight | Qt.AlingVCenter + + width: BSSizes.applyScale(271) + } + } + + CustomTextEdit { + + id: comment_input + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(90) + Layout.preferredWidth: BSSizes.applyScale(552) + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + title_text: qsTr("Comment") + + onTabNavigated: continue_but.forceActiveFocus() + onBackTabNavigated: fee_suggest_combo.forceActiveFocus() + + onEnterKeyPressed: { + event.accepted = true; + continue_but.forceActiveFocus() + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + QLP.FileDialog { + id: exportFileDialog + title: qsTr("Please choose folder to export transaction") + defaultSuffix: "bin" + folder: QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation) + fileMode: QLP.FileDialog.SaveFile + onAccepted: { + bsApp.exportTransaction(exportFileDialog.currentFile, continue_but.prepare_transaction()) + } + } + + CustomButton { + id: continue_but + + enabled: rec_addr_input.isValid && rec_addr_input.input_text.length + && parseFloat(amount_input.input_text) !== 0 && amount_input.input_text.length + + activeFocusOnTab: continue_but.enabled + + width: BSSizes.applyScale(552) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: (tempRequest !== null && tempRequest.isWatchingOnly) ? qsTr("Export transaction") : qsTr("Continue") + + preferred: true + + function prepare_transaction() { + return bsApp.newTXSignRequest(from_wallet_combo.currentIndex + , [rec_addr_input.input_text], [parseFloat(amount_input.input_text)] + , parseFloat(fee_suggest_combo.edit_value()), comment_input.input_text + , true, null, true) + } + + function click_enter() { + if (!fee_suggest_combo.edit_value()) + { + fee_suggest_combo.input_text = fee_suggest_combo.currentText + } + + if (tempRequest.isWatchingOnly) + { + exportFileDialog.currentFile = "file:///" + bsApp.makeExportTransactionFilename(tempRequest) + exportFileDialog.open() + } + else + { + layout.sig_continue( prepare_transaction() ) + } + } + } + + + Keys.onEnterPressed: { + continue_but.click_enter() + } + + Keys.onReturnPressed: { + continue_but.click_enter() + } + + Connections + { + target:tempRequest + function onTxSignReqChanged () + { + //I dont understand why but acceptableInput dont work... + var cur_value = parseFloat(amount_input.input_text) + var bottom = 0 + var top = tempRequest.maxAmount + if(cur_value < bottom || cur_value > top) + { + amount_input.input_text = tempRequest.maxAmount + } + } + } + + function create_temp_request() + { + if (rec_addr_input.isValid && rec_addr_input.input_text.length) { + var fpb = parseFloat(fee_suggest_combo.edit_value()) + tempRequest = bsApp.newTXSignRequest(from_wallet_combo.currentIndex + , [rec_addr_input.input_text], [], (fpb > 0) ? fpb : 1.0) + } + } + + function init() + { + bsApp.requestFeeSuggestions() + rec_addr_input.setActiveFocus() + + //we need set first time currentIndex to 0 + //only after we will have signal rowchanged + if (fee_suggest_combo.currentIndex >= 0) + fee_suggest_combo.currentIndex = 0 + + amount_input.input_text = "" + comment_input.input_text = "" + rec_addr_input.input_text = "" + } +} + diff --git a/GUI/QtQuick/qml/Send/WalletsComboBox.qml b/GUI/QtQuick/qml/Send/WalletsComboBox.qml new file mode 100644 index 000000000..85558710d --- /dev/null +++ b/GUI/QtQuick/qml/Send/WalletsComboBox.qml @@ -0,0 +1,34 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "../StyledControls" + +import wallet.balance 1.0 + +CustomComboBox { + + id: from_wallet_combo + + height: BSSizes.applyScale(70) + + model: walletBalances + currentIndex: walletBalances.selectedWallet + + //aliases + title_text: qsTr("From Wallet") + details_text: walletBalances.totalBalance + + textRole: "name" + valueRole: "name" +} diff --git a/GUI/QtQuick/qml/Send/qmldir b/GUI/QtQuick/qml/Send/qmldir new file mode 100644 index 000000000..e6bec8304 --- /dev/null +++ b/GUI/QtQuick/qml/Send/qmldir @@ -0,0 +1,10 @@ +module SendControls + +SendPopup 1.0 SendPopup.qml +SimpleDetails 1.0 SimpleDetails.qml +SignTransaction 1.0 SignTransaction.qml +AdvancedDetails 1.0 AdvancedDetails.qml +AmountInput 1.0 AmountInput.qml +FeeSuggestComboBox 1.0 FeeSuggestComboBox.qml +RecvAddrTextInput 1.0 RecvAddrTextInput.qml +WalletsComboBox 1.0 WalletsComboBox.qml diff --git a/GUI/QtQuick/qml/Settings/AddArmoryServer.qml b/GUI/QtQuick/qml/Settings/AddArmoryServer.qml new file mode 100644 index 000000000..36c5fbbf7 --- /dev/null +++ b/GUI/QtQuick/qml/Settings/AddArmoryServer.qml @@ -0,0 +1,204 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property var armoryServersModel: ({}) + signal sig_added() + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : title.height + text: qsTr("Add custom server") + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(12) + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.preferredHeight: BSSizes.applyScale(19) + + Label { + Layout.fillWidth: true + } + + Label { + id: radbut_text + + text: qsTr("Network type:") + + Layout.leftMargin: BSSizes.applyScale(25) + Layout.alignment: Qt.AlignVCenter + + width: BSSizes.applyScale(126) + height: BSSizes.applyScale(19) + + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + CustomRadioButton { + id: radbut_main + + Layout.alignment: Qt.AlignVCenter + + text: "MainNet" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: true + } + + CustomRadioButton { + id: radbut_test + + Layout.alignment: Qt.AlignVCenter + + text: "TestNet" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: false + } + + Label { + Layout.fillWidth: true + } + } + + CustomTextInput { + id: name_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("Name") + + onTabNavigated: ip_dns_text_input.setActiveFocus() + } + + CustomTextInput { + id: ip_dns_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("IP/DNS") + + onTabNavigated: port_text_input.setActiveFocus() + } + + CustomTextInput { + id: port_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("Port") + + input_validator: IntValidator {bottom: 80; top: 65535;} + + onTabNavigated: db_key_text_input.setActiveFocus() + } + + CustomTextInput { + id: db_key_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("DB Key (optional)") + + onTabNavigated: save_but.forceActiveFocus() + } + + CustomButton { + id: save_but + text: qsTr("Save") + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + height : BSSizes.applyScale(70) + width: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + enabled: (name_text_input.input_text !== "") + && (ip_dns_text_input.input_text !== "") + && (port_text_input.input_text !== "") + preferred: true + + function click_enter() { + if (!save_but.enabled) return + + var networkType = radbut_main.checked ? 0 : 1 + var name = name_text_input.input_text + var ip_dns = ip_dns_text_input.input_text + var port = parseInt(port_text_input.input_text) + var db_key = db_key_text_input.input_text + + bsApp.addArmoryServer(name, networkType, ip_dns, port, db_key) + + clear() + + sig_added() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + Keys.onEnterPressed: { + save_but.click_enter() + } + + Keys.onReturnPressed: { + save_but.click_enter() + } + + function init() + { + clear() + name_text_input.setActiveFocus() + } + + function clear() + { + ip_dns_text_input.input_text = "" + port_text_input.input_text = "" + db_key_text_input.input_text = "" + name_text_input.input_text = "" + } +} diff --git a/GUI/QtQuick/qml/Settings/DeleteArmoryServer.qml b/GUI/QtQuick/qml/Settings/DeleteArmoryServer.qml new file mode 100644 index 000000000..8f349784e --- /dev/null +++ b/GUI/QtQuick/qml/Settings/DeleteArmoryServer.qml @@ -0,0 +1,101 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +import terminal.models 1.0 + +ColumnLayout { + + id: layout + + property var armoryServersModel: ({}) + property int server_index + + signal sig_back() + signal sig_delete() + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : title.height + text: qsTr("Delete custom server") + } + + Image { + id: wallet_icon + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/delete_custom_server.svg" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + Label { + id: description + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(16) + Layout.preferredHeight : BSSizes.applyScale(16) + + text: qsTr("Are you sure you want to delete the \"%1\" server?").arg(server_index) + .arg(armoryServersModel.data(armoryServersModel.index(server_index, 0), ArmoryServersModel.NameRole)) + + color: BSStyle.titanWhiteColor + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(11) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + CustomButton { + id: back_but + text: qsTr("No, back") + width: BSSizes.applyScale(262) + + preferred: false + function click_enter() { + sig_back() + } + } + + CustomButton { + id: delete_but + text: qsTr("Yes, delete") + width: BSSizes.applyScale(262) + + preferred: true + function click_enter() { + bsApp.delArmoryServer(server_index) + sig_delete() + } + } + } + + function init() + { + delete_but.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/Settings/ModifyArmoryServer.qml b/GUI/QtQuick/qml/Settings/ModifyArmoryServer.qml new file mode 100644 index 000000000..1d0892bdd --- /dev/null +++ b/GUI/QtQuick/qml/Settings/ModifyArmoryServer.qml @@ -0,0 +1,208 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +import terminal.models 1.0 + +ColumnLayout { + + id: layout + + property var armoryServersModel: ({}) + property int server_index + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : title.height + text: qsTr("Modify custom server") + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(12) + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.preferredHeight: BSSizes.applyScale(19) + + Label { + Layout.fillWidth: true + } + + Label { + id: radbut_text + + text: qsTr("Network type:") + + Layout.leftMargin: BSSizes.applyScale(25) + Layout.alignment: Qt.AlignVCenter + + width: BSSizes.applyScale(126) + height: BSSizes.applyScale(19) + + color: "#E2E7FF" + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + CustomRadioButton { + id: radbut_main + + Layout.alignment: Qt.AlignVCenter + + text: "MainNet" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: true + + // netType==0 => MainNet, netType==1 => TestNet + onClicked : { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , 0, ArmoryServersModel.NetTypeRole) + } + } + + CustomRadioButton { + id: radbut_test + + Layout.alignment: Qt.AlignVCenter + + text: "TestNet" + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + checked: false + + // netType==0 => MainNet, netType==1 => TestNet + onClicked : { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , 1, ArmoryServersModel.NetTypeRole) + } + } + + Label { + Layout.fillWidth: true + } + } + + CustomTextInput { + id: name_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("Name") + + onTextEdited: { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , name_text_input.input_text, ArmoryServersModel.NameRole) + } + + onTabNavigated: ip_dns_text_input.setActiveFocus() + } + + CustomTextInput { + id: ip_dns_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("IP/DNS") + + onTextEdited: { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , ip_dns_text_input.input_text, ArmoryServersModel.AddressRole) + } + + onTabNavigated: port_text_input.setActiveFocus() + } + + CustomTextInput { + id: port_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("Port") + + input_validator: IntValidator {bottom: 80; top: 65535;} + + onTextEdited: { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , port_text_input.input_text, ArmoryServersModel.PortRole) + } + + onTabNavigated: db_key_text_input.setActiveFocus() + } + + CustomTextInput { + id: db_key_text_input + + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + title_text: qsTr("DB Key (optional)") + + onTextEdited: { + armoryServersModel.setData(armoryServersModel.index(server_index, 0) + , db_key_text_input.input_text, ArmoryServersModel.KeyRole) + } + + onTabNavigated: name_text_input.setActiveFocus() + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + function init() + { + // netType==0 => MainNet, netType==1 => TestNet + var netType = armoryServersModel.data(armoryServersModel.index(server_index, 0), ArmoryServersModel.NetTypeRole) + if (netType === 0) + { + radbut_main.checked = true + } + else if (netType === 1) + { + radbut_test.checked = true + } + name_text_input.input_text = armoryServersModel.data(armoryServersModel.index(server_index, 0) + , ArmoryServersModel.NameRole) + ip_dns_text_input.input_text = armoryServersModel.data(armoryServersModel.index(server_index, 0) + , ArmoryServersModel.AddressRole) + port_text_input.input_text = armoryServersModel.data(armoryServersModel.index(server_index, 0) + , ArmoryServersModel.PortRole) + db_key_text_input.input_text = armoryServersModel.data(armoryServersModel.index(server_index, 0) + , ArmoryServersModel.KeyRole) + name_text_input.setActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/Settings/SettingsAbout.qml b/GUI/QtQuick/qml/Settings/SettingsAbout.qml new file mode 100644 index 000000000..3b7b0c8c9 --- /dev/null +++ b/GUI/QtQuick/qml/Settings/SettingsAbout.qml @@ -0,0 +1,109 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + + text: qsTr("About") + } + + CustomListItem { + id: about_terminal + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + icon_source: "qrc:/images/about_terminal.svg" + title_text: qsTr("terminal.blocksettle.com") + + onClicked: { + Qt.openUrlExternally("https://terminal.blocksettle.com") + } + } + + CustomListItem { + id: about_hello + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + icon_source: "qrc:/images/about_hello.svg" + title_text: qsTr("hello@blocksettle.com") + + onClicked: { + Qt.openUrlExternally("mailto:hello@blocksettle.com") + } + } + + CustomListItem { + id: about_twitter + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + icon_source: "qrc:/images/about_twitter.svg" + title_text: qsTr("twitter.com/blocksettle") + + onClicked: { + Qt.openUrlExternally("https://twitter.com/blocksettle") + } + } + + CustomListItem { + id: about_telegram + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + icon_source: "qrc:/images/about_telegram.svg" + title_text: qsTr("t.me/blocksettle") + + onClicked: { + Qt.openUrlExternally("https://t.me/blocksettle") + } + } + + CustomListItem { + id: add_github + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + + //aliases + icon_source: "qrc:/images/about_github.svg" + title_text: qsTr("github.com/blocksettle/terminal") + + onClicked: { + Qt.openUrlExternally("https://github.com/blocksettle/terminal") + } + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomLabel { + id: version + + Layout.bottomMargin: BSSizes.applyScale(20) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: qsTr("version 1.000.244.999") + } +} \ No newline at end of file diff --git a/GUI/QtQuick/qml/Settings/SettingsGeneral.qml b/GUI/QtQuick/qml/Settings/SettingsGeneral.qml new file mode 100644 index 000000000..d03ef8a6e --- /dev/null +++ b/GUI/QtQuick/qml/Settings/SettingsGeneral.qml @@ -0,0 +1,98 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + + text: qsTr("General") + } + + CustomTextInput { + id: log_file + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(24) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Log file") + + onTextEdited : { + bsApp.settingLogFile = log_file.input_text + } + } + + CustomTextInput { + id: messages_log_file + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Messages log file") + + onTextEdited : { + bsApp.settingMsgLogFile = messages_log_file.input_text + } + } + + CustomCheckBox { + id: checkbox_advanced_tx + + Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft + Layout.topMargin: BSSizes.applyScale(24) + Layout.leftMargin: BSSizes.applyScale(24) + + text: qsTr("Advanced TX dialog by default") + + spacing: BSSizes.applyScale(6) + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + onClicked: { + bsApp.settingAdvancedTX = checkbox_advanced_tx.checked + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + function init() + { + messages_log_file.input_text = bsApp.settingMsgLogFile + checkbox_advanced_tx.checked = bsApp.settingAdvancedTX + log_file.input_text = bsApp.settingLogFile + + //log_file.setActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/Settings/SettingsMenu.qml b/GUI/QtQuick/qml/Settings/SettingsMenu.qml new file mode 100644 index 000000000..d270fe9ba --- /dev/null +++ b/GUI/QtQuick/qml/Settings/SettingsMenu.qml @@ -0,0 +1,91 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + signal sig_general() + signal sig_network() + signal sig_about() + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + + text: qsTr("Settings") + } + + CustomListItem { + id: general_item + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(24) + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.fillWidth: true + + //aliases + icon_source: "qrc:/images/general.png" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("General") + + onClicked: sig_general() + } + + CustomListItem { + id: network_item + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.fillWidth: true + + //aliases + icon_source: "qrc:/images/network.png" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("Network") + + onClicked: sig_network() + } + + CustomListItem { + id: about_item + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.fillWidth: true + + //aliases + icon_source: "qrc:/images/about.png" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("About") + + onClicked: sig_about() + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + function init() + { + } +} diff --git a/GUI/QtQuick/qml/Settings/SettingsNetwork.qml b/GUI/QtQuick/qml/Settings/SettingsNetwork.qml new file mode 100644 index 000000000..a565ae9fa --- /dev/null +++ b/GUI/QtQuick/qml/Settings/SettingsNetwork.qml @@ -0,0 +1,124 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property var armoryServersModel: ({}) + + signal sig_add_custom() + signal sig_delete_custom(int ind) + signal sig_modify_custom(int ind) + signal request_close() + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + CustomTitleLabel { + id: title + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + + text: qsTr("Network") + } + + ListView { + id: list + + Layout.fillWidth: true + Layout.minimumHeight: BSSizes.applyScale(50) * armoryServersModel.rowCount + BSSizes.applyScale(10) * (armoryServersModel.rowCount - 1) + Layout.leftMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(24) + + spacing: BSSizes.applyScale(10) + + clip: true + boundsBehavior: Flickable.StopAtBounds + + flickDeceleration: 750 + maximumFlickVelocity: 1000 + + ScrollBar.vertical: ScrollBar { + id: verticalScrollBar + policy: list.contentHeight > list.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded + } + + model: armoryServersModel + + ButtonGroup { id: radioGroup } + + delegate: CustomListRadioButton { + id: _delegate + + title_text: display + icon_add_source: isDefault ? "" : "qrc:/images/delete.png" + radio_checked: isCurrent + radio_group: radioGroup + icon_add_z: 1 + + onClicked_add: { + if (!isDefault) + { + sig_delete_custom (index) + } + } + + onClicked: { + if (isCurrent === false) + { + isCurrent = true + request_close() + } + else if (isDefault === false) + { + sig_modify_custom (index) + } + } + } + + Connections + { + target:armoryServersModel + function onRowCountChanged () + { + var new_height = Math.min(armoryServersModel.rowCount * 50 + (armoryServersModel.rowCount - 1) * 10, 425) + list.implicitHeight = new_height + } + } + } + + CustomListItem { + id: add_custom_server + + Layout.alignment: Qt.AlignCenter + Layout.topMargin: BSSizes.applyScale(10) + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.fillWidth: true + + //aliases + icon_source: "qrc:/images/plus.svg" + title_text: qsTr("Add custom server") + + onClicked: sig_add_custom() + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + function init() + { + } +} diff --git a/GUI/QtQuick/qml/Settings/SettingsPopup.qml b/GUI/QtQuick/qml/Settings/SettingsPopup.qml new file mode 100644 index 000000000..f5a32e772 --- /dev/null +++ b/GUI/QtQuick/qml/Settings/SettingsPopup.qml @@ -0,0 +1,128 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +CustomPopup { + id: root + + objectName: "settings_popup" + + _stack_view.initialItem: settings_menu + _arrow_but_visibility: !settings_menu.visible + + property var armoryServersModel: null + + function open_network_menu() { + _stack_view.push(settings_network) + root.updateServersModel() + settings_network.init() + } + + SettingsMenu { + id: settings_menu + visible: false + + onSig_general: { + _stack_view.push(settings_general) + settings_general.init() + } + + onSig_network: { + _stack_view.push(settings_network) + root.updateServersModel() + settings_network.init() + } + + onSig_about: { + _stack_view.push(settings_about) + settings_about.init() + } + } + + SettingsGeneral { + id: settings_general + visible: false + } + + SettingsNetwork { + id: settings_network + visible: false + + armoryServersModel: root.armoryServersModel + + onSig_add_custom: { + _stack_view.push(add_armory_server) + add_armory_server.init() + } + + onSig_modify_custom: (server_index) => { + _stack_view.push(modify_armory_server) + modify_armory_server.server_index = server_index + modify_armory_server.init() + } + + onSig_delete_custom: (server_index) => { + _stack_view.push(delete_armory_server) + delete_armory_server.server_index = server_index + delete_armory_server.init() + } + + onRequest_close: { + _stack_view.pop() + root.close() + } + } + + SettingsAbout { + id: settings_about + visible: false + } + + AddArmoryServer { + id: add_armory_server + visible: false + + armoryServersModel: root.armoryServersModel + + onSig_added: { + _stack_view.pop() + _stack_view.pop() + root.close() + } + } + + ModifyArmoryServer { + id: modify_armory_server + visible: false + + armoryServersModel: root.armoryServersModel + } + + DeleteArmoryServer { + id: delete_armory_server + visible: false + + armoryServersModel: root.armoryServersModel + + onSig_delete: { + _stack_view.pop() + } + + onSig_back: { + _stack_view.pop() + } + } + + function init() { + settings_menu.init() + } + + function updateServersModel() + { + root.armoryServersModel = bsApp.armoryServersModel + } +} diff --git a/GUI/QtQuick/qml/Settings/qmldir b/GUI/QtQuick/qml/Settings/qmldir new file mode 100644 index 000000000..1580ebf96 --- /dev/null +++ b/GUI/QtQuick/qml/Settings/qmldir @@ -0,0 +1,9 @@ +module Settings + +SettingsPopup 1.0 SettingsPopup.qml +SettingsMenu 1.0 SettingsMenu.qml +AddArmoryServer 1.0 AddArmoryServer.qml +ModifyArmoryServer 1.0 ModifyArmoryServer.qml +DeleteArmoryServer 1.0 DeleteArmoryServer.qml +SettingsGeneral 1.0 SettingsGeneral.qml +SettingsNetwork 1.0 SettingsNetwork.qml diff --git a/GUI/QtQuick/qml/StyledControls/CloseIconButton.qml b/GUI/QtQuick/qml/StyledControls/CloseIconButton.qml new file mode 100644 index 000000000..d1c757a37 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CloseIconButton.qml @@ -0,0 +1,22 @@ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +Image { + id: control + + signal close(); + + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + source: "qrc:/images/close_button.svg" + + MouseArea { + anchors.fill: parent + onClicked: { + control.close() + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CopyIconButton.qml b/GUI/QtQuick/qml/StyledControls/CopyIconButton.qml new file mode 100644 index 000000000..ed64c092b --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CopyIconButton.qml @@ -0,0 +1,42 @@ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +Image { + id: control + + signal copy(); + + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + anchors.verticalCenter: parent.verticalCenter + source: "qrc:/images/copy_icon.svg" + MouseArea { + anchors.fill: parent + ToolTip { + id: tool_tip + timeout: 1000 + text: qsTr("Copied") + font.pixelSize: BSSizes.applyScale(10) + font.family: "Roboto" + font.weight: Font.Normal + contentItem: Text { + text: tool_tip.text + font: tool_tip.font + color: BSStyle.textColor + } + background: Rectangle { + color: BSStyle.buttonsStandardColor + border.color: BSStyle.buttonsStandardColor + border.width: BSSizes.applyScale(1) + radius: BSSizes.applyScale(14) + } + } + onClicked: { + control.copy() + tool_tip.visible = true + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomButton.qml b/GUI/QtQuick/qml/StyledControls/CustomButton.qml index 80adca47a..fa5a33c85 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomButton.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomButton.qml @@ -1,7 +1,7 @@ /* *********************************************************************************** -* Copyright (C) 2018 - 2020, BlockSettle AB +* Copyright (C) 2018 - 2022, BlockSettle AB * Distributed under the GNU Affero General Public License (AGPL v3) * See LICENSE or http://www.gnu.org/licenses/agpl.html * @@ -13,82 +13,60 @@ import QtQuick.Controls 2.3 import "../BsStyles" Button { + id: control - property bool capitalize: true - property bool primary: false - text: parent.text - leftPadding: 15 - rightPadding: 15 - anchors.margins: 5 - - contentItem: Text { - text: control.text - opacity: enabled ? 1.0 : 0.3 - color: BSStyle.textColor - font.capitalization: capitalize ? Font.AllUppercase : Font.MixedCase - font.pixelSize: 11 - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter - elide: Text.ElideRight - } + + height: BSSizes.applyScale(50) + + property bool preferred: false + + //aliases + property alias back_radius: back.radius + + activeFocusOnTab: control.enabled + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Bold + palette.buttonText: enabled ? BSStyle.buttonsTextColor : BSStyle.buttonsDisabledTextColor + focusPolicy: Qt.TabFocus + + + icon.color: "transparent" + icon.width: BSSizes.applyScale(24) + icon.height: BSSizes.applyScale(24) background: Rectangle { - id: rect - implicitWidth: 110 - implicitHeight: 35 - opacity: primary ? 1 : (control.enabled ? 1 : 0.3) - border.color: BSStyle.buttonsBorderColor - color: primary ? BSStyle.buttonsPrimaryMainColor : (control.highlighted ? BSStyle.buttonsPrimaryMainColor : BSStyle.buttonsMainColor) - border.width: primary ? 0 : 1 + + id: back + + implicitWidth: control.width + implicitHeight: control.height + + color: preferred ? (!control.enabled ? BSStyle.buttonsDisabledColor : + (control.down ? BSStyle.buttonsPreferredPressedColor : + (control.hovered ? BSStyle.buttonsPreferredHoveredColor : BSStyle.buttonsPreferredColor))): + (!control.enabled ? BSStyle.buttonsDisabledColor : + (control.down ? BSStyle.buttonsStandardPressedColor : + (control.hovered ? BSStyle.buttonsStandardHoveredColor : BSStyle.buttonsStandardColor))) + + radius: BSSizes.applyScale(14) + + border.color: preferred ? BSStyle.buttonsPreferredBorderColor : BSStyle.buttonsStandardBorderColor + border.width: control.activeFocus? 1 : 0 + } - states: [ - State { - name: "" - PropertyChanges { - target: rect - opacity: primary ? 1 : (control.enabled ? 1 : 0.3) - color: primary ? BSStyle.buttonsPrimaryMainColor : (control.highlighted ? BSStyle.buttonsPrimaryMainColor : BSStyle.buttonsMainColor) - } - }, - State { - name: "pressed" - when: control.pressed - PropertyChanges { - target: rect - opacity: primary ? 0.7 : (control.enabled ? 1 : 0.3) - color: primary ? BSStyle.buttonsPrimaryMainColor : (control.highlighted ? BSStyle.buttonsPrimaryPressedColor : BSStyle.buttonsPressedColor) - } - }, - State { - name: "hovered" - when: control.hovered - PropertyChanges { - target: rect - opacity: primary ? 0.85 : (control.enabled ? 1 : 0.3) - color: primary ? BSStyle.buttonsPrimaryMainColor : (control.highlighted ? BSStyle.buttonsPrimaryHoveredColor : BSStyle.buttonsHoveredColor) - } - }, - State { - name: "disabled" - when: !control.enabled - PropertyChanges { - target: rect - opacity: primary ? 0.3 : (control.enabled ? 1 : 0.3) - color: primary ? BSStyle.buttonsPrimaryMainColor : "gray" - } - } - ] - - transitions: [ - Transition { - from: ""; to: "hovered" - ColorAnimation { duration: 100 } - }, - Transition { - from: "*"; to: "pressed" - ColorAnimation { duration: 10 } - } - ] + Keys.onEnterPressed: { + click_enter() + } + + Keys.onReturnPressed: { + click_enter() + } + + onClicked: { + click_enter() + } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomButtonBar.qml b/GUI/QtQuick/qml/StyledControls/CustomButtonBar.qml index 763b98c91..f51f6dc72 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomButtonBar.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomButtonBar.qml @@ -16,5 +16,5 @@ import "../BsStyles" Rectangle { width: parent.width color:"#55000000" - height: 45 + height: BSSizes.applyScale(45) } diff --git a/GUI/QtQuick/qml/StyledControls/CustomButtonLeftIcon.qml b/GUI/QtQuick/qml/StyledControls/CustomButtonLeftIcon.qml new file mode 100644 index 000000000..46f5c7303 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomButtonLeftIcon.qml @@ -0,0 +1,38 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +CustomSmallButton { + id: control + + property alias custom_icon: icon_item + + Image { + id: icon_item + width: BSSizes.applyScale(10) + height: BSSizes.applyScale(10) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(14) + anchors.verticalCenter: parent.verticalCenter + } + + contentItem: Text { + text: control.text + font: control.font + color: BSStyle.titleTextColor + verticalAlignment: Text.AlignVCenter + + leftPadding: icon_item.width + BSSizes.applyScale(10) + } +} \ No newline at end of file diff --git a/GUI/QtQuick/qml/StyledControls/CustomButtonRightIcon.qml b/GUI/QtQuick/qml/StyledControls/CustomButtonRightIcon.qml new file mode 100644 index 000000000..a091e94a8 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomButtonRightIcon.qml @@ -0,0 +1,25 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +CustomSmallButton { + property alias custom_icon: icon_item + + Image { + id: icon_item + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(14) + anchors.verticalCenter: parent.verticalCenter + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomCheckBox.qml b/GUI/QtQuick/qml/StyledControls/CustomCheckBox.qml index 49d89f54b..cc9d20c6b 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomCheckBox.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomCheckBox.qml @@ -12,34 +12,41 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import "../BsStyles" + CheckBox { id: control - text: parent.text + + checked: true + spacing: 0 indicator: Rectangle { - implicitWidth: 16 - implicitHeight: 16 + implicitWidth: BSSizes.applyScale(18) + implicitHeight: BSSizes.applyScale(18) + x: control.leftPadding y: parent.height / 2 - height / 2 - radius: 0 - border.color: control.checked ? BSStyle.buttonsBorderColor : BSStyle.buttonsUncheckedColor + radius: BSSizes.applyScale(6) + border.color: "#416485" color: "transparent" - Rectangle { - width: 8 - height: 8 - x: 4 - y: 4 - radius: 0 - color: control.checked ? BSStyle.buttonsPrimaryMainColor : BSStyle.buttonsUncheckedColor + Image { + id: check_icon + + anchors.centerIn: parent + + width: BSSizes.applyScale(10) + height: BSSizes.applyScale(7) + sourceSize.width: BSSizes.applyScale(10) + sourceSize.height: BSSizes.applyScale(7) + visible: control.checked + source: "qrc:/images/check.svg" } } contentItem: Text { text: control.text - font.pixelSize: 11 - opacity: enabled ? 1.0 : 0.3 - color: control.checked ? BSStyle.textColor : BSStyle.buttonsUncheckedColor + font: control.font + color: control.checked ? "#E2E7FF" : "#7A88B0" verticalAlignment: Text.AlignVCenter leftPadding: control.indicator.width + control.spacing } diff --git a/GUI/QtQuick/qml/StyledControls/CustomComboBox.qml b/GUI/QtQuick/qml/StyledControls/CustomComboBox.qml index 9f1faec97..50a29995b 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomComboBox.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomComboBox.qml @@ -8,102 +8,227 @@ ********************************************************************************** */ + import QtQuick 2.9 import QtQuick.Controls 2.3 + import "../BsStyles" ComboBox { id: control - spacing: 3 - rightPadding: 30 // workaround to decrease width of TextInput - property alias maximumLength: input.maximumLength - - contentItem: TextInput { - id: input - text: control.displayText - font: control.font - color: { control.enabled ? BSStyle.comboBoxItemTextHighlightedColor : BSStyle.disabledTextColor } - leftPadding: 7 - rightPadding: control.indicator.width + control.spacing - verticalAlignment: Text.AlignVCenter - clip: true - readOnly: !editable - validator: control.validator + + property alias title_text: title.text + property alias details_text: details.text + property int fontSize: BSSizes.applyScale(16) + property color fontColor: "#FFFFFF" + + property alias input_accept_input: input.acceptableInput + property alias input_text: input.text + property alias input_item: input + property alias suffix_text: input_suffix_text.text + + signal textEdited() + signal editingFinished() + + activeFocusOnTab: true + focusPolicy: Qt.TabFocus + + leftPadding: BSSizes.applyScale(16) + rightPadding: BSSizes.applyScale(36) + topPadding: BSSizes.applyScale(16) + bottomPadding: BSSizes.applyScale(16) + + contentItem: Rectangle { + + id: input_rect + + + color: "transparent" + + Label { + id: title + + anchors.top: parent.top + anchors.topMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + Label { + id: details + + anchors.bottom: parent.bottom + anchors.bottomMargin: BSSizes.applyScale(1) + anchors.right: parent.right + anchors.rightMargin: 0 + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + TextInput { + id: input + + focus: true + + anchors.bottom: parent.bottom + anchors.bottomMargin: 0 + anchors.left: parent.left + anchors.leftMargin: 0 + + width: details.text.length ? parent.width - details.width - BSSizes.applyScale(16) : parent.width + height: BSSizes.applyScale(19) + + font.pixelSize: control.fontSize + font.family: "Roboto" + font.weight: Font.Normal + + color: control.fontColor + + text: control.currentText + validator: control.validator + enabled: control.editable + + clip: true + + onTextEdited : { + control.textEdited() + } + + onEditingFinished : { + control.editingFinished() + } + + Connections { + target: control + onCurrentTextChanged: input.text = control.currentText + } + + Text { + id: input_suffix_text + anchors.bottom: parent.bottom + leftPadding: input.contentWidth + BSSizes.applyScale(4) + font.pixelSize: control.fontSize + font.family: "Roboto" + font.weight: Font.Normal + color: control.fontColor + } + } } indicator: Canvas { + id: canvas - x: control.width - width + + x: control.width - width - BSSizes.applyScale(17) y: control.topPadding + (control.availableHeight - height) / 2 - width: 30 - height: 8 + width: BSSizes.applyScale(9) + height: BSSizes.applyScale(6) + contextType: "2d" Connections { + target: control - onPressedChanged: canvas.requestPaint() + function onPressedChanged() { + canvas.requestPaint() + } } onPaint: { - context.reset(); - context.moveTo(0, 0); - context.lineTo(8,8); - context.lineTo(16, 0); - context.lineTo(15, 0); - context.lineTo(8,7); - context.lineTo(1, 0); - context.closePath(); - context.fillStyle = BSStyle.comboBoxItemTextHighlightedColor; - context.fill(); + + context.reset() + context.moveTo(0, 0) + context.lineTo(width, 0) + context.lineTo(width / 2, height) + context.closePath() + context.fillStyle = control.popup.visible ? BSStyle.comboBoxIndicatorColor + : BSStyle.comboBoxPopupedIndicatorColor + context.fill() } } background: Rectangle { - implicitWidth: 120 - color: { control.enabled ? BSStyle.comboBoxBgColor : BSStyle.disabledBgColor } - implicitHeight: 25 - border.color: { control.enabled ? BSStyle.inputsBorderColor : BSStyle.disabledColor } - border.width: control.visualFocus ? 2 : 1 - radius: 2 + + color: "#020817" + opacity: 1 + radius: BSSizes.applyScale(14) + + border.color: control.popup.visible ? BSStyle.comboBoxPopupedBorderColor : + (control.hovered ? BSStyle.comboBoxHoveredBorderColor : + (control.activeFocus ? BSStyle.comboBoxFocusedBorderColor : BSStyle.comboBoxBorderColor)) + border.width: 1 + + implicitWidth: control.width + implicitHeight: control.height } delegate: ItemDelegate { - width: control.width + id: menuItem + width: control.width - BSSizes.applyScale(12) + height: BSSizes.applyScale(27) + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + contentItem: Text { - text: modelData - color: menuItem.highlighted ? BSStyle.comboBoxItemTextColor : BSStyle.comboBoxItemTextHighlightedColor - font: control.font + + text: control.textRole + ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) + : modelData + color: menuItem.highlighted ? BSStyle.comboBoxItemTextHighlightedColor : ( menuItem.currented ? BSStyle.comboBoxItemTextCurrentColor : BSStyle.comboBoxItemTextColor) + font.pixelSize: control.fontSize + font.family: "Roboto" + font.weight: Font.Normal + elide: Text.ElideNone verticalAlignment: Text.AlignVCenter } + highlighted: control.highlightedIndex === index + property bool currented: control.currentIndex === index background: Rectangle { - color: menuItem.highlighted ? BSStyle.comboBoxItemBgHighlightedColor : BSStyle.comboBoxItemBgColor + color: menuItem.highlighted ? BSStyle.comboBoxItemHighlightedColor : "transparent" + opacity: menuItem.highlighted ? 0.2 : 1 + radius: BSSizes.applyScale(14) } } popup: Popup { - y: control.height - 1 + id: _popup + + y: control.height - BSSizes.applyScale(1) width: control.width - implicitHeight: contentItem.implicitHeight - padding: 1 + padding: BSSizes.applyScale(6) contentItem: ListView { + id: popup_item + clip: true implicitHeight: contentHeight model: control.popup.visible ? control.delegateModel : null + //model: control.delegateModel currentIndex: control.highlightedIndex ScrollIndicator.vertical: ScrollIndicator { } } background: Rectangle { - color: BSStyle.comboBoxItemBgColor - border.color: BSStyle.inputsBorderColor - radius: 0 + color: "#FFFFFF" + radius: BSSizes.applyScale(14) } } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomCompleterPopup.qml b/GUI/QtQuick/qml/StyledControls/CustomCompleterPopup.qml new file mode 100644 index 000000000..ba7979147 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomCompleterPopup.qml @@ -0,0 +1,110 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + + +Popup { + + id: _popup + + property var comp_vars + property bool not_valid_word: false + property int index + property alias current_index: comp_list.currentIndex + + signal compChoosed () + + padding: 6 + + contentItem: ListView { + id: comp_list + + clip: true + implicitHeight: contentHeight + model: comp_vars + + delegate: ItemDelegate { + + id: delega + + width: _popup.width - BSSizes.applyScale(12) + height: BSSizes.applyScale(27) + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + + contentItem: Text { + + text: comp_vars[index] + color: delega.highlighted ? BSStyle.comboBoxItemTextHighlightedColor : + BSStyle.comboBoxItemTextColor + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + + elide: Text.ElideNone + verticalAlignment: Text.AlignVCenter + } + + highlighted: comp_list.currentIndex === index && !_popup.not_valid_word + + background: Rectangle { + color: delega.highlighted ? BSStyle.comboBoxItemHighlightedColor : "transparent" + opacity: delega.highlighted ? 0.2 : 1 + radius: BSSizes.applyScale(14) + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + onPositionChanged: { + if (containsMouse) + comp_list.currentIndex = index + } + onClicked: { + comp_list.currentIndex = index + compChoosed() + } + } + } + } + + background: Rectangle { + color: "#FFFFFF" + radius: BSSizes.applyScale(14) + } + + function current_increment () + { + if (_popup.visible) + { + comp_list.currentIndex = comp_list.currentIndex + 1 + if (comp_list.currentIndex >= comp_list.count) + comp_list.currentIndex = 0 + } + } + + function current_decrement () + { + if (_popup.visible) + { + comp_list.currentIndex = comp_list.currentIndex - 1 + if (comp_list.currentIndex < 0) + comp_list.currentIndex = comp_list.count - 1 + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomContextMenu.qml b/GUI/QtQuick/qml/StyledControls/CustomContextMenu.qml index 0af639061..555f7ae96 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomContextMenu.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomContextMenu.qml @@ -10,52 +10,57 @@ */ import QtQuick 2.9 import QtQuick.Controls 2.3 + import "../BsStyles" -MouseArea { - hoverEnabled: true - acceptedButtons: Qt.RightButton - cursorShape: Qt.IBeamCursor - onClicked: { - if (mouse.button === Qt.RightButton) { - let selectStart = root.selectionStart - let selectEnd = root.selectionEnd - let curPos = root.cursorPosition - contextMenu.popup() - root.cursorPosition = curPos - root.select(selectStart,selectEnd) +Menu { + id: menu + + leftPadding: BSSizes.applyScale(6) + topPadding: BSSizes.applyScale(4) + bottomPadding: BSSizes.applyScale(4) + rightPadding: BSSizes.applyScale(6) + + delegate: MenuItem { + id: menuItem + visible: menuItem.enabled + width: BSSizes.applyScale(200) + height: menuItem.visible ? BSSizes.applyScale(40) : 0 + + contentItem: Text { + leftPadding: menuItem.indicator.width + rightPadding: menuItem.arrow.width + + text: menuItem.text + + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Normal + + color: BSStyle.wildBlueColor + + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight } - } - onPressAndHold: { - if (mouse.source === Qt.MouseEventNotSynthesized) { - let selectStart = root.selectionStart - let selectEnd = root.selectionEnd - let curPos = root.cursorPosition - contextMenu.popup() - root.cursorPosition = curPos - root.select(selectStart,selectEnd) + + background: Rectangle { + width: parent.width - menu.leftPadding - menu.rightPadding + height: parent.height + + radius: BSSizes.applyScale(14) + color: menuItem.highlighted ? BSStyle.menuItemHoveredColor : BSStyle.menuItemColor } } - - Menu { - id: contextMenu - MenuItem { - text: qsTr("Cut") - onTriggered: { - root.cut() - } - } - MenuItem { - text: qsTr("Copy") - onTriggered: { - root.copy() - } - } - MenuItem { - text: qsTr("Paste") - onTriggered: { - root.paste() - } - } + + + background: Rectangle { + implicitWidth: BSSizes.applyScale(200) + implicitHeight: BSSizes.applyScale(40) + color: BSStyle.popupBackgroundColor + opacity: 1 + radius: BSSizes.applyScale(14) + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomDialog.qml b/GUI/QtQuick/qml/StyledControls/CustomDialog.qml index 9995b302e..d938f82e6 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomDialog.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomDialog.qml @@ -273,8 +273,8 @@ CustomDialogWindow { anchors.centerIn: parent running: false - height: 50 - width: 50 + height: BSSizes.applyScale(50) + width: BSSizes.applyScale(50) } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomExportSuccessDialog.qml b/GUI/QtQuick/qml/StyledControls/CustomExportSuccessDialog.qml new file mode 100644 index 000000000..60444abd3 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomExportSuccessDialog.qml @@ -0,0 +1,131 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + +Window { + id: root + + property alias header: title.text + property alias export_type: export_type.text + property alias path_name: file_path.text + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + height: BSSizes.applyScale(400) + width: BSSizes.applyScale(580) + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width) / 2 + y: mainWindow.y + (mainWindow.height - height) / 2 + + Rectangle { + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + + anchors.fill: parent + + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + ColumnLayout { + id: layout + + anchors.fill: parent + + CustomTitleLabel { + id: title + Layout.topMargin: BSSizes.applyScale(36) + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Success") + } + + + Image { + id: wallet_icon + + Layout.topMargin: BSSizes.applyScale(5) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/success.png" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + Label { + + id: export_type + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(8) + Layout.preferredHeight: BSSizes.applyScale(16) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + text: qsTr("Export file:") + color: BSStyle.wildBlueColor + } + + Label { + + id: file_path + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(4) + Layout.preferredHeight: BSSizes.applyScale(16) + Layout.fillWidth: true + Layout.alignment: Qt.AlignHCenter | Qt.AlingTop + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + text: "/home" + color: BSStyle.titanWhiteColor + } + + CustomButton { + id: finish_but + + width: BSSizes.applyScale(532) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: qsTr("Finish") + + preferred: true + focus:true + + function click_enter() { + root.close() + } + } + + Keys.onEnterPressed: { + click_enter() + } + + Keys.onReturnPressed: { + click_enter() + } + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomFailDialog.qml b/GUI/QtQuick/qml/StyledControls/CustomFailDialog.qml new file mode 100644 index 000000000..a0a15df36 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomFailDialog.qml @@ -0,0 +1,118 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + +Window { + id: root + + property alias header: title.text + property alias fail: details.text + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + height: BSSizes.applyScale(375) + width: BSSizes.applyScale(380) + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width)/2 + y: mainWindow.y + (mainWindow.height - height)/2 + + Rectangle { + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + + anchors.fill: parent + + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + Image { + id: close_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(20) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(22) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: { + root.close() + } + } + } + + ColumnLayout { + id: layout + + anchors.fill: parent + + CustomTitleLabel { + Layout.topMargin: BSSizes.applyScale(36) + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Incorrect Password") + } + + + Image { + Layout.topMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/try_icon.png" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + + Label { + id: details + + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + text: qsTr("The password you entered is incorrect") + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + color: "#E2E7FF" + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: finish_but + text: qsTr("Try again") + + width: BSSizes.applyScale(186) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + preferred: true + focus:true + + function click_enter() { + root.close() + } + } + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomHeader.qml b/GUI/QtQuick/qml/StyledControls/CustomHeader.qml index ea72d1bbc..5279b1146 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomHeader.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomHeader.qml @@ -23,9 +23,10 @@ Button { contentItem: Text { text: parent.text + font.family: "Roboto" font.capitalization: Font.AllUppercase color: { parent.enabled ? textColor : BSStyle.disabledHeaderColor } - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) } Rectangle { diff --git a/GUI/QtQuick/qml/StyledControls/CustomHeaderPanel.qml b/GUI/QtQuick/qml/StyledControls/CustomHeaderPanel.qml index 8dab2e4bd..0c0d02927 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomHeaderPanel.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomHeaderPanel.qml @@ -17,7 +17,7 @@ Rectangle { property string text clip: true color: "transparent" - height: 40 + height: BSSizes.applyScale(40) Rectangle { // visible: qmlTitleVisible @@ -30,13 +30,14 @@ Rectangle { // visible: qmlTitleVisible // height: qmlTitleVisible ? 40 : 0 anchors.fill: parent - leftPadding: 10 - rightPadding: 10 + leftPadding: BSSizes.applyScale(10) + rightPadding: BSSizes.applyScale(10) text: parent.text font.capitalization: Font.AllUppercase color: BSStyle.textColor - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) + font.family: "Roboto" verticalAlignment: Text.AlignVCenter } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomHorizontalHeaderView.qml b/GUI/QtQuick/qml/StyledControls/CustomHorizontalHeaderView.qml new file mode 100644 index 000000000..afe8e4634 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomHorizontalHeaderView.qml @@ -0,0 +1,36 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +HorizontalHeaderView { + id: root + property int text_size + + delegate: Rectangle { + + implicitHeight: BSSizes.applyScale(34) + implicitWidth: BSSizes.applyScale(100) + color: BSStyle.tableCellBackgroundColor + + Text { + text: display + height: parent.height + verticalAlignment: Text.AlignVCenter + clip: true + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: root.text_size + font.letterSpacing: -0.2 + leftPadding: BSSizes.applyScale(10) + } + + Rectangle { + height: BSSizes.applyScale(1) + width: parent.width + color: BSStyle.tableSeparatorColor + + anchors.bottom: parent.bottom + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomLabel.qml b/GUI/QtQuick/qml/StyledControls/CustomLabel.qml index 088570855..07c26f3db 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomLabel.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomLabel.qml @@ -14,10 +14,11 @@ import "../BsStyles" Label { horizontalAlignment: Text.AlignHLeft - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) + font.family: "Roboto" color: { enabled ? BSStyle.labelsTextColor : BSStyle.disabledColor } wrapMode: Text.WordWrap - topPadding: 5 - bottomPadding: 5 + topPadding: BSSizes.applyScale(5) + bottomPadding: BSSizes.applyScale(5) } diff --git a/GUI/QtQuick/qml/StyledControls/CustomLabelCopyableValue.qml b/GUI/QtQuick/qml/StyledControls/CustomLabelCopyableValue.qml index 97a33f77c..1784bc0cc 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomLabelCopyableValue.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomLabelCopyableValue.qml @@ -21,10 +21,11 @@ Label { property alias mouseArea: mouseArea property string textForCopy - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) + font.family: "Roboto" color: "white" wrapMode: Text.WordWrap - padding: 5 + padding: BSSizes.applyScale(5) onLinkActivated: Qt.openUrlExternally(link) MouseArea { diff --git a/GUI/QtQuick/qml/StyledControls/CustomLabelValue.qml b/GUI/QtQuick/qml/StyledControls/CustomLabelValue.qml index 3b021ddaf..e7aa9634c 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomLabelValue.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomLabelValue.qml @@ -13,10 +13,11 @@ import QtQuick.Controls 2.3 import "../BsStyles" Label { - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) + font.family: "Roboto" color: "white" wrapMode: Text.WordWrap - topPadding: 5 - bottomPadding: 5 + topPadding: BSSizes.applyScale(5) + bottomPadding: BSSizes.applyScale(5) onLinkActivated: Qt.openUrlExternally(link) } diff --git a/GUI/QtQuick/qml/StyledControls/CustomListItem.qml b/GUI/QtQuick/qml/StyledControls/CustomListItem.qml new file mode 100644 index 000000000..88c365054 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomListItem.qml @@ -0,0 +1,101 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +Rectangle { + id: rect + + //aliases + property alias icon_source: icon.source + property alias icon_visible: icon.visible + //usually we dont need only if custom margin and size + property alias _icon: icon + property alias icon_add_source: icon_add.source + property alias icon_add_z: icon_add.z + property alias title_text: title.text + + signal clicked_add() + signal clicked() + + width: BSSizes.applyScale(532) + height: BSSizes.applyScale(50) + + color: "transparent" + opacity: 1 + radius: BSSizes.applyScale(14) + + border.color: mouseArea.containsMouse ? BSStyle.listItemHoveredBorderColor : BSStyle.listItemBorderColor + border.width: 1 + + Image { + id: icon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: rect.left + anchors.leftMargin: BSSizes.applyScale(16) + + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + sourceSize.width: BSSizes.applyScale(24) + sourceSize.height: BSSizes.applyScale(24) + } + + Label { + id: title + + anchors.verticalCenter: parent.verticalCenter + anchors.left: icon.right + anchors.leftMargin: BSSizes.applyScale(8) + + horizontalAlignment : Text.AlignLeft + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + Image { + id: icon_add + + visible: source.toString().length > 0 + + anchors.verticalCenter: parent.verticalCenter + anchors.right: rect.right + anchors.rightMargin: BSSizes.applyScale(13) + + z: 0 + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + sourceSize.width: BSSizes.applyScale(24) + sourceSize.height: BSSizes.applyScale(24) + + MouseArea { + anchors.fill: parent + onClicked: { + rect.clicked_add() + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + rect.clicked() + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomListRadioButton.qml b/GUI/QtQuick/qml/StyledControls/CustomListRadioButton.qml new file mode 100644 index 000000000..4a3a77055 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomListRadioButton.qml @@ -0,0 +1,44 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +CustomListItem { + id: root + + //properties + property alias radio_checked: rad_but.checked + property var radio_group + + signal sig_radio_clicked() + + icon_visible: false + + CustomRadioButton { + id: rad_but + + ButtonGroup.group: radio_group + + anchors.verticalCenter: root.verticalCenter + anchors.left: root.left + anchors.leftMargin: BSSizes.applyScale(21) + + leftPadding: 0 + spacing: 0 + + width: BSSizes.applyScale(15) + height: BSSizes.applyScale(15) + + onClicked : root.sig_radio_clicked() + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomMediumButton.qml b/GUI/QtQuick/qml/StyledControls/CustomMediumButton.qml new file mode 100644 index 000000000..985ca5041 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomMediumButton.qml @@ -0,0 +1,50 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +Button { + id: control + + property color background_color: BSStyle.buttonsDisabledColor + property int background_radius: BSSizes.applyScale(14) + + width: BSSizes.applyScale(136) + height: BSSizes.applyScale(36) + hoverEnabled: true + focusPolicy: Qt.TabFocus + + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Normal + font.letterSpacing: 0.3 + + background: Rectangle { + color: control.background_color + radius: control.background_radius + + + border.color: + (control.hovered ? BSStyle.comboBoxHoveredBorderColor : + (control.activeFocus ? BSStyle.comboBoxFocusedBorderColor : BSStyle.comboBoxBorderColor)) + border.width: 1 + } + + contentItem: Text { + text: control.text + font: control.font + color: BSStyle.textColor + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomMessageDialog.qml b/GUI/QtQuick/qml/StyledControls/CustomMessageDialog.qml new file mode 100644 index 000000000..d2cc679dc --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomMessageDialog.qml @@ -0,0 +1,116 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + +Window { + id: root + + property alias header: title.text + property alias error: details.text + property alias action: finish_but.text + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + height: BSSizes.applyScale(375) + width: BSSizes.applyScale(380) + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width)/2 + y: mainWindow.y + (mainWindow.height - height)/2 + + Rectangle { + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + + anchors.fill: parent + + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + Image { + id: close_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(20) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(22) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: { + root.close() + } + } + } + + ColumnLayout { + id: layout + + anchors.fill: parent + + CustomTitleLabel { + Layout.topMargin: BSSizes.applyScale(36) + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + } + + Image { + Layout.topMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/try_icon.png" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + + Label { + id: details + + Layout.topMargin: BSSizes.applyScale(16) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + color: "#E2E7FF" + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: finish_but + text: qsTr("OK") + + width: BSSizes.applyScale(186) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + preferred: true + focus:true + + function click_enter() { + root.close() + } + } + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomPasswordTextInput.qml b/GUI/QtQuick/qml/StyledControls/CustomPasswordTextInput.qml index ef0f05b1e..ecb394e14 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomPasswordTextInput.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomPasswordTextInput.qml @@ -17,15 +17,15 @@ TextField { property bool allowShowPass: true horizontalAlignment: Text.AlignHLeft - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) color: BSStyle.inputsFontColor padding: 0 echoMode: button.pressed ? TextInput.Normal : TextInput.Password selectByMouse: false background: Rectangle { - implicitWidth: 200 - implicitHeight: 25 + implicitWidth: BSSizes.applyScale(200) + implicitHeight: BSSizes.applyScale(25) color:"transparent" border.color: BSStyle.inputsBorderColor @@ -40,11 +40,11 @@ TextField { source: "qrc:/resources/eye.png" } } - padding: 2 - (button.pressed ? 1 : 0) + padding: (button.pressed ? 1 : 0) - BSSizes.applyScale(2) background: Rectangle {color: "transparent"} anchors.right: parent.right - width: 23 - height: 23 + width: BSSizes.applyScale(23) + height: BSSizes.applyScale(23) } } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomPopup.qml b/GUI/QtQuick/qml/StyledControls/CustomPopup.qml new file mode 100644 index 000000000..b08c3def4 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomPopup.qml @@ -0,0 +1,181 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + +Window { + id: root + + property int navig_bar_width: BSSizes.applyScale(36) + property alias _stack_view: stack_popup + property alias _arrow_but_visibility: back_arrow_button.visible + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + maximumHeight: rect.height + maximumWidth: rect.width + + minimumHeight: rect.height + minimumWidth: rect.width + + height: rect.height + width: rect.width + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width) / 2 + y: mainWindow.y + (mainWindow.height - height) / 2 + + signal sig_close_click() + signal sig_back_arrow_click() + + Rectangle { + id: rect + + color: BSStyle.popupBackgroundColor + opacity: 1 + radius: BSSizes.applyScale(16) + height: stack_popup.height + navig_bar_width + width: stack_popup.width + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + + Rectangle { + id: close_rect + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(1) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(1) + radius: BSSizes.applyScale(16) + + color: BSStyle.popupBackgroundColor + height: BSSizes.applyScale(39) + width: BSSizes.applyScale(110) + + Image { + id: close_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(23) + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(23) + + source: "qrc:/images/close_button.svg" + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + } + MouseArea { + anchors.fill: parent + onClicked: { + sig_close_click() + } + } + } + + Image { + id: back_arrow_button + + anchors.top: parent.top + anchors.topMargin: BSSizes.applyScale(24) + anchors.left: parent.left + anchors.leftMargin: BSSizes.applyScale(24) + + source: "qrc:/images/back_arrow.png" + width: BSSizes.applyScale(20) + height: BSSizes.applyScale(16) + MouseArea { + anchors.fill: parent + onClicked: { + sig_back_arrow_click() + } + } + } + + StackView { + id: stack_popup + + anchors.top: close_rect.bottom + anchors.topMargin: 0 + + implicitHeight: currentItem.height + implicitWidth: currentItem.width + + + pushEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 200 + } + } + + pushExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 200 + } + } + + popEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 200 + } + } + + popExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 200 + } + } + + replaceEnter: Transition { + PropertyAnimation { + property: "opacity" + from: 0 + to:1 + duration: 10 + } + } + + replaceExit: Transition { + PropertyAnimation { + property: "opacity" + from: 1 + to:0 + duration: 10 + } + } + } + + } + + onSig_close_click: { + close_click() + } + + onSig_back_arrow_click: { + stack_popup.pop() + } + + function close_click() + { + root.close() + stack_popup.pop(null) + } + +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomProgressBar.qml b/GUI/QtQuick/qml/StyledControls/CustomProgressBar.qml index 08387d148..f95efd5d6 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomProgressBar.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomProgressBar.qml @@ -14,26 +14,27 @@ import "../BsStyles" ProgressBar { id: control - value: 0.5 - topPadding: 1 - bottomPadding: 1 + padding: 0 background: Rectangle { - implicitWidth: 200 - implicitHeight: 6 - color: BSStyle.progressBarBgColor - radius: 3 + implicitWidth: BSSizes.applyScale(532) + implicitHeight: BSSizes.applyScale(8) + color: "transparent" + radius: BSSizes.applyScale(32) + + border.width: BSSizes.applyScale(1) + border.color: "#3C435A" } contentItem: Item { - implicitWidth: 200 - implicitHeight: 4 + implicitWidth: BSSizes.applyScale(532) + implicitHeight: BSSizes.applyScale(8) Rectangle { width: control.visualPosition * parent.width height: parent.height - radius: 2 - color: BSStyle.progressBarColor + radius: BSSizes.applyScale(32) + color: "#45A6FF" } } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomRadioButton.qml b/GUI/QtQuick/qml/StyledControls/CustomRadioButton.qml index 87f4abaa4..294f036d7 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomRadioButton.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomRadioButton.qml @@ -12,27 +12,29 @@ import QtQuick 2.9 import QtQuick.Controls 2.3 import "../BsStyles" + RadioButton { id: control - text: parent.text - focusPolicy: Qt.NoFocus + + checked: true + activeFocusOnTab: false indicator: Rectangle { - implicitWidth: 16 - implicitHeight: 16 + implicitWidth: BSSizes.applyScale(16) + implicitHeight: BSSizes.applyScale(16) x: control.leftPadding y: parent.height / 2 - height / 2 - radius: 11 - border.color: control.checked ? BSStyle.buttonsBorderColor : BSStyle.buttonsUncheckedColor + radius: BSSizes.applyScale(8) + border.color: "#45A6FF" color: "transparent" Rectangle { - width: 8 - height: 8 - x: 4 - y: 4 - radius: 7 - color: control.checked ? BSStyle.buttonsPrimaryMainColor : BSStyle.buttonsUncheckedColor + width: BSSizes.applyScale(8) + height: BSSizes.applyScale(8) + x: BSSizes.applyScale(4) + y: BSSizes.applyScale(4) + radius: BSSizes.applyScale(4) + color: "#45A6FF" visible: control.checked } } @@ -40,8 +42,7 @@ RadioButton { contentItem: Text { text: control.text font: control.font - opacity: enabled ? 1.0 : 0.3 - color: control.checked ? BSStyle.textColor : BSStyle.buttonsUncheckedColor + color: control.checked ? "#E2E7FF" : "#7A88B0" verticalAlignment: Text.AlignVCenter leftPadding: control.indicator.width + control.spacing } diff --git a/GUI/QtQuick/qml/StyledControls/CustomRbfCpfpMenu.qml b/GUI/QtQuick/qml/StyledControls/CustomRbfCpfpMenu.qml new file mode 100644 index 000000000..86d7b9324 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomRbfCpfpMenu.qml @@ -0,0 +1,64 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +import terminal.models 1.0 + +CustomContextMenu { + id: context_menu + + signal openSend (string txId, bool isRBF, bool isCPFP) + signal openExplorer(string txId) + + property int row + property int column + property var model + + Action { + id: rbf_action + text: qsTr("RBF") + onTriggered: { + var txId = model.data(model.index(context_menu.row, context_menu.column), TxListModel.TxIdRole) + context_menu.openSend(txId, true, false) + } + } + + Action { + id: cpfp_action + text: qsTr("CPFP") + onTriggered: { + var txId = model.data(model.index(context_menu.row, context_menu.column), TxListModel.TxIdRole) + context_menu.openSend(txId, false, true) + } + } + + Action { + text: qsTr("View in explorer") + onTriggered: { + var txId = model.data(model.index(context_menu.row, context_menu.column), TxListModel.TxIdRole) + context_menu.openExplorer(txId) + } + } + + onOpened: { + rbf_action.enabled = (model.data(model.index(context_menu.row, context_menu.column), TxListModel.RBFRole) + && model.data(model.index(context_menu.row, context_menu.column), TxListModel.NbConfRole) === 0 + && (model.data(model.index(context_menu.row, context_menu.column), TxListModel.DirectionRole) === 2 + || model.data(model.index(context_menu.row, context_menu.column), TxListModel.DirectionRole) === 3)) + cpfp_action.enabled = (model.data(model.index(context_menu.row, context_menu.column), TxListModel.NbConfRole) === 0 + && (model.data(model.index(context_menu.row, context_menu.column), TxListModel.DirectionRole) === 1 + || model.data(model.index(context_menu.row, context_menu.column), TxListModel.DirectionRole) === 3)) + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomSeedLabel.qml b/GUI/QtQuick/qml/StyledControls/CustomSeedLabel.qml new file mode 100644 index 000000000..b920ad481 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSeedLabel.qml @@ -0,0 +1,54 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +Rectangle { + id: rect + + property alias serial_num: serial_number.text + property alias seed_text: seed.text + + width: BSSizes.applyScale(170) + height: BSSizes.applyScale(46) + + color: "#020817" + opacity: 1 + radius: BSSizes.applyScale(14) + + Label { + id: serial_number + + anchors.top: rect.top + anchors.topMargin: BSSizes.applyScale(8) + anchors.left: rect.left + anchors.leftMargin: BSSizes.applyScale(10) + + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + Label { + id: seed + + anchors.centerIn : rect + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#E2E7FF" + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomSeedTextInput.qml b/GUI/QtQuick/qml/StyledControls/CustomSeedTextInput.qml new file mode 100644 index 000000000..0800a8b06 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSeedTextInput.qml @@ -0,0 +1,22 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +CustomTextInput { + height: BSSizes.applyScale(46) + horizontalAlignment : TextInput.AlignHCenter + input_topMargin: BSSizes.applyScale(13) + title_leftMargin: BSSizes.applyScale(10) + title_topMargin: BSSizes.applyScale(8) + title_font_size: BSSizes.applyScale(12) +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomSmallButton.qml b/GUI/QtQuick/qml/StyledControls/CustomSmallButton.qml new file mode 100644 index 000000000..3fca5b9f5 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSmallButton.qml @@ -0,0 +1,50 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +Button { + id: control + + width: BSSizes.applyScale(134) + height: BSSizes.applyScale(29) + + focusPolicy: Qt.TabFocus + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.letterSpacing: 0.3 + + hoverEnabled: true + + property alias backgroundColor: backgroundItem.color + + background: Rectangle { + id: backgroundItem + color: "#020817" + radius: BSSizes.applyScale(14) + + border.color: + (control.hovered ? BSStyle.comboBoxHoveredBorderColor : + (control.activeFocus ? BSStyle.comboBoxFocusedBorderColor : BSStyle.comboBoxBorderColor)) + border.width: 1 + } + + contentItem: Text { + text: control.text + font: control.font + anchors.fill: parent + color: BSStyle.titleTextColor + verticalAlignment: Text.AlignVCenter + horizontalAlignment: Text.AlignHCenter + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomSmallComboBox.qml b/GUI/QtQuick/qml/StyledControls/CustomSmallComboBox.qml new file mode 100644 index 000000000..cd459ce60 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSmallComboBox.qml @@ -0,0 +1,40 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 + +import "../BsStyles" +import "." + +CustomComboBox { + id: control + + font.family: "Roboto" + font.letterSpacing: 0.3 + fontSize: BSSizes.applyScale(12) + fontColor: BSStyle.titleTextColor + + leftPadding: BSSizes.applyScale(10) + rightPadding: 0 + topPadding: 0 + bottomPadding: BSSizes.applyScale(2) + + indicator: Image { + width: BSSizes.applyScale(6) + height: BSSizes.applyScale(3) + anchors.verticalCenter: parent.verticalCenter + source: "qrc:/images/combobox_open_button.svg" + + anchors.right: parent.right + anchors.rightMargin: BSSizes.applyScale(14) + } +} + diff --git a/GUI/QtQuick/qml/StyledControls/CustomSuccessDialog.qml b/GUI/QtQuick/qml/StyledControls/CustomSuccessDialog.qml new file mode 100644 index 000000000..82314960c --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSuccessDialog.qml @@ -0,0 +1,54 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" + +Window { + id: root + + property alias details_text: success.details_text + + signal sig_finish() + + visible: true + flags: Qt.WindowCloseButtonHint | Qt.FramelessWindowHint | Qt.Dialog + modality: Qt.WindowModal + + height: BSSizes.applyScale(430) + width: BSSizes.applyScale(580) + + color: "transparent" + + x: mainWindow.x + (mainWindow.width - width)/2 + y: mainWindow.y + (mainWindow.height - height)/2 + + Rectangle { + + id: rect + + color: "#191E2A" + opacity: 1 + radius: BSSizes.applyScale(16) + + anchors.fill: parent + + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + CustomSuccessWidget { + id: success + + anchors.topMargin: BSSizes.applyScale(24) + anchors.fill: parent + details_font_size: BSSizes.applyScale(16) + details_font_weight: Font.Medium + + onSig_finish: { + root.close() + root.sig_finish() + } + } + } +} \ No newline at end of file diff --git a/GUI/QtQuick/qml/StyledControls/CustomSuccessWidget.qml b/GUI/QtQuick/qml/StyledControls/CustomSuccessWidget.qml new file mode 100644 index 000000000..fd97c69c8 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomSuccessWidget.qml @@ -0,0 +1,95 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + property alias details_text: details.text + property alias details_font_size: details.font.pixelSize + property alias details_font_weight: details.font.weight + + signal sig_finish() + + height: BSSizes.applyScale(485) + width: BSSizes.applyScale(580) + spacing: 0 + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Success") + } + + + Image { + id: wallet_icon + + Layout.topMargin: BSSizes.applyScale(34) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/success.png" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + + Label { + id: details + + Layout.topMargin: BSSizes.applyScale(26) + Layout.alignment: Qt.AlignTop | Qt.AlignHCenter + text: qsTr("Your wallet has successfully been created") + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + color: "#E2E7FF" + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + Layout.leftMargin: BSSizes.applyScale(10) + Layout.rightMargin: BSSizes.applyScale(10) + } + + Label { + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: finish_but + + width: BSSizes.applyScale(530) + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + text: qsTr("Finish") + + preferred: true + focus:true + + function click_enter() { + sig_finish() + } + } + + Keys.onEnterPressed: { + click_enter() + } + + Keys.onReturnPressed: { + click_enter() + } + + function init() { + finish_but.setActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomSwitch.qml b/GUI/QtQuick/qml/StyledControls/CustomSwitch.qml index df19894c7..739d603cd 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomSwitch.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomSwitch.qml @@ -29,11 +29,11 @@ Switch { indicator: Rectangle { id: border_ - implicitWidth: 40 - implicitHeight: 20 + implicitWidth: BSSizes.applyScale(40) + implicitHeight: BSSizes.applyScale(20) x: control.width - width - control.rightPadding y: parent.height / 2 - height / 2 - radius: 10 + radius: BSSizes.applyScale(10) color: { if (control.enabled) { if (control.checked) { @@ -64,9 +64,9 @@ Switch { Rectangle { id: circle_ x: control.checked ? parent.width - width : 0 - width: 20 - height: 20 - radius: 10 + width: BSSizes.applyScale(20) + height: BSSizes.applyScale(20) + radius: BSSizes.applyScale(10) color: { if (control.enabled) { @@ -100,8 +100,8 @@ Switch { } background: Rectangle { - implicitWidth: 80 - implicitHeight: 20 + implicitWidth: BSSizes.applyScale(80) + implicitHeight: BSSizes.applyScale(20) visible: control.down color: BSStyle.switchBgColor } diff --git a/GUI/QtQuick/qml/StyledControls/CustomTabButton.qml b/GUI/QtQuick/qml/StyledControls/CustomTabButton.qml index 0abd48a18..c0316f756 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomTabButton.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomTabButton.qml @@ -8,39 +8,69 @@ ********************************************************************************** */ -import QtQuick 2.9 -import QtQuick.Controls 2.3 +import QtQuick 2.12 +import QtQuick.Controls 2.4 +import QtQuick.Layouts 1.3 import "../BsStyles" TabButton { id: control - text: parent.text - property alias cText: text_ + + width: BSSizes.applyScale(94) + height: parent.height + focusPolicy: Qt.NoFocus - contentItem: Text { - id: text_ - text: control.text - font.capitalization: Font.AllUppercase - font.pointSize: 10 - color: control.checked ? (control.down ? BSStyle.textPressedColor : BSStyle.textColor) : BSStyle.buttonsUncheckedColor - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter - verticalAlignment: Text.AlignVCenter + font.pixelSize: BSSizes.applyScale(10) + font.family: "Roboto" + font.weight: Font.Medium + font.letterSpacing: 0.2 + + property url selectedIcon_; + property url nonSelectedIcon_; + + contentItem: ColumnLayout { + width: control.width + height: control.height + spacing : BSSizes.applyScale(4) + + Image { + id: image_ + + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + Layout.alignment : Qt.AlignTop | Qt.AlignHCenter + + source: control.checked? selectedIcon_ :nonSelectedIcon_ + sourceSize: Qt.size(parent.width, parent.height) + + smooth: true + } + + Text { + id: text_ + + Layout.alignment : Qt.AlignBottom | Qt.AlignHCenter + + text: control.text + font: control.font + color: !control.enabled ? BSStyle.disabledTextColor : + (control.down ? BSStyle.textPressedColor : + (control.checked? BSStyle.selectedColor : BSStyle.titleTextColor)) + } } + background: Rectangle { - implicitWidth: 100 - implicitHeight: 50 - opacity: enabled ? 1 : 0.3 - color: control.checked ? (control.down ? BSStyle.backgroundPressedColor : BSStyle.backgroundColor) : "0f1f24" - - Rectangle { - width: parent.width - height: 2 - color: control.checked ? (control.down ? BSStyle.textPressedColor : BSStyle.buttonsPrimaryMainColor) : "transparent" - anchors.top: parent.top - } + implicitWidth: parent.width + implicitHeight: parent.height + color: "transparent" + } + + function setIcons(selectedIcon: url, nonselectedIcon: url) + { + selectedIcon_ = selectedIcon; + nonSelectedIcon_ = nonselectedIcon; } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomTableDelegateRow.qml b/GUI/QtQuick/qml/StyledControls/CustomTableDelegateRow.qml new file mode 100644 index 000000000..282e3d425 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTableDelegateRow.qml @@ -0,0 +1,59 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +Component +{ + id: cmpnt + + Row { + id: root + + signal deleteRequested (int row) + signal copyRequested (string tableData) + + Text { + id: internal_text + + visible: model_column !== delete_button_column_index + + text: model_tableData + height: parent.height + verticalAlignment: Text.AlignVCenter + clip: true + + color: get_data_color(model_row, model_column) + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: text_size + font.letterSpacing: -0.2 + + leftPadding: get_text_left_padding(model_row, model_column, model_is_expandable) + } + + DeleteIconButton { + id: delete_icon + x: 0 + visible: model_column === delete_button_column_index + onDeleteRequested: root.deleteRequested(model_row) + } + + CopyIconButton { + id: copy_icon + x: internal_text.contentWidth + copy_icon.width / 2 + visible: model_column === copy_button_column_index && model_row === selected_row_index + onCopy: root.copyRequested(model_tableData) + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTableView.qml b/GUI/QtQuick/qml/StyledControls/CustomTableView.qml new file mode 100644 index 000000000..dae89875b --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTableView.qml @@ -0,0 +1,206 @@ + +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +Column { + id: root + spacing: BSSizes.applyScale(2) + + property alias model: component.model + property alias delegate: component.delegate + property alias selected_row_index: component.selected_row_index + property bool is_expandable: false + property var columnWidths + property int copy_button_column_index: 0 + property int delete_button_column_index: -1 + property int left_text_padding: BSSizes.applyScale(10) + property bool has_header: true + property int text_header_size: BSSizes.applyScale(12) + property int cell_text_size: BSSizes.applyScale(13) + + signal copyRequested(var id) + signal deleteRequested(int id) + signal cellClicked(var row, var column, var data, var mouse) + signal cellDoubleClicked(var row, var column, var data, var mouse) + + function update() { + component.forceLayout() + verticalScrollBar.position = 0 + } + + CustomHorizontalHeaderView { + id: tableHeader + width: parent.width + syncView: component + height: BSSizes.applyScale(32) + visible: root.has_header + text_size: root.text_header_size + } + + TableView { + id: component + width: parent.width + height: parent.height - tableHeader.height - root.spacing + + reuseItems: false + + columnSpacing: 0 + rowSpacing: 0 + clip: true + boundsBehavior: Flickable.StopAtBounds + + flickDeceleration: 750 + maximumFlickVelocity: 1000 + + ScrollBar.vertical: ScrollBar { + id: verticalScrollBar + policy: component.contentHeight > component.height ? ScrollBar.AlwaysOn : ScrollBar.AsNeeded + } + + columnWidthProvider: function (column) { + return columnWidths[column] * component.width + } + + property int selected_row_index: -1 + + onWidthChanged: component.forceLayout() + + delegate: Rectangle { + id: delega + + implicitHeight: BSSizes.applyScale(34) + implicitWidth: BSSizes.applyScale(100) + color: row === component.selected_row_index ? BSStyle.tableCellSelectedBackgroundColor : BSStyle.tableCellBackgroundColor + + MouseArea { + anchors.fill: parent + preventStealing: true + propagateComposedEvents: true + hoverEnabled: true + acceptedButtons: Qt.LeftButton | Qt.RightButton + + onEntered: component.selected_row_index = row + onExited: component.selected_row_index = -1 + onClicked: (mouse) => { + root.cellClicked(row, column, tableData, mouse) + } + onDoubleClicked: (mouse) => { + root.cellDoubleClicked(row, column, tableData, mouse) + } + } + + Loader { + id: row_loader + + width: parent.width + height: childrenRect.height + + property int delete_button_column_index: root.delete_button_column_index + property int copy_button_column_index: root.copy_button_column_index + property int selected_row_index: component.selected_row_index + property int model_row: row + property int model_column: column + property int text_size: root.cell_text_size + + property string model_tableData: (typeof tableData !== "undefined") ? tableData : ({}) + property bool model_selected: (typeof selected !== "undefined") ? selected : ({}) + property bool model_expanded: (typeof expanded !== "undefined") ? expanded : ({}) + property bool model_is_expandable: (typeof is_expandable !== "undefined") ? is_expandable : ({}) + property bool model_is_editable: (typeof is_editable !== "undefined") ? is_editable : ({}) + + function get_text_left_padding(row, column, isExpandable) + { + if (typeof component.get_text_left_padding === "function") + return component.get_text_left_padding(row, column, isExpandable) + else + return left_text_padding + } + + function get_data_color(row, column) + { + if (typeof component.get_data_color === "function") + { + var res = component.get_data_color(row, column) + if (res!== null) + return res + } + + return (typeof dataColor !== "undefined") ? dataColor : ({}) + } + + Component.onCompleted: { + delega.update_row_loader_size() + } + } + + Connections { + target: row_loader.item + ignoreUnknownSignals: true + function onDeleteRequested (row) + { + root.deleteRequested(row) + } + function onCopyRequested (tableData) + { + root.copyRequested(tableData) + } + } + + onWidthChanged: { + delega.update_row_loader_size() + } + + onHeightChanged: { + delega.update_row_loader_size() + } + + function update_row_loader_size() + { + row_loader.width = delega.width + row_loader.height = childrenRect.height + if (row_loader.width && row_loader.height && !row_loader.sourceComponent) + { + row_loader.sourceComponent = choose_row_source_component(row, column) + } + } + + Rectangle { + + anchors.left: parent.left + anchors.leftMargin: get_line_left_padding(row, column, is_expandable) + + height: 1 + width: parent.width + color: BSStyle.tableSeparatorColor + + anchors.bottom: parent.bottom + } + } + } + + CustomTableDelegateRow { + id: cmpnt_table_delegate + } + + function choose_row_source_component(row, column) + { + return cmpnt_table_delegate + } + + function get_line_left_padding(row, column, isExpandable) + { + return 0 + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTextArea.qml b/GUI/QtQuick/qml/StyledControls/CustomTextArea.qml index 0654d4621..1ed296272 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomTextArea.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomTextArea.qml @@ -15,13 +15,14 @@ import "../BsStyles" TextArea { id: root horizontalAlignment: Text.AlignHLeft - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) + font.family: "Roboto" color: "white" wrapMode: TextEdit.WordWrap background: Rectangle { - implicitWidth: 200 - implicitHeight: 50 + implicitWidth: BSSizes.applyScale(200) + implicitHeight: BSSizes.applyScale(50) color:"transparent" border.color: BSStyle.inputsBorderColor } diff --git a/GUI/QtQuick/qml/StyledControls/CustomTextEdit.qml b/GUI/QtQuick/qml/StyledControls/CustomTextEdit.qml new file mode 100644 index 000000000..674031334 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTextEdit.qml @@ -0,0 +1,118 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +Rectangle { + id: rect + + //aliases - title + property alias title_text: title.text + property alias title_leftMargin: title.anchors.leftMargin + property alias title_topMargin: title.anchors.topMargin + property alias title_font_size: title.font.pixelSize + + //aliases - input + property alias input_text: input.text + property alias horizontalAlignment: input.horizontalAlignment + property alias input_topMargin: input.anchors.topMargin + property alias inputHints: input.inputMethodHints + + signal textChanged() + signal tabNavigated() + signal backTabNavigated() + signal enterKeyPressed() + + color: "#020817" + opacity: 1 + radius: BSSizes.applyScale(14) + + border.color: input.activeFocus ? "#45A6FF" : BSStyle.defaultBorderColor + border.width: 1 + + Label { + id: title + + anchors.top: rect.top + anchors.topMargin: BSSizes.applyScale(16) + anchors.left: rect.left + anchors.leftMargin: BSSizes.applyScale(16) + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + TextEdit { + id: input + + focus: true + activeFocusOnTab: true + + clip: true + + anchors.top: rect.top + anchors.topMargin: BSSizes.applyScale(35) + anchors.left: rect.left + anchors.leftMargin: title.anchors.leftMargin + width: rect.width - 2 * title.anchors.leftMargin + height: BSSizes.applyScale(39) + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + + wrapMode : TextEdit.Wrap + + color: "#E2E7FF" + + onTextChanged : { + rect.textChanged() + + var pos = input.positionAt(1, input.height + 1); + if(input.length >= pos) + { + input.remove(pos, input.length); + } + } + + Keys.onTabPressed: { + tabNavigated() + } + + Keys.onBacktabPressed: { + backTabNavigated() + } + + Keys.onEnterPressed: { + enterKeyPressed() + } + Keys.onReturnPressed: { + enterKeyPressed() + } + } + + MouseArea { + anchors.fill: parent + propagateComposedEvents: true + onClicked: { + input.forceActiveFocus() + mouse.accepted = false + } + } + + function setActiveFocus() { + input.forceActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTextInput.qml b/GUI/QtQuick/qml/StyledControls/CustomTextInput.qml index 644c77d7c..d15a87268 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomTextInput.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomTextInput.qml @@ -10,25 +10,162 @@ */ import QtQuick 2.9 import QtQuick.Controls 2.3 - import "../BsStyles" -TextField { - id: root - horizontalAlignment: Text.AlignHLeft - font.pixelSize: 11 - color: BSStyle.inputsFontColor - padding: 0 - selectByMouse: true - - background: Rectangle { - implicitWidth: 200 - implicitHeight: 25 - color: "transparent" - border.color: BSStyle.inputsBorderColor +Rectangle { + id: rect + + //aliases - title + property alias title_text: title.text + property string placeholder_text + property bool hide_placeholder_when_activefocus: true + property int title_leftMargin: BSSizes.applyScale(16) + property int title_topMargin: BSSizes.applyScale(16) + property int title_font_size: BSSizes.applyScale(13) + + //aliases - input + property alias input_text: input.text + property alias horizontalAlignment: input.horizontalAlignment + property int input_topMargin: BSSizes.applyScale(35) + property alias input_validator: input.validator + + property bool isValid: true + property bool isPassword: false + property bool isHiddenText: false + + property int input_right_margin: BSSizes.applyScale(16) + + property var completer: null + + signal textEdited() + signal textChanged() + signal editingFinished() + signal activeFocusChanged() + signal tabNavigated() + signal backTabNavigated() + signal enterPressed() + signal returnPressed() + + + color: "#020817" + opacity: 1 + radius: BSSizes.applyScale(14) + + border.color: isValid ? (input.activeFocus ? "#45A6FF" : BSStyle.defaultBorderColor) : "#EB6060" + border.width: 1 + + Label { + id: title + + anchors.top: rect.top + anchors.topMargin: rect.title_topMargin + anchors.left: rect.left + anchors.leftMargin: rect.title_leftMargin + + font.pixelSize: rect.title_font_size + font.family: "Roboto" + font.weight: Font.Normal + + color: "#7A88B0" + } + + TextInput { + id: input + + focus: true + activeFocusOnTab: true + + clip: true + + anchors.top: rect.top + anchors.topMargin: rect.input_topMargin + anchors.left: rect.left + anchors.leftMargin: rect.title_leftMargin + width: rect.width - rect.title_leftMargin - rect.input_right_margin + height: BSSizes.applyScale(19) + + echoMode: isHiddenText? TextInput.Password : TextInput.Normal + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + + color: "#E2E7FF" + + Text { + text: rect.placeholder_text + color: "#7A88B0" + visible: !input.text && (!hide_placeholder_when_activefocus || !input.activeFocus) + + font.pixelSize: BSSizes.applyScale(16) + font.family: "Roboto" + font.weight: Font.Normal + } + + Keys.onEnterPressed: { + enterPressed() + } + + Keys.onReturnPressed: { + returnPressed() + } + + onTextEdited : { + rect.textEdited() + } + + onTextChanged : { + rect.textChanged() + } + + onEditingFinished : { + rect.editingFinished() + } + + onActiveFocusChanged: { + rect.activeFocusChanged() + } + + Keys.onTabPressed: { + tabNavigated() + } + + Keys.onBacktabPressed: { + backTabNavigated() + } + } + + Image { + id: eye_icon + + visible: isPassword + + anchors.top: rect.top + anchors.topMargin: BSSizes.applyScale(23) + anchors.right: rect.right + anchors.rightMargin: BSSizes.applyScale(23) + + source: isHiddenText? "qrc:/images/Eye_icon _unvisible.png" : "qrc:/images/Eye_icon _visible.png" + z: 1 + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + + MouseArea { + anchors.fill: parent + onClicked: { + isHiddenText = !isHiddenText + } + } } - CustomContextMenu { + MouseArea { anchors.fill: parent + onClicked: { + input.forceActiveFocus() + } + } + + function setActiveFocus() { + input.forceActiveFocus() } } diff --git a/GUI/QtQuick/qml/StyledControls/CustomTextSwitch.qml b/GUI/QtQuick/qml/StyledControls/CustomTextSwitch.qml new file mode 100644 index 000000000..70a16365b --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTextSwitch.qml @@ -0,0 +1,111 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +Rectangle { + id: root + + property bool isFullChoosed: true + signal sig_full_changed (bool isFull) + + width: BSSizes.applyScale(530) + height: BSSizes.applyScale(40) + + color: "transparent" + radius: BSSizes.applyScale(37) + border.color : BSStyle.defaultBorderColor + border.width : BSSizes.applyScale(1) + + Rectangle { + id: left_rect + + width: BSSizes.applyScale(260) + height: BSSizes.applyScale(34) + + anchors.top: root.top + anchors.topMargin: BSSizes.applyScale(3) + anchors.left: root.left + anchors.leftMargin: BSSizes.applyScale(3) + + color: isFullChoosed? "#32394F": "transparent" + radius: BSSizes.applyScale(37) + + Label { + id: left_label + + width: BSSizes.applyScale(260) + height: BSSizes.applyScale(15) + + anchors.centerIn : left_rect + horizontalAlignment : Text.AlignHCenter + + text: "Full" + + color: isFullChoosed? "#E2E7FF": "#7A88B0" + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Medium + } + + MouseArea { + anchors.fill: parent + onClicked: { + isFullChoosed = true + sig_full_changed(isFullChoosed) + } + } + } + + Rectangle { + id: right_rect + + width: BSSizes.applyScale(260) + height: BSSizes.applyScale(34) + + anchors.top: root.top + anchors.topMargin: BSSizes.applyScale(3) + anchors.right: root.right + anchors.rightMargin: BSSizes.applyScale(3) + + color: !isFullChoosed? "#32394F": "transparent" + radius: BSSizes.applyScale(37) + + Label { + id: right_label + + width: BSSizes.applyScale(260) + height: BSSizes.applyScale(15) + + anchors.centerIn : right_rect + horizontalAlignment : Text.AlignHCenter + + text: "Import watching-only wallet" + + color: !isFullChoosed? "#E2E7FF": "#7A88B0" + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Medium + } + + MouseArea { + anchors.fill: parent + onClicked: { + isFullChoosed = false + sig_full_changed(isFullChoosed) + } + } + } + +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTitleDialogWindowWithExpander.qml b/GUI/QtQuick/qml/StyledControls/CustomTitleDialogWindowWithExpander.qml index dc09e8ce5..99ac15865 100644 --- a/GUI/QtQuick/qml/StyledControls/CustomTitleDialogWindowWithExpander.qml +++ b/GUI/QtQuick/qml/StyledControls/CustomTitleDialogWindowWithExpander.qml @@ -32,7 +32,7 @@ BSWalletHandlerDialog { property string text : root.title clip: true color: "transparent" - height: 40 + height: BSSizes.applyScale(40) Layout.fillWidth: true @@ -43,19 +43,19 @@ BSWalletHandlerDialog { Text { anchors.fill: rect - leftPadding: 10 - rightPadding: 10 + leftPadding: BSSizes.applyScale(10) + rightPadding: BSSizes.applyScale(10) text: rect.text font.capitalization: Font.AllUppercase color: BSStyle.textColor - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) verticalAlignment: Text.AlignVCenter } Button { id: btnExpand - width: 100 + width: BSSizes.applyScale(100) anchors.right: rect.right anchors.top: rect.top anchors.bottom: rect.bottom @@ -63,7 +63,7 @@ BSWalletHandlerDialog { contentItem: Text { text: headerButtonText color: BSStyle.textColor - font.pixelSize: 11 + font.pixelSize: BSSizes.applyScale(11) font.underline: true horizontalAlignment: Text.AlignRight verticalAlignment: Text.AlignVCenter @@ -71,8 +71,8 @@ BSWalletHandlerDialog { } background: Rectangle { - implicitWidth: 70 - implicitHeight: 35 + implicitWidth: BSSizes.applyScale(70) + implicitHeight: BSSizes.applyScale(35) color: "transparent" } diff --git a/GUI/QtQuick/qml/StyledControls/CustomTitleLabel.qml b/GUI/QtQuick/qml/StyledControls/CustomTitleLabel.qml new file mode 100644 index 000000000..2113d3e49 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTitleLabel.qml @@ -0,0 +1,22 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import "../BsStyles" + +Label { + id: title + height : BSSizes.applyScale(23) + color: BSStyle.titanWhiteColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Medium +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTitleToolButton.qml b/GUI/QtQuick/qml/StyledControls/CustomTitleToolButton.qml new file mode 100644 index 000000000..e8187ac84 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTitleToolButton.qml @@ -0,0 +1,60 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2022, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.9 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 +import "../BsStyles" + + +ToolButton { + id: control + + property bool _isSelected: false + + Layout.fillHeight: true + implicitWidth: BSSizes.applyScale(110) + + hoverEnabled: true + + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: !enabled ? BSStyle.buttonsDisabledTextColor : + (control.down ? BSStyle.textPressedColor : + (_isSelected ? BSStyle.selectedColor : BSStyle.titleTextColor)) + + icon.color: palette.buttonText + icon.width: BSSizes.applyScale(16) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + anchors.fill: parent + id: btn_background + clip: true + color: !control.enabled ? BSStyle.buttonsDisabledColor : + (control.down ? BSStyle.buttonsPressedColor : + (control.hovered ? BSStyle.buttonsHoveredColor : BSStyle.buttonsMainColor)) + Rectangle { + color: 'transparent' + anchors.fill: parent + anchors.rightMargin: -border.width + anchors.topMargin: -border.width + anchors.bottomMargin: -border.width + border.width: BSSizes.applyScale(1) + border.color: BSStyle.defaultBorderColor + } + } + + function select(isSelected: bool) + { + _isSelected = isSelected; + } +} diff --git a/GUI/QtQuick/qml/StyledControls/CustomTransactionsTableView.qml b/GUI/QtQuick/qml/StyledControls/CustomTransactionsTableView.qml new file mode 100644 index 000000000..59cd82ca7 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/CustomTransactionsTableView.qml @@ -0,0 +1,66 @@ + +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" +import ".." + +import terminal.models 1.0 + +CustomTableView { + id: control + + signal openSend (string txId, bool isRBF, bool isCPFP) + signal openExplorer (string txId) + + copy_button_column_index: 3 + columnWidths: [0.12, 0.1, 0.08, 0.3, 0.1, 0.1, 0.1, 0.1] + onCopyRequested: bsApp.copyAddressToClipboard(id) + + CustomRbfCpfpMenu { + id: context_menu + + model: control.model + + onOpenSend: (txId, isRBF, isCPFP) => control.openSend(txId, isRBF, isCPFP) + onOpenExplorer: (txId) => control.openExplorer(txId) + } + + onCellClicked: (row, column, data, mouse) => { + if (mouse.button === Qt.RightButton) + { + context_menu.row = row + context_menu.column = column + context_menu.popup() + } + else + { + const txHash = model.data(model.index(row, 0), TxListModel.TxIdRole) + transactionDetails.walletName = model.data(model.index(row, 1), TxListModel.TableDataRole) + transactionDetails.address = model.data(model.index(row, 3), TxListModel.TableDataRole) + transactionDetails.txDateTime = model.data(model.index(row, 0), TxListModel.TableDataRole) + transactionDetails.txType = model.data(model.index(row, 2), TxListModel.TableDataRole) + transactionDetails.txTypeColor = model.data(model.index(row, 2), TxListModel.ColorRole) + transactionDetails.txComment = model.data(model.index(row, 7), TxListModel.TableDataRole) + transactionDetails.txAmount = model.data(model.index(row, 4), TxListModel.TableDataRole) + transactionDetails.txConfirmationsColor = model.data(model.index(row, 5), TxListModel.ColorRole) + transactionDetails.tx = bsApp.getTXDetails(txHash) + transactionDetails.open() + } + } + + TransactionDetails { + id: transactionDetails + visible: false + } +} diff --git a/GUI/QtQuick/qml/StyledControls/DeleteIconButton.qml b/GUI/QtQuick/qml/StyledControls/DeleteIconButton.qml new file mode 100644 index 000000000..9cfda18fc --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/DeleteIconButton.qml @@ -0,0 +1,26 @@ + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import "../BsStyles" + +Image { + id: control + + signal deleteRequested() + + width: BSSizes.applyScale(24) + height: BSSizes.applyScale(24) + sourceSize.width: BSSizes.applyScale(24) + sourceSize.height: BSSizes.applyScale(24) + + anchors.verticalCenter: parent.verticalCenter + source: "qrc:/images/delete.png" + MouseArea { + anchors.fill: parent + + onClicked: { + control.deleteRequested() + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/InputOutputTableView.qml b/GUI/QtQuick/qml/StyledControls/InputOutputTableView.qml new file mode 100644 index 000000000..52c804d68 --- /dev/null +++ b/GUI/QtQuick/qml/StyledControls/InputOutputTableView.qml @@ -0,0 +1,128 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Controls 2.3 + +import "../BsStyles" + +CustomTableView { + id: component + + FontMetrics { + id: fontMetrics + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + } + + delegate: Rectangle { + implicitHeight: BSSizes.applyScale(58) + color: (row === component.selected_row_index ? BSStyle.tableCellSelectedBackgroundColor : BSStyle.tableCellBackgroundColor) + + MouseArea { + anchors.fill: parent + preventStealing: true + propagateComposedEvents: true + hoverEnabled: true + + onEntered: component.selected_row_index = row + onExited: component.selected_row_index = -1 + onClicked: component.cellClicked(row, column, tableData, mouse) + onDoubleClicked: component.cellDoubleClicked(row, column, tableData, mouse) + } + + Item { + width: parent.width + height: parent.height + + Text { + id: internal_text + visible: column !== 1 + text: tableData + height: parent.height + verticalAlignment: Text.AlignTop + clip: true + + color: dataColor + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: BSSizes.applyScale(13) + + leftPadding: BSSizes.applyScale(10) + topPadding: BSSizes.applyScale(9) + } + + Column { + spacing: BSSizes.applyScale(8) + visible: column === 1 + width: parent.width + anchors.centerIn: parent + + Row { + width: parent.width + + Text { + id: address_label_item + text: qsTr("Ad.:") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: BSSizes.applyScale(13) + font.letterSpacing: -0.2 + leftPadding: BSSizes.applyScale(10) + } + Text { + text: tableData + width: parent.width - fontMetrics.advanceWidth(address_label_item.text) - BSSizes.applyScale(10) + color: BSStyle.textColor + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: BSSizes.applyScale(13) + font.letterSpacing: -0.2 + clip: true + } + } + Row { + width: parent.width + + Text { + id: transaction_label_item + text: qsTr("Tx.:") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: BSSizes.applyScale(13) + font.letterSpacing: -0.2 + leftPadding: BSSizes.applyScale(10) + } + Text { + text: txHash + width: parent.width - fontMetrics.advanceWidth(transaction_label_item.text) - BSSizes.applyScale(10) + color: BSStyle.textColor + font.family: "Roboto" + font.weight: Font.Normal + font.pixelSize: BSSizes.applyScale(13) + font.letterSpacing: -0.2 + clip: true + } + } + } + } + + Rectangle { + height: 1 + width: parent.width + color: BSStyle.tableSeparatorColor + + anchors.bottom: parent.bottom + } + } +} diff --git a/GUI/QtQuick/qml/StyledControls/qmldir b/GUI/QtQuick/qml/StyledControls/qmldir index c5f3cae76..5a51888a7 100644 --- a/GUI/QtQuick/qml/StyledControls/qmldir +++ b/GUI/QtQuick/qml/StyledControls/qmldir @@ -20,4 +20,5 @@ CustomTextArea 1.0 CustomTextArea.qml CustomTextInput 1.0 CustomTextInput.qml CustomPasswordTextInput 1.0 CustomPasswordTextInput.qml CustomTitleDialogWindow 1.0 CustomTitleDialogWindow.qml +CustomTitleToolButton 1.0 CustomTitleToolButton.qml CustomTitleDialogWindowWithExpander 1.0 CustomTitleDialogWindowWithExpander.qml diff --git a/GUI/QtQuick/qml/TransactionDetails.qml b/GUI/QtQuick/qml/TransactionDetails.qml new file mode 100644 index 000000000..248d6da44 --- /dev/null +++ b/GUI/QtQuick/qml/TransactionDetails.qml @@ -0,0 +1,321 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.15 +import QtQuick.Window 2.2 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.3 +import terminal.models 1.0 + +import "StyledControls" +import "BsStyles" + +Popup { + id: transaction_details + + property var tx: null + property string walletName: '' + property string address: '' + property string txAmount: '' + property string txDateTime: '' + property string txType: '' + property color txTypeColor + property string txComment: '' + property color txConfirmationsColor + + width: BSSizes.applyWindowWidthScale(916) + height: BSSizes.applyWindowHeightScale(718) + anchors.centerIn: Overlay.overlay + + modal: true + focus: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: Rectangle { + color: BSStyle.popupBackgroundColor + border.width: BSSizes.applyScale(1) + border.color: BSStyle.popupBorderColor + radius: BSSizes.applyScale(14) + } + + contentItem: Rectangle { + color: "transparent" + + Column { + anchors.fill: parent + anchors.topMargin: BSSizes.applyScale(12) + anchors.leftMargin: BSSizes.applyScale(12) + anchors.rightMargin: BSSizes.applyScale(12) + anchors.bottomMargin: BSSizes.applyScale(12) + spacing: BSSizes.applyScale(14) + + Label { + text: qsTr("Transaction details") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + } + + Grid { + columns: 2 + rowSpacing: BSSizes.applyScale(8) + + Text { + text: qsTr("Hash (RPC byte order)") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Row { + Text { + text: tx !== null ? tx.txId : "" + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + anchors.verticalCenter: parent.verticalCenter + } + CopyIconButton { + anchors.verticalCenter: parent.verticalCenter + onCopy: bsApp.copyAddressToClipboard(tx.txId) + } + } + + Text { + text: qsTr("Time") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Text { + text: transaction_details.txDateTime + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Height") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Text { + text: tx !== null ? tx.height : "" + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Amount") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Text { + text: transaction_details.txAmount + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Type") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Text { + text: transaction_details.txType !== "" ? transaction_details.txType : "..." + color: transaction_details.txTypeColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Virtual size (Bytes)") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: tx !== null ? tx.virtSize : "" + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("sat / virtual byte") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: tx !== null ? tx.feePerByte : "" + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Fee") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: tx !== null ? tx.fee : "" + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Confirmations") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: tx !== null ? tx.nbConf : "" + color: txConfirmationsColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Wallet") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: transaction_details.walletName !== "" ? transaction_details.walletName : "..." + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + + Text { + text: qsTr("Address") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Row { + Text { + text: transaction_details.address + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + anchors.verticalCenter: parent.verticalCenter + } + CopyIconButton { + anchors.verticalCenter: parent.verticalCenter + onCopy: bsApp.copyAddressToClipboard(transaction_details.address) + } + } + + Text { + text: qsTr("Comment") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: BSSizes.applyScale(170) + } + Label { + text: transaction_details.txComment === '' ? '-' : transaction_details.txComment + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + } + } + + Label { + text: qsTr("Input addresses") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(16) + font.weight: Font.DemiBold + font.family: "Roboto" + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(110) + color: "transparent" + radius: BSSizes.applyScale(14) + border.color: BSStyle.popupBorderColor + border.width: BSSizes.applyScale(1) + + CustomTableView { + width: parent.width - BSSizes.applyScale(20) + height: parent.height + anchors.centerIn: parent + model: tx.inputs + + copy_button_column_index: 1 + columnWidths: [0.1, 0.5, 0.2, 0.2] + onCopyRequested: bsApp.copyAddressToClipboard(id) + } + } + + Text { + text: qsTr("Output addresses") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(16) + font.weight: Font.DemiBold + font.family: "Roboto" + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(110) + color: "transparent" + radius: BSSizes.applyScale(14) + border.color: BSStyle.popupBorderColor + border.width: BSSizes.applyScale(1) + + CustomTableView { + width: parent.width - BSSizes.applyScale(20) + height: parent.height + anchors.centerIn: parent + model: tx.outputs + + copy_button_column_index: 1 + columnWidths: [0.1, 0.5, 0.2, 0.2] + onCopyRequested: bsApp.copyAddressToClipboard(id) + } + } + } + } + + CloseIconButton { + anchors.topMargin: BSSizes.applyScale(5) + anchors.rightMargin: BSSizes.applyScale(5) + anchors.right: parent.right + anchors.top: parent.top + + onClose: transaction_details.close() + } +} diff --git a/GUI/QtQuick/qml/TransactionsPage.qml b/GUI/QtQuick/qml/TransactionsPage.qml new file mode 100644 index 000000000..07c328110 --- /dev/null +++ b/GUI/QtQuick/qml/TransactionsPage.qml @@ -0,0 +1,216 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.12 +import QtQml.Models 2 +import QtQuick.Dialogs 1.3 + +import "StyledControls" +import "BsStyles" + +import terminal.models 1.0 + +Rectangle { + id: transactions + color: BSStyle.backgroundColor + + width: BSSizes.applyScale(1200) + height: BSSizes.applyScale(788) + + signal openSend (string txId, bool isRBF, bool isCPFP) + signal openExplorer (string txId) + + FileDialog { + id: fileDialogCSV + visible: false + title: qsTr("Choose CSV file name") + folder: shortcuts.documents + defaultSuffix: "csv" + selectExisting: false + onAccepted: { + var csvFile = fileUrl.toString() + if (txListModel.exportCSVto(csvFile)) { + ibInfo.displayMessage(qsTr("TX list CSV saved to %1").arg(csvFile)) + } + else { + ibFailure.displayMessage(qsTr("Failed to save CSV to %1").arg(csvFile)) + } + } + } + + CustomFailDialog { + id: fail_dialog + visible: false; + } + + CustomExportSuccessDialog{ + id: succes_dialog + header: "Success" + visible: false + } + + Column { + spacing: 18 + anchors.fill: parent + anchors.topMargin: BSSizes.applyScale(14) + anchors.leftMargin: BSSizes.applyScale(18) + anchors.rightMargin: BSSizes.applyScale(18) + + Row { + id: transaction_header_menu + width: parent.width + height: BSSizes.applyScale(45) + spacing: BSSizes.applyScale(15) + + Label { + text: qsTr("Transactions list") + color: BSStyle.textColor + font.pixelSize: BSSizes.applyScale(20) + font.family: "Roboto" + font.weight: Font.Bold + font.letterSpacing: 0.35 + } + + Row + { + spacing: BSSizes.applyScale(8) + height: parent.height + anchors.right: parent.right + + CustomSmallComboBox { + id: txWalletsComboBox + model: bsApp.txWalletsList + + width: BSSizes.applyScale(124) + height: BSSizes.applyScale(29) + + anchors.verticalCenter: parent.verticalCenter + + onActivated: (index) => { + transactionFilterModel.walletName = index == 0 ? "" : txWalletsComboBox.currentValue + tableView.update() + } + + Connections { + target: transactionFilterModel + onChanged: { + if (transactionFilterModel.walletName != txWalletsComboBox.currentValue) { + for (var i = 0; i < bsApp.txWalletsList.length; ++i) { + if (bsApp.txWalletsList[i] == transactionFilterModel.walletName) { + txWalletsComboBox.currentIndex = i + } + } + } + } + } + } + + CustomSmallComboBox { + id: txTypesComboBox + model: bsApp.txTypesList + + width: BSSizes.applyScale(124) + height: BSSizes.applyScale(29) + + anchors.verticalCenter: parent.verticalCenter + + onActivated: (index) => { + transactionFilterModel.transactionType = index == 0 ? "" : txTypesComboBox.currentValue + tableView.update() + } + + Connections { + target: transactionFilterModel + onChanged: { + if (transactionFilterModel.transactionType != txTypesComboBox.currentValue) { + for (var i = 0; i < bsApp.txTypesList.length; ++i) { + if (bsApp.txTypesList[i] == transactionFilterModel.transactionType) { + txTypesComboBox.currentIndex = i + } + } + } + } + } + } + + Row { + spacing: BSSizes.applyScale(4) + anchors.verticalCenter: parent.verticalCenter + + CustomButtonLeftIcon { + text: qsTr("From") + + custom_icon.source: "qrc:/images/calendar_icon.svg" + + } + + Rectangle { + height: BSSizes.applyScale(1) + width: BSSizes.applyScale(8) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + CustomButtonLeftIcon { + text: qsTr("To") + + custom_icon.source: "qrc:/images/calendar_icon.svg" + } + } + + CustomButtonRightIcon { + text: qsTr("CSV download") + + custom_icon.source: "qrc:/images/download_icon.svg" + custom_icon.width: BSSizes.applyScale(10) + custom_icon.height: BSSizes.applyScale(10) + + onClicked: { + var csvFile = "%1/BlockSettle_%2_%3_%4.csv" + .arg(fileDialogCSV.folder) + .arg(txWalletsComboBox.currentIndex === 0 ? "all" : txWalletsComboBox.currentText) + .arg(txListModel.getBegDate()) + .arg(txListModel.getEndDate()) + + if (txListModel.exportCSVto(csvFile)) { + succes_dialog.path_name = csvFile.replace(/^(file:\/{3})/,"") + show_popup(succes_dialog) + } + else { + fail_dialog.header = "Export CSV Error" + fail_dialog.fail = "Failed to export CSV File" + show_popup(fail_dialog) + } + } + anchors.verticalCenter: parent.verticalCenter + } + } + } + + CustomTransactionsTableView { + id: tableView + width: parent.width + height: parent.height - transaction_header_menu.height - transaction_header_menu.spacing - 1 + model: transactionFilterModel + + onOpenSend: (txId, isRBF, isCPFP) => control.openSend(txId, isRBF, isCPFP) + onOpenExplorer: (txId) => transactions.openExplorer(txId) + } + + function show_popup (id) + { + id.show() + id.raise() + id.requestActivate() + } + } +} diff --git a/GUI/QtQuick/qml/VerifyTX.qml b/GUI/QtQuick/qml/VerifyTX.qml new file mode 100644 index 000000000..aa0320d6b --- /dev/null +++ b/GUI/QtQuick/qml/VerifyTX.qml @@ -0,0 +1,126 @@ +/* + +*********************************************************************************** +* Copyright (C) 2018 - 2020, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +import QtQuick 2 +import QtQuick.Controls 2.9 +import QtQuick.Layouts 1.3 +import QtQml.Models 2 + +import "StyledControls" +import "BsStyles" +//import "BsControls" +//import "BsDialogs" +//import "js/helper.js" as JsHelper + +Item { + property var txSignRequest + + Column { + spacing: BSSizes.applyScale(23) + anchors.fill: parent + + Button { + icon.source: "qrc:/images/send_icon.png" + onClicked: { + stack.pop() + } + } + GridLayout { + columns: 2 + Label { + text: qsTr("Output address:") + font.pointSize: BSSizes.applyScale(BSSizes.applyScale(12)) + } + Label { + text: qsTr("%1").arg(txSignRequest.outputAddresses[0]) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Output amount:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.outputAmount) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Input amount:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.inputAmount) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Return amount:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.returnAmount) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Transaction fee:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.fee) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Transaction size:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.txSize) + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("Fee per byte:") + font.pointSize: BSSizes.applyScale(12) + } + Label { + text: qsTr("%1").arg(txSignRequest.feePerByte) + font.pointSize: BSSizes.applyScale(12) + } + } + TextInput { + id: password + width: BSSizes.applyScale(500) + height: BSSizes.applyScale(32) + color: 'lightgrey' + font.pointSize: BSSizes.applyScale(14) + horizontalAlignment: TextEdit.AlignHCenter + verticalAlignment: TextEdit.AlignVCenter + echoMode: TextInput.Password + passwordCharacter: '*' + Text { + text: qsTr("Password") + font.pointSize: BSSizes.applyScale(6) + color: 'darkgrey' + anchors.left: parent + anchors.top: parent + } + } + Button { + width: BSSizes.applyScale(900) + text: qsTr("Broadcast") + font.pointSize: BSSizes.applyScale(14) + enabled: txSignRequest.isValid && password.text.length + + onClicked: { + bsApp.signAndBroadcast(txSignRequest, password.text) + stack.pop() + stack.pop() + password.text = "" + } + } + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/ChangePassword.qml b/GUI/QtQuick/qml/WalletProperties/ChangePassword.qml new file mode 100644 index 000000000..ca3dbe6cb --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/ChangePassword.qml @@ -0,0 +1,266 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + signal sig_success() + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + property var wallet_properties_vm + + Connections + { + target:bsApp + function onSuccessChangePassword () + { + layout.sig_success() + } + } + + CustomMessageDialog { + id: error_dialog + error: qsTr("Password strength is insufficient,\nplease use at least 6 characters") + visible: false + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Change password") + } + + CustomTextInput { + id: password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + activeFocusOnTab: true + + title_text: qsTr("Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + click_enter() + } + + onReturnPressed: { + click_enter() + } + + onTabNavigated: { + new_password.setActiveFocus() + } + onBackTabNavigated: { + if (confirm_but.enabled){ + confirm_but.setActiveFocus() + } + else { + confirm_password.setActiveFocus() + } + } + + function click_enter() { + if (confirm_but.enabled) { + confirm_but.click_enter() + } + else { + new_password.setActiveFocus() + } + } + } + + CustomTextInput { + id: new_password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + activeFocusOnTab: true + + title_text: qsTr("New Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + click_enter() + } + + onReturnPressed: { + click_enter() + } + + onTabNavigated: { + if (checkPasswordLength()) { + confirm_password.setActiveFocus() + } + else { + new_password.setActiveFocus() + } + } + onBackTabNavigated: { + if (checkPasswordLength()) { + password.setActiveFocus() + } + else { + new_password.setActiveFocus() + } + } + + function click_enter() { + if (confirm_but.enabled) { + confirm_but.click_enter() + } + else { + if (checkPasswordLength()) { + confirm_password.setActiveFocus() + } + else { + new_password.setActiveFocus() + } + } + } + } + + CustomTextInput { + id: confirm_password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Confirm Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + click_enter() + } + + onReturnPressed: { + click_enter() + } + + onTabNavigated: { + if (confirm_but.enabled) { + confirm_but.setActiveFocus() + } + else { + password.setActiveFocus() + } + } + onBackTabNavigated: new_password.setActiveFocus() + + function click_enter() { + if (confirm_but.enabled) { + confirm_but.click_enter() + } + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + text: qsTr("Save") + preferred: true + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + enabled: (password.input_text !== "") + && (new_password.input_text !== "") + && (confirm_password.input_text !== "") + && (new_password.input_text === confirm_password.input_text) + + function click_enter() { + if (!confirm_but.enabled) { + return + } + + const result = bsApp.changePassword( + wallet_properties_vm.walletId, + password.input_text, + new_password.input_text, + confirm_password.input_text + ) + if (result === 0) { + init() + } + } + } + + // Keys.onEnterPressed: { + // confirm_but.click_enter() + // } + + // Keys.onReturnPressed: { + // confirm_but.click_enter() + // } + + function init() + { + clear() + password.setActiveFocus() + } + + function clear() + { + password.isValid = true + new_password.isValid = true + confirm_password.isValid = true + password.input_text = "" + new_password.input_text = "" + confirm_password.input_text = "" + } + + function checkPasswordLength() + { + if (!visible) { + return false + } + + if (!bsApp.verifyPasswordIntegrity(new_password.input_text)) { + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + return false + } + return true + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/DeleteWallet.qml b/GUI/QtQuick/qml/WalletProperties/DeleteWallet.qml new file mode 100644 index 000000000..354432396 --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/DeleteWallet.qml @@ -0,0 +1,150 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + signal back() + signal walletDeleted() + signal sig_success() + + property var wallet_properties_vm + property bool is_password_requried: !(wallet_properties_vm.isHardware || wallet_properties_vm.isWatchingOnly) + + Connections + { + target:bsApp + function onSuccessDeleteWallet () + { + if (!layout.visible) { + return + } + + layout.sig_success() + } + function onFailedDeleteWallet() + { + if (!layout.visible) { + return + } + + showError(qsTr("Failed to delete")) + } + } + + CustomMessageDialog { + id: error_dialog + error: qsTr("Password is incorrect") + visible: false + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Delete wallet") + } + + CustomTextInput { + id: password + visible: is_password_requried + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + delete_btn.click_enter() + } + onReturnPressed: { + delete_btn.click_enter() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + CustomButton { + text: qsTr("Back") + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(260) + + onClicked: back() + } + + CustomButton { + id: delete_btn + text: qsTr("Delete") + preferred: true + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + enabled: (password.input_text !== "" || !is_password_requried) + + width: BSSizes.applyScale(260) + + function click_enter() { + const result = bsApp.deleteWallet( + wallet_properties_vm.walletId, + password.input_text + ) + + if (result === -1) { + showError(qsTr("Failed to delete")) + + init() + } + } + } + } + + function init() + { + clear() + password.setActiveFocus() + } + + function clear() + { + password.isValid = true + password.input_text = "" + } + + function showError(msg) + { + error_dialog.error = msg + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/DeleteWalletWarn.qml b/GUI/QtQuick/qml/WalletProperties/DeleteWalletWarn.qml new file mode 100644 index 000000000..1a4364dd9 --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/DeleteWalletWarn.qml @@ -0,0 +1,147 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + signal viewWalletSeed() + signal exportWOWallet() + signal deleteSWWallet() + signal sig_success() + + property var wallet_properties_vm + + Connections + { + target:bsApp + function onSuccessDeleteWallet () + { + if (!layout.visible) { + return + } + + layout.sig_success() + } + function onFailedDeleteWallet() + { + if (!layout.visible) { + return + } + + showError(qsTr("Failed to delete")) + } + } + + CustomMessageDialog { + id: error_dialog + error: qsTr("Failed to delete") + visible: false + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Delete wallet") + } + + Image { + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(120) + Layout.preferredWidth : BSSizes.applyScale(120) + + source: "qrc:/images/wallet_icon_warn.svg" + width: BSSizes.applyScale(120) + height: BSSizes.applyScale(120) + } + + CustomTitleLabel { + font.pixelSize: BSSizes.applyScale(14) + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: isHW() ? qsTr("Are you sure you want to delete this wallet?") : qsTr("Save the seed before deleting the wallet") + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + CustomButton { + text: qsTr("View wallet seed") + visible: !isHW() + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom + + width: BSSizes.applyScale(160) + + onClicked: viewWalletSeed() + } + + CustomButton { + text: qsTr("Export watching-only wallet") + visible: !isHW() + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom + + width: BSSizes.applyScale(180) + + onClicked: exportWOWallet() + } + + CustomButton { + text: isHW() ? qsTr("Delete") : qsTr("Continue") + preferred: true + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom + + width: BSSizes.applyScale(isHW() ? 260 : 160) + + onClicked: { + if (isHW()) { + const result = bsApp.deleteWallet( + wallet_properties_vm.walletId, + "" + ) + + if (result === -1) { + showError(qsTr("Failed to delete")) + } + } + else{ + deleteSWWallet() + } + } + } + } + + function isHW(){ + return wallet_properties_vm.isHardware || wallet_properties_vm.isWatchingOnly + } + + function showError(msg) + { + error_dialog.error = msg + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/ExportWOWallet.qml b/GUI/QtQuick/qml/WalletProperties/ExportWOWallet.qml new file mode 100644 index 000000000..24fb54d7c --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/ExportWOWallet.qml @@ -0,0 +1,216 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import QtQuick.Dialogs 1.0 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + signal sig_success(string nameExport, string pathExport) + + property var wallet_properties_vm + property bool isExitWhenSuccess + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + focus: true + + spacing: 0 + + + Connections + { + target:bsApp + function onSuccessExport (nameExport) + { + layout.sig_success(nameExport, bsApp.settingExportDir) + } + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Export watching-only wallet") + } + + Rectangle { + width: BSSizes.applyScale(530) + height: BSSizes.applyScale(82) + radius: BSSizes.applyScale(14) + color: BSStyle.exportWalletLabelBackground + + Layout.topMargin: BSSizes.applyScale(24) + Layout.alignment: Qt.AlignCenter + + Grid { + columns: 2 + rowSpacing: BSSizes.applyScale(14) + width: parent.width + anchors.centerIn: parent + + Text { + text: qsTr("Wallet name") + color: BSStyle.exportWalletLabelNameColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + leftPadding: BSSizes.applyScale(20) + } + Text { + text: wallet_properties_vm.walletName + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + rightPadding: BSSizes.applyScale(20) + } + + Text { + text: qsTr("Wallet ID") + color: BSStyle.exportWalletLabelNameColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + leftPadding: BSSizes.applyScale(20) + } + Text { + text: wallet_properties_vm.walletId + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + rightPadding: BSSizes.applyScale(20) + } + } + } + + Label { + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(32) + Layout.preferredHeight: BSSizes.applyScale(16) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + text: qsTr("Backup file:") + color: BSStyle.wildBlueColor + } + + Label { + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.rightMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(8) + Layout.preferredHeight: BSSizes.applyScale(16) + Layout.fillWidth: true + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + font.pixelSize: BSSizes.applyScale(14) + font.family: "Roboto" + font.weight: Font.Normal + + text: bsApp.settingExportDir + color: BSStyle.titanWhiteColor + } + + Button { + + Layout.leftMargin: BSSizes.applyScale(24) + Layout.topMargin: BSSizes.applyScale(12) + Layout.alignment: Qt.AlignLeft | Qt.AlingTop + + activeFocusOnTab: false + + font.pixelSize: BSSizes.applyScale(13) + font.family: "Roboto" + font.weight: Font.Normal + palette.buttonText: BSStyle.buttonsHeaderTextColor + + text: qsTr("Select target dir") + + icon.color: BSStyle.wildBlueColor + icon.source: "qrc:/images/folder_icon.png" + icon.width: BSSizes.applyScale(20) + icon.height: BSSizes.applyScale(16) + + background: Rectangle { + implicitWidth: BSSizes.applyScale(160) + implicitHeight: BSSizes.applyScale(34) + color: "transparent" + + radius: BSSizes.applyScale(14) + + border.color: BSStyle.defaultBorderColor + border.width: BSSizes.applyScale(1) + + } + + onClicked: { + fileDialog.open() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + + enabled: bsApp.settingExportDir.length !== 0 + preferred: true + focus: true + text: qsTr("Export") + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + + function click_enter() { + bsApp.exportWallet(wallet_properties_vm.walletId, bsApp.settingExportDir) + layout.sig_export() + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } + + FileDialog { + id: fileDialog + title: qsTr("Please choose a directory") + folder: shortcuts.documents + selectFolder: true + + onAccepted: { + var res = fileDialog.fileUrl.toString().replace(/^(file:\/{3})/,"") + if (res) + { + bsApp.settingExportDir = res + } + } + } + + function init() { + confirm_but.setActiveFocus() + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/RenameWallet.qml b/GUI/QtQuick/qml/WalletProperties/RenameWallet.qml new file mode 100644 index 000000000..ed06501eb --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/RenameWallet.qml @@ -0,0 +1,128 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + + id: layout + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + anchors.fill: parent + + signal back() + + property var wallet_properties_vm + + CustomMessageDialog { + id: error_dialog + error: qsTr("Wallet name already exist") + visible: false + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Rename your wallet") + } + + CustomTextInput { + id: input + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + activeFocusOnTab: true + + title_text: qsTr("Wallet Name") + + onEnterPressed: { + accept_but.click_enter() + } + onReturnPressed: { + accept_but.click_enter() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + RowLayout { + id: row + spacing: BSSizes.applyScale(10) + + anchors.bottom: parent.bottom + anchors.bottomMargin: BSSizes.applyScale(40) + anchors.horizontalCenter: parent.horizontalCenter + + CustomButton { + id: cancel_but + text: qsTr("Cancel") + width: BSSizes.applyScale(260) + + preferred: false + function click_enter() { + back() + } + } + + CustomButton { + id: accept_but + text: qsTr("Accept") + width: BSSizes.applyScale(260) + + preferred: true + + function click_enter() { + if (bsApp.isWalletNameExist(input.input_text)) { + showError(qsTr("Wallet name already exist")) + } + else { + if (bsApp.renameWallet(wallet_properties_vm.walletId, input.input_text) === 0) { + layout.back() + } + else { + showError(qsTr("Failed to rename wallet")) + } + } + } + + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } + + function init() + { + input.input_text = wallet_properties_vm.walletName + input.setActiveFocus() + } + + function showError(error) + { + error_dialog.error = error + error_dialog.show() + error_dialog.raise() + error_dialog.requestActivate() + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/WalletPropertiesPopup.qml b/GUI/QtQuick/qml/WalletProperties/WalletPropertiesPopup.qml new file mode 100644 index 000000000..fc30086ac --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/WalletPropertiesPopup.qml @@ -0,0 +1,457 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import terminal.models 1.0 + +import "." +import "../BsStyles" +import "../StyledControls" + +CustomPopup { + id: root + + _stack_view.initialItem: properties + _arrow_but_visibility: !properties.visible + + property var wallet_properties_vm + property bool walletSeedRequested: false + + x: mainWindow.x + (mainWindow.width - width) / 2 + y: mainWindow.y + (mainWindow.height - height) / 2 + + RenameWallet { + id: rename_wallet + visible: false + wallet_properties_vm: root.wallet_properties_vm + + onBack: _stack_view.pop() + } + + ChangePassword { + id: change_password + visible: false + + wallet_properties_vm: root.wallet_properties_vm + + onSig_success: { + root.close_click() + + show_popup(qsTr("Your password has successfully been changed")) + } + } + + ExportWOWallet { + id: export_wo_wallet + visible: false + + wallet_properties_vm: root.wallet_properties_vm + + onSig_success: (nameExport, pathExport) => { + if (export_wo_wallet.isExitWhenSuccess) { + root.close_click() + } + else { + _stack_view.pop() + } + + show_popup(qsTr("Your watching-only wallet has successfully been exported\n\nFilename:\t%1\nFolder:\t%2") + .arg(nameExport) + .arg(pathExport)) + } + } + + WalletSeedAuth { + id: wallet_seed_auth + visible: false + onAuthorized: { + _stack_view.replace(wallet_seed) + } + + wallet_properties_vm: root.wallet_properties_vm + } + + WalletSeed { + id: wallet_seed + visible: false + + wallet_properties_vm: root.wallet_properties_vm + + onClose: root.close_click() + } + + DeleteWalletWarn { + id: delete_wallet_warn + visible: false + wallet_properties_vm: root.wallet_properties_vm + onExportWOWallet: { + export_wo_wallet.isExitWhenSuccess = false + _stack_view.push(export_wo_wallet) + } + onViewWalletSeed: { + _stack_view.push(wallet_seed_auth) + wallet_seed_auth.init() + } + onDeleteSWWallet: { + _stack_view.push(delete_wallet) + delete_wallet.init() + } + onSig_success: { + root.close_click() + show_popup(qsTr("Wallet has successfully been deleted")) + } + } + + DeleteWallet { + id: delete_wallet + visible: false + onBack: _stack_view.pop() + + wallet_properties_vm: root.wallet_properties_vm + + onWalletDeleted: { + root.close_click() + + show_popup(qsTr("Wallet %1 has successfully been deleted").arg(wallet_properties_vm.walletName)) + } + onSig_success: { + root.close_click() + + show_popup(qsTr("Wallet has successfully been deleted")) + } + } + + CustomSuccessDialog { + id: success_dialog + + visible: false + } + + Rectangle { + id: properties + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + color: "transparent" + + Column { + spacing: BSSizes.applyScale(40) + width: parent.width - BSSizes.applyScale(48) + height: parent.height - BSSizes.applyScale(48) + anchors.centerIn: parent + + + Text { + text: qsTr("Wallet properties") + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(20) + anchors.horizontalCenter: parent.horizontalCenter + } + + Row { + width: parent.width - BSSizes.applyScale(75) + spacing: BSSizes.applyScale(37) + + Column { + spacing: BSSizes.applyScale(8) + width: parent.width / 2 + height: parent.height + + Row { + width: parent.width + + Text { + text: qsTr("Wallet name") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + id: wallet_name + text: wallet_properties_vm.walletName + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + + Image { + id: rename_button + + anchors.left: parent.right + + source: "qrc:/images/edit_wallet_name.png" + width: BSSizes.applyScale(32) + height: BSSizes.applyScale(16) + + horizontalAlignment: Image.AlignHCenter + fillMode: Image.PreserveAspectFit; + MouseArea { + anchors.fill: parent + onClicked: { + _stack_view.push(rename_wallet) + rename_wallet.init() + } + } + } + } + } + + + Row { + width: parent.width + + Text { + text: qsTr("Wallet type") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletType + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + } + + Row { + width: parent.width + + Text { + text: qsTr("Wallet ID") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletId + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + } + +/* Row { + width: parent.width + + Text { + text: qsTr("Group / Leaves") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: 14 + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletGroups + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: 14 + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + }*/ + } + + Rectangle { + width: BSSizes.applyScale(1) + height: BSSizes.applyScale(80) + color: BSStyle.tableSeparatorColor + anchors.verticalCenter: parent.verticalCenter + } + + Column { + spacing: BSSizes.applyScale(8) + width: parent.width / 2 + height: parent.height + + Row { + width: parent.width + + Text { + text: qsTr("Generated addresses") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletGeneratedAddresses + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + } + + Row { + width: parent.width + + Text { + text: qsTr("Used addresses") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletUsedAddresses + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + } + + Row { + width: parent.width + + Text { + text: qsTr("Available UTXOs") + color: BSStyle.titleTextColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + } + Text { + text: wallet_properties_vm.walletAvailableUtxo + color: BSStyle.textColor + font.family: "Roboto" + font.pixelSize: BSSizes.applyScale(14) + width: parent.width / 2 + horizontalAlignment: Text.AlignRight + } + } + } + } + + Column { + spacing: 10 + width: parent.width + height: parent.height * 0.6 + anchors.margins: BSSizes.applyScale(24) + + CustomListItem { + width: parent.width + visible: (!wallet_properties_vm.isHardware && !wallet_properties_vm.isWatchingOnly) + + icon_source: "qrc:/images/lock_icon.svg" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("Change password") + + onClicked: { + _stack_view.push(change_password) + change_password.init() + } + } + + CustomListItem { + width: parent.width + + icon_source: "qrc:/images/eye_icon.svg" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("Export watching-only wallet") + + onClicked: { + export_wo_wallet.isExitWhenSuccess = true + _stack_view.push(export_wo_wallet) + } + } + + CustomListItem { + width: parent.width + + visible: (!wallet_properties_vm.isHardware && !wallet_properties_vm.isWatchingOnly) + + icon_source: "qrc:/images/shield_icon.svg" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("View wallet seed") + + onClicked: { + _stack_view.push(wallet_seed_auth) + wallet_seed_auth.init() + } + } + + CustomListItem { + width: parent.width + + icon_source: "qrc:/images/scan_icon.svg" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("Rescan wallet") + + onClicked: { + rescanWalletBlockingPanel.visible = true + bsApp.rescanWallet(wallet_properties_vm.walletId) + } + } + + CustomListItem { + width: parent.width + + icon_source: "qrc:/images/delete_icon.svg" + icon_add_source: "qrc:/images/arrow.png" + title_text: qsTr("Delete wallet") + + onClicked: _stack_view.push(delete_wallet_warn) + } + } + } + } + + Rectangle { + id: rescanWalletBlockingPanel + anchors.fill: parent + visible: false + color: BSStyle.loadingPanelBackgroundColor + + Column { + anchors.centerIn: parent + + BusyIndicator { + running: true + palette.dark: BSStyle.wildBlueColor + } + + } + + MouseArea { + anchors.fill: parent + } + } + + CustomSuccessDialog { + id: messageDialog + details_text: qsTr("Rescan successful") + visible: false + + onSig_finish: root.close_click() + } + + Connections { + target: bsApp + + function onRescanCompleted() { + rescanWalletBlockingPanel.visible = false + messageDialog.show() + } + } + + function show_popup(success_) { + success_dialog.details_text = success_ + + success_dialog.show() + success_dialog.raise() + success_dialog.requestActivate() + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/WalletSeed.qml b/GUI/QtQuick/qml/WalletProperties/WalletSeed.qml new file mode 100644 index 000000000..c5b891f08 --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/WalletSeed.qml @@ -0,0 +1,80 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import Qt.labs.platform 1.1 as QLP + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + signal close(); + + height: BSSizes.applyScale(608) + width: BSSizes.applyScale(580) + + spacing: 0 + + property var wallet_properties_vm + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("Wallet seed") + } + + GridView { + id: grid + + clip: true + Layout.fillHeight: true + Layout.fillWidth: true + Layout.leftMargin: BSSizes.applyScale(25) + Layout.rightMargin: BSSizes.applyScale(15) + Layout.topMargin: BSSizes.applyScale(32) + + ScrollBar.vertical: ScrollBar { + policy: grid.height > grid.contentHeight ? ScrollBar.AlwaysOff : ScrollBar.AlwaysOn + } + + cellHeight : BSSizes.applyScale(56) + cellWidth : BSSizes.applyScale(180) + + model: wallet_properties_vm.seed + delegate: CustomSeedLabel { + seed_text: modelData + serial_num: index + 1 + } + } + + QLP.FileDialog { + id: exportFileDialog + title: qsTr("Please choose folder to export transaction") + defaultSuffix: "pdf" + fileMode: QLP.FileDialog.SaveFile + folder: QLP.StandardPaths.writableLocation(QLP.StandardPaths.DocumentsLocation) + onAccepted: { + var exportPath = bsApp.exportWalletToPdf(exportFileDialog.currentFile, wallet_properties_vm.seed) + Qt.openUrlExternally(exportFileDialog.currentFile); + } + } + + CustomButton { + id: confirm_but + text: qsTr("Export PDF") + preferred: true + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + + onClicked: { + exportFileDialog.currentFile = "file:///" + bsApp.makeExportWalletToPdfPath(wallet_properties_vm.seed) + exportFileDialog.open() + } + } +} diff --git a/GUI/QtQuick/qml/WalletProperties/WalletSeedAuth.qml b/GUI/QtQuick/qml/WalletProperties/WalletSeedAuth.qml new file mode 100644 index 000000000..bb09882d0 --- /dev/null +++ b/GUI/QtQuick/qml/WalletProperties/WalletSeedAuth.qml @@ -0,0 +1,131 @@ +import QtQuick 2.12 +import QtQuick.Window 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 + +import "../BsStyles" +import "../StyledControls" + +ColumnLayout { + id: layout + + height: BSSizes.applyScale(548) + width: BSSizes.applyScale(580) + + spacing: 0 + + property var wallet_properties_vm + + signal authorized() + + Connections { + target: bsApp + function onWalletSeedAuthFailed(error) { + init() + show_error(error) + } + function onWalletSeedAuthSuccess() { + authorized() + clear() + } + } + + CustomFailDialog { + id: fail_dialog + header: qsTr("Error") + visible: false + } + + CustomTitleLabel { + id: title + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : title.height + text: qsTr("View wallet seed") + } + + CustomTextInput { + id: password + + Layout.alignment: Qt.AlignCenter + Layout.preferredHeight : BSSizes.applyScale(70) + Layout.preferredWidth: BSSizes.applyScale(532) + Layout.topMargin: BSSizes.applyScale(10) + + input_topMargin: BSSizes.applyScale(35) + title_leftMargin: BSSizes.applyScale(16) + title_topMargin: BSSizes.applyScale(16) + + title_text: qsTr("Password") + + isPassword: true + isHiddenText: true + + onEnterPressed: { + confirm_but.click_enter() + } + onReturnPressed: { + confirm_but.click_enter() + } + } + + Label { + id: spacer + Layout.fillWidth: true + Layout.fillHeight: true + } + + CustomButton { + id: confirm_but + text: qsTr("Continue") + preferred: true + + Layout.bottomMargin: BSSizes.applyScale(40) + Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter + + width: BSSizes.applyScale(530) + enabled: (password.input_text !== "") + + function click_enter() { + if (!confirm_but.enabled) { + return + } + + const result = bsApp.viewWalletSeedAuth( + wallet_properties_vm.walletId, + password.input_text + ) + if (result !== 0) { + show_error(qsTr("Failed to send wallet seed message")) + clear() + } + } + } + + Keys.onEnterPressed: { + confirm_but.click_enter() + } + + Keys.onReturnPressed: { + confirm_but.click_enter() + } + + function init() + { + clear() + password.setActiveFocus() + } + + function clear() + { + password.isValid = true + password.input_text = "" + } + + function show_error(error) + { + fail_dialog.fail = error + fail_dialog.show() + fail_dialog.raise() + fail_dialog.requestActivate() + } +} diff --git a/GUI/QtQuick/qml/main.qml b/GUI/QtQuick/qml/main.qml index 0d9e070aa..08707f3c4 100644 --- a/GUI/QtQuick/qml/main.qml +++ b/GUI/QtQuick/qml/main.qml @@ -8,40 +8,173 @@ ********************************************************************************** */ + import QtQuick 2 -import QtQuick.Controls 2 +import QtQuick.Controls 2.9 import QtQuick.Layouts 1.3 import QtQuick.Window 2 +import Qt.labs.platform 1.1 + import "StyledControls" 1 import "BsStyles" 1 -//import Qt.labs.settings 1.0 - -/* -import "BsControls" -import "BsDialogs" -import "js/helper.js" as JsHelper -*/ +import "Receive" 1 +import "Send" 1 +import "CreateWallet" 1 +import "Pin" 1 +import "Settings" 1 +import "Plugins/Common" 1 ApplicationWindow { id: mainWindow - width: 800 - height: 600 + + minimumWidth: BSSizes.applyWindowWidthScale(1200) + minimumHeight: BSSizes.applyWindowHeightScale(800) + x: Screen.width / 2 - width / 2 + y: Screen.height / 2 - height / 2 visible: false title: qsTr("BlockSettle Terminal") property var currentDialog: ({}) + property bool isNoWalletsWizard: false readonly property int resizeAnimationDuration: 25 + property var armoryServers: bsApp.armoryServersModel + + onXChanged: scaleController.update() + onYChanged: scaleController.update() + + FontLoader { + source: "qrc:/fonts/Roboto-Regular.ttf" + } Component.onCompleted: { - mainWindow.flags = Qt.CustomizeWindowHint | Qt.MSWindowsFixedSizeDialogHint | - Qt.Dialog | Qt.WindowSystemMenuHint | - Qt.WindowTitleHint | Qt.WindowCloseButtonHint hide() // qmlFactory.installEventFilterToObj(mainWindow) // qmlFactory.applyWindowFix(mainWindow) } + CreateWallet { + id: create_wallet + visible: false + } + + ReceivePopup { + id: receive_popup + visible: false + onClosing: { + btnReceive.select(false) + } + } + + SendPopup { + id: send_popup + visible: false + onClosing: { + btnSend.select(false) + } + } + + SettingsPopup { + id: settings_popup + visible: false + onClosing: { + btnSettings.select(false) + } + } + + PinEntriesPopup { + id: pin_popup + visible: false + } + + PasswordEntryPopup { + id: password_popup + visible: false + } + + CustomMessageDialog { + id: error_dialog + header: qsTr("Error") + action: qsTr("OK") + visible: false + } + + CustomFailDialog { + id: fail_dialog + visible: false; + } + + CustomSuccessDialog { + id: success_dialog + visible: false + } + + Connections + { + target:bsApp + function onInvokePINentry () + { + show_popup(pin_popup) + } + + function onInvokePasswordEntry(devName, acceptOnDevice) + { + password_popup.device_name = devName + password_popup.accept_on_device = acceptOnDevice + show_popup(password_popup) + } + + function onShowError(text) + { + error_dialog.error = text + show_popup(error_dialog) + } + + function onShowFail(header, fail) + { + fail_dialog.header = header + fail_dialog.fail = fail + show_popup(fail_dialog) + } + + function onShowSuccess(success_) + { + success_dialog.details_text = success_ + show_popup(success_dialog) + } + + function onWalletsLoaded (nb) + { + if (nb === 0) + { + isNoWalletsWizard = true + } + } + + function onShowNotification(title, text) { + trayIcon.showMessage(title, text, SystemTrayIcon.Information, 5000) + } + } + + Timer { + id: timerNoWalletsWizard + + interval: 1000 + running: false + repeat: false + onTriggered: { + show_popup(create_wallet) + } + } + + onVisibleChanged: { + if (mainWindow.visible && isNoWalletsWizard) + { + isNoWalletsWizard = false + timerNoWalletsWizard.running = true + } + } + color: BSStyle.backgroundColor overlay.modal: Rectangle { @@ -51,11 +184,6 @@ ApplicationWindow { color: BSStyle.backgroundModeLessColor } - // attached to use from c++ - function messageBoxCritical(title, text, details) { - return JsHelper.messageBoxCritical(title, text, details) - } - InfoBanner { id: ibSuccess bgColor: "darkgreen" @@ -64,25 +192,294 @@ ApplicationWindow { id: ibFailure bgColor: "darkred" } + InfoBanner { + id: ibInfo + bgColor: "darkgrey" + } -/* function raiseWindow() { - JsHelper.raiseWindow(mainWindow) + StackView { + id: stack + initialItem: swipeView + anchors.fill: parent } - function hideWindow() { - JsHelper.hideWindow(mainWindow) + + header: Column { + height: BSSizes.applyScale(57) + width: parent.width + spacing: 0 + + RowLayout { + height: BSSizes.applyScale(56) + width: parent.width + spacing: 0 + + Image { + width: BSSizes.applyScale(129) + height: BSSizes.applyScale(24) + source: "qrc:/images/logo.png" + Layout.leftMargin : BSSizes.applyScale(18) + } + + Label { + Layout.fillWidth: true + } + + Rectangle { + color: hoverArea.containsMouse ? BSStyle.buttonsHoveredColor : "transparent" + width: BSSizes.applyScale(120) + Layout.fillHeight: true + + RowLayout { + id: innerStatusLayout + anchors.fill: parent + spacing: BSSizes.applyScale(5) + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + } + + Label { + text: bsApp.armoryState === 7 ? armoryServers.currentNetworkName + : bsApp.armoryState === 0 ? qsTr("Offline") : qsTr("Connecting") + font.pixelSize: BSSizes.applyScale(12) + font.family: "Roboto" + font.weight: Font.Normal + color: BSStyle.titleTextColor + horizontalAlignment: Qt.AlignRight + } + + Rectangle { + id: animatedConnectionStateArea + width: BSSizes.applyScale(16) + height: BSSizes.applyScale(16) + Layout.rightMargin: BSSizes.applyScale(10) + color: "transparent" + + Image { + id: imgEnvKind + anchors.fill: parent + source: (bsApp.armoryState !== 7 ? "qrc:/images/bitcoin-disabled.png" : + (bsApp.networkType === 0 ? "qrc:/images/bitcoin-main-net.png" : "qrc:/images/bitcoin-test-net.png")) + } + + RotationAnimation on rotation { + id: connectionAnimation + loops: Animation.Infinite + from: 0 + to: 360 + running: bsApp.armoryState === 1 + duration: 1000 + } + + Connections { + target: bsApp + function onArmoryStateChanged() { + if (bsApp.armoryState === 0 || bsApp.armoryState === 7) { + connectionAnimation.complete() + animatedConnectionStateArea.rotation = 0 + } + else if (!connectionAnimation.running) { + connectionAnimation.start() + } + } + } + } + } + + MouseArea { + id: hoverArea + hoverEnabled: true + anchors.fill: parent + + onClicked: { + show_popup(settings_popup) + settings_popup.open_network_menu() + } + } + } + + CustomTitleToolButton { + id: btnSend + + enabled: (walletBalances.rowCount > 0) + + text: qsTr("Send") + icon.source: "qrc:/images/send_icon.png" + font.pixelSize: BSSizes.applyScale(12) + font.letterSpacing: 0.3 + Layout.fillHeight: true + + onClicked: { + topMenuBtnClicked(btnSend) + show_popup(send_popup) + } + } + + CustomTitleToolButton { + id: btnReceive + + enabled: (walletBalances.rowCount > 0) + + text: qsTr("Receive") + icon.source: "qrc:/images/receive_icon.png" + + font.pixelSize: BSSizes.applyScale(12) + font.letterSpacing: 0.3 + Layout.fillHeight: true + + onClicked: { + topMenuBtnClicked(btnReceive) + bsApp.generateNewAddress(walletBalances.selectedWallet, true) + show_popup(receive_popup) + } + } + + CustomTitleToolButton { + id: btnSettings + text: qsTr("Settings") + font.pixelSize: BSSizes.applyScale(12) + font.letterSpacing: 0.3 + icon.source: "qrc:/images/settings_icon.png" + onClicked: { + topMenuBtnClicked(btnSettings) + show_popup(settings_popup) + } + } + } + + Rectangle { + width: parent.width + height: BSSizes.applyScale(1) + color: BSStyle.defaultGreyColor + } + } + + SwipeView { + anchors.fill: parent + id: swipeView + currentIndex: tabBar.currentIndex + + OverviewPage { + id: overviewPage + onNewWalletClicked: { + show_popup(create_wallet) + } + onCurWalletIndexChanged: (ind) => { + //overviewWalletIndex = ind + } + onOpenSend: (txId, isRBF, isCPFP, selWalletIdx) => { + send_popup.open(txId, isRBF, isCPFP, selWalletIdx) + show_popup(send_popup, true) + } + + onOpenExplorer: (txId) => { + tabBar.currentIndex = 2 + explorerPage.openTransaction(txId) + } + } + + TransactionsPage { + id: transactionsPage + + onOpenSend: (txId, isRBF, isCPFP) => { + send_popup.open(txId, isRBF, isCPFP) + show_popup(send_popup, true) + } + onOpenExplorer: (txId) => { + tabBar.currentIndex = 2 + explorerPage.openTransaction(txId) + } + } + + ExplorerPage { + id: explorerPage + } + + PluginsPage { + id: pluginPage + } + + onCurrentIndexChanged: { + if (currentIndex === 2) { + explorerPage.setFocus() + } + } } - function customDialogRequest(dialogName, data) { - var newDialog = JsHelper.customDialogRequest(dialogName, data) - if (newDialog) { - raiseWindow() - JsHelper.prepareDialog(newDialog) + footer: Rectangle { + height: BSSizes.applyScale(56) + width: parent.width + color :"#191E2A" + + RowLayout { + spacing: 0 + anchors.fill: parent + + Label { + Layout.fillWidth: true + } + + TabBar { + id: tabBar + currentIndex: swipeView.currentIndex + padding: 0 + spacing: 0 + Layout.fillWidth: false + + background: Rectangle { + color: "transparent" + } + + CustomTabButton { + id: btnOverview + text: qsTr("Overview") + Component.onCompleted: { + btnOverview.setIcons ("qrc:/images/overview_icon.png", "qrc:/images/overview_icon_not_choosed.png") + } + } + CustomTabButton { + id: btnTransactions + text: qsTr("Transactions") + Component.onCompleted: { + btnTransactions.setIcons ("qrc:/images/transactions_icon.png", "qrc:/images/transactions_icon_unchoosed.png") + } + } + + CustomTabButton { + id: btnExplorer + text: qsTr("Explorer") + Component.onCompleted: { + btnExplorer.setIcons ("qrc:/images/explorer_icon.png", "qrc:/images/explorer_icon_unchoosed.png") + } + onClicked: { + explorerPage.setFocus() + } + } + + CustomTabButton { + id: btnPlugins + text: qsTr("Apps") + Component.onCompleted: { + btnPlugins.setIcons ("qrc:/images/plugins_icon.png", "qrc:/images/plugins_icon_unchoosed.png") + } + } + } + + Label { + Layout.fillWidth: true + } } } - function invokeQmlMethod(method, cppCallback, argList) { - JsHelper.evalWorker(method, cppCallback, argList) - }*/ + function topMenuBtnClicked(clickedBtn) + { + btnSend.select(false) + btnReceive.select(false) + btnSettings.select(false) + + clickedBtn.select(true) + } function moveMainWindowToScreenCenter() { mainWindow.x = Screen.virtualX + (Screen.width - mainWindow.width) / 2 @@ -132,4 +529,43 @@ ApplicationWindow { property: "y" duration: resizeAnimationDuration } + + + //global functions + function getWalletData (index: int, role: string) + { + return walletBalances.data(walletBalances.index(index, 0), role) + } + + function getFeeSuggData (index: int, role: string) + { + return feeSuggestions.data(feeSuggestions.index(index, 0), role) + } + + function show_popup (id, noInit = false) + { + if (typeof id.init === "function" && !noInit) + { + id.init() + } + id.show() + id.raise() + id.requestActivate() + } + + SystemTrayIcon { + id: trayIcon + visible: true + icon.source: "qrc:/images/terminal.ico" + + onActivated: { + mainWindow.show() + mainWindow.raise() + mainWindow.requestActivate() + } + + Component.onCompleted: { + trayIcon.show() + } + } } diff --git a/GUI/QtQuick/qtquick.qrc b/GUI/QtQuick/qtquick.qrc index 22130886b..4bf247061 100644 --- a/GUI/QtQuick/qtquick.qrc +++ b/GUI/QtQuick/qtquick.qrc @@ -1,8 +1,25 @@ images/full_logo.png - images/bs_logo.png + images/terminal.ico + images/bitcoin-test-net.png + images/bitcoin-main-net.png + images/bitcoin-disabled.png + images/send_icon.png + images/receive_icon.png + images/settings_icon.png + images/overview_icon.png + images/transactions_icon.png + images/explorer_icon.png qml/main.qml + qml/OverviewPage.qml + qml/AddressDetails.qml + qml/TransactionsPage.qml + qml/ExplorerPage.qml + qml/ExplorerEmpty.qml + qml/ExplorerAddress.qml + qml/ExplorerTX.qml + qml/VerifyTX.qml qml/InfoBanner.qml qml/InfoBannerComponent.qml qml/InfoBar.qml @@ -10,6 +27,7 @@ qml/StyledControls/CustomButtonBar.qml qml/StyledControls/CustomCheckBox.qml qml/StyledControls/CustomComboBox.qml + qml/StyledControls/CustomSmallComboBox.qml qml/StyledControls/CustomContainer.qml qml/StyledControls/CustomDialog.qml qml/StyledControls/CustomHeader.qml @@ -24,6 +42,188 @@ qml/StyledControls/CustomTextInput.qml qml/StyledControls/qmldir qml/BsStyles/BSStyle.qml + qml/BsStyles/BSSizes.qml qml/BsStyles/qmldir + qml/StyledControls/CustomTitleToolButton.qml + images/logo.png + images/plugins_icon.png + images/overview_icon_not_choosed.png + images/plugins_icon_unchoosed.png + images/transactions_icon_unchoosed.png + images/explorer_icon_unchoosed.png + TermsAndConditions.txt + images/close_button.svg + qml/StyledControls/CustomTitleLabel.qml + images/wallet icon.png + images/back_arrow.png + qml/StyledControls/CustomSeedLabel.qml + qml/StyledControls/CustomSeedTextInput.qml + images/warning_icon.png + images/Eye_icon _unvisible.png + images/Eye_icon _visible.png + images/USB_icon_conn.png + images/USB_icon_disconn.png + qml/StyledControls/CustomTextSwitch.qml + images/wallet_file.png + images/file_drop.png + images/folder_icon.png + images/File.png + images/success.png + qml/StyledControls/CustomPopup.qml + qml/Receive/ReceiveQrCode.qml + images/copy_icon.svg + qml/Receive/qmldir + qml/Receive/ReceivePopup.qml + qml/CreateWallet/WalletName.qml + qml/CreateWallet/ConfirmPassword.qml + qml/CreateWallet/CreateWallet.qml + qml/CreateWallet/ImportHardware.qml + qml/CreateWallet/ImportWallet.qml + qml/CreateWallet/ImportWatchingWallet.qml + qml/CreateWallet/qmldir + qml/CreateWallet/StartCreateWallet.qml + qml/CreateWallet/TermsAndConditions.qml + qml/CreateWallet/WalletSeed.qml + qml/CreateWallet/WalletSeedSkipAccept.qml + qml/CreateWallet/WalletSeedVerify.qml + images/advanced_icon.png + qml/StyledControls/CustomContextMenu.qml + qml/StyledControls/CustomDialogWindow.qml + qml/StyledControls/CustomLabelCopyableValue.qml + qml/StyledControls/CustomPasswordTextInput.qml + qml/StyledControls/CustomTextEdit.qml + qml/StyledControls/CustomTitleDialogWindow.qml + qml/StyledControls/CustomTitleDialogWindowWithExpander.qml + qml/StyledControls/CustomExportSuccessDialog.qml + qml/Send/qmldir + qml/Send/SendPopup.qml + qml/Send/SimpleDetails.qml + qml/Send/SignTransaction.qml + images/paste_icon.png + qml/StyledControls/CustomCompleterPopup.qml + qml/Send/AdvancedDetails.qml + qml/Pin/PinEntriesPopup.qml + qml/Pin/qmldir + qml/StyledControls/CustomMessageDialog.qml + qml/StyledControls/CustomFailDialog.qml + qml/StyledControls/CustomMediumButton.qml + qml/Overview/BalanceBar.qml + qml/Overview/BaseBalanceLabel.qml + qml/Overview/OverviewPanel.qml + qml/Overview/OverviewWalletBar.qml + qml/Pin/PasswordEntryPopup.qml + images/about.png + images/arrow.png + images/general.png + images/network.png + images/notification_critical.png + images/notification_info.png + images/notification_question.png + images/notification_success.png + images/notification_warning.png + images/plus.svg + qml/Settings/qmldir + qml/Settings/SettingsMenu.qml + qml/Settings/SettingsNetwork.qml + qml/Settings/SettingsPopup.qml + qml/StyledControls/CustomListItem.qml + qml/StyledControls/CustomListRadioButton.qml + qml/Settings/SettingsGeneral.qml + qml/Settings/SettingsAbout.qml + images/about_github.svg + images/about_hello.svg + images/about_telegram.svg + images/about_terminal.svg + images/about_twitter.svg + images/combobox_open_button.svg + qml/StyledControls/CustomTableView.qml + qml/StyledControls/CustomSmallButton.qml + qml/StyledControls/CustomButtonRightIcon.qml + qml/StyledControls/CustomButtonLeftIcon.qml + images/calendar_icon.svg + images/download_icon.svg + images/search_icon.svg + images/logo_no_text.svg + qml/StyledControls/CopyIconButton.qml + qml/StyledControls/InputOutputTableView.qml + images/down_arrow.svg + images/up_arrow.svg + qml/Send/WalletsComboBox.qml + qml/Send/AmountInput.qml + qml/Send/FeeSuggestComboBox.qml + qml/Send/RecvAddrTextInput.qml + qml/StyledControls/DeleteIconButton.qml + images/delete.png + images/delete.svg + qml/TransactionDetails.qml + qml/StyledControls/CloseIconButton.qml + qml/StyledControls/CustomTableDelegateRow.qml + qml/Send/SelectInputs.qml + images/check.svg + images/collapsed.svg + images/expanded.svg + qml/Send/SignTransactionAdvanced.qml + qml/Send/PasswordWithTimer.qml + images/delete_icon.svg + images/eye_icon.svg + images/shield_icon.svg + images/lock_icon.svg + images/scan_icon.svg + qml/WalletProperties/WalletPropertiesPopup.qml + qml/WalletProperties/ChangePassword.qml + qml/WalletProperties/RenameWallet.qml + qml/WalletProperties/DeleteWallet.qml + qml/WalletProperties/DeleteWalletWarn.qml + qml/WalletProperties/ExportWOWallet.qml + qml/WalletProperties/WalletSeed.qml + qml/WalletProperties/WalletSeedAuth.qml + images/wallet_icon_warn.svg + images/edit_wallet_name.png + qml/StyledControls/CustomHorizontalHeaderView.qml + qml/StyledControls/CustomRbfCpfpMenu.qml + qml/StyledControls/CustomSuccessWidget.qml + qml/StyledControls/CustomSuccessDialog.qml + qml/Settings/AddArmoryServer.qml + images/delete_custom_server.svg + qml/Settings/DeleteArmoryServer.qml + qml/Settings/ModifyArmoryServer.qml + qml/StyledControls/CustomTransactionsTableView.qml + fonts/Roboto-Regular.ttf + qml/Plugins/Common/Card.qml + qml/Plugins/Common/PluginsPage.qml + images/sideshift_plugin.png + images/try_icon.png + qml/Plugins/Common/PluginPopup.qml + qml/Plugins/SideShift/SideShiftButton.qml + qml/Plugins/SideShift/SideShiftCombobox.qml + qml/Plugins/SideShift/SideShiftComboboxWithIcon.qml + qml/Plugins/SideShift/SideShiftIconButton.qml + qml/Plugins/SideShift/SideShiftPopup.qml + qml/Plugins/SideShift/SideShiftTextInput.qml + images/transfer_icon.png + qml/Plugins/SideShift/SideShiftMainPage.qml + qml/Plugins/SideShift/SideShiftBuyPage.qml + qml/Plugins/SideShift/SideShiftCopyButton.qml + images/sideshift_right_arrow.svg + images/sideswap_plugin.png + images/leverex_plugin.png + qml/Plugins/SideSwap/SideSwapPopup.qml + qml/Plugins/SideSwap/SideSwapMainPage.qml + qml/Plugins/SideSwap/Controls/CurrencyLabel.qml + qml/Plugins/SideSwap/Controls/CustomButton.qml + qml/Plugins/SideSwap/Controls/CustomCombobox.qml + qml/Plugins/SideSwap/Controls/CustomSwitch.qml + qml/Plugins/SideSwap/Controls/CustomTextEdit.qml + qml/Plugins/SideSwap/Controls/IconButton.qml + qml/Plugins/SideSwap/Styles/qmldir + qml/Plugins/SideSwap/Styles/SideSwapStyles.qml + images/sideswap/btc_icon.svg + images/sideswap/lbtc_icon.svg + qml/Plugins/SideSwap/Controls/CustomBorderedButton.qml + qml/Plugins/SideSwap/SideSwapPegOut.qml + qml/Plugins/SideSwap/SideSwapPegIn.qml + images/import_icon.svg + images/RPK12.png + images/RPK24.png diff --git a/GUI/QtQuick/viewmodels/WalletPropertiesVM.cpp b/GUI/QtQuick/viewmodels/WalletPropertiesVM.cpp new file mode 100644 index 000000000..1e11d98ef --- /dev/null +++ b/GUI/QtQuick/viewmodels/WalletPropertiesVM.cpp @@ -0,0 +1,149 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ + +#include "WalletPropertiesVM.h" + +#include + +using namespace qtquick_gui; + +namespace { + static inline QString walletTypeFromInfo(const bs::sync::WalletInfo& info) + { + if (info.isHardware) { + return QObject::tr("Hardware"); + } + if (info.watchOnly) { + return QObject::tr("Watch-only"); + } + return QObject::tr("Software"); + } +} + +WalletPropertiesVM::WalletPropertiesVM(const std::shared_ptr & logger, QObject* parent) + : QObject(parent), + logger_(logger) +{ +} + +void WalletPropertiesVM::setWalletInfo(const QString& walletId, const bs::sync::WalletInfo& info) +{ + info_.name = QString::fromStdString(info.name); + info_.walletId = walletId; + info_.groups = QString::fromLatin1("1/") + QString::number(info.leaves.size()); + info_.walletType = walletTypeFromInfo(info); + info_.generatedAddresses = info.nbAddresses; + info_.isHardware = info.isHardware; + info_.isWatchingOnly = info.watchOnly; + nbUsedAddrs_ = 0; + nbUTXOs_ = 0; + seed_.clear(); + emit changed(); +} + +void qtquick_gui::WalletPropertiesVM::setWalletSeed(const std::string& walletId, const std::string& seed) +{ + if (walletId != info_.walletId.toStdString()) { + return; + } + seed_ = QString::fromStdString(seed).split(QLatin1Char(' ')); + logger_->debug("[{}] seed length: {}", __func__, seed_.size()); + emit seedChanged(); +} + +void qtquick_gui::WalletPropertiesVM::setNbUsedAddrs(const std::string& walletId, uint32_t nb) +{ + if (info_.walletId.isEmpty() || (info_.walletId.toStdString() != walletId)) { + return; + } + nbUsedAddrs_ = nb; + emit changed(); +} + +void qtquick_gui::WalletPropertiesVM::incNbUsedAddrs(const std::string& walletId, uint32_t nb) +{ + if (info_.walletId.isEmpty() || (info_.walletId.toStdString() != walletId)) { + logger_->warn("[{}] wallet id mismatch: {} vs {}", __func__ + , info_.walletId.toStdString(), walletId); + return; + } + nbUsedAddrs_ += nb; + emit changed(); +} + +void qtquick_gui::WalletPropertiesVM::setNbUTXOs(const std::string& walletId, uint32_t nb) +{ + if (info_.walletId.isEmpty() || (info_.walletId.toStdString() != walletId)) { + return; + } + nbUTXOs_ = nb; + emit changed(); +} + +void qtquick_gui::WalletPropertiesVM::rename(const std::string& walletId, const std::string& newName) +{ + if (info_.walletId != QString::fromStdString(walletId)) { + logger_->warn("[{}] wallet {} does not match", __func__, walletId); + return; + } + info_.name = QString::fromStdString(newName); + emit changed(); +} + +const QString& WalletPropertiesVM::walletName() const +{ + return info_.name; +} + +const QString& WalletPropertiesVM::walletId() const +{ + return info_.walletId; +} + +const QString& WalletPropertiesVM::walletGroups() const +{ + return info_.groups; +} + +const QString& WalletPropertiesVM::walletType() const +{ + return info_.walletType; +} + +quint32 WalletPropertiesVM::walletGeneratedAddresses() const +{ + return info_.generatedAddresses; +} + +quint32 WalletPropertiesVM::walletUsedAddresses() const +{ + return nbUsedAddrs_; +} + +quint32 WalletPropertiesVM::walletAvailableUtxo() const +{ + return nbUTXOs_; +} + +bool WalletPropertiesVM::isHardware() const +{ + return info_.isHardware; +} + +bool WalletPropertiesVM::isWatchingOnly() const +{ + return info_.isWatchingOnly; +} + +const QStringList& WalletPropertiesVM::seed() const +{ + return seed_; +} diff --git a/GUI/QtQuick/viewmodels/WalletPropertiesVM.h b/GUI/QtQuick/viewmodels/WalletPropertiesVM.h new file mode 100644 index 000000000..0d18ef146 --- /dev/null +++ b/GUI/QtQuick/viewmodels/WalletPropertiesVM.h @@ -0,0 +1,85 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include + +#include +#include + +namespace spdlog { + class logger; +} + +namespace qtquick_gui +{ + +struct WalletInfo +{ + QString name; + QString walletId; + QString groups; + QString walletType; + quint32 generatedAddresses; + bool isHardware; + bool isWatchingOnly; +}; + +class WalletPropertiesVM: public QObject +{ + Q_OBJECT + Q_PROPERTY(QString walletName READ walletName NOTIFY changed) + Q_PROPERTY(QString walletId READ walletId NOTIFY changed) + Q_PROPERTY(QString walletGroups READ walletGroups NOTIFY changed) + Q_PROPERTY(QString walletType READ walletType NOTIFY changed) + Q_PROPERTY(quint32 walletGeneratedAddresses READ walletGeneratedAddresses NOTIFY changed) + Q_PROPERTY(quint32 walletUsedAddresses READ walletUsedAddresses NOTIFY changed) + Q_PROPERTY(quint32 walletAvailableUtxo READ walletAvailableUtxo NOTIFY changed) + Q_PROPERTY(bool isHardware READ isHardware NOTIFY changed) + Q_PROPERTY(bool isWatchingOnly READ isWatchingOnly NOTIFY changed) + Q_PROPERTY(QStringList seed READ seed NOTIFY seedChanged) + +public: + WalletPropertiesVM(const std::shared_ptr & logger, QObject* parent = nullptr); + + void setWalletInfo(const QString& walletId, const bs::sync::WalletInfo& info); + void setWalletSeed(const std::string& walletId, const std::string& seed); + void setNbUsedAddrs(const std::string& walletId, uint32_t nb); + void incNbUsedAddrs(const std::string& walletId, uint32_t nb = 1); + void setNbUTXOs(const std::string& walletId, uint32_t nb); + + void rename(const std::string& walletId, const std::string& newName); + + const QString& walletName() const; + const QString& walletId() const; + const QString& walletGroups() const; + const QString& walletType() const; + quint32 walletGeneratedAddresses() const; + quint32 walletUsedAddresses() const; + quint32 walletAvailableUtxo() const; + bool isHardware() const; + bool isWatchingOnly() const; + + const QStringList& seed() const; + +signals: + void changed(); + void seedChanged(); + +private: + std::shared_ptr logger_; + WalletInfo info_; + uint32_t nbUsedAddrs_{ 0 }; + uint32_t nbUTXOs_{ 0 }; + QStringList seed_; +}; + +} diff --git a/GUI/QtQuick/viewmodels/plugins/PluginsListModel.cpp b/GUI/QtQuick/viewmodels/plugins/PluginsListModel.cpp new file mode 100644 index 000000000..feddcaef3 --- /dev/null +++ b/GUI/QtQuick/viewmodels/plugins/PluginsListModel.cpp @@ -0,0 +1,76 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023, BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#include "PluginsListModel.h" +#include + +namespace +{ + static const QHash kRoles { + {PluginsListModel::PluginRoles::Name, "name_role"}, + {PluginsListModel::PluginRoles::Description, "description_role"}, + {PluginsListModel::PluginRoles::Icon, "icon_role"}, + {PluginsListModel::PluginRoles::Path, "path_role"} + }; +} + +PluginsListModel::PluginsListModel(QObject* parent) + : QAbstractListModel(parent) +{} + +void PluginsListModel::addPlugins(const std::vector& plugins) +{ + if (plugins.empty()) { + return; + } + QMetaObject::invokeMethod(this, [this, plugins] { + beginInsertRows(QModelIndex(), rowCount(), rowCount() + plugins.size() - 1); + plugins_.insert(plugins_.cend(), plugins.cbegin(), plugins.cend()); + endInsertRows(); + }); +} + +int PluginsListModel::rowCount(const QModelIndex&) const +{ + return plugins_.size(); +} + +QVariant PluginsListModel::data(const QModelIndex& index, int role) const +{ + const int row = index.row(); + try { + switch(role) { + case Name: return plugins_.at(row)->name(); + case Description: return plugins_.at(row)->description(); + case Icon: return plugins_.at(row)->icon(); + case Path: return plugins_.at(row)->path(); + default: break; + } + } + catch (const std::exception&) { + return QString{}; + } + return QVariant(); +} + +QHash PluginsListModel::roleNames() const +{ + return kRoles; +} + +QObject* PluginsListModel::getPlugin(int index) +{ + try { + return plugins_.at(index); + } + catch (...) { + } + return nullptr; +} diff --git a/GUI/QtQuick/viewmodels/plugins/PluginsListModel.h b/GUI/QtQuick/viewmodels/plugins/PluginsListModel.h new file mode 100644 index 000000000..f76b6a294 --- /dev/null +++ b/GUI/QtQuick/viewmodels/plugins/PluginsListModel.h @@ -0,0 +1,45 @@ +/* + +*********************************************************************************** +* Copyright (C) 2023 BlockSettle AB +* Distributed under the GNU Affero General Public License (AGPL v3) +* See LICENSE or http://www.gnu.org/licenses/agpl.html +* +********************************************************************************** + +*/ +#pragma once + +#include +#include +#include +#include +#include +#include "Plugin.h" + + +class PluginsListModel: public QAbstractListModel +{ + Q_OBJECT +public: + enum PluginRoles + { + Name = Qt::UserRole + 1, + Description, + Icon, + Path + }; + Q_ENUM(PluginRoles) + + PluginsListModel(QObject* parent = nullptr); + void addPlugins(const std::vector&); + + int rowCount(const QModelIndex & = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role) const override; + QHash roleNames() const override; + + Q_INVOKABLE QObject* getPlugin(int index); + +private: + std::vector plugins_; +}; diff --git a/GUI/QtWidgets/QtGuiAdapter.cpp b/GUI/QtWidgets/QtGuiAdapter.cpp index b494fe42b..6ed739de7 100644 --- a/GUI/QtWidgets/QtGuiAdapter.cpp +++ b/GUI/QtWidgets/QtGuiAdapter.cpp @@ -11,6 +11,7 @@ #include "CommonTypes.h" #include "QtGuiAdapter.h" #include +#include #include #include #include @@ -35,10 +36,13 @@ #include "common.pb.h" #include "terminal.pb.h" +#include "../../BlockSettleApp/macosapp.h" + using namespace BlockSettle::Common; using namespace BlockSettle::Terminal; using namespace bs::message; +/* #if defined (Q_OS_MAC) class MacOsApp : public QApplication { @@ -74,7 +78,7 @@ class MacOsApp : public QApplication bool activationRequired_ = false; }; #endif // Q_OS_MAC - +*/ static void checkStyleSheet(QApplication &app) { @@ -212,8 +216,8 @@ void QtGuiAdapter::run(int &argc, char **argv) logger_->debug("[QtGuiAdapter::run] initial setup done"); #if defined (Q_OS_MAC) - MacOsApp *macApp = (MacOsApp*)(app); - QObject::connect(macApp, &MacOsApp::reactivateTerminal, mainWindow +// MacOsApp *macApp = &app; + QObject::connect(&app, &MacOsApp::reactivateTerminal, mainWindow_ , &bs::gui::qt::MainWindow::onReactivate); #endif bs::disableAppNap(); @@ -223,7 +227,7 @@ void QtGuiAdapter::run(int &argc, char **argv) } } -bool QtGuiAdapter::process(const Envelope &env) +ProcessingResult QtGuiAdapter::process(const Envelope &env) { if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { @@ -248,7 +252,7 @@ bool QtGuiAdapter::process(const Envelope &env) default: break; } } - return true; + return ProcessingResult::Ignored; } bool QtGuiAdapter::processBroadcast(const bs::message::Envelope& env) @@ -256,37 +260,37 @@ bool QtGuiAdapter::processBroadcast(const bs::message::Envelope& env) if (std::dynamic_pointer_cast(env.sender)) { switch (env.sender->value()) { case TerminalUsers::System: - return processAdminMessage(env); + return (processAdminMessage(env) != ProcessingResult::Ignored); case TerminalUsers::Settings: - return processSettings(env); + return (processSettings(env) != ProcessingResult::Ignored); case TerminalUsers::Blockchain: - return processBlockchain(env); + return (processBlockchain(env) != ProcessingResult::Ignored); case TerminalUsers::Signer: - return processSigner(env); + return (processSigner(env) != ProcessingResult::Ignored); case TerminalUsers::Wallets: - return processWallets(env); + return (processWallets(env) != ProcessingResult::Ignored); case TerminalUsers::BsServer: - return processBsServer(env); + return (processBsServer(env) != ProcessingResult::Ignored); case TerminalUsers::Matching: - return processMatching(env); + return (processMatching(env) != ProcessingResult::Ignored); case TerminalUsers::MktData: - return processMktData(env); + return (processMktData(env) != ProcessingResult::Ignored); case TerminalUsers::OnChainTracker: - return processOnChainTrack(env); + return (processOnChainTrack(env) != ProcessingResult::Ignored); case TerminalUsers::Assets: - return processAssets(env); + return (processAssets(env) != ProcessingResult::Ignored); default: break; } } return false; } -bool QtGuiAdapter::processSettings(const Envelope &env) +ProcessingResult QtGuiAdapter::processSettings(const Envelope &env) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetResponse: @@ -299,10 +303,10 @@ bool QtGuiAdapter::processSettings(const Envelope &env) return processArmoryServers(msg.armory_servers()); default: break; } - return true; + return ProcessingResult::Ignored; } -bool QtGuiAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) +ProcessingResult QtGuiAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResponse &response) { std::map settings; for (const auto &setting : response.responses()) { @@ -346,51 +350,53 @@ bool QtGuiAdapter::processSettingsGetResponse(const SettingsMessage_SettingsResp } } if (!settings.empty()) { - return QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, settings] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, settings] { for (const auto& setting : settings) { mw->onSetting(setting.first, setting.second); } }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } - return true; + return ProcessingResult::Ignored; } -bool QtGuiAdapter::processSettingsState(const SettingsMessage_SettingsResponse& response) +ProcessingResult QtGuiAdapter::processSettingsState(const SettingsMessage_SettingsResponse& response) { ApplicationSettings::State state; for (const auto& setting : response.responses()) { state[static_cast(setting.request().index())] = fromResponse(setting); } - return QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, state] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, state] { mw->onSettingsState(state); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processArmoryServers(const SettingsMessage_ArmoryServers& response) +ProcessingResult QtGuiAdapter::processArmoryServers(const SettingsMessage_ArmoryServers& response) { QList servers; for (const auto& server : response.servers()) { - servers << ArmoryServer{ QString::fromStdString(server.server_name()) + servers << ArmoryServer{ server.server_name() , static_cast(server.network_type()) - , QString::fromStdString(server.server_address()) - , std::stoi(server.server_port()), QString::fromStdString(server.server_key()) + , server.server_address(), server.server_port(), server.server_key() , SecureBinaryData::fromString(server.password()) , server.run_locally(), server.one_way_auth() }; } logger_->debug("[{}] {} servers, cur: {}, conn: {}", __func__, servers.size() , response.idx_current(), response.idx_connected()); - return QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, servers, response] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [mw = mainWindow_, servers, response] { mw->onArmoryServers(servers, response.idx_current(), response.idx_connected()); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processAdminMessage(const Envelope &env) +ProcessingResult QtGuiAdapter::processAdminMessage(const Envelope &env) { AdministrativeMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse admin msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case AdministrativeMessage::kComponentCreated: @@ -412,10 +418,10 @@ bool QtGuiAdapter::processAdminMessage(const Envelope &env) default: break; } updateSplashProgress(); - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processBlockchain(const Envelope &env) +ProcessingResult QtGuiAdapter::processBlockchain(const Envelope &env) { ArmoryMessage msg; if (!msg.ParseFromString(env.message)) { @@ -424,7 +430,7 @@ bool QtGuiAdapter::processBlockchain(const Envelope &env) if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case ArmoryMessage::kLoading: @@ -464,12 +470,12 @@ bool QtGuiAdapter::processBlockchain(const Envelope &env) return processZC(msg.zc_received()); case ArmoryMessage::kZcInvalidated: return processZCInvalidated(msg.zc_invalidated()); - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processSigner(const Envelope &env) +ProcessingResult QtGuiAdapter::processSigner(const Envelope &env) { SignerMessage msg; if (!msg.ParseFromString(env.message)) { @@ -478,7 +484,7 @@ bool QtGuiAdapter::processSigner(const Envelope &env) if (!env.receiver) { logger_->debug("[{}] no receiver", __func__); } - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SignerMessage::kState: @@ -512,17 +518,17 @@ bool QtGuiAdapter::processSigner(const Envelope &env) }); } break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processWallets(const Envelope &env) +ProcessingResult QtGuiAdapter::processWallets(const Envelope &env) { WalletsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case WalletsMessage::kLoading: @@ -608,41 +614,41 @@ bool QtGuiAdapter::processWallets(const Envelope &env) break; case WalletsMessage::kLedgerEntries: return processLedgerEntries(msg.ledger_entries()); - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processOnChainTrack(const Envelope &env) +ProcessingResult QtGuiAdapter::processOnChainTrack(const Envelope &env) { OnChainTrackMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case OnChainTrackMessage::kLoading: loadingComponents_.insert(env.sender->value()); updateSplashProgress(); break; - default: break; + default: return ProcessingResult::Ignored; } - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processAssets(const bs::message::Envelope& env) +ProcessingResult QtGuiAdapter::processAssets(const bs::message::Envelope& env) { AssetsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case AssetsMessage::kBalance: return processBalance(msg.balance()); default: break; } - return true; + return ProcessingResult::Ignored; } void QtGuiAdapter::updateStates() @@ -678,7 +684,7 @@ void QtGuiAdapter::updateSplashProgress() c += std::to_string(cc) + " "; } logger_->debug("[{}] {}/{}", __func__, l, c);*/ - int percent = 100 * loadingComponents_.size() / createdComponents_.size(); + int percent = (int)(100 * loadingComponents_.size() / createdComponents_.size()); QMetaObject::invokeMethod(splashScreen_, [this, percent] { splashScreen_->SetProgress(percent); }); @@ -870,10 +876,10 @@ void QtGuiAdapter::onAddArmoryServer(const ArmoryServer& server) SettingsMessage msg; auto msgReq = msg.mutable_add_armory_server(); msgReq->set_network_type((int)server.netType); - msgReq->set_server_name(server.name.toStdString()); - msgReq->set_server_address(server.armoryDBIp.toStdString()); - msgReq->set_server_port(std::to_string(server.armoryDBPort)); - msgReq->set_server_key(server.armoryDBKey.toStdString()); + msgReq->set_server_name(server.name); + msgReq->set_server_address(server.armoryDBIp); + msgReq->set_server_port(server.armoryDBPort); + msgReq->set_server_key(server.armoryDBKey); msgReq->set_run_locally(server.runLocally); msgReq->set_one_way_auth(server.oneWayAuth_); msgReq->set_password(server.password.toBinStr()); @@ -894,10 +900,10 @@ void QtGuiAdapter::onUpdArmoryServer(int index, const ArmoryServer& server) msgReq->set_index(index); auto msgSrv = msgReq->mutable_server(); msgSrv->set_network_type((int)server.netType); - msgSrv->set_server_name(server.name.toStdString()); - msgSrv->set_server_address(server.armoryDBIp.toStdString()); - msgSrv->set_server_port(std::to_string(server.armoryDBPort)); - msgSrv->set_server_key(server.armoryDBKey.toStdString()); + msgSrv->set_server_name(server.name); + msgSrv->set_server_address(server.armoryDBIp); + msgSrv->set_server_port(server.armoryDBPort); + msgSrv->set_server_key(server.armoryDBKey); msgSrv->set_run_locally(server.runLocally); msgSrv->set_one_way_auth(server.oneWayAuth_); msgSrv->set_password(server.password.toBinStr()); @@ -1015,7 +1021,9 @@ void QtGuiAdapter::onNeedTXDetails(const std::vector &txWall auto request = msgReq->add_requests(); //logger_->debug("[{}] {}", __func__, txw.txHash.toHexStr()); request->set_tx_hash(txw.txHash.toBinStr()); - request->set_wallet_id(txw.walletId); + for (const auto& walletId : txw.walletIds) { + request->add_wallet_ids(walletId); + } request->set_value(txw.value); } if (!addr.empty()) { @@ -1251,7 +1259,8 @@ void QtGuiAdapter::onNeedWalletDialog(bs::signer::ui::GeneralDialogType dlgType QMetaObject::invokeMethod(mainWindow_, [this, rootId, walletName] { if (mainWindow_->deleteWallet(rootId, walletName)) { SignerMessage msg; - msg.set_delete_wallet(rootId); + auto msgReq = msg.mutable_delete_wallet(); + msgReq->set_wallet_id(rootId); pushRequest(user_, userSigner_, msg.SerializeAsString()); } }); @@ -1273,25 +1282,24 @@ void QtGuiAdapter::processWalletLoaded(const bs::sync::WalletInfo &wi) } } -bool QtGuiAdapter::processWalletData(uint64_t msgId +ProcessingResult QtGuiAdapter::processWalletData(uint64_t msgId , const WalletsMessage_WalletData& response) { const auto& itWallet = walletGetMap_.find(msgId); if (itWallet == walletGetMap_.end()) { - return true; + return ProcessingResult::Ignored; } const auto& walletId = itWallet->second; const auto& walletData = bs::sync::WalletData::fromCommonMessage(response); if (QMetaObject::invokeMethod(mainWindow_, [this, walletId, walletData] { - mainWindow_->onWalletData(walletId, walletData); - })) { + mainWindow_->onWalletData(walletId, walletData); })) { walletGetMap_.erase(itWallet); - return true; + return ProcessingResult::Success; } - return false; + return ProcessingResult::Retry; } -bool QtGuiAdapter::processWalletBalances(const bs::message::Envelope & +ProcessingResult QtGuiAdapter::processWalletBalances(const bs::message::Envelope & , const WalletsMessage_WalletBalances &response) { bs::sync::WalletBalanceData wbd; @@ -1305,16 +1313,17 @@ bool QtGuiAdapter::processWalletBalances(const bs::message::Envelope & , addrBal.tx_count(), (int64_t)addrBal.total_balance(), (int64_t)addrBal.spendable_balance() , (int64_t)addrBal.unconfirmed_balance() }); } - return QMetaObject::invokeMethod(mainWindow_, [this, wbd] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, wbd] { mainWindow_->onWalletBalance(wbd); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetailsResponse &response) +ProcessingResult QtGuiAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetailsResponse &response) { std::vector txDetails; for (const auto &resp : response.responses()) { - bs::sync::TXWalletDetails txDet{ BinaryData::fromString(resp.tx_hash()), resp.wallet_id() + bs::sync::TXWalletDetails txDet{ BinaryData::fromString(resp.tx_hash()), resp.hd_wallet_id() , resp.wallet_name(), static_cast(resp.wallet_type()) , resp.wallet_symbol(), static_cast(resp.direction()) , resp.comment(), resp.valid(), resp.amount() }; @@ -1343,9 +1352,9 @@ bool QtGuiAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetai for (const auto &inAddr : resp.input_addresses()) { try { txDet.inputAddresses.push_back({ bs::Address::fromAddressString(inAddr.address()) - , inAddr.value(), inAddr.value_string(), inAddr.wallet_name() + , inAddr.value(), inAddr.value_string(), inAddr.wallet_id(), inAddr.wallet_name() , static_cast(inAddr.script_type()) - , BinaryData::fromString(inAddr.out_hash()), inAddr.out_index() }); + , BinaryData::fromString(inAddr.out_hash()), (uint32_t)inAddr.out_index() }); } catch (const std::exception &e) { logger_->warn("[QtGuiAdapter::processTXDetails] input deser error: {}", e.what()); } @@ -1353,23 +1362,23 @@ bool QtGuiAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetai for (const auto &outAddr : resp.output_addresses()) { try { txDet.outputAddresses.push_back({ bs::Address::fromAddressString(outAddr.address()) - , outAddr.value(), outAddr.value_string(), outAddr.wallet_name() + , outAddr.value(), outAddr.value_string(), outAddr.wallet_id(), outAddr.wallet_name() , static_cast(outAddr.script_type()) - , BinaryData::fromString(outAddr.out_hash()), outAddr.out_index() }); - } catch (const std::exception &e) { // OP_RETURN data for valueStr + , BinaryData::fromString(outAddr.out_hash()), (uint32_t)outAddr.out_index() }); + } catch (const std::exception &) { // OP_RETURN data for valueStr txDet.outputAddresses.push_back({ bs::Address{} - , outAddr.value(), outAddr.address(), outAddr.wallet_name() + , outAddr.value(), outAddr.address(), outAddr.wallet_id(), outAddr.wallet_name() , static_cast(outAddr.script_type()), ownTxHash - , outAddr.out_index() }); + , (uint32_t)outAddr.out_index() }); } } try { txDet.changeAddress = { bs::Address::fromAddressString(resp.change_address().address()) , resp.change_address().value(), resp.change_address().value_string() - , resp.change_address().wallet_name() + , resp.change_address().wallet_id(), resp.change_address().wallet_name() , static_cast(resp.change_address().script_type()) , BinaryData::fromString(resp.change_address().out_hash()) - , resp.change_address().out_index() }; + , (uint32_t)resp.change_address().out_index() }; } catch (const std::exception &) {} txDetails.emplace_back(std::move(txDet)); @@ -1382,16 +1391,18 @@ bool QtGuiAdapter::processTXDetails(uint64_t msgId, const WalletsMessage_TXDetai const auto& itZC = newZCs_.find(msgId); if (itZC != newZCs_.end()) { newZCs_.erase(itZC); - return QMetaObject::invokeMethod(mainWindow_, [this, txDetails] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, txDetails] { mainWindow_->onNewZCs(txDetails); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } - return QMetaObject::invokeMethod(mainWindow_, [this, txDetails] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, txDetails] { mainWindow_->onTXDetails(txDetails); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processLedgerEntries(const LedgerEntries &response) +ProcessingResult QtGuiAdapter::processLedgerEntries(const LedgerEntries &response) { std::vector entries; for (const auto &entry : response.entries()) { @@ -1415,15 +1426,16 @@ bool QtGuiAdapter::processLedgerEntries(const LedgerEntries &response) } entries.push_back(std::move(txEntry)); } - return QMetaObject::invokeMethod(mainWindow_, [this, entries, filter=response.filter() + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, entries, filter=response.filter() , totPages=response.total_pages(), curPage=response.cur_page() , curBlock=response.cur_block()] { mainWindow_->onLedgerEntries(filter, totPages, curPage, curBlock, entries); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processAddressHist(const ArmoryMessage_AddressHistory& response) +ProcessingResult QtGuiAdapter::processAddressHist(const ArmoryMessage_AddressHistory& response) { bs::Address addr; try { @@ -1431,7 +1443,7 @@ bool QtGuiAdapter::processAddressHist(const ArmoryMessage_AddressHistory& respon } catch (const std::exception& e) { logger_->error("[{}] invalid address: {}", __func__, e.what()); - return true; + return ProcessingResult::Error; } std::vector entries; for (const auto& entry : response.entries()) { @@ -1455,35 +1467,38 @@ bool QtGuiAdapter::processAddressHist(const ArmoryMessage_AddressHistory& respon } entries.push_back(std::move(txEntry)); } - return QMetaObject::invokeMethod(mainWindow_, [this, entries, addr, curBlock = response.cur_block()] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, entries, addr, curBlock = response.cur_block()] { mainWindow_->onAddressHistory(addr, curBlock, entries); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processFeeLevels(const ArmoryMessage_FeeLevelsResponse& response) +ProcessingResult QtGuiAdapter::processFeeLevels(const ArmoryMessage_FeeLevelsResponse& response) { std::map feeLevels; for (const auto& pair : response.fee_levels()) { feeLevels[pair.level()] = pair.fee(); } - return QMetaObject::invokeMethod(mainWindow_, [this, feeLevels]{ + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, feeLevels]{ mainWindow_->onFeeLevels(feeLevels); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processWalletsList(const WalletsMessage_WalletsListResponse& response) +ProcessingResult QtGuiAdapter::processWalletsList(const WalletsMessage_WalletsListResponse& response) { std::vector wallets; for (const auto& wallet : response.wallets()) { wallets.push_back(bs::sync::HDWalletData::fromCommonMessage(wallet)); } - return QMetaObject::invokeMethod(mainWindow_, [this, wallets, id = response.id()]{ + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, wallets, id = response.id()]{ mainWindow_->onWalletsList(id, wallets); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processUTXOs(const WalletsMessage_UtxoListResponse& response) +ProcessingResult QtGuiAdapter::processUTXOs(const WalletsMessage_UtxoListResponse& response) { std::vector utxos; for (const auto& serUtxo : response.utxos()) { @@ -1491,56 +1506,59 @@ bool QtGuiAdapter::processUTXOs(const WalletsMessage_UtxoListResponse& response) utxo.unserialize(BinaryData::fromString(serUtxo)); utxos.push_back(std::move(utxo)); } - return QMetaObject::invokeMethod(mainWindow_, [this, utxos, response]{ + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, utxos, response]{ mainWindow_->onUTXOs(response.id(), response.wallet_id(), utxos); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse& response) +ProcessingResult QtGuiAdapter::processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse& response) { - return QMetaObject::invokeMethod(mainWindow_, [this, response] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, response] { mainWindow_->onSignedTX(response.id(), BinaryData::fromString(response.signed_tx()) , static_cast(response.error_code())); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived& zcs) +ProcessingResult QtGuiAdapter::processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived& zcs) { WalletsMessage msg; auto msgReq = msg.mutable_tx_details_request(); for (const auto& zcEntry : zcs.tx_entries()) { auto txReq = msgReq->add_requests(); txReq->set_tx_hash(zcEntry.tx_hash()); - if (zcEntry.wallet_ids_size() > 0) { - txReq->set_wallet_id(zcEntry.wallet_ids(0)); + for (const auto& walletId : zcEntry.wallet_ids()) { + txReq->add_wallet_ids(walletId); } txReq->set_value(zcEntry.value()); } const auto msgId = pushRequest(user_, userWallets_, msg.SerializeAsString()); if (!msgId) { - return false; + return ProcessingResult::Error; } newZCs_.insert(msgId); - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processZCInvalidated(const ArmoryMessage_ZCInvalidated& zcInv) +ProcessingResult QtGuiAdapter::processZCInvalidated(const ArmoryMessage_ZCInvalidated& zcInv) { std::vector txHashes; for (const auto& hashStr : zcInv.tx_hashes()) { txHashes.push_back(BinaryData::fromString(hashStr)); } - return QMetaObject::invokeMethod(mainWindow_, [this, txHashes] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, txHashes] { mainWindow_->onZCsInvalidated(txHashes); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processBsServer(const bs::message::Envelope& env) +ProcessingResult QtGuiAdapter::processBsServer(const bs::message::Envelope& env) { BsServerMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case BsServerMessage::kStartLoginResult: @@ -1549,18 +1567,19 @@ bool QtGuiAdapter::processBsServer(const bs::message::Envelope& env) return processLogin(msg.login_result()); default: break; } - return true; + return ProcessingResult::Ignored; } -bool QtGuiAdapter::processStartLogin(const BsServerMessage_StartLoginResult& response) +ProcessingResult QtGuiAdapter::processStartLogin(const BsServerMessage_StartLoginResult& response) { - return QMetaObject::invokeMethod(mainWindow_, [this, response] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, response] { mainWindow_->onLoginStarted(response.login(), response.success() , response.error_text()); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processLogin(const BsServerMessage_LoginResult& response) +ProcessingResult QtGuiAdapter::processLogin(const BsServerMessage_LoginResult& response) { #if 0 result.login = response.login(); @@ -1583,16 +1602,16 @@ bool QtGuiAdapter::processLogin(const BsServerMessage_LoginResult& response) mainWindow_->onLoggedIn(result); }); #else - return true; + return ProcessingResult::Ignored; #endif 0 } -bool QtGuiAdapter::processMatching(const bs::message::Envelope& env) +ProcessingResult QtGuiAdapter::processMatching(const bs::message::Envelope& env) { MatchingMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MatchingMessage::kLoggedIn: @@ -1603,22 +1622,23 @@ bool QtGuiAdapter::processMatching(const bs::message::Envelope& env) }); #endif case MatchingMessage::kLoggedOut: - return QMetaObject::invokeMethod(mainWindow_, [this] { + QMetaObject::invokeMethod(mainWindow_, [this] { //mainWindow_->onMatchingLogout(); }); + return ProcessingResult::Success; /* case MatchingMessage::kQuoteNotif: return processQuoteNotif(msg.quote_notif());*/ default: break; } - return true; + return ProcessingResult::Ignored; } -bool QtGuiAdapter::processMktData(const bs::message::Envelope& env) +ProcessingResult QtGuiAdapter::processMktData(const bs::message::Envelope& env) { MktDataMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MktDataMessage::kConnected: @@ -1626,29 +1646,29 @@ bool QtGuiAdapter::processMktData(const bs::message::Envelope& env) case MktDataMessage::kDisconnected: mdInstrumentsReceived_ = false; //return QMetaObject::invokeMethod(mainWindow_, [this] { mainWindow_->onMDDisconnected(); }); - return true; + return ProcessingResult::Success; case MktDataMessage::kNewSecurity: return processSecurity(msg.new_security().name(), msg.new_security().asset_type()); case MktDataMessage::kAllInstrumentsReceived: mdInstrumentsReceived_ = true; - return true; // sendPooledOrdersUpdate(); + return ProcessingResult::Success; // sendPooledOrdersUpdate(); case MktDataMessage::kPriceUpdate: return processMdUpdate(msg.price_update()); default: break; } - return true; + return ProcessingResult::Ignored; } -bool QtGuiAdapter::processSecurity(const std::string& name, int assetType) +ProcessingResult QtGuiAdapter::processSecurity(const std::string& name, int assetType) { const auto &at = static_cast(assetType); assetTypes_[name] = at; - return true; + return ProcessingResult::Success; } -bool QtGuiAdapter::processMdUpdate(const MktDataMessage_Prices& msg) +ProcessingResult QtGuiAdapter::processMdUpdate(const MktDataMessage_Prices& msg) { - return QMetaObject::invokeMethod(mainWindow_, [this, msg] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, msg] { const bs::network::MDFields fields{ { bs::network::MDField::Type::PriceBid, msg.bid() }, { bs::network::MDField::Type::PriceOffer, msg.ask() }, @@ -1659,16 +1679,18 @@ bool QtGuiAdapter::processMdUpdate(const MktDataMessage_Prices& msg) mainWindow_->onMDUpdated(static_cast(msg.security().asset_type()) , QString::fromStdString(msg.security().name()), fields); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processBalance(const AssetsMessage_Balance& bal) +ProcessingResult QtGuiAdapter::processBalance(const AssetsMessage_Balance& bal) { - return QMetaObject::invokeMethod(mainWindow_, [this, bal] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, bal] { mainWindow_->onBalance(bal.currency(), bal.value()); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } -bool QtGuiAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& response) +ProcessingResult QtGuiAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& response) { std::vector utxos; for (const auto& utxoSer : response.utxos()) { @@ -1676,9 +1698,10 @@ bool QtGuiAdapter::processReservedUTXOs(const WalletsMessage_ReservedUTXOs& resp utxo.unserialize(BinaryData::fromString(utxoSer)); utxos.push_back(std::move(utxo)); } - return QMetaObject::invokeMethod(mainWindow_, [this, response, utxos] { + const bool rc = QMetaObject::invokeMethod(mainWindow_, [this, response, utxos] { mainWindow_->onReservedUTXOs(response.id(), response.sub_id(), utxos); }); + return rc ? ProcessingResult::Success : ProcessingResult::Retry; } #include "QtGuiAdapter.moc" diff --git a/GUI/QtWidgets/QtGuiAdapter.h b/GUI/QtWidgets/QtGuiAdapter.h index 9ac8fd566..c0518b71e 100644 --- a/GUI/QtWidgets/QtGuiAdapter.h +++ b/GUI/QtWidgets/QtGuiAdapter.h @@ -77,7 +77,7 @@ class QtGuiAdapter : public QObject, public ApiBusAdapter, public bs::MainLoopRu QtGuiAdapter(const std::shared_ptr &); ~QtGuiAdapter() override; - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope&) override; Users supportedReceivers() const override { return { user_ }; } @@ -86,16 +86,16 @@ class QtGuiAdapter : public QObject, public ApiBusAdapter, public bs::MainLoopRu void run(int &argc, char **argv) override; private: - bool processSettings(const bs::message::Envelope &); - bool processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - bool processSettingsState(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); - bool processArmoryServers(const BlockSettle::Terminal::SettingsMessage_ArmoryServers&); - bool processAdminMessage(const bs::message::Envelope &); - bool processBlockchain(const bs::message::Envelope &); - bool processSigner(const bs::message::Envelope &); - bool processWallets(const bs::message::Envelope &); - bool processOnChainTrack(const bs::message::Envelope &); - bool processAssets(const bs::message::Envelope&); + bs::message::ProcessingResult processSettings(const bs::message::Envelope &); + bs::message::ProcessingResult processSettingsGetResponse(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + bs::message::ProcessingResult processSettingsState(const BlockSettle::Terminal::SettingsMessage_SettingsResponse&); + bs::message::ProcessingResult processArmoryServers(const BlockSettle::Terminal::SettingsMessage_ArmoryServers&); + bs::message::ProcessingResult processAdminMessage(const bs::message::Envelope &); + bs::message::ProcessingResult processBlockchain(const bs::message::Envelope &); + bs::message::ProcessingResult processSigner(const bs::message::Envelope &); + bs::message::ProcessingResult processWallets(const bs::message::Envelope &); + bs::message::ProcessingResult processOnChainTrack(const bs::message::Envelope &); + bs::message::ProcessingResult processAssets(const bs::message::Envelope&); void requestInitialSettings(); void updateSplashProgress(); @@ -106,30 +106,30 @@ class QtGuiAdapter : public QObject, public ApiBusAdapter, public bs::MainLoopRu void makeMainWinConnections(); void processWalletLoaded(const bs::sync::WalletInfo &); - bool processWalletData(const uint64_t msgId + bs::message::ProcessingResult processWalletData(const uint64_t msgId , const BlockSettle::Common::WalletsMessage_WalletData&); - bool processWalletBalances(const bs::message::Envelope & + bs::message::ProcessingResult processWalletBalances(const bs::message::Envelope & , const BlockSettle::Common::WalletsMessage_WalletBalances &); - bool processTXDetails(uint64_t msgId, const BlockSettle::Common::WalletsMessage_TXDetailsResponse &); - bool processLedgerEntries(const BlockSettle::Common::LedgerEntries &); - bool processAddressHist(const BlockSettle::Common::ArmoryMessage_AddressHistory&); - bool processFeeLevels(const BlockSettle::Common::ArmoryMessage_FeeLevelsResponse&); - bool processWalletsList(const BlockSettle::Common::WalletsMessage_WalletsListResponse&); - bool processUTXOs(const BlockSettle::Common::WalletsMessage_UtxoListResponse&); - bool processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse&); - bool processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived&); - bool processZCInvalidated(const BlockSettle::Common::ArmoryMessage_ZCInvalidated&); - - bool processBsServer(const bs::message::Envelope&); - bool processStartLogin(const BlockSettle::Terminal::BsServerMessage_StartLoginResult&); - bool processLogin(const BlockSettle::Terminal::BsServerMessage_LoginResult&); - - bool processMatching(const bs::message::Envelope&); - bool processMktData(const bs::message::Envelope&); - bool processSecurity(const std::string&, int); - bool processMdUpdate(const BlockSettle::Terminal::MktDataMessage_Prices &); - bool processBalance(const BlockSettle::Terminal::AssetsMessage_Balance&); - bool processReservedUTXOs(const BlockSettle::Common::WalletsMessage_ReservedUTXOs&); + bs::message::ProcessingResult processTXDetails(uint64_t msgId, const BlockSettle::Common::WalletsMessage_TXDetailsResponse &); + bs::message::ProcessingResult processLedgerEntries(const BlockSettle::Common::LedgerEntries &); + bs::message::ProcessingResult processAddressHist(const BlockSettle::Common::ArmoryMessage_AddressHistory&); + bs::message::ProcessingResult processFeeLevels(const BlockSettle::Common::ArmoryMessage_FeeLevelsResponse&); + bs::message::ProcessingResult processWalletsList(const BlockSettle::Common::WalletsMessage_WalletsListResponse&); + bs::message::ProcessingResult processUTXOs(const BlockSettle::Common::WalletsMessage_UtxoListResponse&); + bs::message::ProcessingResult processSignTX(const BlockSettle::Common::SignerMessage_SignTxResponse&); + bs::message::ProcessingResult processZC(const BlockSettle::Common::ArmoryMessage_ZCReceived&); + bs::message::ProcessingResult processZCInvalidated(const BlockSettle::Common::ArmoryMessage_ZCInvalidated&); + + bs::message::ProcessingResult processBsServer(const bs::message::Envelope&); + bs::message::ProcessingResult processStartLogin(const BlockSettle::Terminal::BsServerMessage_StartLoginResult&); + bs::message::ProcessingResult processLogin(const BlockSettle::Terminal::BsServerMessage_LoginResult&); + + bs::message::ProcessingResult processMatching(const bs::message::Envelope&); + bs::message::ProcessingResult processMktData(const bs::message::Envelope&); + bs::message::ProcessingResult processSecurity(const std::string&, int); + bs::message::ProcessingResult processMdUpdate(const BlockSettle::Terminal::MktDataMessage_Prices &); + bs::message::ProcessingResult processBalance(const BlockSettle::Terminal::AssetsMessage_Balance&); + bs::message::ProcessingResult processReservedUTXOs(const BlockSettle::Common::WalletsMessage_ReservedUTXOs&); private slots: void onGetSettings(const std::vector&); diff --git a/README-dockerfile.md b/README-dockerfile.md new file mode 100644 index 000000000..79fc5d793 --- /dev/null +++ b/README-dockerfile.md @@ -0,0 +1,11 @@ +# How to run Ubuntu build in docker +## Run this command in terminal repo folder +``` +docker build . -t bsterminal:latest -f ubuntu.Dockerfile +``` +## Get bs terminal app (bsterminal) from docker container image +``` +docker create --name=terminal bsterminal:latest +docker cp terminal:/app/build_terminal/RelWithDebInfo/bin/blocksettle bsterminal +docker rm terminal +``` diff --git a/README.md b/README.md index cb926dab4..545863f56 100644 --- a/README.md +++ b/README.md @@ -43,9 +43,9 @@ This section is for those who are interested in running Terminal application for The BlockSettle terminal is available for download for Microsoft Windows 7 and Above, which meets the following minimum system requirements. To get started visit our [Download](http://blocksettle.com/downloads/terminal) Page, and download the windows or macOS binary file that matches your operating system. ### Operating System Requirements. -- Linux - [Ubuntu](https://www.ubuntu.com/) (16.04 LTS/18.04 LTS/18.10/19.04) +- Linux - [Ubuntu](https://www.ubuntu.com/) (20.04 LTS/22.04 LTS) - [macOS](https://www.apple.com/macos/) (10.12 or higher) -- [Windows](https://www.microsoft.com/en-us/windows) (Windows 7 or higher) +- [Windows](https://www.microsoft.com/en-us/windows) (Windows 10 or higher) ### Hardware Requirements. * Dual Core CPU @@ -112,15 +112,14 @@ For access to trade our FX and XBT products please upgrade your account to Tradi 4. Check your home directory for spaces, which aren't allowed. For example, `C:\Satoshi Nakamoto` won't work. (`C:\Satoshi` would be okay.) If your home directory has a space in it, add the `DEV_3RD_ROOT` environment variable to Windows, as seen in the ["Terminal prerequisites"](#terminal-prerequisites) section. - 5. Click the Start button and select the `x64 Native Tools Command Prompt for VS 2017` program. You may have to type the name until the option appears. It is *critical* that you type `x64` and *not* `x86`. + 5. Click the Start button and select the `x64 Native Tools Command Prompt for VS 2022` program. You may have to type the name until the option appears. It is *critical* that you type `x64` and *not* `x86`. ## Ubuntu - 1. Open Software Updates -> Ubuntu Software. Set the "Source Code" checkbox (required for `qt5-default`). - - 2. Execute the following commands. + Execute the following commands: - sudo apt install python-pip cmake libmysqlclient-dev autoconf libtool yasm nasm g++ - sudo apt build-dep qt5-default + sudo apt install python-pip cmake libmysqlclient-dev autoconf libtool yasm nasm libgmp3-dev libdouble-conversion-dev + sudo apt install qttools5-dev-tools libfreetype-dev libfontconfig-dev libcups2-dev xcb libudev-dev libxi-dev libsm-dev libxrender-dev libdbus-1-dev + sudo apt install libx11-xcb-dev libxcb-xkb-dev libxcb-xinput-dev libxcb-sync-dev libxcb-render-util0-dev libxcb-xfixes0-dev libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-icccm4-dev libxcb-glx0-dev libxkbcommon-x11-dev ### MacOS 1. Get an Apple developer account (free), log in, and download the latest version of `Command Line Tools for Xcode`. As an alternative, install the latest version of [Xcode](https://itunes.apple.com/us/app/xcode/id497799835) and download `Command Line Tools` via Xcode. Either choice will be updated via the App Store. diff --git a/UnitTests/CMakeLists.txt b/UnitTests/CMakeLists.txt index c7adb7c63..34da09f6c 100644 --- a/UnitTests/CMakeLists.txt +++ b/UnitTests/CMakeLists.txt @@ -22,27 +22,28 @@ LIST (APPEND SOURCES ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BDM_mainthread.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BDM_Server.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BitcoinP2P.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/Blockchain.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainScanner.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainScanner_Super.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockDataMap.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/Blockchain.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/BlockchainScanner.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/BlockchainScanner_Super.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/BlockDataMap.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockDataViewer.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockObj.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockUtils.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/BlockObj.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/BlockUtils.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BtcWallet.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/DatabaseBuilder.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/DatabaseBuilder.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/HistoryPager.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/LedgerEntry.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/nodeRPC.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/Progress.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/ScrAddrFilter.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/ScrAddrFilter.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/ScrAddrObj.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/Server.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/SshParser.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/StoredBlockObj.cpp - ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/txio.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/SshParser.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/StoredBlockObj.cpp + ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/BlockchainDatabase/txio.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/ZeroConf.cpp ${TERMINAL_GUI_ROOT}/common/ArmoryDB/cppForSwig/gtest/NodeUnitTest.cpp + ${TERMINAL_GUI_ROOT}/common/WalletsLib/WalletBackupFile.cpp ) INCLUDE_DIRECTORIES( ${BLOCKSETTLE_UI_INCLUDE_DIR} ) diff --git a/UnitTests/MockTerminal.cpp b/UnitTests/MockTerminal.cpp index 1ff4ba799..16058a05b 100644 --- a/UnitTests/MockTerminal.cpp +++ b/UnitTests/MockTerminal.cpp @@ -87,20 +87,20 @@ class SettingsMockAdapter : public bs::message::Adapter ArmoryMessage msgReply; auto msgResp = msgReply.mutable_settings_response(); ArmorySettings armorySettings; - armorySettings.name = QLatin1Literal("test"); + armorySettings.name = "test"; armorySettings.netType = NetworkType::TestNet; - armorySettings.armoryDBIp = QLatin1String("127.0.0.1"); - armorySettings.armoryDBPort = 82; + armorySettings.armoryDBIp = "127.0.0.1"; + armorySettings.armoryDBPort = "82"; msgResp->set_socket_type(armorySettings.socketType); msgResp->set_network_type((int)armorySettings.netType); - msgResp->set_host(armorySettings.armoryDBIp.toStdString()); - msgResp->set_port(std::to_string(armorySettings.armoryDBPort)); - msgResp->set_bip15x_key(armorySettings.armoryDBKey.toStdString()); + msgResp->set_host(armorySettings.armoryDBIp); + msgResp->set_port(armorySettings.armoryDBPort); + msgResp->set_bip15x_key(armorySettings.armoryDBKey); msgResp->set_run_locally(armorySettings.runLocally); - msgResp->set_data_dir(armorySettings.dataDir.toStdString()); - msgResp->set_executable_path(armorySettings.armoryExecutablePath.toStdString()); - msgResp->set_bitcoin_dir(armorySettings.bitcoinBlocksDir.toStdString()); - msgResp->set_db_dir(armorySettings.dbDir.toStdString()); + msgResp->set_data_dir(armorySettings.dataDir); + msgResp->set_executable_path(armorySettings.armoryExecutablePath); + msgResp->set_bitcoin_dir(armorySettings.bitcoinBlocksDir); + msgResp->set_db_dir(armorySettings.dbDir); pushResponse(user_, env, msgReply.SerializeAsString()); return true; } @@ -108,13 +108,13 @@ class SettingsMockAdapter : public bs::message::Adapter return false; } - bool process(const bs::message::Envelope& env) override + ProcessingResult process(const bs::message::Envelope& env) override { if (env.receiver->value() == TerminalUsers::Settings) { SettingsMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse settings msg #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case SettingsMessage::kGetRequest: @@ -143,7 +143,7 @@ class SettingsMockAdapter : public bs::message::Adapter default: break; } } - return true; + return ProcessingResult::Ignored; } bs::message::Adapter::Users supportedReceivers() const override @@ -156,7 +156,7 @@ class SettingsMockAdapter : public bs::message::Adapter // std::shared_ptr createOnChainPlug() const; private: - bool processGetRequest(const bs::message::Envelope& env + ProcessingResult processGetRequest(const bs::message::Envelope& env , const BlockSettle::Terminal::SettingsMessage_SettingsRequest& request) { SettingsMessage msg; @@ -175,9 +175,9 @@ class SettingsMockAdapter : public bs::message::Adapter } } if (msgResp->responses_size()) { - return pushResponse(user_, env, msg.SerializeAsString()); + pushResponse(user_, env, msg.SerializeAsString()); } - return true; + return ProcessingResult::Success; } private: @@ -192,7 +192,7 @@ class ApiMockAdapter : public bs::message::Adapter ApiMockAdapter() {} ~ApiMockAdapter() override = default; - bool process(const bs::message::Envelope&) override { return true; } + ProcessingResult process(const bs::message::Envelope&) override { return ProcessingResult::Ignored; } bool processBroadcast(const bs::message::Envelope&) override { return false; } bs::message::Adapter::Users supportedReceivers() const override { diff --git a/UnitTests/TestAdapters.cpp b/UnitTests/TestAdapters.cpp index 46350581a..16bee1bd0 100644 --- a/UnitTests/TestAdapters.cpp +++ b/UnitTests/TestAdapters.cpp @@ -27,7 +27,7 @@ using namespace BlockSettle::Terminal; constexpr auto kExpirationTimeout = std::chrono::seconds{ 5 }; -bool TestSupervisor::process(const Envelope &env) +ProcessingResult TestSupervisor::process(const Envelope &env) { #ifdef MSG_DEBUGGING StaticLogger::loggerPtr->debug("[{}] {}{}: {}({}) -> {}({}), {} bytes" @@ -48,7 +48,7 @@ bool TestSupervisor::process(const Envelope &env) filtersToDelete.push_back(seqNo); break; } - return false; + return ProcessingResult::Success; } } for (const auto &filter : filterWait_) { @@ -65,10 +65,10 @@ bool TestSupervisor::process(const Envelope &env) filterMap_.erase(seqNo); filterWait_.erase(seqNo); } - return false; + return ProcessingResult::Success; } } - return true; + return ProcessingResult::Ignored; } bs::message::SeqId TestSupervisor::send(bs::message::TerminalUsers sender, bs::message::TerminalUsers receiver @@ -124,13 +124,13 @@ MatchingMock::MatchingMock(const std::shared_ptr& logger , userSettl_(UserTerminal::create(TerminalUsers::Settlement)) {} -bool MatchingMock::process(const bs::message::Envelope& env) +ProcessingResult MatchingMock::process(const bs::message::Envelope& env) { if (env.isRequest() && (env.receiver->value() == user_->value())) { MatchingMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own request #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case MatchingMessage::kSendRfq: @@ -184,7 +184,8 @@ bool MatchingMock::process(const bs::message::Envelope& env) else { match.second.order = fromMsg(msg.order()); } - return pushResponse(user_, userSettl_, msg.SerializeAsString()); + pushResponse(user_, userSettl_, msg.SerializeAsString()); + return ProcessingResult::Success; } } } @@ -212,27 +213,31 @@ bool MatchingMock::process(const bs::message::Envelope& env) BsServerMessage msg; if (!msg.ParseFromString(env.message)) { logger_->error("[{}] failed to parse own request #{}", __func__, env.foreignId()); - return true; + return ProcessingResult::Error; } switch (msg.data_case()) { case BsServerMessage::kSendUnsignedPayin: - return processUnsignedPayin(msg.send_unsigned_payin()); + return processUnsignedPayin(msg.send_unsigned_payin()) ? + ProcessingResult::Success : ProcessingResult::Error; case BsServerMessage::kSendSignedPayin: - return processSignedTX(msg.send_signed_payin(), true, true); + return processSignedTX(msg.send_signed_payin(), true, true) ? + ProcessingResult::Success : ProcessingResult::Error; case BsServerMessage::kSendSignedPayout: - return processSignedTX(msg.send_signed_payout(), false, true); - default: break; + return processSignedTX(msg.send_signed_payout(), false, true) ? + ProcessingResult::Success : ProcessingResult::Error; + default: return ProcessingResult::Ignored; } } else if (env.receiver->value() == env.sender->value() && (env.sender->value() == user_->value())) { //own to self for (const auto& match : matches_) { if (match.second.quote.quoteId == env.message) { - return sendPendingOrder(match.first); + return sendPendingOrder(match.first) ? ProcessingResult::Success + : ProcessingResult::Error; } } } - return true; + return ProcessingResult::Success; } bool MatchingMock::processBroadcast(const bs::message::Envelope&) diff --git a/UnitTests/TestAdapters.h b/UnitTests/TestAdapters.h index 657ada321..ecf6a8561 100644 --- a/UnitTests/TestAdapters.h +++ b/UnitTests/TestAdapters.h @@ -39,16 +39,16 @@ class TestSupervisor : public bs::message::Adapter TestSupervisor(const std::string& name) : name_(name) {} - bool process(const bs::message::Envelope &) override; + bs::message::ProcessingResult process(const bs::message::Envelope &) override; bool processBroadcast(const bs::message::Envelope& env) override { - return process(env); + return (process(env) != bs::message::ProcessingResult::Ignored); } bs::message::Adapter::Users supportedReceivers() const override { - return { std::make_shared() }; + return { std::make_shared() }; } std::string name() const override { return "sup" + name_; } @@ -79,7 +79,7 @@ class MatchingMock : public bs::message::Adapter , const std::string& name, const std::string& email , const std::shared_ptr &); // for pushing ZCs (mocking PB) - bool process(const bs::message::Envelope&) override; + bs::message::ProcessingResult process(const bs::message::Envelope&) override; bool processBroadcast(const bs::message::Envelope&) override; bs::message::Adapter::Users supportedReceivers() const override diff --git a/UnitTests/TestArmory.cpp b/UnitTests/TestArmory.cpp index 6b2a195c7..a752d5b80 100644 --- a/UnitTests/TestArmory.cpp +++ b/UnitTests/TestArmory.cpp @@ -129,3 +129,16 @@ TEST(TestArmory, CrashOnNonExistentHashInTxBatch) armoryConn->getTXsByHash({ nonExHash }, cbTXs, true); EXPECT_TRUE(fut.get()); } + +#include "common.pb.h" +using namespace BlockSettle::Common; + +TEST(MessageBus, timed_out) +{ + ArmoryMessage msg; + + const auto& msg1 = BinaryData::CreateFromHex("8a020e0a0c0204060c18309001f803f007"); + + ASSERT_TRUE(msg.ParseFromString(msg1.toBinStr())); + StaticLogger::loggerPtr->debug(msg.DebugString()); +} diff --git a/UnitTests/TestCommon.cpp b/UnitTests/TestCommon.cpp index 61f09ff62..3ef7198fa 100644 --- a/UnitTests/TestCommon.cpp +++ b/UnitTests/TestCommon.cpp @@ -28,6 +28,7 @@ #include "CacheFile.h" #include "CurrencyPair.h" #include "EasyCoDec.h" +#include "Message/Worker.h" #include "Wallets/HeadlessContainer.h" #include "Wallets/InprocSigner.h" #include "MDCallbacksQt.h" @@ -425,3 +426,157 @@ TEST(TestCommon, PriceAmount) EXPECT_EQ(bs::CentAmount(0.12345).to_string(), "0.12"); EXPECT_EQ(bs::CentAmount(-0.12345).to_string(), "0.12"); } + +TEST(TestCommon, Workers) +{ + struct DataIn1 : public bs::InData + { + ~DataIn1() override = default; + std::string message; + }; + struct DataOut1 : public bs::OutData + { + ~DataOut1() override = default; + std::string message; + }; + struct DataIn2 : public bs::InData + { + ~DataIn2() override = default; + std::string message; + }; + struct DataOut2 : public bs::OutData + { + ~DataOut2() override = default; + std::string message; + }; + + class Handler1 : public bs::HandlerImpl + { + protected: + std::shared_ptr processData(const std::shared_ptr& in) override + { + DataOut1 out; + out.message = in->message; + for (auto& c : out.message) { + c = std::tolower(c); + } + out.message = "h1: " + out.message; + return std::make_shared(out); + } + }; + + class Handler2 : public bs::HandlerImpl + { + protected: + std::shared_ptr processData(const std::shared_ptr& in) override + { + DataOut2 out; + out.message = in->message; + for (auto& c : out.message) { + c = std::toupper(c); + } + out.message = "h2: " + out.message; + return std::make_shared(out); + } + }; + + class TestWorkerMgr : public bs::WorkerPool + { + public: + TestWorkerMgr() : bs::WorkerPool() {} + + void test1() + { + DataIn1 data; + data.message = "TEST1 message"; + const auto& inData = std::make_shared(data); + + const auto& cb = [](const std::shared_ptr& result) + { + auto data = std::static_pointer_cast(result); + StaticLogger::loggerPtr->debug("[TestWorkerMgr::test1] {}", data ? data->message : "null"); + ASSERT_NE(data, nullptr); + EXPECT_EQ(data->message, "h1: test1 message"); + }; + processQueued(inData, cb); + } + + void test2() + { + DataIn2 data; + data.message = "TEST2 message"; + const auto& inData = std::make_shared(data); + + const auto& cb = [](const std::shared_ptr& result) + { + auto data = std::static_pointer_cast(result); + StaticLogger::loggerPtr->debug("[TestWorkerMgr::test2] {}", data ? data->message : "null"); + ASSERT_NE(data, nullptr); + EXPECT_EQ(data->message, "h2: TEST2 MESSAGE"); + }; + processQueued(inData, cb); + } + + void testNested() + { + DataIn1 data1; + data1.message = "NESTED message"; + const auto& inData1 = std::make_shared(data1); + + const auto& cb1 = [](const std::shared_ptr& result) + { + auto data = std::static_pointer_cast(result); + StaticLogger::loggerPtr->debug("[TestWorkerMgr::nested1] {}", data ? data->message : "null"); + ASSERT_NE(data, nullptr); + EXPECT_EQ(data->message, "h1: nested message"); + }; + + DataIn2 data2; + data2.message = "NORMAL message"; + const auto& inData2 = std::make_shared(data2); + + const auto& cb2 = [this, cb1, inData1] + (const std::shared_ptr& result) + { + auto data = std::static_pointer_cast(result); + StaticLogger::loggerPtr->debug("[TestWorkerMgr::nested2] {}", data ? data->message : "null"); + ASSERT_NE(data, nullptr); + EXPECT_EQ(data->message, "h2: NORMAL MESSAGE"); + processQueued(inData1, cb1); + }; + processQueued(inData2, cb2); + } + + protected: + std::shared_ptr worker(const std::shared_ptr&) override + { + const std::vector> handlers{ + std::make_shared(), std::make_shared() }; + return std::make_shared(handlers); + } + }; + + TestWorkerMgr wm; + for (int i = 0; i < 5; ++i) { + wm.test1(); + wm.test2(); + wm.testNested(); + } + StaticLogger::loggerPtr->debug("{} thread[s] used", wm.nbThreads()); + while (!wm.finished()) { + std::this_thread::sleep_for(std::chrono::milliseconds{ 1 }); + } +} + +#include "common.pb.h" +using namespace BlockSettle::Common; +TEST(TestCommon, unparsable_proto) +{ + const auto& data = "fa01f0060aed060a200a95d6ee27cdb6c5cf56e052cfb826d5a383f64bea54f5adef4f20c45f700443300140014a0a302e3030303031353030521433dd7099a58f7730c4c57eda6cb30c0c4ec27e1b5a8505010000000001011a50e276d80729cf66f5ba49e3d0d1319e84073efef579995b3bdd45edc27da70000000000fdffffff01dc050000000000002251206059a08c63741bce44793c30d941723a7157c19b97681d531f6af35f5de4a16f03403dfec38046483d2c520754c4e61124463ab58d701a85ecd21083aefd93a4dac9434414cd0a197b52d3531e55e1956a6492f2fbfb297b08d06dcf3061424ffaa6fdbe012082c9e496d987a3a64d948cd10bc083472d6b8edf609863251c4d463511942388ac0063036f7264010109696d6167652f706e67004d850189504e470d0a1a0a0000000d4948445200000018000000180806000000e0773df8000000017352474200aece1ce90000000467414d410000b18f0bfc6105000000097048597300000ec300000ec301c76fa8640000011a49444154484be591b10dc2301045b3040b50b205d414f4cc1010743474200a2a6acac018f4acc00ad4144848c63fe2c371b9d804990a4b4f8e63df7f17279b1557f74bfe44b0c8327730b0ce6aa282ba7062d54882825838b16a4963c171973fe13bab9654040825323884ce9004bf20f3c5b18133562d890a701518addeb9a41c103fe4c904e3cded25184dd20bea46124188a402ac491201d012492c1c4405404ad6d37ec927e1207a454476be5f0ddff6ac5a620a50d4f1e49eb9c79daa12acf17ee9e790a422c0e1ae67e0d93e80cc5d5e9232dcafb98fb375125380ae59887006a1630463d6fb5f0950acbfa46ebf91809de119c5ba535ca15c63bf91401663d69db6d5bab18057c0623c0306c97f8435cfeaac59717577f032db66d32fac780000000049454e44ae4260826821c182c9e496d987a3a64d948cd10bc083472d6b8edf609863251c4d4635119423880000000060a1ee2f6a4a0a14fd9208e542e2001907786b18c232c15f18cacd8518cc16220b2d302e3030303032383932280532201a50e276d80729cf66f5ba49e3d0d1319e84073efef579995b3bdd45edc27da7724b0a1433dd7099a58f7730c4c57eda6cb30c0c4ec27e1b18dc0b220a302e30303030313530302805322251206059a08c63741bce44793c30d941723a7157c19b97681d531f6af35f5de4a16f"; + const auto binData = BinaryData::CreateFromHex(data); + ASSERT_FALSE(binData.empty()); + WalletsMessage msg; + EXPECT_TRUE(msg.ParseFromString(binData.toBinStr())); + StaticLogger::loggerPtr->debug("[{}] data case: {}\n{}", __func__ + , msg.data_case(), msg.DebugString()); +} \ No newline at end of file diff --git a/UnitTests/TestEnv.cpp b/UnitTests/TestEnv.cpp index 675de4822..ec9f3e603 100644 --- a/UnitTests/TestEnv.cpp +++ b/UnitTests/TestEnv.cpp @@ -102,37 +102,34 @@ void TestEnv::requireArmory(bool waitForReady) armoryInstance_ = std::make_shared(); auto armoryConnection = std::make_shared( - armoryInstance_, logger_, "", false); + armoryInstance_, logger_, ""); ArmorySettings settings; settings.runLocally = false; settings.socketType = appSettings()->GetArmorySocketType(); settings.netType = NetworkType::TestNet; - settings.armoryDBIp = QLatin1String("127.0.0.1"); - settings.armoryDBPort = armoryInstance_->port_; - settings.dataDir = QLatin1String("armory_regtest_db"); + settings.armoryDBIp = "127.0.0.1"; + settings.armoryDBPort = std::to_string(armoryInstance_->port_); + settings.dataDir = "armory_regtest_db"; - auto keyCb = [](const BinaryData&, const std::string&)->bool + const auto& keyCb = [](const BinaryData&, const std::string&)->bool { return true; }; - armoryConnection->setupConnection(settings, keyCb); - armoryConnection_ = armoryConnection; - - blockMonitor_ = std::make_shared(armoryConnection_); + const auto& cbConnected = [this, armoryConnection, waitForReady] + { + armoryConnection_ = armoryConnection; + blockMonitor_ = std::make_shared(armoryConnection_); + armoryConnection_->goOnline(); + }; + const auto& cbConnFailed = [] + { + ASSERT_TRUE(false) << "ArmoryDB connection failed"; + }; + armoryConnection->setupConnection(settings, cbConnected, cbConnFailed, keyCb); - qDebug() << "Waiting for ArmoryDB connection..."; - while (armoryConnection_->state() != ArmoryState::Connected) { - std::this_thread::sleep_for(std::chrono::milliseconds{ 1 }); - } - if (waitForReady) { - qDebug() << "Armory connected - waiting for ready state..."; - } - else { - qDebug() << "Armory connected - go online"; - } - armoryConnection_->goOnline(); if (waitForReady) { - while (armoryConnection_->state() != ArmoryState::Ready) { + qDebug() << "Waiting for ArmoryDB connection..."; + while (armoryConnection->state() != ArmoryState::Ready) { std::this_thread::sleep_for(std::chrono::milliseconds{ 1 }); } logger_->debug("Armory is ready - continue execution"); diff --git a/UnitTests/TestEnv.h b/UnitTests/TestEnv.h index 74424054f..1dff0b4d0 100644 --- a/UnitTests/TestEnv.h +++ b/UnitTests/TestEnv.h @@ -416,9 +416,8 @@ class TestArmoryConnection : public ArmoryObject TestArmoryConnection( std::shared_ptr armoryInstance, const std::shared_ptr &loggerRef, - const std::string &txCacheFN, - bool cbInMainThread = true) : - ArmoryObject(loggerRef, txCacheFN, cbInMainThread) + const std::string &txCacheFN) : + ArmoryObject(loggerRef, txCacheFN) , armoryInstance_(armoryInstance) {} diff --git a/UnitTests/TestOtc.cpp b/UnitTests/TestOtc.cpp index 86770a8c8..d1869b6e9 100644 --- a/UnitTests/TestOtc.cpp +++ b/UnitTests/TestOtc.cpp @@ -312,10 +312,12 @@ class TestOtc : public ::testing::Test mineRandomBlocks(6); auto utxosPromise = std::promise>(); +#ifdef OLD_WALLETS_CODE bool result = wallet->getSpendableTxOutList([&utxosPromise](const std::vector &utxos) { utxosPromise.set_value(utxos); }, UINT64_MAX, true); ASSERT_TRUE(result); +#endif auto utxos = utxosPromise.get_future().get(); ASSERT_FALSE(utxos.empty()); diff --git a/UnitTests/TestWallet.cpp b/UnitTests/TestWallet.cpp index cb3a4e3bc..c8d161bc6 100644 --- a/UnitTests/TestWallet.cpp +++ b/UnitTests/TestWallet.cpp @@ -1854,7 +1854,9 @@ TEST_F(TestWallet, TxIdNestedSegwit) promUtxo->set_value(inputs.front()); } }; +#ifdef OLD_WALLETS_CODE ASSERT_TRUE(syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true)); +#endif const auto input = futUtxo.get(); ASSERT_TRUE(input.isInitialized()); @@ -2055,10 +2057,12 @@ TEST_F(TestWallet, WalletMeta) EXPECT_EQ(result.first, settlCp.settlementId); EXPECT_EQ(result.second, settlCp.cpAddr); } +#if 0 for (auto &settlMeta : settlMetas) { auto result = leafNative->getSettlAuthAddr(settlMeta.settlementId); EXPECT_EQ(result, settlMeta.authAddr); } +#endif for (auto &addr : addrComments) { EXPECT_EQ(leafNative->getAddressComment(addr.first), addr.second); } diff --git a/UnitTests/TestWalletArmory.cpp b/UnitTests/TestWalletArmory.cpp index 8b1d21efd..f117b6328 100644 --- a/UnitTests/TestWalletArmory.cpp +++ b/UnitTests/TestWalletArmory.cpp @@ -166,7 +166,9 @@ TEST_F(TestWalletWithArmory, AddressChainExtension) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance); +#endif fut2.wait(); //check balance @@ -216,7 +218,9 @@ TEST_F(TestWalletWithArmory, AddressChainExtension) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true); +#endif ASSERT_TRUE(fut1.get()); //mine 6 more blocks @@ -234,7 +238,9 @@ TEST_F(TestWalletWithArmory, AddressChainExtension) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance2); +#endif fut4.wait(); //check balance @@ -338,7 +344,9 @@ TEST_F(TestWalletWithArmory, RestoreWallet_CheckChainLength) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance); +#endif fut2.wait(); //check balance @@ -396,7 +404,9 @@ TEST_F(TestWalletWithArmory, RestoreWallet_CheckChainLength) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true); +#endif fut1.wait(); //mine 6 more blocks @@ -414,7 +424,9 @@ TEST_F(TestWalletWithArmory, RestoreWallet_CheckChainLength) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance2); +#endif fut4.wait(); //check balance @@ -509,7 +521,9 @@ TEST_F(TestWalletWithArmory, RestoreWallet_CheckChainLength) }; //async, has to wait +#ifdef OLD_WALLETS_CODE ASSERT_TRUE(syncLeaf->updateBalances(cbBalance)); +#endif fut2.wait(); //check balance @@ -640,7 +654,9 @@ TEST_F(TestWalletWithArmory, RestoreWallet_CheckChainLength) promPtr2->set_value(true); }; //async, has to wait +#ifdef OLD_WALLETS_CODE ASSERT_TRUE(syncLeaf->updateBalances(cbBalance)); +#endif fut2.wait(); //check balance @@ -717,7 +733,9 @@ TEST_F(TestWalletWithArmory, Comments) { promPtr->set_value(inputs); }; +#ifdef OLD_WALLETS_CODE EXPECT_TRUE(syncWallet->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true)); +#endif const auto inputs = fut.get(); ASSERT_FALSE(inputs.empty()); const auto recip = addr.getRecipient(bs::XBTAmount{ (int64_t)12000 }); @@ -771,7 +789,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) { balProm->set_value(true); }; +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(waitOnBalance); +#endif balFut.wait(); EXPECT_DOUBLE_EQ(syncLeaf->getTotalBalance(), 0); EXPECT_DOUBLE_EQ(syncLeaf->getSpendableBalance(), 0); @@ -794,7 +814,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) { balProm1->set_value(true); }; +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(waitOnBalance1); +#endif balFut1.wait(); EXPECT_DOUBLE_EQ(syncLeaf->getTotalBalance(), 300); EXPECT_DOUBLE_EQ(syncLeaf->getSpendableBalance(), 300); @@ -811,7 +833,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) promPtr1->set_value(inputs); }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true); +#endif const auto inputs = fut1.get(); ASSERT_GE(inputs.size(), 1); @@ -849,7 +873,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance); +#endif fut2.wait(); EXPECT_DOUBLE_EQ(syncLeaf->getTotalBalance(), @@ -882,7 +908,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) prom3->set_value(true); }; +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableZCList(zcTxOutLbd); +#endif fut3.wait(); blockCount = 1; @@ -898,7 +926,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) promUpdBal->set_value(true); }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance4); +#endif futUpdBal.wait(); EXPECT_EQ(syncLeaf->getTotalBalance(), @@ -920,7 +950,9 @@ TEST_F(TestWalletWithArmory, ZCBalance) promUpdBal->set_value(true); }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance5); +#endif futUpdBal.wait(); EXPECT_EQ(syncLeaf->getTotalBalance(), @@ -976,8 +1008,9 @@ TEST_F(TestWalletWithArmory, SimpleTX_bech32) { promPtr1->set_value(inputs); }; - +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList1, UINT64_MAX, true); +#endif const auto inputs1 = fut1.get(); ASSERT_FALSE(inputs1.empty()); @@ -1010,8 +1043,9 @@ TEST_F(TestWalletWithArmory, SimpleTX_bech32) { promPtr2->set_value(true); }; - +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance); +#endif fut2.wait(); EXPECT_EQ(syncLeaf->getAddrBalance(addr2)[0], amount1); @@ -1021,8 +1055,9 @@ TEST_F(TestWalletWithArmory, SimpleTX_bech32) { promPtr3->set_value(inputs); }; - +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList2, UINT64_MAX, true); +#endif const auto inputs2 = fut3.get(); ASSERT_FALSE(inputs2.empty()); @@ -1264,7 +1299,9 @@ TEST_F(TestWalletWithArmory, GlobalDelegateConf) promPtrBal->set_value(true); }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->updateBalances(cbBalance); +#endif futBal.wait(); //check balance @@ -1336,7 +1373,9 @@ TEST_F(TestWalletWithArmory, GlobalDelegateConf) promTxOut->set_value(inputs); }; //async, has to wait +#ifdef OLD_WALLETS_CODE EXPECT_TRUE(syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true)); +#endif const auto &inputs = futTxOut.get(); ASSERT_FALSE(inputs.empty()); @@ -1469,7 +1508,9 @@ TEST_F(TestWalletWithArmory, PushZC_retry) promPtr1->set_value(inputs); }; //async, has to wait +#ifdef OLD_WALLETS_CODE syncLeaf->getSpendableTxOutList(cbTxOutList, UINT64_MAX, true); +#endif const auto inputs = fut1.get(); ASSERT_GE(inputs.size(), 1); diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..d8fa74f99 --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/bash +python generate.py release --appimage +cd terminal.release +make -j$(nproc) diff --git a/common b/common index 7a79d2caa..7351eb6ef 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 7a79d2caa5f8541efeeb11ab3861df1dbe7cf8ba +Subproject commit 7351eb6ef742c2bd448cdd0ea70b9dbad77c2798 diff --git a/generate.py b/generate.py index b67f28ae8..bef52068c 100644 --- a/generate.py +++ b/generate.py @@ -17,7 +17,7 @@ # Set the minimum macOS target environment. Applies to prereqs and to BS code. # If the min target changes, update CMakeLists.txt too. if sys.platform == "darwin": - os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.12' + os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.15' sys.path.insert(0, os.path.join('common')) sys.path.insert(0, os.path.join('common', 'build_scripts')) @@ -39,10 +39,11 @@ from build_scripts.settings import Settings from build_scripts.spdlog_settings import SpdlogSettings from build_scripts.trezor_common_settings import TrezorCommonSettings +#from build_scripts.zeromq_settings import ZeroMQSettings +from build_scripts.curl_settings import CurlSettings from build_scripts.websockets_settings import WebsocketsSettings -from build_scripts.zeromq_settings import ZeroMQSettings -def generate_project(build_mode, link_mode, build_production, hide_warnings, cmake_flags, build_tests, build_tracker): +def generate_project(build_mode, link_mode, build_production, hide_warnings, cmake_flags, build_tests, build_tracker, signature_cert_name, build_appimage): project_settings = Settings(build_mode, link_mode) print('Build mode : {} ( {} )'.format(project_settings.get_build_mode(), ('Production' if build_production else 'Development'))) @@ -54,27 +55,30 @@ def generate_project(build_mode, link_mode, build_production, hide_warnings, cma required_3rdparty = [] if project_settings._is_windows: + WebsocketsSettings(project_settings), required_3rdparty.append(JomSettings(project_settings)) required_3rdparty += [ ProtobufSettings(project_settings), OpenSslSettings(project_settings), + CurlSettings(project_settings), SpdlogSettings(project_settings), - ZeroMQSettings(project_settings), +# ZeroMQSettings(project_settings), LibQREncode(project_settings), - MPIRSettings(project_settings), LibBTC(project_settings), # static LibChaCha20Poly1305Settings(project_settings), # static - WebsocketsSettings(project_settings), BotanSettings(project_settings), QtSettings(project_settings), HidapiSettings(project_settings), LibusbSettings(project_settings), TrezorCommonSettings(project_settings), BipProtocolsSettings(project_settings), - NLohmanJson(project_settings) + NLohmanJson(project_settings), ] + if project_settings._is_windows: + required_3rdparty.append(MPIRSettings(project_settings)) + if build_tests: required_3rdparty.append(GtestSettings(project_settings)) @@ -139,9 +143,14 @@ def generate_project(build_mode, link_mode, build_production, hide_warnings, cma if build_tests: command.append('-DBUILD_TESTS=1') + if build_appimage: + command.append('-DBUILD_APPIMAGE=1') + if build_tracker: command.append('-DBUILD_TRACKER=1') + if signature_cert_name != None: + command.append(f'-DM_SIGN_CERT_NAME={signature_cert_name}') if cmake_flags != None: for flag in cmake_flags.split(): command.append(flag) @@ -150,9 +159,20 @@ def generate_project(build_mode, link_mode, build_production, hide_warnings, cma cmdStr = r' '.join(command) result = subprocess.call(cmdStr) else: - result = subprocess.call(command) + try: + response = subprocess.run(command, check=True, capture_output=True) + result = response.returncode + print('response: ', response) + print('stderr: ', response.stderr) + print('stdout: ', response.stdout) + except subprocess.CalledProcessError as error: + result = error.returncode + print('response: ', error) + print('stderr: ', error.stderr) + print('stdout: ', error.stdout) + if result == 0: - print('Project generated to :' + build_dir) + print('Project generated to: ' + build_dir) return 0 else: print('Cmake failed') @@ -187,6 +207,14 @@ def generate_project(build_mode, link_mode, build_production, hide_warnings, cma action='store', type=str, help='Additional CMake flags. Example: "-DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_FLAGS=-fuse-ld=gold"') + input_parser.add_argument('--signature-cert-name', + action='store', + type=str, + help='Signature certificate name Example: "Apple Distribution: FirstName LastName (XXXXXXXXXX)') + input_parser.add_argument('--appimage', + action='store_true', + dest='build_appimage', + help='Linux build AppImage file') input_parser.add_argument('--test', help='Select to also build tests', action='store_true') @@ -196,4 +224,4 @@ def generate_project(build_mode, link_mode, build_production, hide_warnings, cma args = input_parser.parse_args() - sys.exit(generate_project(args.build_mode, args.link_mode, args.build_production, args.hide_warnings, args.cmake_flags, args.test, args.tracker)) + sys.exit(generate_project(args.build_mode, args.link_mode, args.build_production, args.hide_warnings, args.cmake_flags, args.test, args.tracker, args.signature_cert_name, args.build_appimage)) diff --git a/ubuntu.Dockerfile b/ubuntu.Dockerfile new file mode 100644 index 000000000..2976caa54 --- /dev/null +++ b/ubuntu.Dockerfile @@ -0,0 +1,40 @@ +FROM ubuntu:22.04 as terminal_build_machine + +ENV TZ="Etc/UTC" + +RUN sed -Ei 's/^# deb-src /deb-src /' /etc/apt/sources.list \ + && apt update \ + # timezone thing + && apt install -y tzdata \ + && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \ + && echo $TZ > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata \ + # main libs + && apt install -y python3 \ + python3-pip cmake libmysqlclient-dev autoconf libtool yasm nasm libgmp3-dev libdouble-conversion-dev \ + qttools5-dev-tools libfreetype-dev libfontconfig-dev libcups2-dev xcb fuse libfuse2 \ + libx11-xcb-dev libxcb-xkb-dev libxcb-xinput-dev libxcb-sync-dev libxcb-render-util0-dev libxcb-xfixes0-dev \ + libxcb-xinerama0-dev libxcb-randr0-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-icccm4-dev libxcb-glx0-dev libxkbcommon-x11-dev \ + libudev-dev libxi-dev libsm-dev libxrender-dev libdbus-1-dev \ + && pip install wget requests pathlib \ + # free up space + && rm -rf /var/lib/apt/lists/* \ + && ln -s /usr/bin/python3 /usr/bin/python + + +WORKDIR app + +COPY . . + +CMD ["/app/build.sh"] + + +### OLD STYLE BUILDING +#RUN python generate.py release --appimage +# +#RUN cd terminal.release \ +# && make -j$(nproc) +# +#RUN ls -la /app/build_terminal/RelWithDebInfo/bin + +#RUN cd Deploy \ +# && ./deploy.sh