/* * This file is part of the Flowee project * Copyright (C) 2022-2026 Tom Zander * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ import QtQuick import QtQuick.Layouts import QtQuick.Controls.Basic as QQC2 import "../Flowee" as Flowee import "../Utils.js" as Utils import Flowee.org.pay Page { id: root headerText: qsTr("Import Wallet") states: [ State { name: "entryPage" PropertyChanges { target: entryPage; x: 0 } PropertyChanges { target: privKeydetailsPage; x: width + 10 } PropertyChanges { target: seedDetailsPage; x: width + 10 } PropertyChanges { target: root; menuItems: [] } }, State { name: "privKeyDetailsPage" PropertyChanges { target: entryPage; x: -10 - width } PropertyChanges { target: privKeydetailsPage; x: 0 } PropertyChanges { target: root; menuItems: [] } }, State { name: "seedDetailsPage" PropertyChanges { target: entryPage; x: -10 - width } PropertyChanges { target: seedDetailsPage; x: 0 } PropertyChanges { target: root; menuItems: [showPasswordAction] } } ] state: "entryPage" property QtObject showPasswordAction: QQC2.Action { checkable: true checked: false text: qsTr("Enter Password", "Toggle to show a password field") onTriggered: passwordBox.visible = checked } property alias secret: secretText.text backHandler: function handler() { // we practically have two or 3 pages inside this on Page object, plus a popup! // we should make the 'back' button be aware of this. if (checkBlockchainPopup.visible) { checkBlockchainPopup.whenDone = undefined // cancel import checkBlockchainPopup.visible = false } else if (state !== "entryPage") state = "entryPage" else thePile.pop() } Keys.onPressed: (event)=> { if (event.key === Qt.Key_Escape || event.key === Qt.Key_Back) { event.accepted = true backHandler() } } function toNextPage() { var type = entryPage.typedData if (type === Wallet.PrivateKey) { state = "privKeyDetailsPage" privKeydetailsPage.takeFocus() } else if (type === Wallet.CorrectMnemonic || type === Wallet.ElectrumMnemonic) { state = "seedDetailsPage" seedDetailsPage.takeFocus() } } function dateOnHeight(height) { // This class, in contrary to the similar method on the Pay class, returns -1 if // the client doesn't have the headers available to do the lookup. var m = seedImportHelper.monthOnHeight(height) if (m === -1) { // use our lookup table to select the historical month / year. let heights = Utils.historicalHeights() for (var index = 0; index < heights.length; ++index) { if (heights[index] > height) break } index = index - 1 m = index % 12 + 1 var y = 2011 + Math.floor(index / 12) } else { y = seedImportHelper.yearOnHeight(height) } return { month: m, year: y } } Column { id: entryPage width: parent.width y: 10 spacing: 15 property var typedData: Pay.identifyString(secretText.totalText) property bool finished: typedData === Wallet.PrivateKey || typedData === Wallet.CorrectMnemonic || typedData === Wallet.ElectrumMnemonic onFinishedChanged: root.toNextPage() PageTitledBox { id: buttonsBox title: qsTr("Select import method") width: parent.width Item { width: parent.width height: scanButton.height + 20 Flowee.ImageButton { id: scanButton source: "qrc:/qr-code-scan" + (Pay.useDarkSkin ? "-light.svg" : ".svg") onClicked: scanner.start() iconSize: Math.min(entryPage.width / 4, 100) x: (parent.width - width) / 2 // while NFC is not enabled.. // x: (parent.width - width * 2 - 20) / 2 y: 10 text: qsTr("Scan QR") } QRScanner { id: scanner onFinished: { if ((scanType === QRScanner.Seed || scanType === QRScanner.PrivateKeyWIF) && scanResult !== "") secretText.text = scanResult // make sure to give focus back to this page after the camera took it. scanButton.forceActiveFocus() } } Rectangle { id: nfcButton width: scanButton.width visible: false height: width radius: 90 color: "#00000000" border.color: "yellow" border.width: 2 x: (parent.width - width * 2 - 20) / 2 + width + 20 y: 10 Flowee.Label { anchors.centerIn: parent text: "NFC" } } } } Row { spacing: 15 x: (entryPage.width - width) / 2 Rectangle { width: 50 height: 1 color: palette.button anchors.verticalCenter: parent.verticalCenter } Flowee.Label { text: qsTr("OR") } Rectangle { width: 50 height: 1 color: palette.button anchors.verticalCenter: parent.verticalCenter } } PageTitledBox { id: textSecretBox title: qsTr("Secret as text", "The seed-phrase or private key") width: parent.width Item { width: parent.width height: pasteButton.visibe ? pasteButton.height / 3 * 2 : 0 + secretText.height Flowee.MultilineTextField { id: secretText width: parent.width clip: true height: Math.max((pasteButton.height - 10) * 2.3, implicitHeight) font.family: "monospace" inputMethodHints: Qt.ImhNoAutoUppercase } Flowee.TextPasteDecorator { id: pasteButton buddy: secretText clipboardTypes: ClipboardHelper.PrivateKey + ClipboardHelper.MnemonicSeed } Item { clip: true height: Math.min(flowLayout.height + 12, textSecretBox.y) width: parent.width y: -height visible: flowLayout.height > 1 Rectangle { anchors.fill: parent color: palette.base border.color: palette.midlight border.width: 1 radius: 5 } Flickable { anchors.fill: parent anchors.margins: 6 contentWidth: width contentHeight: flowLayout.height Flow { id: flowLayout width: parent.width spacing: 10 Repeater { model: Pay.mnemonicProposals Rectangle { width: txt.width + 16 height: txt.height + 6 color: mainWindow.floweeBlue radius: 5 Flowee.Label { id: txt text: modelData anchors.centerIn: parent color: "white" } MouseArea { anchors.fill: parent onClicked: { var widget = secretText var bareText = widget.text // if inputMethodComposing true, that makes it simple to avoid the word that // is being edited the 'text' property of secretText omits that one. if (!secretText.inputMethodComposing) { let lastWordPos = bareText.lastIndexOf(' ') if (lastWordPos > 0) bareText = bareText.substr(0, lastWordPos) } var newText = bareText + " " + modelData + " " widget.text = newText widget.cursorPosition = newText.length } } } } } } } } Flowee.Label { text: entryPage.typedData === Wallet.PartialMnemonicWithTypo ? qsTr("Unknown word(s) found") : "" color: mainWindow.errorRed visible: text !== "" } } Flowee.Button { anchors.right: parent.right text: qsTr("Next") visible: parent.finished onClicked: root.toNextPage() } Behavior on x { NumberAnimation { } } } Item { id: privKeydetailsPage x: width + 10 width: parent.width height: parent.height - 20 y: 10 function takeFocus() { singleAddress.forceActiveFocus() } ColumnLayout { spacing: 10 width: parent.width PageTitledBox { title: qsTr("Address to import") Layout.fillWidth: true Flowee.LabelWithClipboard { // this shows the bitcoincash address matching the private key font.pixelSize: singleAddress.font.pixelSize * 0.9 text: { if (root.state !== "privKeyDetailsPage") return "" return Pay.addressForPrivKey(secretText.text) } } } PageTitledBox { title: qsTr("New Wallet Name") visible: { // don't ask for a name when the user imports a // wallet the first thing in a new instance. var all = portfolio.rawAccounts if (all.length === 1 && !all[0].isUserOwned) return false return true } Flowee.TextField { id: accountName width: parent.width } } Flowee.CheckBox { id: singleAddress Layout.fillWidth: true text: qsTr("Force Single Address") toolTipText: qsTr("When enabled, no extra addresses will be auto-generated in this wallet.\nChange will come back to the imported key.") checked: true } PageTitledBox { title: qsTr("Oldest Transaction") Item { implicitWidth: parent.width implicitHeight: ageButton.height Flowee.BigButton { id: ageButton text: qsTr("Check Age", "online check for wallet age") enabled: !privKeyImportHelper.checking isMainButton: true onClicked: { // setting new values here will start the check. privKeyImportHelper.secretType = Wallet.PrivateKey privKeyImportHelper.secret = secretText.text } ImportHelper { id: privKeyImportHelper onCheckingChanged: { if (checking) return emptyPrivKeyWarningLabel.visible = false ageButton.isMainButton = false if (resultCount === 0) { emptyPrivKeyWarningLabel.visible = true } else if (resultCount === 1) { let date = root.dateOnHeight(startHeight(0)) oldestTransactionChooser.item.selectedMonth = date.month - 1 oldestTransactionChooser.item.selectedYear = date.year - 2011 oldestTransactionChooser.item.enabled = false privImportStartButton.isMainButton = true } } } // TODO add warning } } Loader { id: oldestTransactionChooser width: parent.width sourceComponent: oldestTransactionChooser_component } } Flowee.Label { id: emptyPrivKeyWarningLabel color: mainWindow.errorRed text: qsTr("Nothing found for wallet") visible: false } Flowee.BigButton { id: privImportStartButton text: qsTr("Import") Layout.alignment: Qt.AlignRight onClicked: { var bh = 0 if (privKeyImportHelper.resultCount > 0) bh = privKeyImportHelper.startHeight(0) else bh = Utils.heightOfBlockAtTime(oldestTransactionChooser.item.selectedDate) checkBlockchainPopup.oldestBlock = bh checkBlockchainPopup.whenDone = startImport checkBlockchainPopup.open() } function startImport() { if (privKeyImportHelper.resultCount > 0) { var options = Pay.createImportedWallet(secretText.totalText, accountName.totalText, privKeyImportHelper.startHeight(0)) } else { var height = Utils.heightOfBlockAtTime(oldestTransactionChooser.item.selectedDate) options = Pay.createImportedWallet(secretText.totalText, accountName.totalText, height) } options.forceSingleAddress = singleAddress.checked for (let a of portfolio.accounts) { if (a.id === options.accountId) { portfolio.current = a break } } thePile.pop() thePile.pop() } } } Behavior on x { NumberAnimation { } } } Column { id: seedDetailsPage x: width + 10 y: 10 width: parent.width height: parent.height - 20 spacing: 10 function takeFocus() { if (nameField.visible) nameField.forceActiveFocus() else seedCheckButton.forceActiveFocus() } PageTitledBox { id: nameField title: qsTr("New Wallet Name") width: parent.width visible: { // don't ask for a name when the user imports a // wallet the first thing in a new instance. var all = portfolio.rawAccounts if (all.length === 1 && !all[0].isUserOwned) return false return true } Flowee.TextField { id: accountName2 width: parent.width } } Flowee.BigButton { id: seedCheckButton text: qsTr("Discover Details", "online check for wallet details") enabled: !seedImportHelper.checking isMainButton: true onClicked: { // setting new values here will start the check. seedImportHelper.secretType = entryPage.typedData seedImportHelper.secret = secretText.text seedImportHelper.password = passwordField.text } ImportHelper { id: seedImportHelper onCheckingChanged: { if (checking) return emptySeedWarningLabel.visible = false if (resultCount === 0) { // empty seedCheckButton.isMainButton = false emptySeedWarningLabel.visible = true showPasswordAction.checked = true } else if (resultCount >= 1) { // TODO what to do if there are more then 1? let date = root.dateOnHeight(startHeight(0)) oldestTransactionChooser2.item.selectedMonth = date.month - 1 oldestTransactionChooser2.item.selectedYear = date.year - 2011 oldestTransactionChooser2.item.enabled = false derivationPath.text = derivation(0) derivationPath.enabled = false seedCheckButton.isMainButton = false seedStartButton.isMainButton = true } } } // TODO add warning } PageTitledBox { title: qsTr("Oldest Transaction") width: parent.width Loader { id: oldestTransactionChooser2 width: parent.width sourceComponent: oldestTransactionChooser_component } } PageTitledBox { id: derivationLabel title: qsTr("Derivation Path") width: parent.width Flowee.TextField { id: derivationPath property bool derivationOk: Pay.checkDerivation(text) width: parent.width text: "m/44'/0'/0'" // What most BCH wallets are created with color: derivationOk ? palette.text : "red" } } Flowee.Label { id: emptySeedWarningLabel color: mainWindow.errorRed text: seedImportHelper.failed ? qsTr("Discover failed. Set start date manually") : qsTr("Nothing found for seed. Does it have a password?") visible: false width: parent.width wrapMode: Text.Wrap } PageTitledBox { id: passwordBox title: qsTr("Password") width: parent.width visible: false Flowee.TextField { id: passwordField width: parent.width placeholderText: qsTr("imported wallet password") onDisplayTextChanged: seedCheckButton.isMainButton = true inputMethodHints: Qt.ImhNoAutoUppercase } } Flowee.BigButton { id: seedStartButton text: qsTr("Start") anchors.right: parent.right onClicked: { var bh = 0 if (seedImportHelper.resultCount > 0) bh = seedImportHelper.startHeight(0) else bh = Utils.heightOfBlockAtTime(oldestTransactionChooser2.item.selectedDate) checkBlockchainPopup.oldestBlock = bh checkBlockchainPopup.whenDone = startImport checkBlockchainPopup.open() } function startImport() { if (seedImportHelper.resultCount > 0) { var options = Pay.createImportedHDWallet(secretText.text, passwordField.text, derivationPath.text, accountName2.totalText, seedImportHelper.startHeight(0), seedImportHelper.isElectrumSeed(0)) } else { var height = Utils.heightOfBlockAtTime(oldestTransactionChooser2.item.selectedDate) options = Pay.createImportedHDWallet(secretText.text, passwordField.text, derivationPath.text, accountName2.totalText, height) } for (let a of portfolio.accounts) { if (a.id === options.accountId) { portfolio.current = a break } } thePile.pop() thePile.pop() } } Behavior on x { NumberAnimation { } } } Item { // non-(default) visible items below. /* * The users wallet may not actually have the old headers on the device because the * user never needed them. So to make the import work, we first sill need to actually * download the missing headers. Use the block-headers checker (from the module 'blocks') * to verify and initiate this. */ Flowee.Popup { id: checkBlockchainPopup property int oldestBlock: 0 property var whenDone: null width: root.width - 20 height: checkerLoader.height + 20 background: Rectangle { color: palette.light border.color: palette.midlight border.width: 1 radius: 5 } modal: true closePolicy: QQC2.Popup.NoAutoClose Loader { id: checkerLoader width: parent.width - 20 height: { if (item == null) return 100 return item.implicitHeight } onLoaded: { closeMonitor.target = item // console.log("Loading completed, start checking on height: " + checkBlockchainPopup.oldestBlock) item.wantedHeight = checkBlockchainPopup.oldestBlock } Connections { id: closeMonitor function onVisibleChanged() { let i = checkerLoader.item if (i == null) // shouldn't really happen.. return let visible = i.visible if (!visible) { closeMonitor.target = null checkerLoader.source = "" checkBlockchainPopup.close() // call the callback to initiate the actual import. checkBlockchainPopup.whenDone() } } } } onVisibleChanged: { if (!visible) return // Shipped in module 'blocks' we invoke the 'checker', should it exist. var section = ModuleManager.sectionOnPlugin("blocks", "checker") if (section !== null) checkerLoader.source = section.qml // This loads the plugin. else close() } } Component { id: oldestTransactionChooser_component Item { property date selectedDate: { return new Date(yearThumbler.currentIndex + 2011, monthThumbler.currentIndex, 1) } property alias selectedMonth: monthThumbler.currentIndex property alias selectedYear: yearThumbler.currentIndex width: parent.width height: 60 Flowee.Thumbler { id: monthThumbler width: Math.max(parent.width / 2, 120) model: 12 placeholderText: qsTr("Pick Month") stringForData: function(data) { return Qt.locale().monthName(data) } } Flowee.Thumbler { id: yearThumbler anchors.left: monthThumbler.right anchors.right: parent.right model: new Date().getFullYear() - 2010 currentIndex: 12 placeholderText: qsTr("Pick Year") stringForData: function(data) { return data + 2011 } wrap: false } } } } }