Files
pay/guis/mobile/ImportWalletPage.qml
2026-05-05 19:24:42 +02:00

708 lines
26 KiB
QML

/*
* This file is part of the Flowee project
* Copyright (C) 2022-2026 Tom Zander <tom@flowee.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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
}
}
}
}
}