551 lines
20 KiB
QML
551 lines
20 KiB
QML
/*
|
|
* This file is part of the Flowee project
|
|
* Copyright (C) 2021-2025 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.Controls.Basic as QQC2
|
|
import QtQuick.Layouts
|
|
import "../Flowee" as Flowee
|
|
import Flowee.org.pay
|
|
import "../Utils.js" as Utils
|
|
|
|
Item {
|
|
id: root
|
|
|
|
implicitWidth: {
|
|
var w = columnWidth // columnWidth is defined by loader in NewAccountPane
|
|
if (privKeyColumn.visible)
|
|
w += columnWidth + 10
|
|
return w;
|
|
}
|
|
implicitHeight: 200
|
|
|
|
states: [
|
|
State {
|
|
name: "inputState"
|
|
PropertyChanges { target: privKeyColumn; opacity: 0 }
|
|
PropertyChanges { target: seedDetailsColumn; opacity: 0 }
|
|
},
|
|
State {
|
|
name: "privKeyDetailsState"
|
|
PropertyChanges { target: inputColumn; opacity: 0.65 }
|
|
PropertyChanges { target: privKeyColumn; opacity: 1 }
|
|
PropertyChanges { target: seedDetailsColumn; opacity: 0 }
|
|
StateChangeScript { script: {
|
|
if (accountName.visible)
|
|
accountName.forceActiveFocus();
|
|
else
|
|
ageButton.forceActiveFocus();
|
|
}
|
|
}
|
|
},
|
|
State {
|
|
name: "seedDetailsState"
|
|
PropertyChanges { target: inputColumn; opacity: 0.65 }
|
|
PropertyChanges { target: privKeyColumn; opacity: 0 }
|
|
PropertyChanges { target: seedDetailsColumn; opacity: 1 }
|
|
StateChangeScript { script: {
|
|
if (accountName2.visible)
|
|
accountName2.forceActiveFocus();
|
|
else
|
|
seedCheckButton.forceActiveFocus();
|
|
}
|
|
}
|
|
}
|
|
]
|
|
state: "inputState"
|
|
|
|
function toNextPage() {
|
|
var type = inputColumn.typedData;
|
|
if (type === Wallet.PrivateKey) {
|
|
state = "privKeyDetailsState";
|
|
} else if (type === Wallet.CorrectMnemonic || type === Wallet.ElectrumMnemonic) {
|
|
state = "seedDetailsState";
|
|
}
|
|
}
|
|
|
|
Column {
|
|
id: inputColumn
|
|
width: columnWidth
|
|
spacing: 10
|
|
|
|
property var typedData: Pay.identifyString(secretText.totalText)
|
|
property bool finished: typedData === Wallet.PrivateKey || typedData === Wallet.CorrectMnemonic || typedData === Wallet.ElectrumMnemonic
|
|
|
|
onFinishedChanged: root.toNextPage();
|
|
Flowee.GroupBox {
|
|
id: buttonsBox
|
|
title: qsTr("Select import method")
|
|
collapsable: false
|
|
width: parent.width
|
|
Item {
|
|
width: parent.width
|
|
height: scanButton.height
|
|
Flowee.ImageButton {
|
|
id: scanButton
|
|
source: "qrc:/qr-code-scan" + (Pay.useDarkSkin ? "-light.svg" : ".svg");
|
|
onClicked: scanner.start();
|
|
iconSize: Math.min(inputColumn.width / 4, 100)
|
|
x: (parent.width - width) / 2 // while NFC is not enabled..
|
|
// x: (parent.width - width * 2 - 20) / 2
|
|
text: qsTr("Camera")
|
|
}
|
|
QRScanner {
|
|
id: scanner
|
|
onFinished: {
|
|
if ((scanType === QRScanner.Seed || scanType === QRScanner.PrivateKeyWIF)
|
|
&& scanResult !== "")
|
|
secretText.text = scanResult;
|
|
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
|
|
|
|
Flowee.Label {
|
|
anchors.centerIn: parent
|
|
text: "NFC"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Row {
|
|
spacing: 15
|
|
x: (inputColumn.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
|
|
}
|
|
}
|
|
|
|
Flowee.GroupBox {
|
|
id: textSecretBox
|
|
title: qsTr("Secret as text", "The seed-phrase or private key")
|
|
collapsable: false
|
|
width: parent.width
|
|
Item {
|
|
width: parent.width
|
|
height: pasteButton.height / 3 * 2 + 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
|
|
}
|
|
}
|
|
|
|
Flow {
|
|
width: parent.width
|
|
spacing: 10
|
|
|
|
Repeater {
|
|
model: Pay.mnemonicProposals
|
|
Rectangle {
|
|
width: txt.width + 16
|
|
height: txt.height + 6
|
|
color: mainWindow.floweeBlue
|
|
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: inputColumn.typedData === Wallet.PartialMnemonicWithTypo ? qsTr("Unknown word(s) found") : ""
|
|
color: mainWindow.errorRed
|
|
visible: text !== ""
|
|
}
|
|
}
|
|
|
|
Behavior on opacity { NumberAnimation { } }
|
|
}
|
|
Item {
|
|
id: privKeyColumn
|
|
anchors.left: inputColumn.right
|
|
anchors.leftMargin: 10
|
|
width: columnWidth
|
|
height: parent.height
|
|
enabled: visible
|
|
visible: opacity > 0.1
|
|
|
|
ColumnLayout {
|
|
spacing: 10
|
|
width: parent.width
|
|
Flowee.GroupBox {
|
|
title: qsTr("Address to import")
|
|
collapsable: false
|
|
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 !== "privKeyDetailsState")
|
|
return "";
|
|
return Pay.addressForPrivKey(secretText.text);
|
|
}
|
|
}
|
|
}
|
|
|
|
Flowee.GroupBox {
|
|
title: qsTr("New Wallet Name")
|
|
collapsable: false
|
|
Layout.fillWidth: true
|
|
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
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
Flowee.GroupBox {
|
|
title: qsTr("Oldest Transaction")
|
|
collapsable: false
|
|
Layout.fillWidth: true
|
|
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 height = startHeight(0);
|
|
oldestTransactionChooser.item.month.currentIndex
|
|
= monthOnHeight(height) - 1;
|
|
oldestTransactionChooser.item.year.currentIndex
|
|
= yearOnHeight(height) - 2011;
|
|
oldestTransactionChooser.item.enabled = false;
|
|
|
|
privImportStartButton.isMainButton = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO add warning
|
|
}
|
|
}
|
|
Loader {
|
|
id: oldestTransactionChooser
|
|
Layout.fillWidth: true
|
|
sourceComponent: oldestTransactionChooser_component
|
|
}
|
|
}
|
|
Flowee.Label {
|
|
id: emptyPrivKeyWarningLabel
|
|
color: mainWindow.errorRed
|
|
text: qsTr("Nothing found for wallet")
|
|
visible: false
|
|
}
|
|
|
|
|
|
Flowee.BigButton {
|
|
id: privImportStartButton
|
|
text: qsTr("Start")
|
|
Layout.alignment: Qt.AlignRight
|
|
|
|
onClicked: {
|
|
if (privKeyImportHelper.resultCount > 0) {
|
|
var options = Pay.createImportedWallet(secretText.text, accountName.text, privKeyImportHelper.startHeight(0))
|
|
} else {
|
|
var sh = new Date(oldestTransactionChooser.item.year.currentIndex + 2011, oldestTransactionChooser.item.month.currentIndex, 1)
|
|
var height = Utils.heightOfBlockAtTime(sh)
|
|
options = Pay.createImportedWallet(secretText.text, accountName.text, height)
|
|
}
|
|
options.forceSingleAddress = singleAddress.checked;
|
|
|
|
for (let a of portfolio.accounts) {
|
|
if (a.id === options.accountId) {
|
|
portfolio.current = a;
|
|
break;
|
|
}
|
|
}
|
|
newAccountsPane.visible = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on opacity { NumberAnimation { } }
|
|
}
|
|
|
|
Column {
|
|
id: seedDetailsColumn
|
|
anchors.left: inputColumn.right
|
|
anchors.leftMargin: 10
|
|
width: columnWidth
|
|
height: parent.height
|
|
spacing: 10
|
|
enabled: visible
|
|
visible: opacity > 0.1
|
|
|
|
Flowee.GroupBox {
|
|
title: qsTr("New Wallet Name")
|
|
width: parent.width
|
|
collapsable: false
|
|
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
|
|
Layout.fillWidth: true
|
|
}
|
|
}
|
|
|
|
|
|
Flowee.BigButton {
|
|
id: seedCheckButton
|
|
text: qsTr("Discover Details", "online check for wallet details")
|
|
enabled: !seedImportHelper.checking
|
|
isMainButton: true;
|
|
x: 20
|
|
|
|
onClicked: {
|
|
// setting new values here will start the check.
|
|
seedImportHelper.secretType = inputColumn.typedData
|
|
seedImportHelper.secret = secretText.text
|
|
seedImportHelper.password = passwordField.text
|
|
}
|
|
ImportHelper {
|
|
id: seedImportHelper
|
|
onCheckingChanged: {
|
|
if (checking)
|
|
return;
|
|
emptySeedWarningLabel.visible = false;
|
|
if (failed || resultCount === 0) { // failed or empty
|
|
seedCheckButton.isMainButton = false;
|
|
emptySeedWarningLabel.visible = true;
|
|
passwordBox.collapsed = false;
|
|
}
|
|
else if (resultCount >= 1) {
|
|
// TODO what to do if there are more then 1?
|
|
|
|
let height = startHeight(0);
|
|
oldestTransactionChooser2.item.month.currentIndex
|
|
= monthOnHeight(height) - 1;
|
|
oldestTransactionChooser2.item.year.currentIndex
|
|
= yearOnHeight(height) - 2011;
|
|
oldestTransactionChooser2.item.enabled = false;
|
|
derivationPath.text = derivation(0);
|
|
derivationPath.enabled = false;
|
|
|
|
seedCheckButton.isMainButton = false;
|
|
seedStartButton.isMainButton = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Flowee.GroupBox {
|
|
title: qsTr("Oldest Transaction")
|
|
width: parent.width
|
|
collapsable: false
|
|
Loader {
|
|
id: oldestTransactionChooser2
|
|
Layout.fillWidth: true
|
|
sourceComponent: oldestTransactionChooser_component
|
|
}
|
|
}
|
|
Flowee.GroupBox {
|
|
id: derivationLabel
|
|
title: qsTr("Derivation")
|
|
width: parent.width
|
|
collapsable: false
|
|
Flowee.TextField {
|
|
id: derivationPath
|
|
property bool derivationOk: Pay.checkDerivation(text);
|
|
Layout.fillWidth: true
|
|
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
|
|
}
|
|
|
|
Flowee.GroupBox {
|
|
id: passwordBox
|
|
title: qsTr("Password")
|
|
width: parent.width
|
|
collapsable: true
|
|
collapsed: true
|
|
Flowee.TextField {
|
|
id: passwordField
|
|
Layout.fillWidth: true
|
|
placeholderText: qsTr("imported wallet password")
|
|
onDisplayTextChanged: seedCheckButton.isMainButton = true;
|
|
}
|
|
}
|
|
|
|
Flowee.BigButton {
|
|
id: seedStartButton
|
|
text: qsTr("Start")
|
|
anchors.right: parent.right
|
|
onClicked: {
|
|
if (seedImportHelper.resultCount > 0) {
|
|
var options = Pay.createImportedHDWallet(secretText.text, passwordField.text,
|
|
derivationPath.text, accountName2.text, seedImportHelper.startHeight(0),
|
|
seedImportHelper.isElectrumSeed(0));
|
|
} else {
|
|
var sh = new Date(oldestTransactionChooser2.item.year.currentIndex + 2011, oldestTransactionChooser2.item.month.currentIndex, 1);
|
|
var height = Utils.heightOfBlockAtTime(sh)
|
|
options = Pay.createImportedHDWallet(secretText.text, passwordField.text,
|
|
derivationPath.text, accountName2.text, height);
|
|
}
|
|
|
|
for (let a of portfolio.accounts) {
|
|
if (a.id === options.accountId) {
|
|
portfolio.current = a;
|
|
break;
|
|
}
|
|
}
|
|
newAccountsPane.visible = false;
|
|
tabbar.currentIndex = 0;
|
|
}
|
|
}
|
|
|
|
Behavior on opacity { NumberAnimation { } }
|
|
}
|
|
|
|
Item {
|
|
// non-gui items below.
|
|
|
|
Component {
|
|
id: oldestTransactionChooser_component
|
|
Flow {
|
|
property alias month: inner_month
|
|
property alias year: inner_year
|
|
width: parent.width
|
|
spacing: 10
|
|
Flowee.ComboBox {
|
|
id: inner_month
|
|
model: {
|
|
let locale = Qt.locale();
|
|
var list = [];
|
|
for (let i = QQC2.Calendar.January; i <= QQC2.Calendar.December; ++i) {
|
|
list.push(locale.monthName(i));
|
|
}
|
|
return list;
|
|
}
|
|
width: implicitWidth * 1.3 // this makes it fit for bigger fonts.
|
|
}
|
|
Flowee.ComboBox {
|
|
id: inner_year
|
|
model: {
|
|
var list = [];
|
|
let last = new Date().getFullYear();
|
|
for (let i = 2011; i <= last; ++i) {
|
|
list.push(i);
|
|
}
|
|
return list;
|
|
}
|
|
currentIndex: 12;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|