diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7507e80c57..185a74a9df 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -19,12 +19,7 @@ option(WITH_UPDATER "Regularly check for new updates" ON)
option(DEV_MODE "Checkout latest monero master on build" OFF)
cmake_dependent_option(QML_TESTS "Build QML tests" ON "NOT STATIC;NOT ANDROID;NOT IOS" OFF)
-if(DEV_MODE)
- # DEV_MODE checks out the monero submodule to master, which requires C++17.
- set(CMAKE_CXX_STANDARD 17)
-else()
- set(CMAKE_CXX_STANDARD 14)
-endif()
+set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_SOURCE_DIR}/cmake")
diff --git a/components/DevicePairingCodeDialog.qml b/components/DevicePairingCodeDialog.qml
new file mode 100644
index 0000000000..0fd77f7c72
--- /dev/null
+++ b/components/DevicePairingCodeDialog.qml
@@ -0,0 +1,232 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import QtQuick 2.9
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.1
+
+import "../components" as MoneroComponents
+
+// Modal that prompts the user for the 6-digit pairing code shown on a
+// Trezor Safe 7 during the THP CodeEntry pairing flow.
+FocusScope {
+ id: root
+ visible: false
+
+ // onAcceptedCallback is invoked with the entered code (string of 6
+ // ASCII digits). onRejectedCallback is invoked when the user cancels.
+ // Both are cleared once one of them has fired.
+ property var onAcceptedCallback
+ property var onRejectedCallback
+
+ // Message shown above the input, set by the caller when a previous
+ // code was rejected by the device.
+ property string errorText
+
+ function open() {
+ // Same modal behaviour as PasswordDialog: a wallet pool thread
+ // is blocked waiting for the code, so the UI behind the dialog
+ // must not accept input (e.g. starting a second wallet open
+ // would hang forever on the same device).
+ leftPanel.enabled = false;
+ middlePanel.enabled = false;
+ wizard.enabled = false;
+ titleBar.state = "essentials";
+ root.visible = true;
+ codeInput.text = "";
+ codeInput.forceActiveFocus();
+ }
+
+ function close() {
+ leftPanel.enabled = true;
+ middlePanel.enabled = true;
+ wizard.enabled = !wizard.deviceWalletCreationInProgress;
+ if (rootItem.state == "wizard") {
+ titleBar.state = "essentials";
+ } else {
+ titleBar.state = "default";
+ }
+ root.visible = false;
+ root.errorText = "";
+ }
+
+ function onOk() {
+ if (codeInput.text.length !== 6) {
+ return;
+ }
+ var entered = codeInput.text;
+ var callback = root.onAcceptedCallback;
+ root.onAcceptedCallback = null;
+ root.onRejectedCallback = null;
+ root.close();
+ if (callback) {
+ callback(entered);
+ }
+ }
+
+ function onCancel() {
+ if (!root.visible) {
+ return;
+ }
+ var callback = root.onRejectedCallback;
+ root.onAcceptedCallback = null;
+ root.onRejectedCallback = null;
+ root.close();
+ if (callback) {
+ callback();
+ }
+ }
+
+ Keys.enabled: root.visible
+ Keys.onEscapePressed: root.onCancel()
+
+ ColumnLayout {
+ id: mainLayout
+ spacing: 10
+ anchors.fill: parent
+ anchors.margins: 35
+
+ ColumnLayout {
+ id: column
+
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
+ Layout.maximumWidth: 480
+
+ Label {
+ Layout.fillWidth: true
+ text: qsTr("Trezor Safe 7 - pair this computer") + translationManager.emptyString
+ font.pixelSize: 18
+ font.family: MoneroComponents.Style.fontBold.name
+ font.bold: true
+ color: MoneroComponents.Style.defaultFontColor
+ }
+
+ Label {
+ Layout.fillWidth: true
+ Layout.topMargin: 12
+ text: qsTr("Your Trezor is showing a 6-digit code on its screen. " +
+ "Type that exact code here to confirm this computer is allowed to talk to it. " +
+ "After this one-time pairing, the computer is remembered and you won't be asked again.") + translationManager.emptyString
+ font.pixelSize: 14
+ font.family: MoneroComponents.Style.fontLight.name
+ color: MoneroComponents.Style.defaultFontColor
+ wrapMode: Text.WordWrap
+ }
+
+ Label {
+ id: errorTextLabel
+ visible: text !== ""
+ text: root.errorText
+ Layout.fillWidth: true
+ Layout.topMargin: 12
+ font.pixelSize: 14
+ font.family: MoneroComponents.Style.fontLight.name
+ color: MoneroComponents.Style.errorColor
+ wrapMode: Text.WordWrap
+ }
+
+ MoneroComponents.Input {
+ id: codeInput
+ focus: true
+ Layout.topMargin: 16
+ Layout.fillWidth: true
+ horizontalAlignment: TextInput.AlignHCenter
+ verticalAlignment: TextInput.AlignVCenter
+ font.family: MoneroComponents.Style.fontBold.name
+ font.pixelSize: 32
+ font.letterSpacing: 6
+ bottomPadding: 12
+ leftPadding: 10
+ topPadding: 12
+ color: MoneroComponents.Style.defaultFontColor
+ selectionColor: MoneroComponents.Style.textSelectionColor
+ selectedTextColor: MoneroComponents.Style.textSelectedColor
+ inputMethodHints: Qt.ImhDigitsOnly
+
+ // js replacement for `RegExpValidator { regExp: /[0-9]{0,6}/ }`,
+ // which rejects a paste outright: a code copied as
+ // "12 34 56" or "123-456" would insert nothing at all.
+ onTextChanged: {
+ var digits = codeInput.text.replace(/[^0-9]/g, "").substring(0, 6);
+ if (digits !== codeInput.text) {
+ codeInput.text = digits;
+ codeInput.cursorPosition = digits.length;
+ }
+ }
+
+ background: Rectangle {
+ radius: 2
+ border.color: MoneroComponents.Style.inputBorderColorActive
+ border.width: 1
+ color: MoneroComponents.Style.blackTheme ? "black" : "#A9FFFFFF"
+ }
+
+ Keys.enabled: root.visible
+ Keys.onEnterPressed: root.onOk()
+ Keys.onReturnPressed: root.onOk()
+ Keys.onEscapePressed: root.onCancel()
+ }
+
+ Label {
+ Layout.fillWidth: true
+ Layout.topMargin: 8
+ text: qsTr("Tip: nobody, not even a fake Trezor, can guess this code. " +
+ "If you mistype it, pairing starts over and the device shows a fresh code.") + translationManager.emptyString
+ font.pixelSize: 12
+ font.italic: true
+ font.family: MoneroComponents.Style.fontLight.name
+ color: MoneroComponents.Style.dimmedFontColor
+ wrapMode: Text.WordWrap
+ }
+
+ RowLayout {
+ spacing: 16
+ Layout.topMargin: 16
+ Layout.alignment: Qt.AlignRight
+
+ MoneroComponents.StandardButton {
+ primary: false
+ small: true
+ width: 120
+ fontSize: 14
+ text: qsTr("Cancel") + translationManager.emptyString
+ onClicked: root.onCancel()
+ }
+ MoneroComponents.StandardButton {
+ small: true
+ width: 120
+ fontSize: 14
+ text: qsTr("Confirm") + translationManager.emptyString
+ enabled: codeInput.text.length === 6
+ onClicked: root.onOk()
+ }
+ }
+ }
+ }
+}
diff --git a/components/ProcessingSplash.qml b/components/ProcessingSplash.qml
index 469cc1fad8..ada8738411 100644
--- a/components/ProcessingSplash.qml
+++ b/components/ProcessingSplash.qml
@@ -43,23 +43,74 @@ Rectangle {
border.width: 1
z: 11
property alias messageText: messageTitle.text
+ property alias subMessageText: messageSub.text
+
+ // Optional Retry / Cancel row, shown instead of leaving the user
+ // stuck on a splash they can't dismiss. To use it, set
+ // retryCallback and cancelCallback, set showActionButtons, then
+ // call show(). Leaving retryCallback null hides Retry.
+ property bool showActionButtons: false
+ property var retryCallback: null
+ property var cancelCallback: null
width: 100
height: 50
+ focus: visible && showActionButtons
+ Keys.onPressed: {
+ if (!root.showActionButtons) return;
+ if (event.key === Qt.Key_Escape) {
+ root.fireCancel();
+ event.accepted = true;
+ } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
+ if (cancelButton.activeFocus || !retryButton.visible) {
+ root.fireCancel();
+ } else {
+ root.fireRetry();
+ }
+ event.accepted = true;
+ }
+ }
+
function show() {
root.visible = true;
+ if (root.showActionButtons) {
+ if (retryButton.visible) {
+ retryButton.forceActiveFocus();
+ } else {
+ cancelButton.forceActiveFocus();
+ }
+ }
}
function close() {
root.visible = false;
+ root.showActionButtons = false;
+ root.retryCallback = null;
+ root.cancelCallback = null;
+ }
+
+ function fireRetry() {
+ var cb = root.retryCallback;
+ root.close();
+ if (cb) cb();
+ }
+
+ function fireCancel() {
+ var cb = root.cancelCallback;
+ root.close();
+ if (cb) cb();
}
ColumnLayout {
id: rootLayout
- anchors.centerIn: parent
-
+ // Anchor to the splash sides so a wrapping sub-message stays
+ // inside the box; anchors.centerIn leaves the layout width
+ // unconstrained and long messages spill outside.
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 30
anchors.rightMargin: 30
@@ -79,7 +130,9 @@ Rectangle {
}
BusyIndicator {
- running: parent.visible
+ // Nothing is in progress while the action row waits for
+ // the user, so the spinner stops.
+ running: parent.visible && !root.showActionButtons
anchors.centerIn: imgLogo
style: BusyIndicatorStyle {
indicator: Image {
@@ -108,5 +161,49 @@ Rectangle {
themeTransition: false
color: MoneroComponents.Style.defaultFontColor
}
+
+ MoneroComponents.TextPlain {
+ id: messageSub
+ text: ""
+ visible: text.length > 0
+ font.pixelSize: 15
+ horizontalAlignment: Text.AlignHCenter
+ wrapMode: Text.WordWrap
+ Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
+ Layout.fillWidth: true
+ Layout.leftMargin: 10
+ Layout.rightMargin: 10
+ themeTransition: false
+ color: MoneroComponents.Style.dimmedFontColor
+ }
+
+ RowLayout {
+ id: actionRow
+ visible: root.showActionButtons
+ spacing: 16
+ Layout.alignment: Qt.AlignHCenter
+ Layout.topMargin: 4
+
+ MoneroComponents.StandardButton {
+ id: cancelButton
+ primary: !retryButton.visible
+ small: false
+ text: qsTr("Cancel") + translationManager.emptyString
+ onClicked: root.fireCancel()
+ KeyNavigation.tab: retryButton.visible ? retryButton : cancelButton
+ KeyNavigation.backtab: retryButton.visible ? retryButton : cancelButton
+ }
+
+ MoneroComponents.StandardButton {
+ id: retryButton
+ visible: root.retryCallback !== null
+ primary: true
+ small: false
+ text: qsTr("Try again") + translationManager.emptyString
+ onClicked: root.fireRetry()
+ KeyNavigation.tab: cancelButton
+ KeyNavigation.backtab: cancelButton
+ }
+ }
}
}
diff --git a/images/trezor7.png b/images/trezor7.png
new file mode 100644
index 0000000000..e5cdbcff65
Binary files /dev/null and b/images/trezor7.png differ
diff --git a/main.qml b/main.qml
index 0345f5d29f..cd52cdc6dc 100644
--- a/main.qml
+++ b/main.qml
@@ -98,6 +98,7 @@ ApplicationWindow {
property int estimatedBlockchainSize: persistentSettings.pruneBlockchain ? 110 : 270 // GB
property alias viewState: rootItem.state
property string prevSplashText;
+ property string prevSplashSubText;
property bool splashDisplayedBeforeButtonRequest;
property bool themeTransition: false
property int backgroundSyncType: Wallet.BackgroundSync_Off;
@@ -282,6 +283,7 @@ ApplicationWindow {
if (isQuitting)
return;
isQuitting = true;
+ devicePairingCodeDialog.onCancel();
closeWallet(function() {
gracefulShutdownComplete();
})
@@ -312,6 +314,7 @@ ApplicationWindow {
currentWallet.deviceButtonRequest.disconnect(onDeviceButtonRequest);
currentWallet.deviceButtonPressed.disconnect(onDeviceButtonPressed);
currentWallet.walletPassphraseNeeded.disconnect(onWalletPassphraseNeededWallet);
+ currentWallet.walletPairingCodeNeeded.disconnect(onWalletPairingCodeNeededWallet);
currentWallet.transactionCommitted.disconnect(onTransactionCommitted);
middlePanel.paymentClicked.disconnect(handlePayment);
middlePanel.sweepUnmixableClicked.disconnect(handleSweepUnmixable);
@@ -363,6 +366,7 @@ ApplicationWindow {
currentWallet.deviceButtonRequest.connect(onDeviceButtonRequest);
currentWallet.deviceButtonPressed.connect(onDeviceButtonPressed);
currentWallet.walletPassphraseNeeded.connect(onWalletPassphraseNeededWallet);
+ currentWallet.walletPairingCodeNeeded.connect(onWalletPairingCodeNeededWallet);
currentWallet.transactionCommitted.connect(onTransactionCommitted);
currentWallet.proxyAddress = Qt.binding(persistentSettings.getWalletProxyAddress);
middlePanel.paymentClicked.connect(handlePayment);
@@ -498,7 +502,10 @@ ApplicationWindow {
}
} else {
prevSplashText = splash.messageText;
+ prevSplashSubText = splash.subMessageText;
splashDisplayedBeforeButtonRequest = splash.visible;
+ // showProcessingSplash clears the subtitle, so onDeviceButtonPressed
+ // puts prevSplashSubText back once the device input is done.
appWindow.showProcessingSplash(qsTr("Please proceed to the device..."));
}
}
@@ -510,6 +517,7 @@ ApplicationWindow {
} else {
if (splashDisplayedBeforeButtonRequest){
appWindow.showProcessingSplash(prevSplashText);
+ splash.subMessageText = prevSplashSubText;
} else {
hideProcessingSplash();
}
@@ -536,6 +544,78 @@ ApplicationWindow {
appWindow.showStatusMessage(qsTr("Repairing incompatible wallet cache. Resyncing wallet."),6);
return;
default:
+ // Trezor errors get categorical UX: friendly retry
+ // hint, neutral cancel, a fresh pairing code, or a
+ // red protocol error. The category comes from the
+ // device exception the wallet API classified, not
+ // from the text of the error.
+ var es = wallet.errorString || "";
+ var trezorError = wallet.trezorError;
+ if (trezorError === Wallet.TrezorError_Cancelled) {
+ // Deliberate user action, e.g. an on-device
+ // dismiss: neutral status hint, no red error.
+ console.log("Trezor open cancelled by user: " + es)
+ appWindow.showStatusMessage(
+ qsTr("Wallet open cancelled.") + translationManager.emptyString,
+ 5);
+ closeWallet();
+ // Back to the wizard. This matters for the
+ // daemon-switch reopen flow, where rootItem.state
+ // is already "normal" and the user would be left
+ // in the wallet view with no wallet.
+ if (rootItem.state !== "wizard") {
+ rootItem.state = "wizard";
+ }
+ return;
+ }
+ if (trezorError === Wallet.TrezorError_Unreachable) {
+ // The wallet password is still cached in
+ // appWindow.walletPassword from the first attempt,
+ // so Retry can re-fire openWalletAsync without
+ // asking for it again.
+ console.log("Trezor not reachable on wallet open: " + es)
+ closeWallet();
+ appWindow.showRetryableTrezorErrorSplash(
+ qsTr("Couldn't reach your Trezor") + translationManager.emptyString,
+ qsTr("Make sure your Trezor is connected and unlocked, then try again.")
+ + translationManager.emptyString,
+ function() {
+ appWindow.initialize();
+ },
+ function() {
+ if (rootItem.state !== "wizard") {
+ rootItem.state = "wizard";
+ }
+ });
+ return;
+ }
+ if (trezorError === Wallet.TrezorError_PairingRejected) {
+ // The device rejected the pairing code the user
+ // typed. Reopening restarts pairing: the device
+ // shows a fresh code and the pairing dialog
+ // re-prompts, carrying the message below.
+ console.log("Trezor pairing code rejected: " + es)
+ devicePairingCodeDialog.errorText =
+ qsTr("That code did not match. Check the code on your device and try again.")
+ + translationManager.emptyString;
+ appWindow.initialize();
+ return;
+ }
+ if (trezorError === Wallet.TrezorError_FirmwareUnsupported) {
+ // Nothing to retry: the device needs different
+ // firmware before it can open a Monero wallet.
+ console.error("Trezor firmware without Monero support: ", es)
+ closeWallet();
+ informationPopup.title = qsTr("Trezor firmware without Monero support") + translationManager.emptyString;
+ informationPopup.text = qsTr("This Trezor runs firmware that has no Monero support, such as the bitcoin-only build. Install the standard firmware with Trezor Suite, then open the wallet again.") + translationManager.emptyString;
+ informationPopup.icon = StandardIcon.Critical;
+ informationPopup.onCloseCallback = null;
+ informationPopup.open();
+ if (rootItem.state !== "wizard") {
+ rootItem.state = "wizard";
+ }
+ return;
+ }
// opening with password but password doesn't match
console.error("Error opening wallet with password: ", wallet.errorString);
passwordDialog.showError(qsTr("Couldn't open wallet: ") + wallet.errorString);
@@ -595,6 +675,34 @@ ApplicationWindow {
devicePassphraseDialog.open(on_device)
}
+ function onWalletPairingCodeNeededManager(){
+ // The wizard binds the same manager signal and prompts with its
+ // own splash wording while it creates a wallet from a device.
+ if (wizard.deviceWalletCreationInProgress) {
+ return;
+ }
+ onWalletPairingCodeNeeded(walletManager)
+ }
+
+ function onWalletPairingCodeNeededWallet(){
+ onWalletPairingCodeNeeded(currentWallet)
+ }
+
+ function onWalletPairingCodeNeeded(handler){
+ hideProcessingSplash();
+
+ console.log(">>> wallet pairing code needed: ");
+ devicePairingCodeDialog.onAcceptedCallback = function(code) {
+ handler.onPairingCodeEntered(code, false);
+ appWindow.onWalletOpening();
+ }
+ devicePairingCodeDialog.onRejectedCallback = function() {
+ handler.onPairingCodeEntered("", true);
+ appWindow.onWalletOpening();
+ }
+ devicePairingCodeDialog.open();
+ }
+
function onWalletUpdate(stoppedBackgroundSync) {
if (!currentWallet)
return;
@@ -1026,6 +1134,11 @@ ApplicationWindow {
transaction.setFilename(path);
}
appWindow.showProcessingSplash(qsTr("Sending transaction ..."));
+ // Signing on a hardware wallet takes one tap per input, which can
+ // run to tens of seconds with no other sign of progress.
+ if (currentWallet && currentWallet.isHwBacked()) {
+ splash.subMessageText = qsTr("Confirm each transaction step on your hardware wallet. Each input requires a tap on the device.") + translationManager.emptyString;
+ }
currentWallet.commitTransactionAsync(transaction);
}
@@ -1165,8 +1278,15 @@ ApplicationWindow {
function showProcessingSplash(message) {
console.log("Displaying processing splash")
+ // Drop any stale Retry/Cancel state so a regular splash never
+ // renders the action row of a previous error.
+ splash.showActionButtons = false;
+ splash.retryCallback = null;
+ splash.cancelCallback = null;
if (typeof message != 'undefined') {
splash.messageText = message
+ // Callers that want a subtitle assign it after this call.
+ splash.subMessageText = ""
}
leftPanel.enabled = false;
@@ -1177,6 +1297,7 @@ ApplicationWindow {
function hideProcessingSplash() {
console.log("Hiding processing splash")
+ splash.subMessageText = "";
splash.close();
if (!passwordDialog.visible) {
@@ -1186,6 +1307,32 @@ ApplicationWindow {
}
}
+ // Surface a retry-recoverable failure on the splash with a Retry /
+ // Cancel button row instead of bouncing the user back to the
+ // password dialog, so retrying after reconnecting the device does
+ // not require re-typing the wallet password.
+ //
+ // The splash is modal, so cancelling has to put the panels back.
+ // That happens here rather than in each caller's cancel callback:
+ // a caller that forgot would leave the app permanently disabled.
+ function showRetryableTrezorErrorSplash(title, subMessage, retryCallback, cancelCallback) {
+ splash.messageText = title;
+ splash.subMessageText = subMessage;
+ splash.retryCallback = retryCallback;
+ splash.cancelCallback = function() {
+ hideProcessingSplash();
+ if (cancelCallback) {
+ cancelCallback();
+ }
+ };
+ splash.showActionButtons = true;
+
+ leftPanel.enabled = false;
+ middlePanel.enabled = false;
+ titleBar.enabled = false;
+ splash.show();
+ }
+
// close wallet and show wizard
function showWizard(){
walletInitialized = false;
@@ -1391,6 +1538,7 @@ ApplicationWindow {
walletManager.deviceButtonPressed.connect(onDeviceButtonPressed);
walletManager.checkUpdatesComplete.connect(onWalletCheckUpdatesComplete);
walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeededManager);
+ walletManager.walletPairingCodeNeeded.connect(onWalletPairingCodeNeededManager);
IPC.uriHandler.connect(onUriHandler);
if(typeof daemonManager != "undefined") {
@@ -1803,6 +1951,13 @@ ApplicationWindow {
anchors.fill: parent
}
+ DevicePairingCodeDialog {
+ id: devicePairingCodeDialog
+ visible: false
+ z: parent.z + 1
+ anchors.fill: parent
+ }
+
InputDialog {
id: inputDialog
visible: false
@@ -1832,7 +1987,10 @@ ApplicationWindow {
ProcessingSplash {
id: splash
width: appWindow.width / 2
- height: appWindow.height / 2.66
+ // The Retry/Cancel row needs more room than a plain splash.
+ height: showActionButtons
+ ? appWindow.height / 1.9
+ : appWindow.height / 2.66
x: (appWindow.width - width) / 2
y: (appWindow.height - height) / 2
messageText: qsTr("Please wait...") + translationManager.emptyString
@@ -1947,7 +2105,7 @@ ApplicationWindow {
source: blurredArea
radius: 64
visible: passwordDialog.visible || inputDialog.visible || splash.visible || updateDialog.visible ||
- devicePassphraseDialog.visible || txConfirmationPopup.visible || successfulTxPopup.visible ||
+ devicePassphraseDialog.visible || devicePairingCodeDialog.visible || txConfirmationPopup.visible || successfulTxPopup.visible ||
remoteNodeDialog.visible
}
@@ -2155,10 +2313,13 @@ ApplicationWindow {
visible: false
property alias text: statusMessageText.text
anchors.bottom: parent.bottom
- width: statusMessageText.contentWidth + 20
+ // Cap the width so a long message wraps inside the toast instead
+ // of spilling off the right edge, and grow the height to match.
+ width: Math.min(statusMessageText.implicitWidth + 20,
+ Math.max(appWindow.width - 80, 320))
+ height: statusMessageText.paintedHeight + 20
anchors.horizontalCenter: parent.horizontalCenter
color: MoneroComponents.Style.blackTheme ? "black" : "white"
- height: 40
MoneroComponents.TextPlain {
id: statusMessageText
anchors.fill: parent
@@ -2166,6 +2327,8 @@ ApplicationWindow {
font.pixelSize: 14
color: MoneroComponents.Style.defaultFontColor
themeTransition: false
+ wrapMode: Text.WordWrap
+ horizontalAlignment: Text.AlignHCenter
}
}
@@ -2218,6 +2381,10 @@ ApplicationWindow {
}
+ // An unanswered pairing prompt holds a wallet pool thread, which
+ // would then never finish. Cancel it so the thread unwinds.
+ devicePairingCodeDialog.onCancel();
+
// If daemon is running - prompt user before exiting
if(daemonManager == undefined || persistentSettings.useRemoteNode) {
closeAccepted();
diff --git a/qml.qrc b/qml.qrc
index 9223423dc8..599ab41029 100644
--- a/qml.qrc
+++ b/qml.qrc
@@ -111,6 +111,7 @@
components/ProgressBar.qml
components/StandardDialog.qml
components/DevicePassphraseDialog.qml
+ components/DevicePairingCodeDialog.qml
pages/Sign.qml
components/DaemonManagerDialog.qml
version.js
@@ -286,6 +287,7 @@
images/ledgerFlex.png
images/trezor3.png
images/trezor5.png
+ images/trezor7.png
images/trezorT.png
images/trezorT@2x.png
qtquickcontrols2.conf
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index bcbda89cfe..9942b02782 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -14,6 +14,7 @@ file(GLOB SOURCE_FILES
"libwalletqt/WalletListenerImpl.cpp"
"libwalletqt/Wallet.cpp"
"libwalletqt/PassphraseHelper.cpp"
+ "libwalletqt/PairingCodeHelper.cpp"
"libwalletqt/PendingTransaction.cpp"
"libwalletqt/TransactionHistory.cpp"
"libwalletqt/TransactionInfo.cpp"
@@ -25,6 +26,7 @@ file(GLOB SOURCE_FILES
"libwalletqt/WalletManager.h"
"libwalletqt/Wallet.h"
"libwalletqt/PassphraseHelper.h"
+ "libwalletqt/PairingCodeHelper.h"
"libwalletqt/PendingTransaction.h"
"libwalletqt/TransactionHistory.h"
"libwalletqt/TransactionInfo.h"
diff --git a/src/libwalletqt/PairingCodeHelper.cpp b/src/libwalletqt/PairingCodeHelper.cpp
new file mode 100644
index 0000000000..a6dfa3edad
--- /dev/null
+++ b/src/libwalletqt/PairingCodeHelper.cpp
@@ -0,0 +1,65 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "PairingCodeHelper.h"
+#include
+#include
+
+Monero::optional PairingCodeHelper::onDevicePairingCodeRequest()
+{
+ qDebug() << __FUNCTION__;
+ QMutexLocker locker(&m_mutex);
+ m_abort = false;
+ m_answered = false;
+ m_code.clear();
+
+ if (m_prompter != nullptr) {
+ m_prompter->onWalletPairingCodeNeeded();
+ }
+
+ while (!m_answered) {
+ m_cond.wait(&m_mutex);
+ }
+
+ if (m_abort) {
+ return Monero::optional(std::string{});
+ }
+ auto result = m_code.toStdString();
+ m_code.clear();
+ return Monero::optional(result);
+}
+
+void PairingCodeHelper::onPairingCodeEntered(const QString &code, bool entry_abort)
+{
+ qDebug() << __FUNCTION__;
+ QMutexLocker locker(&m_mutex);
+ m_code = code;
+ m_abort = entry_abort;
+ m_answered = true;
+ m_cond.wakeAll();
+}
diff --git a/src/libwalletqt/PairingCodeHelper.h b/src/libwalletqt/PairingCodeHelper.h
new file mode 100644
index 0000000000..d9aca2f70d
--- /dev/null
+++ b/src/libwalletqt/PairingCodeHelper.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2026, The Monero Project
+//
+// All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without modification, are
+// permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this list of
+// conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice, this list
+// of conditions and the following disclaimer in the documentation and/or other
+// materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its contributors may be
+// used to endorse or promote products derived from this software without specific
+// prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
+// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef MONERO_GUI_PAIRINGCODEHELPER_H
+#define MONERO_GUI_PAIRINGCODEHELPER_H
+
+#include
+#include
+#include
+#include
+#include
+
+/**
+ * THP CodeEntry pairing code helper. The wallet's worker thread blocks
+ * inside onDevicePairingCodeRequest waiting for the user to type the
+ * 6-digit code shown on the Trezor. The QML side displays a modal,
+ * collects the code, and calls onPairingCodeEntered to wake the worker.
+ *
+ * Mirrors the existing PassphraseHelper pattern.
+ */
+class PairingCodePrompter {
+public:
+ virtual void onWalletPairingCodeNeeded() = 0;
+ virtual ~PairingCodePrompter() = default;
+};
+
+class PairingCodeReceiver {
+public:
+ virtual void onPairingCodeEntered(const QString &code, bool entry_abort) = 0;
+ virtual ~PairingCodeReceiver() = default;
+};
+
+class PairingCodeHelper {
+public:
+ PairingCodeHelper(PairingCodePrompter *prompter = nullptr): m_prompter(prompter) {}
+ PairingCodeHelper(const PairingCodeHelper &h): PairingCodeHelper(h.m_prompter) {}
+
+ // Blocks until onPairingCodeEntered is called. Returns the code as
+ // a UTF-8 string (ASCII digits in practice). Returns an empty
+ // string on user cancel.
+ Monero::optional onDevicePairingCodeRequest();
+
+ void onPairingCodeEntered(const QString &code, bool entry_abort);
+
+private:
+ PairingCodePrompter *m_prompter;
+ QWaitCondition m_cond;
+ QMutex m_mutex;
+ QString m_code;
+ bool m_abort = false;
+ bool m_answered = false;
+};
+
+#endif // MONERO_GUI_PAIRINGCODEHELPER_H
diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp
index 64f968707b..9fbab619c3 100644
--- a/src/libwalletqt/Wallet.cpp
+++ b/src/libwalletqt/Wallet.cpp
@@ -92,6 +92,11 @@ Wallet::Status Wallet::status() const
return static_cast(m_walletImpl->status());
}
+Wallet::TrezorError Wallet::trezorError() const
+{
+ return static_cast(m_walletImpl->trezorError());
+}
+
NetworkType::Type Wallet::nettype() const
{
return static_cast(m_walletImpl->nettype());
@@ -1173,6 +1178,19 @@ void Wallet::onPassphraseEntered(const QString &passphrase, bool enter_on_device
}
}
+void Wallet::onWalletPairingCodeNeeded()
+{
+ emit this->walletPairingCodeNeeded();
+}
+
+void Wallet::onPairingCodeEntered(const QString &code, bool entry_abort)
+{
+ if (m_walletListener != nullptr)
+ {
+ m_walletListener->onPairingCodeEntered(code, entry_abort);
+ }
+}
+
Wallet::Wallet(Monero::Wallet *w, QObject *parent)
: QObject(parent)
, m_walletImpl(w)
diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h
index 1cac75a505..14b1ee9d97 100644
--- a/src/libwalletqt/Wallet.h
+++ b/src/libwalletqt/Wallet.h
@@ -43,6 +43,7 @@
#include "PendingTransaction.h" // we need to have an access to the PendingTransaction::Priority enum here;
#include "UnsignedTransaction.h"
#include "NetworkType.h"
+#include "PairingCodeHelper.h"
#include "PassphraseHelper.h"
#include "WalletListenerImpl.h"
@@ -61,7 +62,7 @@ class SubaddressModel;
class SubaddressAccount;
class SubaddressAccountModel;
-class Wallet : public QObject, public PassprasePrompter
+class Wallet : public QObject, public PassprasePrompter, public PairingCodePrompter
{
Q_OBJECT
Q_PROPERTY(bool disconnected READ disconnected NOTIFY disconnectedChanged)
@@ -69,6 +70,7 @@ class Wallet : public QObject, public PassprasePrompter
Q_PROPERTY(QString seed READ getSeed)
Q_PROPERTY(QString seedLanguage READ getSeedLanguage)
Q_PROPERTY(Status status READ status)
+ Q_PROPERTY(TrezorError trezorError READ trezorError)
Q_PROPERTY(NetworkType::Type nettype READ nettype)
// Q_PROPERTY(ConnectionStatus connected READ connected)
Q_PROPERTY(quint32 currentSubaddressAccount READ currentSubaddressAccount NOTIFY currentSubaddressAccountChanged)
@@ -120,6 +122,21 @@ class Wallet : public QObject, public PassprasePrompter
Q_ENUM(BackgroundSyncType)
+ //! Category of the Trezor failure behind the last open or create, so
+ //! QML can offer the recovery that fits without matching the English
+ //! text of an error.
+ enum TrezorError {
+ TrezorError_None = Monero::Wallet::TrezorError_None,
+ TrezorError_Unreachable = Monero::Wallet::TrezorError_Unreachable,
+ TrezorError_Cancelled = Monero::Wallet::TrezorError_Cancelled,
+ TrezorError_Protocol = Monero::Wallet::TrezorError_Protocol,
+ TrezorError_Other = Monero::Wallet::TrezorError_Other,
+ TrezorError_FirmwareUnsupported = Monero::Wallet::TrezorError_FirmwareUnsupported,
+ TrezorError_PairingRejected = Monero::Wallet::TrezorError_PairingRejected
+ };
+
+ Q_ENUM(TrezorError)
+
//! returns mnemonic seed
QString getSeed() const;
@@ -132,6 +149,9 @@ class Wallet : public QObject, public PassprasePrompter
//! returns last operation's status
Status status() const;
+ //! returns the category of the Trezor failure behind the last status
+ TrezorError trezorError() const;
+
//! returns network type of the wallet.
NetworkType::Type nettype() const;
@@ -374,6 +394,10 @@ class Wallet : public QObject, public PassprasePrompter
Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort=false);
virtual void onWalletPassphraseNeeded(bool on_device) override;
+ // THP CodeEntry pairing code entry for Trezor Safe 7
+ Q_INVOKABLE void onPairingCodeEntered(const QString &code, bool entry_abort=false);
+ virtual void onWalletPairingCodeNeeded() override;
+
// TODO: setListenter() when it implemented in API
signals:
// emitted on every event happened with wallet
@@ -397,6 +421,7 @@ class Wallet : public QObject, public PassprasePrompter
void deviceButtonRequest(quint64 buttonCode);
void deviceButtonPressed();
void walletPassphraseNeeded(bool onDevice);
+ void walletPairingCodeNeeded();
void transactionCommitted(bool status, PendingTransaction *t, const QStringList& txid);
void heightRefreshed(quint64 walletHeight, quint64 daemonHeight, quint64 targetHeight) const;
void deviceShowAddressShowed();
diff --git a/src/libwalletqt/WalletListenerImpl.cpp b/src/libwalletqt/WalletListenerImpl.cpp
index d2ccc24442..db1dd96162 100644
--- a/src/libwalletqt/WalletListenerImpl.cpp
+++ b/src/libwalletqt/WalletListenerImpl.cpp
@@ -32,6 +32,7 @@
WalletListenerImpl::WalletListenerImpl(Wallet * w)
: m_wallet(w)
, m_phelper(w)
+ , m_pchelper(w)
{
}
@@ -95,3 +96,15 @@ Monero::optional WalletListenerImpl::onDevicePassphraseRequest(bool
qDebug() << __FUNCTION__;
return m_phelper.onDevicePassphraseRequest(on_device);
}
+
+Monero::optional WalletListenerImpl::onDevicePairingCodeRequest()
+{
+ qDebug() << __FUNCTION__;
+ return m_pchelper.onDevicePairingCodeRequest();
+}
+
+void WalletListenerImpl::onPairingCodeEntered(const QString &code, bool entry_abort)
+{
+ qDebug() << __FUNCTION__;
+ m_pchelper.onPairingCodeEntered(code, entry_abort);
+}
diff --git a/src/libwalletqt/WalletListenerImpl.h b/src/libwalletqt/WalletListenerImpl.h
index 46f38fdf32..b6720b787e 100644
--- a/src/libwalletqt/WalletListenerImpl.h
+++ b/src/libwalletqt/WalletListenerImpl.h
@@ -30,11 +30,12 @@
#define MONERO_GUI_WALLETLISTENERIMPL_H
#include "wallet/api/wallet2_api.h"
+#include "PairingCodeHelper.h"
#include "PassphraseHelper.h"
class Wallet;
-class WalletListenerImpl : public Monero::WalletListener, public PassphraseReceiver
+class WalletListenerImpl : public Monero::WalletListener, public PassphraseReceiver, public PairingCodeReceiver
{
public:
WalletListenerImpl(Wallet * w);
@@ -60,9 +61,14 @@ class WalletListenerImpl : public Monero::WalletListener, public PassphraseRecei
virtual Monero::optional onDevicePassphraseRequest(bool & on_device) override;
+ virtual Monero::optional onDevicePairingCodeRequest() override;
+
+ virtual void onPairingCodeEntered(const QString &code, bool entry_abort) override;
+
private:
Wallet * m_wallet;
PassphraseHelper m_phelper;
+ PairingCodeHelper m_pchelper;
};
#endif //MONERO_GUI_WALLETLISTENERIMPL_H
diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp
index 73046afa1f..8699f2bc30 100644
--- a/src/libwalletqt/WalletManager.cpp
+++ b/src/libwalletqt/WalletManager.cpp
@@ -46,10 +46,10 @@
#include "qt/updater.h"
#include "qt/ScopeGuard.h"
-class WalletPassphraseListenerImpl : public Monero::WalletListener, public PassphraseReceiver
+class WalletPassphraseListenerImpl : public Monero::WalletListener, public PassphraseReceiver, public PairingCodeReceiver
{
public:
- WalletPassphraseListenerImpl(WalletManager * mgr): m_mgr(mgr), m_phelper(mgr) {}
+ WalletPassphraseListenerImpl(WalletManager * mgr): m_mgr(mgr), m_phelper(mgr), m_pchelper(mgr) {}
virtual void moneySpent(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; };
virtual void moneyReceived(const std::string &txId, uint64_t amount) override { (void)txId; (void)amount; };
@@ -70,6 +70,18 @@ class WalletPassphraseListenerImpl : public Monero::WalletListener, public Pass
return m_phelper.onDevicePassphraseRequest(on_device);
}
+ virtual void onPairingCodeEntered(const QString &code, bool entry_abort) override
+ {
+ qDebug() << __FUNCTION__;
+ m_pchelper.onPairingCodeEntered(code, entry_abort);
+ }
+
+ virtual Monero::optional onDevicePairingCodeRequest() override
+ {
+ qDebug() << __FUNCTION__;
+ return m_pchelper.onDevicePairingCodeRequest();
+ }
+
virtual void onDeviceButtonRequest(uint64_t code) override
{
qDebug() << __FUNCTION__;
@@ -85,6 +97,7 @@ class WalletPassphraseListenerImpl : public Monero::WalletListener, public Pass
private:
WalletManager * m_mgr;
PassphraseHelper m_phelper;
+ PairingCodeHelper m_pchelper;
};
Wallet *WalletManager::createWallet(const QString &path, const QString &password,
@@ -108,9 +121,14 @@ Wallet *WalletManager::openWallet(const QString &path, const QString &password,
m_mutex_passphraseReceiver.lock();
m_passphraseReceiver = &tmpListener;
m_mutex_passphraseReceiver.unlock();
+ m_mutex_pairingCodeReceiver.lock();
+ m_pairingCodeReceiver = &tmpListener;
+ m_mutex_pairingCodeReceiver.unlock();
const auto cleanup = sg::make_scope_guard([this]() noexcept {
QMutexLocker passphrase_locker(&m_mutex_passphraseReceiver);
this->m_passphraseReceiver = nullptr;
+ QMutexLocker pairing_locker(&m_mutex_pairingCodeReceiver);
+ this->m_pairingCodeReceiver = nullptr;
});
if (m_currentWallet) {
@@ -178,9 +196,14 @@ Wallet *WalletManager::createWalletFromDevice(const QString &path, const QString
m_mutex_passphraseReceiver.lock();
m_passphraseReceiver = &tmpListener;
m_mutex_passphraseReceiver.unlock();
+ m_mutex_pairingCodeReceiver.lock();
+ m_pairingCodeReceiver = &tmpListener;
+ m_mutex_pairingCodeReceiver.unlock();
const auto cleanup = sg::make_scope_guard([this]() noexcept {
QMutexLocker passphrase_locker(&m_mutex_passphraseReceiver);
this->m_passphraseReceiver = nullptr;
+ QMutexLocker pairing_locker(&m_mutex_pairingCodeReceiver);
+ this->m_pairingCodeReceiver = nullptr;
});
if (m_currentWallet) {
@@ -589,6 +612,20 @@ void WalletManager::onPassphraseEntered(const QString &passphrase, bool enter_on
}
}
+void WalletManager::onWalletPairingCodeNeeded()
+{
+ emit this->walletPairingCodeNeeded();
+}
+
+void WalletManager::onPairingCodeEntered(const QString &code, bool entry_abort)
+{
+ QMutexLocker locker(&m_mutex_pairingCodeReceiver);
+ if (m_pairingCodeReceiver != nullptr)
+ {
+ m_pairingCodeReceiver->onPairingCodeEntered(code, entry_abort);
+ }
+}
+
QString WalletManager::proxyAddress() const
{
QMutexLocker locker(&m_proxyMutex);
diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h
index 649a9f9289..3b4362f592 100644
--- a/src/libwalletqt/WalletManager.h
+++ b/src/libwalletqt/WalletManager.h
@@ -38,6 +38,7 @@
#include
#include "qt/FutureScheduler.h"
#include "NetworkType.h"
+#include "PairingCodeHelper.h"
#include "PassphraseHelper.h"
class Wallet;
@@ -45,7 +46,7 @@ namespace Monero {
struct WalletManager;
}
-class WalletManager : public QObject, public PassprasePrompter
+class WalletManager : public QObject, public PassprasePrompter, public PairingCodePrompter
{
Q_OBJECT
Q_PROPERTY(bool connected READ connected)
@@ -195,6 +196,9 @@ class WalletManager : public QObject, public PassprasePrompter
Q_INVOKABLE void onPassphraseEntered(const QString &passphrase, bool enter_on_device, bool entry_abort=false);
virtual void onWalletPassphraseNeeded(bool on_device) override;
+ Q_INVOKABLE void onPairingCodeEntered(const QString &code, bool entry_abort=false);
+ virtual void onWalletPairingCodeNeeded() override;
+
QString proxyAddress() const;
void setProxyAddress(QString address);
@@ -203,6 +207,7 @@ class WalletManager : public QObject, public PassprasePrompter
void walletOpened(Wallet * wallet);
void walletCreated(Wallet * wallet);
void walletPassphraseNeeded(bool onDevice);
+ void walletPairingCodeNeeded();
void deviceButtonRequest(quint64 buttonCode);
void deviceButtonPressed();
void checkUpdatesComplete(
@@ -226,6 +231,8 @@ public slots:
QPointer m_currentWallet;
PassphraseReceiver * m_passphraseReceiver;
QMutex m_mutex_passphraseReceiver;
+ PairingCodeReceiver * m_pairingCodeReceiver = nullptr;
+ QMutex m_mutex_pairingCodeReceiver;
QString m_proxyAddress;
mutable QMutex m_proxyMutex;
FutureScheduler m_scheduler;
diff --git a/wizard/WizardController.qml b/wizard/WizardController.qml
index 6e722d893c..d3af259927 100644
--- a/wizard/WizardController.qml
+++ b/wizard/WizardController.qml
@@ -35,6 +35,7 @@ import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.2
import QtQuick.Dialogs 1.2
import moneroComponents.Wallet 1.0
+import moneroComponents.WalletManager 1.0
import "../js/Wizard.js" as Wizard
import "../js/Windows.js" as Windows
@@ -48,7 +49,7 @@ Rectangle {
anchors.fill: parent
signal useMoneroClicked()
- signal walletCreatedFromDevice(bool success)
+ signal walletCreatedFromDevice(bool success, bool cancelled)
function restart(generatingNewSeed) {
// Clear up any state, including `m_wallet`, which
@@ -425,6 +426,7 @@ Rectangle {
function disconnect(){
walletManager.walletCreated.disconnect(onWalletCreated);
walletManager.walletPassphraseNeeded.disconnect(onWalletPassphraseNeeded);
+ walletManager.walletPairingCodeNeeded.disconnect(onWalletPairingCodeNeeded);
walletManager.deviceButtonRequest.disconnect(onDeviceButtonRequest);
walletManager.deviceButtonPressed.disconnect(onDeviceButtonPressed);
}
@@ -432,6 +434,7 @@ Rectangle {
function connect(){
walletManager.walletCreated.connect(onWalletCreated);
walletManager.walletPassphraseNeeded.connect(onWalletPassphraseNeeded);
+ walletManager.walletPairingCodeNeeded.connect(onWalletPairingCodeNeeded);
walletManager.deviceButtonRequest.connect(onDeviceButtonRequest);
walletManager.deviceButtonPressed.connect(onDeviceButtonPressed);
}
@@ -456,6 +459,9 @@ Rectangle {
var kdfRounds = persistentSettings.kdfRounds;
var restoreHeight = wizardController.walletOptionsRestoreHeight;
var subaddressLookahead = wizardController.walletOptionsSubaddressLookahead;
+ // Device descriptor. The segment before the first ':' selects the
+ // device class, always "Trezor" here; an empty transport path
+ // matches every USB transport and the first one wins.
var deviceName = wizardController.walletOptionsDeviceName;
connect();
@@ -470,6 +476,7 @@ Rectangle {
wizardController.enabled = true;
splash.close()
+ var cancelled = false;
var success = wallet.status === Wallet.Status_Ok;
if (success) {
wizardController.m_wallet = wallet;
@@ -479,13 +486,59 @@ Rectangle {
wizardController.walletOptionsRestoreHeight = wizardController.m_wallet.walletCreationHeight;
}
} else {
+ // The wallet-create path hits the same Trezor error
+ // categories as wallet-open. The raw errorString is worth
+ // logging but is wallet2-internal phrasing that does not
+ // tell the user what to do.
console.log(wallet.errorString)
- appWindow.showStatusMessage(qsTr(wallet.errorString), 5);
+ var es = wallet.errorString || "";
+ var trezorError = wallet.trezorError;
+ if (trezorError === Wallet.TrezorError_Unreachable) {
+ // Retry re-fires createWalletFromDevice with the same
+ // wizard options; they all persist on wizardController.
+ walletManager.closeWallet();
+ // Unwire the handlers first, otherwise the retry path's
+ // connect() double-binds onWalletCreated.
+ disconnect();
+ appWindow.showRetryableTrezorErrorSplash(
+ qsTr("Couldn't reach your Trezor")
+ + translationManager.emptyString,
+ qsTr("Make sure your Trezor is connected and unlocked, then try again.")
+ + translationManager.emptyString,
+ function() { wizardController.createWalletFromDevice(); },
+ function() { walletCreatedFromDevice(false, true); });
+ return;
+ } else if (trezorError === Wallet.TrezorError_PairingRejected) {
+ // The device rejected the pairing code the user just
+ // typed. Starting over makes it show a fresh code, and
+ // the pairing dialog re-prompts carrying the message.
+ walletManager.closeWallet();
+ disconnect();
+ devicePairingCodeDialog.errorText =
+ qsTr("That code did not match. Check the code on your device and try again.")
+ + translationManager.emptyString;
+ wizardController.createWalletFromDevice();
+ return;
+ } else if (trezorError === Wallet.TrezorError_Cancelled) {
+ appWindow.showStatusMessage(
+ qsTr("Wallet creation cancelled.") + translationManager.emptyString,
+ 5);
+ cancelled = true;
+ } else if (trezorError === Wallet.TrezorError_FirmwareUnsupported) {
+ // Nothing to retry: the device needs different firmware
+ // before it can hold a Monero wallet.
+ appWindow.showStatusMessage(
+ qsTr("This Trezor runs firmware that has no Monero support, such as the bitcoin-only build. Install the standard firmware with Trezor Suite, then try again.")
+ + translationManager.emptyString,
+ 10);
+ } else {
+ appWindow.showStatusMessage(qsTr(wallet.errorString), 5);
+ }
walletManager.closeWallet();
}
disconnect();
- walletCreatedFromDevice(success);
+ walletCreatedFromDevice(success, cancelled);
}
function onWalletPassphraseNeeded(on_device){
@@ -508,6 +561,22 @@ Rectangle {
devicePassphraseDialog.open(on_device)
}
+ function onWalletPairingCodeNeeded(){
+ splash.close()
+
+ console.log(">>> wallet pairing code needed: ");
+ devicePairingCodeDialog.onAcceptedCallback = function(code) {
+ walletManager.onPairingCodeEntered(code, false);
+ creatingWalletDeviceSplash();
+ }
+ devicePairingCodeDialog.onRejectedCallback = function() {
+ walletManager.onPairingCodeEntered("", true);
+ creatingWalletDeviceSplash();
+ }
+
+ devicePairingCodeDialog.open()
+ }
+
function onDeviceButtonRequest(code){
deviceAttentionSplash();
}
diff --git a/wizard/WizardCreateDevice1.qml b/wizard/WizardCreateDevice1.qml
index 268091c3e2..3a70382f80 100644
--- a/wizard/WizardCreateDevice1.qml
+++ b/wizard/WizardCreateDevice1.qml
@@ -61,6 +61,7 @@ Rectangle {
ListElement { column1: "Trezor Model T"; column2: "Trezor";}
ListElement { column1: "Trezor Safe 3"; column2: "Trezor";}
ListElement { column1: "Trezor Safe 5"; column2: "Trezor";}
+ ListElement { column1: "Trezor Safe 7"; column2: "Trezor";}
}
ColumnLayout {
@@ -174,6 +175,8 @@ Rectangle {
return "qrc:///images/trezor3.png";
} else if (trezorType == "Trezor Safe 5") {
return "qrc:///images/trezor5.png";
+ } else if (trezorType == "Trezor Safe 7") {
+ return "qrc:///images/trezor7.png";
}
} else if (hardwareWalletType == "Ledger") {
if (ledgerType == "Ledger Nano S") {
@@ -297,11 +300,11 @@ Rectangle {
}
}
- function onCreateWalletFromDeviceCompleted(written){
+ function onCreateWalletFromDeviceCompleted(written, cancelled){
hideProcessingSplash();
if(written){
wizardStateView.state = "wizardCreateWallet3";
- } else {
+ } else if(!cancelled){
errorMsg.text = qsTr("Error writing wallet from hardware device. Check application logs.") + translationManager.emptyString;
}
wizardController.walletCreatedFromDevice.disconnect(onCreateWalletFromDeviceCompleted);