708 lines
26 KiB
QML
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
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|