@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.Pillow import PillowRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert PillowRecipe._version == "7.0.0" |
|||
assert PillowRecipe.depends == ['png', 'jpeg', 'freetype', 'setuptools', 'python3'] |
|||
assert PillowRecipe.python_depends == [] |
|||
|
|||
|
|||
class PillowRecipePinned(util.InheritedRecipeMixin, PillowRecipe): |
|||
sha512sum = "187173a525d4f3f01b4898633263b53a311f337aa7b159c64f79ba8c7006fd44798a058e7cc5d8f1116bad008e4142ff303456692329fe73b0e115ef5c225d73" |
|||
|
|||
|
|||
recipe = PillowRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.freetype import FreetypeRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert FreetypeRecipe._version == "2.10.1" |
|||
assert FreetypeRecipe.depends == [] |
|||
assert FreetypeRecipe.python_depends == [] |
|||
|
|||
|
|||
class FreetypeRecipePinned(util.InheritedRecipeMixin, FreetypeRecipe): |
|||
sha512sum = "346c682744bcf06ca9d71265c108a242ad7d78443eff20142454b72eef47ba6d76671a6e931ed4c4c9091dd8f8515ebdd71202d94b073d77931345ff93cfeaa7" |
|||
|
|||
|
|||
recipe = FreetypeRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.jpeg import JpegRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert JpegRecipe._version == "2.0.1" |
|||
assert JpegRecipe.depends == [] |
|||
assert JpegRecipe.python_depends == [] |
|||
|
|||
|
|||
class JpegRecipePinned(util.InheritedRecipeMixin, JpegRecipe): |
|||
sha512sum = "d456515dcda7c5e2e257c9fd1441f3a5cff0d33281237fb9e3584bbec08a181c4b037947a6f87d805977ec7528df39b12a5d32f6e8db878a62bcc90482f86e0e" |
|||
|
|||
|
|||
recipe = JpegRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.libiconv import LibIconvRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert LibIconvRecipe._version == "1.15" |
|||
assert LibIconvRecipe.depends == [] |
|||
assert LibIconvRecipe.python_depends == [] |
|||
|
|||
|
|||
class LibIconvRecipePinned(util.InheritedRecipeMixin, LibIconvRecipe): |
|||
sha512sum = "1233fe3ca09341b53354fd4bfe342a7589181145a1232c9919583a8c9979636855839049f3406f253a9d9829908816bb71fd6d34dd544ba290d6f04251376b1a" |
|||
|
|||
|
|||
recipe = LibIconvRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.libzbar import LibZBarRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert LibZBarRecipe._version == "0.10" |
|||
assert LibZBarRecipe.depends == ['libiconv'] |
|||
assert LibZBarRecipe.python_depends == [] |
|||
|
|||
|
|||
class LibZBarRecipePinned(util.InheritedRecipeMixin, LibZBarRecipe): |
|||
sha512sum = "d624f8ab114bf59c62e364f8b3e334bece48f5c11654739d810ed2b8553b8390a70763b0ae12d83c1472cfeda5d9e1a0b7c9c60228a79bf9f5a6fae4a9f7ccb9" |
|||
|
|||
|
|||
recipe = LibZBarRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.png import PngRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert PngRecipe._version == "1.6.37" |
|||
assert PngRecipe.depends == [] |
|||
assert PngRecipe.python_depends == [] |
|||
|
|||
|
|||
class PngRecipePinned(util.InheritedRecipeMixin, PngRecipe): |
|||
sha512sum = "f304f8aaaee929dbeff4ee5260c1ab46d231dcb0261f40f5824b5922804b6b4ed64c91cbf6cc1e08554c26f50ac017899a5971190ca557bc3c11c123379a706f" |
|||
|
|||
|
|||
recipe = PngRecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.pyqt5 import PyQt5Recipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert PyQt5Recipe._version == "5.15.6" |
|||
assert PyQt5Recipe.depends == ['qt5', 'pyjnius', 'setuptools', 'pyqt5sip'] |
|||
assert PyQt5Recipe.python_depends == [] |
|||
|
|||
|
|||
class PyQt5RecipePinned(util.InheritedRecipeMixin, PyQt5Recipe): |
|||
sha512sum = "65fd663cb70e8701e49bd4b39dc9384546cf2edd1b3bab259ca64b50908f48bdc02ca143f36cd6b429075f5616dcc7b291607dcb63afa176e828cded3b82f5c7" |
|||
|
|||
|
|||
recipe = PyQt5RecipePinned() |
@ -0,0 +1,18 @@ |
|||
import os |
|||
|
|||
from pythonforandroid.recipes.pyqt5sip import PyQt5SipRecipe |
|||
from pythonforandroid.util import load_source |
|||
|
|||
util = load_source('util', os.path.join(os.path.dirname(os.path.dirname(__file__)), 'util.py')) |
|||
|
|||
|
|||
assert PyQt5SipRecipe._version == "12.9.0" |
|||
assert PyQt5SipRecipe.depends == ['setuptools', 'python3'] |
|||
assert PyQt5SipRecipe.python_depends == [] |
|||
|
|||
|
|||
class PyQt5SipRecipePinned(util.InheritedRecipeMixin, PyQt5SipRecipe): |
|||
sha512sum = "ca6f3b18b64391fded88732a8109a04d85727bbddecdf126679b187c7f0487c3c1f69ada3e8c54051281a43c6f2de70390ac5ff18a1bed79994070ddde730c5f" |
|||
|
|||
|
|||
recipe = PyQt5SipRecipePinned() |
@ -0,0 +1,8 @@ |
|||
from pythonforandroid.recipes.qt5 import Qt5Recipe |
|||
|
|||
|
|||
assert Qt5Recipe._version == "9b43a43ee96198674060c6b9591e515e2d27c28f" |
|||
assert Qt5Recipe.depends == ['python3'] |
|||
assert Qt5Recipe.python_depends == [] |
|||
|
|||
recipe = Qt5Recipe() |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 5.0 KiB |
After Width: | Height: | Size: 880 B |
After Width: | Height: | Size: 453 B |
After Width: | Height: | Size: 5.8 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 946 B |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 824 B |
@ -0,0 +1,117 @@ |
|||
import os |
|||
import signal |
|||
import sys |
|||
import traceback |
|||
import threading |
|||
import re |
|||
from typing import Optional, TYPE_CHECKING, List |
|||
|
|||
try: |
|||
import PyQt5 |
|||
except Exception: |
|||
sys.exit("Error: Could not import PyQt5 on Linux systems, you may try 'sudo apt-get install python3-pyqt5'") |
|||
|
|||
try: |
|||
import PyQt5.QtQml |
|||
except Exception: |
|||
sys.exit("Error: Could not import PyQt5.QtQml on Linux systems, you may try 'sudo apt-get install python3-pyqt5.qtquick'") |
|||
|
|||
from PyQt5.QtCore import QLocale, QTimer |
|||
from PyQt5.QtGui import QGuiApplication |
|||
import PyQt5.QtCore as QtCore |
|||
|
|||
from electrum.i18n import _, set_language, languages |
|||
from electrum.plugin import run_hook |
|||
from electrum.base_wizard import GoBack |
|||
from electrum.util import (UserCancelled, profiler, |
|||
WalletFileException, BitcoinException, get_new_wallet_name) |
|||
from electrum.wallet import Wallet, Abstract_Wallet |
|||
from electrum.wallet_db import WalletDB |
|||
from electrum.logging import Logger, get_logger |
|||
|
|||
if TYPE_CHECKING: |
|||
from electrum.daemon import Daemon |
|||
from electrum.simple_config import SimpleConfig |
|||
from electrum.plugin import Plugins |
|||
|
|||
from .qeapp import ElectrumQmlApplication |
|||
|
|||
class UncaughtException(Exception): |
|||
pass |
|||
|
|||
|
|||
class ElectrumGui(Logger): |
|||
|
|||
@profiler |
|||
def __init__(self, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugins'): |
|||
set_language(config.get('language', self.get_default_language())) |
|||
Logger.__init__(self) |
|||
#os.environ['QML_IMPORT_TRACE'] = '1' |
|||
#os.environ['QT_DEBUG_PLUGINS'] = '1' |
|||
|
|||
self.logger.info(f"Qml GUI starting up... Qt={QtCore.QT_VERSION_STR}, PyQt={QtCore.PYQT_VERSION_STR}") |
|||
self.logger.info("CWD=%s" % os.getcwd()) |
|||
# Uncomment this call to verify objects are being properly |
|||
# GC-ed when windows are closed |
|||
#network.add_jobs([DebugMem([Abstract_Wallet, SPV, Synchronizer, |
|||
# ElectrumWindow], interval=5)]) |
|||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) |
|||
if hasattr(QtCore.Qt, "AA_ShareOpenGLContexts"): |
|||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_ShareOpenGLContexts) |
|||
if hasattr(QGuiApplication, 'setDesktopFileName'): |
|||
QGuiApplication.setDesktopFileName('electrum.desktop') |
|||
if hasattr(QtCore.Qt, "AA_EnableHighDpiScaling"): |
|||
QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling); |
|||
|
|||
if not "QT_QUICK_CONTROLS_STYLE" in os.environ: |
|||
os.environ["QT_QUICK_CONTROLS_STYLE"] = "Material" |
|||
|
|||
self.gui_thread = threading.current_thread() |
|||
self.plugins = plugins |
|||
self.app = ElectrumQmlApplication(sys.argv, config, daemon) |
|||
# timer |
|||
self.timer = QTimer(self.app) |
|||
self.timer.setSingleShot(False) |
|||
self.timer.setInterval(500) # msec |
|||
self.timer.timeout.connect(lambda: None) # periodically enter python scope |
|||
|
|||
sys.excepthook = self.excepthook |
|||
threading.excepthook = self.texcepthook |
|||
|
|||
# Initialize any QML plugins |
|||
run_hook('init_qml', self) |
|||
self.app.engine.load('electrum/gui/qml/components/main.qml') |
|||
|
|||
def close(self): |
|||
self.app.quit() |
|||
|
|||
def excepthook(self, exc_type, exc_value, exc_tb): |
|||
tb = "".join(traceback.format_exception(exc_type, exc_value, exc_tb)) |
|||
self.logger.exception(tb) |
|||
self.app._valid = False |
|||
self.close() |
|||
|
|||
def texcepthook(self, arg): |
|||
tb = "".join(traceback.format_exception(arg.exc_type, arg.exc_value, arg.exc_tb)) |
|||
self.logger.exception(tb) |
|||
self.app._valid = False |
|||
self.close() |
|||
|
|||
def main(self): |
|||
if not self.app._valid: |
|||
return |
|||
|
|||
self.timer.start() |
|||
signal.signal(signal.SIGINT, lambda *args: self.stop()) |
|||
|
|||
self.logger.info('Entering main loop') |
|||
self.app.exec_() |
|||
|
|||
def stop(self): |
|||
self.logger.info('closing GUI') |
|||
self.app.quit() |
|||
|
|||
def get_default_language(self): |
|||
name = QLocale.system().name() |
|||
return name if name in languages else 'en_UK' |
|||
|
@ -0,0 +1,58 @@ |
|||
from functools import wraps, partial |
|||
|
|||
from PyQt5.QtCore import pyqtSignal, pyqtSlot |
|||
|
|||
from electrum.logging import get_logger |
|||
|
|||
def auth_protect(func=None, reject=None, method='pin'): |
|||
if func is None: |
|||
return partial(auth_protect, reject=reject, method=method) |
|||
|
|||
@wraps(func) |
|||
def wrapper(self, *args, **kwargs): |
|||
self._logger.debug(str(self)) |
|||
if hasattr(self, '__auth_fcall'): |
|||
self._logger.debug('object already has a pending authed function call') |
|||
raise Exception('object already has a pending authed function call') |
|||
setattr(self, '__auth_fcall', (func,args,kwargs,reject)) |
|||
getattr(self, 'authRequired').emit(method) |
|||
|
|||
return wrapper |
|||
|
|||
class AuthMixin: |
|||
_auth_logger = get_logger(__name__) |
|||
|
|||
authRequired = pyqtSignal([str],arguments=['method']) |
|||
|
|||
@pyqtSlot() |
|||
def authProceed(self): |
|||
self._auth_logger.debug('Proceeding with authed fn()') |
|||
try: |
|||
self._auth_logger.debug(str(getattr(self, '__auth_fcall'))) |
|||
(func,args,kwargs,reject) = getattr(self, '__auth_fcall') |
|||
r = func(self, *args, **kwargs) |
|||
return r |
|||
except Exception as e: |
|||
self._auth_logger.error('Error executing wrapped fn(): %s' % repr(e)) |
|||
raise e |
|||
finally: |
|||
delattr(self,'__auth_fcall') |
|||
|
|||
@pyqtSlot() |
|||
def authCancel(self): |
|||
self._auth_logger.debug('Cancelling authed fn()') |
|||
if not hasattr(self, '__auth_fcall'): |
|||
return |
|||
|
|||
try: |
|||
(func,args,kwargs,reject) = getattr(self, '__auth_fcall') |
|||
if reject is not None: |
|||
if hasattr(self, reject): |
|||
getattr(self, reject)() |
|||
else: |
|||
self._auth_logger.error('Reject method \'%s\' not defined' % reject) |
|||
except Exception as e: |
|||
self._auth_logger.error('Error executing reject function \'%s\': %s' % (reject, repr(e))) |
|||
raise e |
|||
finally: |
|||
delattr(self, '__auth_fcall') |
@ -0,0 +1,90 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Pane { |
|||
property string title: qsTr("About Electrum") |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
columns: 2 |
|||
width: parent.width |
|||
|
|||
Item { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.preferredWidth: parent.width |
|||
Layout.preferredHeight: parent.width * 3/4 // reduce height, empty space in png |
|||
|
|||
Image { |
|||
id: electrum_logo |
|||
width: parent.width |
|||
height: width |
|||
source: '../../icons/electrum_presplash.png' |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Version') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: BUILD.electrum_version |
|||
} |
|||
Label { |
|||
text: qsTr('APK Version') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: BUILD.apk_version |
|||
} |
|||
Label { |
|||
text: qsTr('Protocol version') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: BUILD.protocol_version |
|||
} |
|||
Label { |
|||
text: qsTr('License') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: qsTr('MIT License') |
|||
} |
|||
Label { |
|||
text: qsTr('Homepage') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: qsTr('<a href="https://electrum.org">https://electrum.org</a>') |
|||
textFormat: Text.RichText |
|||
onLinkActivated: Qt.openUrlExternally(link) |
|||
} |
|||
Label { |
|||
text: qsTr('Developers') |
|||
Layout.alignment: Qt.AlignRight |
|||
} |
|||
Label { |
|||
text: 'Thomas Voegtlin\nSomberNight\nSander van Grieken' |
|||
} |
|||
Item { |
|||
width: 1 |
|||
height: constants.paddingXLarge |
|||
Layout.columnSpan: 2 |
|||
} |
|||
Label { |
|||
text: qsTr('Distributed by Electrum Technologies GmbH') |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,268 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property string address |
|||
|
|||
property string title: qsTr("Address details") |
|||
|
|||
signal addressDetailsChanged |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Spend from') |
|||
//onTriggered: |
|||
icon.source: '../../icons/tab_send.png' |
|||
enabled: false |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Sign/Verify') |
|||
icon.source: '../../icons/key.png' |
|||
enabled: false |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Encrypt/Decrypt') |
|||
icon.source: '../../icons/mail_icon.png' |
|||
enabled: false |
|||
} |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Address') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: root.address |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Address'), 'text': root.address }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Label') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
id: labelContent |
|||
|
|||
property bool editmode: false |
|||
|
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
visible: !labelContent.editmode |
|||
text: addressdetails.label |
|||
wrapMode: Text.Wrap |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
ToolButton { |
|||
visible: !labelContent.editmode |
|||
icon.source: '../../icons/pen.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelEdit.text = addressdetails.label |
|||
labelContent.editmode = true |
|||
labelEdit.focus = true |
|||
} |
|||
} |
|||
TextField { |
|||
id: labelEdit |
|||
visible: labelContent.editmode |
|||
text: addressdetails.label |
|||
font.pixelSize: constants.fontSizeLarge |
|||
Layout.fillWidth: true |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/confirmed.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelContent.editmode = false |
|||
addressdetails.set_label(labelEdit.text) |
|||
} |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/delete.png' |
|||
icon.color: 'transparent' |
|||
onClicked: labelContent.editmode = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Public keys') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Repeater { |
|||
model: addressdetails.pubkeys |
|||
delegate: TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: modelData |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Public key'), 'text': modelData }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Script type') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: addressdetails.scriptType |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Balance') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
font.family: FixedFont |
|||
text: Config.formatSats(addressdetails.balance) |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(addressdetails.balance) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Transactions') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: addressdetails.numTx |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Derivation path') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: addressdetails.derivationPath |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Frozen') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: addressdetails.isFrozen ? qsTr('Frozen') : qsTr('Not frozen') |
|||
} |
|||
|
|||
ColumnLayout { |
|||
Layout.columnSpan: 2 |
|||
|
|||
Button { |
|||
text: addressdetails.isFrozen ? qsTr('Unfreeze') : qsTr('Freeze') |
|||
onClicked: addressdetails.freeze(!addressdetails.isFrozen) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
AddressDetails { |
|||
id: addressdetails |
|||
wallet: Daemon.currentWallet |
|||
address: root.address |
|||
onFrozenChanged: addressDetailsChanged() |
|||
onLabelChanged: addressDetailsChanged() |
|||
} |
|||
|
|||
Component { |
|||
id: share |
|||
GenericShareDialog {} |
|||
} |
|||
} |
@ -0,0 +1,172 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Pane { |
|||
id: rootItem |
|||
padding: 0 |
|||
width: parent.width |
|||
property string title: Daemon.currentWallet.name + ' - ' + qsTr('Addresses') |
|||
|
|||
ColumnLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
Item { |
|||
width: parent.width |
|||
Layout.fillHeight: true |
|||
|
|||
ListView { |
|||
id: listview |
|||
width: parent.width |
|||
height: parent.height |
|||
clip: true |
|||
model: Daemon.currentWallet.addressModel |
|||
currentIndex: -1 |
|||
|
|||
section.property: 'type' |
|||
section.criteria: ViewSection.FullString |
|||
section.delegate: sectionDelegate |
|||
|
|||
delegate: ItemDelegate { |
|||
id: delegate |
|||
width: ListView.view.width |
|||
height: delegateLayout.height |
|||
highlighted: ListView.isCurrentItem |
|||
|
|||
font.pixelSize: constants.fontSizeMedium // set default font size for child controls |
|||
|
|||
onClicked: { |
|||
var page = app.stack.push(Qt.resolvedUrl('AddressDetails.qml'), {'address': model.address}) |
|||
page.addressDetailsChanged.connect(function() { |
|||
// update listmodel when details change |
|||
listview.model.update_address(model.address) |
|||
}) |
|||
} |
|||
|
|||
ColumnLayout { |
|||
id: delegateLayout |
|||
spacing: 0 |
|||
x: constants.paddingMedium |
|||
width: parent.width - 2*constants.paddingMedium |
|||
|
|||
Item { |
|||
Layout.preferredWidth: 1 |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 2 |
|||
Label { |
|||
id: indexLabel |
|||
font.bold: true |
|||
text: '#' + ('00'+model.iaddr).slice(-2) |
|||
Layout.fillWidth: true |
|||
} |
|||
Label { |
|||
font.family: FixedFont |
|||
text: model.address |
|||
elide: Text.ElideMiddle |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
Rectangle { |
|||
id: useIndicator |
|||
Layout.preferredWidth: constants.iconSizeMedium |
|||
Layout.preferredHeight: constants.iconSizeMedium |
|||
color: model.held |
|||
? Qt.rgba(1,0,0,0.75) |
|||
: model.numtx > 0 |
|||
? model.balance.satsInt == 0 |
|||
? Qt.rgba(0.5,0.5,0.5,1) |
|||
: Qt.rgba(0.75,0.75,0.75,1) |
|||
: model.type == 'receive' |
|||
? Qt.rgba(0,1,0,0.5) |
|||
: Qt.rgba(1,0.93,0,0.75) |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
id: labelLabel |
|||
font.pixelSize: model.label != '' ? constants.fontSizeLarge : constants.fontSizeSmall |
|||
text: model.label != '' ? model.label : '<no label>' |
|||
opacity: model.label != '' ? 1.0 : 0.8 |
|||
elide: Text.ElideRight |
|||
maximumLineCount: 2 |
|||
wrapMode: Text.WordWrap |
|||
Layout.fillWidth: true |
|||
} |
|||
Label { |
|||
font.family: FixedFont |
|||
text: Config.formatSats(model.balance, false) |
|||
visible: model.balance.satsInt != 0 |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: Config.baseUnit + ',' |
|||
visible: model.balance.satsInt != 0 |
|||
} |
|||
Label { |
|||
text: model.numtx |
|||
visible: model.numtx > 0 |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: qsTr('tx') |
|||
visible: model.numtx > 0 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { |
|||
Layout.preferredWidth: 1 |
|||
Layout.preferredHeight: constants.paddingSmall |
|||
} |
|||
} |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: sectionDelegate |
|||
Rectangle { |
|||
id: root |
|||
width: ListView.view.width |
|||
height: childrenRect.height |
|||
color: 'transparent' |
|||
|
|||
required property string section |
|||
|
|||
RowLayout { |
|||
x: constants.paddingMedium |
|||
width: parent.width - 2 * constants.paddingMedium |
|||
|
|||
Rectangle { |
|||
Layout.preferredHeight: 1 |
|||
Layout.fillWidth: true |
|||
color: Material.accentColor |
|||
} |
|||
Label { |
|||
padding: constants.paddingMedium |
|||
text: root.section + ' ' + qsTr('addresses') |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
Rectangle { |
|||
Layout.preferredHeight: 1 |
|||
Layout.fillWidth: true |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,160 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Frame { |
|||
id: root |
|||
height: layout.height |
|||
font.pixelSize: constants.fontSizeMedium |
|||
|
|||
property string formattedBalance |
|||
property string formattedBalanceFiat |
|||
property string formattedUnconfirmed |
|||
property string formattedUnconfirmedFiat |
|||
property string formattedFrozen |
|||
property string formattedFrozenFiat |
|||
property string formattedLightningBalance |
|||
property string formattedLightningBalanceFiat |
|||
|
|||
function setBalances() { |
|||
root.formattedBalance = Config.formatSats(Daemon.currentWallet.confirmedBalance) |
|||
root.formattedUnconfirmed = Config.formatSats(Daemon.currentWallet.unconfirmedBalance) |
|||
root.formattedFrozen = Config.formatSats(Daemon.currentWallet.frozenBalance) |
|||
root.formattedLightningBalance = Config.formatSats(Daemon.currentWallet.lightningBalance) |
|||
if (Daemon.fx.enabled) { |
|||
root.formattedBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.confirmedBalance, false) |
|||
root.formattedUnconfirmedFiat = Daemon.fx.fiatValue(Daemon.currentWallet.unconfirmedBalance, false) |
|||
root.formattedFrozenFiat = Daemon.fx.fiatValue(Daemon.currentWallet.frozenBalance, false) |
|||
root.formattedLightningBalanceFiat = Daemon.fx.fiatValue(Daemon.currentWallet.lightningBalance, false) |
|||
} |
|||
} |
|||
|
|||
GridLayout { |
|||
id: layout |
|||
|
|||
columns: 2 |
|||
Label { |
|||
font.pixelSize: constants.fontSizeLarge |
|||
text: qsTr('Balance: ') |
|||
} |
|||
RowLayout { |
|||
Label { |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
text: formattedBalance |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeMedium |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeMedium |
|||
text: Daemon.fx.enabled |
|||
? '(' + root.formattedBalanceFiat + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
Label { |
|||
visible: Daemon.currentWallet.unconfirmedBalance.satsInt > 0 |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: qsTr('Unconfirmed: ') |
|||
} |
|||
RowLayout { |
|||
visible: Daemon.currentWallet.unconfirmedBalance.satsInt > 0 |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
font.family: FixedFont |
|||
text: formattedUnconfirmed |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: Daemon.fx.enabled |
|||
? '(' + root.formattedUnconfirmedFiat + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
Label { |
|||
visible: Daemon.currentWallet.frozenBalance.satsInt > 0 |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: qsTr('Frozen: ') |
|||
} |
|||
RowLayout { |
|||
visible: Daemon.currentWallet.frozenBalance.satsInt > 0 |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
font.family: FixedFont |
|||
text: root.formattedFrozen |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: Daemon.fx.enabled |
|||
? '(' + root.formattedFrozenFiat + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
Label { |
|||
visible: Daemon.currentWallet.isLightning |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: qsTr('Lightning: ') |
|||
} |
|||
RowLayout { |
|||
visible: Daemon.currentWallet.isLightning |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
font.family: FixedFont |
|||
text: formattedLightningBalance |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: Daemon.fx.enabled |
|||
? '(' + root.formattedLightningBalanceFiat + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
} |
|||
|
|||
// instead of all these explicit connections, we should expose |
|||
// formatted balances directly as a property |
|||
Connections { |
|||
target: Config |
|||
function onBaseUnitChanged() { setBalances() } |
|||
function onThousandsSeparatorChanged() { setBalances() } |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon |
|||
function onWalletLoaded() { setBalances() } |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.fx |
|||
function onEnabledUpdated() { setBalances() } |
|||
function onQuotesUpdated() { setBalances() } |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onBalanceChanged() { |
|||
setBalances() |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: setBalances() |
|||
} |
@ -0,0 +1,270 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property string channelid |
|||
|
|||
property string title: qsTr("Channel details") |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Backup'); |
|||
enabled: false |
|||
onTriggered: {} |
|||
icon.source: '../../icons/file.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Close channel'); |
|||
enabled: channeldetails.canClose |
|||
onTriggered: { |
|||
var dialog = closechannel.createObject(root, { 'channelid': channelid }) |
|||
dialog.open() |
|||
} |
|||
icon.source: '../../icons/closebutton.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Delete channel'); |
|||
enabled: channeldetails.canDelete |
|||
onTriggered: { |
|||
var dialog = app.messageDialog.createObject(root, |
|||
{ |
|||
'text': qsTr('Are you sure you want to delete this channel? This will purge associated transactions from your wallet history.'), |
|||
'yesno': true |
|||
} |
|||
) |
|||
dialog.yesClicked.connect(function() { |
|||
channeldetails.deleteChannel() |
|||
app.stack.pop() |
|||
Daemon.currentWallet.historyModel.init_model() // needed here? |
|||
Daemon.currentWallet.channelModel.remove_channel(channelid) |
|||
}) |
|||
dialog.open() |
|||
} |
|||
icon.source: '../../icons/delete.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
enabled: channeldetails.isOpen |
|||
text: channeldetails.frozenForSending ? qsTr('Unfreeze (for sending)') : qsTr('Freeze (for sending)') |
|||
onTriggered: channeldetails.freezeForSending() |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
enabled: channeldetails.isOpen |
|||
text: channeldetails.frozenForReceiving ? qsTr('Unfreeze (for receiving)') : qsTr('Freeze (for receiving)') |
|||
onTriggered: channeldetails.freezeForReceiving() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Channel name') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.name |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Short channel ID') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.short_cid |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('State') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.state |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Initiator') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.initiator |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Capacity') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
font.family: FixedFont |
|||
text: Config.formatSats(channeldetails.capacity) |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(channeldetails.capacity) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Can send') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
visible: !channeldetails.frozenForSending && channeldetails.isOpen |
|||
Label { |
|||
font.family: FixedFont |
|||
text: Config.formatSats(channeldetails.canSend) |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(channeldetails.canSend) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
Label { |
|||
visible: channeldetails.frozenForSending && channeldetails.isOpen |
|||
text: qsTr('n/a (frozen)') |
|||
} |
|||
Label { |
|||
visible: !channeldetails.isOpen |
|||
text: qsTr('n/a (channel not open)') |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Can Receive') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
visible: !channeldetails.frozenForReceiving && channeldetails.isOpen |
|||
Label { |
|||
font.family: FixedFont |
|||
text: Config.formatSats(channeldetails.canReceive) |
|||
} |
|||
Label { |
|||
color: Material.accentColor |
|||
text: Config.baseUnit |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(channeldetails.canReceive) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
Label { |
|||
visible: channeldetails.frozenForReceiving && channeldetails.isOpen |
|||
text: qsTr('n/a (frozen)') |
|||
} |
|||
Label { |
|||
visible: !channeldetails.isOpen |
|||
text: qsTr('n/a (channel not open)') |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Channel type') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.channelType |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Remote node ID') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: channeldetails.pubkey |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Channel node ID'), 'text': channeldetails.pubkey }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
ChannelDetails { |
|||
id: channeldetails |
|||
wallet: Daemon.currentWallet |
|||
channelid: root.channelid |
|||
} |
|||
|
|||
Component { |
|||
id: share |
|||
GenericShareDialog {} |
|||
} |
|||
|
|||
Component { |
|||
id: closechannel |
|||
CloseChannelDialog {} |
|||
} |
|||
} |
@ -0,0 +1,162 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
property string title: qsTr("Lightning Channels") |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Swap'); |
|||
enabled: Daemon.currentWallet.lightningCanSend.satsInt > 0 || Daemon.currentWallet.lightningCanReceive.satInt > 0 |
|||
onTriggered: { |
|||
var dialog = swapDialog.createObject(root) |
|||
dialog.open() |
|||
} |
|||
icon.source: '../../icons/status_waiting.png' |
|||
} |
|||
} |
|||
MenuSeparator {} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Open Channel'); |
|||
onTriggered: app.stack.push(Qt.resolvedUrl('OpenChannel.qml')) |
|||
icon.source: '../../icons/lightning.png' |
|||
} |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
GridLayout { |
|||
id: summaryLayout |
|||
Layout.preferredWidth: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
Layout.columnSpan: 2 |
|||
text: qsTr('You have %1 open channels').arg(Daemon.currentWallet.channelModel.numOpenChannels) |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('You can send:') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
text: Config.formatSats(Daemon.currentWallet.lightningCanSend) |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(Daemon.currentWallet.lightningCanSend) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('You can receive:') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
text: Config.formatSats(Daemon.currentWallet.lightningCanReceive) |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
Label { |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(Daemon.currentWallet.lightningCanReceive) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
Frame { |
|||
id: channelsFrame |
|||
Layout.preferredWidth: parent.width |
|||
Layout.fillHeight: true |
|||
verticalPadding: 0 |
|||
horizontalPadding: 0 |
|||
background: PaneInsetBackground {} |
|||
|
|||
ColumnLayout { |
|||
spacing: 0 |
|||
anchors.fill: parent |
|||
|
|||
Item { |
|||
Layout.preferredHeight: hitem.height |
|||
Layout.preferredWidth: parent.width |
|||
Rectangle { |
|||
anchors.fill: parent |
|||
color: Qt.lighter(Material.background, 1.25) |
|||
} |
|||
RowLayout { |
|||
id: hitem |
|||
width: parent.width |
|||
Label { |
|||
text: qsTr('Channels') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
|
|||
ListView { |
|||
id: listview |
|||
Layout.preferredWidth: parent.width |
|||
Layout.fillHeight: true |
|||
clip: true |
|||
model: Daemon.currentWallet.channelModel |
|||
|
|||
delegate: ChannelDelegate { |
|||
onClicked: { |
|||
app.stack.push(Qt.resolvedUrl('ChannelDetails.qml'), { 'channelid': model.cid }) |
|||
} |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
} |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.fillWidth: true |
|||
Button { |
|||
text: qsTr('Open Channel') |
|||
onClicked: app.stack.push(Qt.resolvedUrl('OpenChannel.qml')) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
Component { |
|||
id: swapDialog |
|||
Swap {} |
|||
} |
|||
} |
@ -0,0 +1,130 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property string channelid |
|||
|
|||
title: qsTr('Close Channel') |
|||
standardButtons: closing ? 0 : Dialog.Cancel |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
property bool closing: false |
|||
|
|||
closePolicy: Popup.NoAutoClose |
|||
|
|||
GridLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Channel name') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.name |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Short channel ID') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: channeldetails.short_cid |
|||
} |
|||
|
|||
InfoTextArea { |
|||
Layout.columnSpan: 2 |
|||
text: qsTr(channeldetails.message_force_close) |
|||
} |
|||
|
|||
ColumnLayout { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
|
|||
ButtonGroup { |
|||
id: closetypegroup |
|||
} |
|||
|
|||
RadioButton { |
|||
ButtonGroup.group: closetypegroup |
|||
property string closetype: 'cooperative' |
|||
checked: true |
|||
enabled: !closing && channeldetails.canCoopClose |
|||
text: qsTr('Cooperative close') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: closetypegroup |
|||
property string closetype: 'force' |
|||
enabled: !closing && channeldetails.canForceClose |
|||
text: qsTr('Request Force-close') |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: qsTr('Close') |
|||
enabled: !closing |
|||
onClicked: { |
|||
closing = true |
|||
channeldetails.close_channel(closetypegroup.checkedButton.closetype) |
|||
} |
|||
|
|||
} |
|||
|
|||
ColumnLayout { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Label { |
|||
id: errorText |
|||
visible: !closing && errorText |
|||
wrapMode: Text.Wrap |
|||
Layout.preferredWidth: layout.width |
|||
} |
|||
Label { |
|||
text: qsTr('Closing...') |
|||
visible: closing |
|||
} |
|||
BusyIndicator { |
|||
visible: closing |
|||
} |
|||
} |
|||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } |
|||
|
|||
} |
|||
|
|||
ChannelDetails { |
|||
id: channeldetails |
|||
wallet: Daemon.currentWallet |
|||
channelid: dialog.channelid |
|||
|
|||
onChannelCloseSuccess: { |
|||
closing = false |
|||
dialog.close() |
|||
} |
|||
|
|||
onChannelCloseFailed: { |
|||
closing = false |
|||
errorText.text = message |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,262 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
|
|||
required property QtObject finalizer |
|||
required property Amount satoshis |
|||
property string address |
|||
property string message |
|||
property alias amountLabelText: amountLabel.text |
|||
property alias sendButtonText: sendButton.text |
|||
|
|||
signal txcancelled |
|||
signal txaccepted |
|||
|
|||
title: qsTr('Confirm Transaction') |
|||
|
|||
// copy these to finalizer |
|||
onAddressChanged: finalizer.address = address |
|||
onSatoshisChanged: finalizer.amount = satoshis |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
function updateAmountText() { |
|||
btcValue.text = Config.formatSats(finalizer.effectiveAmount, false) |
|||
fiatValue.text = Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(finalizer.effectiveAmount, false) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
} |
|||
|
|||
GridLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
columns: 2 |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
id: amountLabel |
|||
text: qsTr('Amount to send') |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
id: btcValue |
|||
font.bold: true |
|||
} |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
id: fiatValue |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
|
|||
Component.onCompleted: updateAmountText() |
|||
Connections { |
|||
target: finalizer |
|||
function onEffectiveAmountChanged() { |
|||
updateAmountText() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Mining fee') |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
id: fee |
|||
text: Config.formatSats(finalizer.fee) |
|||
} |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Fee rate') |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
id: feeRate |
|||
text: finalizer.feeRate |
|||
} |
|||
|
|||
Label { |
|||
text: 'sat/vB' |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Target') |
|||
} |
|||
|
|||
Label { |
|||
id: targetdesc |
|||
text: finalizer.target |
|||
} |
|||
|
|||
Slider { |
|||
id: feeslider |
|||
snapMode: Slider.SnapOnRelease |
|||
stepSize: 1 |
|||
from: 0 |
|||
to: finalizer.sliderSteps |
|||
onValueChanged: { |
|||
if (activeFocus) |
|||
finalizer.sliderPos = value |
|||
} |
|||
Component.onCompleted: { |
|||
value = finalizer.sliderPos |
|||
} |
|||
Connections { |
|||
target: finalizer |
|||
function onSliderPosChanged() { |
|||
feeslider.value = finalizer.sliderPos |
|||
} |
|||
} |
|||
} |
|||
|
|||
ComboBox { |
|||
id: target |
|||
textRole: 'text' |
|||
valueRole: 'value' |
|||
model: [ |
|||
{ text: qsTr('ETA'), value: 1 }, |
|||
{ text: qsTr('Mempool'), value: 2 }, |
|||
{ text: qsTr('Static'), value: 0 } |
|||
] |
|||
onCurrentValueChanged: { |
|||
if (activeFocus) |
|||
finalizer.method = currentValue |
|||
} |
|||
Component.onCompleted: { |
|||
currentIndex = indexOfValue(finalizer.method) |
|||
} |
|||
} |
|||
|
|||
InfoTextArea { |
|||
Layout.columnSpan: 2 |
|||
visible: finalizer.warning != '' |
|||
text: finalizer.warning |
|||
iconStyle: InfoTextArea.IconStyle.Warn |
|||
} |
|||
|
|||
CheckBox { |
|||
id: final_cb |
|||
text: qsTr('Replace-by-Fee') |
|||
Layout.columnSpan: 2 |
|||
checked: finalizer.rbf |
|||
visible: finalizer.canRbf |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Outputs') |
|||
Layout.columnSpan: 2 |
|||
} |
|||
|
|||
Repeater { |
|||
model: finalizer.outputs |
|||
delegate: TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: modelData.address |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
color: modelData.is_mine ? constants.colorMine : Material.foreground |
|||
} |
|||
Label { |
|||
text: Config.formatSats(modelData.value_sats) |
|||
font.pixelSize: constants.fontSizeMedium |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
font.pixelSize: constants.fontSizeMedium |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
|
|||
Button { |
|||
text: qsTr('Cancel') |
|||
onClicked: { |
|||
txcancelled() |
|||
dialog.close() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
id: sendButton |
|||
text: qsTr('Pay') |
|||
enabled: finalizer.valid |
|||
onClicked: { |
|||
txaccepted() |
|||
finalizer.send_onchain() |
|||
dialog.close() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,35 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Item { |
|||
readonly property int paddingTiny: 4 //deprecated |
|||
readonly property int paddingXXSmall: 4 |
|||
readonly property int paddingXSmall: 6 |
|||
readonly property int paddingSmall: 8 |
|||
readonly property int paddingMedium: 12 |
|||
readonly property int paddingLarge: 16 |
|||
readonly property int paddingXLarge: 20 |
|||
readonly property int paddingXXLarge: 28 |
|||
|
|||
readonly property int fontSizeXSmall: 10 |
|||
readonly property int fontSizeSmall: 12 |
|||
readonly property int fontSizeMedium: 15 |
|||
readonly property int fontSizeLarge: 18 |
|||
readonly property int fontSizeXLarge: 22 |
|||
readonly property int fontSizeXXLarge: 28 |
|||
|
|||
readonly property int iconSizeSmall: 16 |
|||
readonly property int iconSizeMedium: 24 |
|||
readonly property int iconSizeLarge: 32 |
|||
readonly property int iconSizeXLarge: 48 |
|||
readonly property int iconSizeXXLarge: 64 |
|||
|
|||
property color colorCredit: "#ff80ff80" |
|||
property color colorDebit: "#ffff8080" |
|||
property color mutedForeground: 'gray' //Qt.lighter(Material.background, 2) |
|||
property color colorMine: "yellow" |
|||
|
|||
property color colorLightningLocal: "blue" |
|||
property color colorLightningRemote: "yellow" |
|||
|
|||
} |
@ -0,0 +1,211 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
import QtQml.Models 2.2 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Pane { |
|||
id: rootItem |
|||
visible: Daemon.currentWallet !== undefined |
|||
clip: true |
|||
|
|||
ListView { |
|||
id: listview |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
model: visualModel |
|||
|
|||
section.property: 'section' |
|||
section.criteria: ViewSection.FullString |
|||
section.delegate: RowLayout { |
|||
width: ListView.view.width |
|||
required property string section |
|||
Label { |
|||
text: section == 'today' |
|||
? qsTr('Today') |
|||
: section == 'yesterday' |
|||
? qsTr('Yesterday') |
|||
: section == 'lastweek' |
|||
? qsTr('Last week') |
|||
: section == 'lastmonth' |
|||
? qsTr('Last month') |
|||
: qsTr('Older') |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.topMargin: constants.paddingLarge |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
DelegateModel { |
|||
id: visualModel |
|||
model: Daemon.currentWallet.historyModel |
|||
|
|||
groups: [ |
|||
DelegateModelGroup { name: 'today'; includeByDefault: false }, |
|||
DelegateModelGroup { name: 'yesterday'; includeByDefault: false }, |
|||
DelegateModelGroup { name: 'lastweek'; includeByDefault: false }, |
|||
DelegateModelGroup { name: 'lastmonth'; includeByDefault: false }, |
|||
DelegateModelGroup { name: 'older'; includeByDefault: false } |
|||
] |
|||
|
|||
delegate: Item { |
|||
id: delegate |
|||
width: ListView.view.width |
|||
height: delegateLayout.height |
|||
|
|||
ColumnLayout { |
|||
id: delegateLayout |
|||
width: parent.width |
|||
spacing: 0 |
|||
|
|||
ItemDelegate { |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: txinfo.height |
|||
|
|||
onClicked: { |
|||
if (model.lightning) { |
|||
var page = app.stack.push(Qt.resolvedUrl('LightningPaymentDetails.qml'), {'key': model.key}) |
|||
page.detailsChanged.connect(function() { |
|||
// update listmodel when details change |
|||
visualModel.model.update_tx_label(model.key, page.label) |
|||
}) |
|||
} else { |
|||
var page = app.stack.push(Qt.resolvedUrl('TxDetails.qml'), {'txid': model.key}) |
|||
page.detailsChanged.connect(function() { |
|||
// update listmodel when details change |
|||
visualModel.model.update_tx_label(model.key, page.label) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
GridLayout { |
|||
id: txinfo |
|||
columns: 3 |
|||
|
|||
x: constants.paddingSmall |
|||
width: delegate.width - 2*constants.paddingSmall |
|||
|
|||
Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1} |
|||
|
|||
Image { |
|||
readonly property variant tx_icons : [ |
|||
"../../../gui/icons/unconfirmed.png", |
|||
"../../../gui/icons/clock1.png", |
|||
"../../../gui/icons/clock2.png", |
|||
"../../../gui/icons/clock3.png", |
|||
"../../../gui/icons/clock4.png", |
|||
"../../../gui/icons/clock5.png", |
|||
"../../../gui/icons/confirmed_bw.png" |
|||
] |
|||
|
|||
Layout.preferredWidth: constants.iconSizeLarge |
|||
Layout.preferredHeight: constants.iconSizeLarge |
|||
Layout.alignment: Qt.AlignVCenter |
|||
Layout.rowSpan: 2 |
|||
source: model.lightning ? "../../../gui/icons/lightning.png" : tx_icons[Math.min(6,model.confirmations)] |
|||
} |
|||
|
|||
Label { |
|||
Layout.fillWidth: true |
|||
font.pixelSize: model.label !== '' ? constants.fontSizeLarge : constants.fontSizeMedium |
|||
text: model.label !== '' ? model.label : '<no label>' |
|||
color: model.label !== '' ? Material.foreground : constants.mutedForeground |
|||
wrapMode: Text.Wrap |
|||
maximumLineCount: 2 |
|||
elide: Text.ElideRight |
|||
} |
|||
Label { |
|||
id: valueLabel |
|||
font.family: FixedFont |
|||
font.pixelSize: constants.fontSizeMedium |
|||
Layout.alignment: Qt.AlignRight |
|||
font.bold: true |
|||
color: model.incoming ? constants.colorCredit : constants.colorDebit |
|||
|
|||
function updateText() { |
|||
text = Config.formatSats(model.value) |
|||
} |
|||
Component.onCompleted: updateText() |
|||
} |
|||
Label { |
|||
font.pixelSize: constants.fontSizeSmall |
|||
text: model.date |
|||
color: constants.mutedForeground |
|||
} |
|||
Label { |
|||
id: fiatLabel |
|||
font.pixelSize: constants.fontSizeSmall |
|||
Layout.alignment: Qt.AlignRight |
|||
color: constants.mutedForeground |
|||
|
|||
function updateText() { |
|||
if (!Daemon.fx.enabled) { |
|||
text = '' |
|||
} else if (Daemon.fx.historicRates) { |
|||
text = Daemon.fx.fiatValueHistoric(model.value, model.timestamp) + ' ' + Daemon.fx.fiatCurrency |
|||
} else { |
|||
text = Daemon.fx.fiatValue(model.value, false) + ' ' + Daemon.fx.fiatCurrency |
|||
} |
|||
} |
|||
Component.onCompleted: updateText() |
|||
} |
|||
Item { Layout.columnSpan: 3; Layout.preferredWidth: 1; Layout.preferredHeight: 1 } |
|||
} |
|||
} |
|||
|
|||
Rectangle { |
|||
visible: delegate.ListView.section == delegate.ListView.nextSection |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
color: Qt.rgba(0,0,0,0.10) |
|||
} |
|||
|
|||
} |
|||
// as the items in the model are not bindings to QObjects, |
|||
// hook up events that might change the appearance |
|||
Connections { |
|||
target: Config |
|||
function onBaseUnitChanged() { valueLabel.updateText() } |
|||
function onThousandsSeparatorChanged() { valueLabel.updateText() } |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.fx |
|||
function onHistoricRatesChanged() { fiatLabel.updateText() } |
|||
function onQuotesUpdated() { fiatLabel.updateText() } |
|||
function onHistoryUpdated() { fiatLabel.updateText() } |
|||
function onEnabledUpdated() { fiatLabel.updateText() } |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
if (model.section == 'today') { |
|||
delegate.DelegateModel.inToday = true |
|||
} else if (model.section == 'yesterday') { |
|||
delegate.DelegateModel.inYesterday = true |
|||
} else if (model.section == 'lastweek') { |
|||
delegate.DelegateModel.inLastweek = true |
|||
} else if (model.section == 'lastmonth') { |
|||
delegate.DelegateModel.inLastmonth = true |
|||
} else if (model.section == 'older') { |
|||
delegate.DelegateModel.inOlder = true |
|||
} |
|||
} |
|||
|
|||
} // delegate |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
|
|||
} |
|||
|
|||
Connections { |
|||
target: Network |
|||
function onHeightChanged(height) { |
|||
Daemon.currentWallet.historyModel.updateBlockchainHeight(height) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,233 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
|
|||
property Invoice invoice |
|||
property string invoice_key |
|||
|
|||
signal doPay |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
title: qsTr('Invoice') |
|||
standardButtons: invoice_key != '' ? Dialog.Close : Dialog.Cancel |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
GridLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
columns: 2 |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Type') |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Image { |
|||
Layout.preferredWidth: constants.iconSizeSmall |
|||
Layout.preferredHeight: constants.iconSizeSmall |
|||
source: invoice.invoiceType == Invoice.LightningInvoice |
|||
? "../../icons/lightning.png" |
|||
: "../../icons/bitcoin.png" |
|||
} |
|||
|
|||
Label { |
|||
text: invoice.invoiceType == Invoice.OnchainInvoice |
|||
? qsTr('On chain') |
|||
: invoice.invoiceType == Invoice.LightningInvoice |
|||
? qsTr('Lightning') |
|||
: '' |
|||
Layout.fillWidth: true |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Amount to send') |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
font.bold: true |
|||
text: Config.formatSats(invoice.amount, false) |
|||
} |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
id: fiatValue |
|||
Layout.fillWidth: true |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(invoice.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Description') |
|||
} |
|||
|
|||
Label { |
|||
text: invoice.message |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
elide: Text.ElideRight |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.OnchainInvoice |
|||
text: qsTr('Address') |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.OnchainInvoice |
|||
Layout.fillWidth: true |
|||
text: invoice.address |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
text: qsTr('Remote Pubkey') |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
Layout.fillWidth: true |
|||
text: invoice.lnprops.pubkey |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
text: qsTr('Route via (t)') |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
Layout.fillWidth: true |
|||
text: invoice.lnprops.t |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
text: qsTr('Route via (r)') |
|||
} |
|||
|
|||
Label { |
|||
visible: invoice.invoiceType == Invoice.LightningInvoice |
|||
Layout.fillWidth: true |
|||
text: invoice.lnprops.r |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Status') |
|||
} |
|||
|
|||
Label { |
|||
text: invoice.status_str |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Item { Layout.preferredHeight: constants.paddingLarge; Layout.preferredWidth: 1 } |
|||
|
|||
InfoTextArea { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
visible: invoice.userinfo |
|||
text: invoice.userinfo |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
spacing: constants.paddingMedium |
|||
|
|||
Button { |
|||
text: qsTr('Delete') |
|||
icon.source: '../../icons/delete.png' |
|||
visible: invoice_key != '' |
|||
onClicked: { |
|||
invoice.wallet.delete_invoice(invoice_key) |
|||
dialog.close() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr('Save') |
|||
icon.source: '../../icons/save.png' |
|||
visible: invoice_key == '' |
|||
enabled: invoice.canSave |
|||
onClicked: { |
|||
invoice.save_invoice() |
|||
dialog.close() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr('Pay now') |
|||
icon.source: '../../icons/confirmed.png' |
|||
enabled: invoice.invoiceType != Invoice.Invalid && invoice.canPay |
|||
onClicked: { |
|||
if (invoice_key == '') // save invoice if not retrieved from key |
|||
invoice.save_invoice() |
|||
dialog.close() |
|||
if (invoice.invoiceType == Invoice.OnchainInvoice) { |
|||
doPay() // only signal here |
|||
} else if (invoice.invoiceType == Invoice.LightningInvoice) { |
|||
doPay() // only signal here |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
if (invoice_key != '') { |
|||
invoice.initFromKey(invoice_key) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,261 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property string title: qsTr("Lightning payment details") |
|||
|
|||
property string key |
|||
|
|||
property alias label: lnpaymentdetails.label |
|||
|
|||
signal detailsChanged |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip: true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Status') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: lnpaymentdetails.status |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Date') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: lnpaymentdetails.date |
|||
} |
|||
|
|||
Label { |
|||
text: lnpaymentdetails.amount.msatsInt > 0 |
|||
? qsTr('Amount received') |
|||
: qsTr('Amount sent') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
text: Config.formatMilliSats(lnpaymentdetails.amount) |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
visible: lnpaymentdetails.amount.msatsInt < 0 |
|||
text: qsTr('Transaction fee') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
visible: lnpaymentdetails.amount.msatsInt < 0 |
|||
Label { |
|||
text: Config.formatMilliSats(lnpaymentdetails.fee) |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Label') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
id: labelContent |
|||
|
|||
property bool editmode: false |
|||
|
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
visible: !labelContent.editmode |
|||
text: lnpaymentdetails.label |
|||
wrapMode: Text.Wrap |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
ToolButton { |
|||
visible: !labelContent.editmode |
|||
icon.source: '../../icons/pen.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelEdit.text = lnpaymentdetails.label |
|||
labelContent.editmode = true |
|||
labelEdit.focus = true |
|||
} |
|||
} |
|||
TextField { |
|||
id: labelEdit |
|||
visible: labelContent.editmode |
|||
text: lnpaymentdetails.label |
|||
font.pixelSize: constants.fontSizeLarge |
|||
Layout.fillWidth: true |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/confirmed.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelContent.editmode = false |
|||
lnpaymentdetails.set_label(labelEdit.text) |
|||
} |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/delete.png' |
|||
icon.color: 'transparent' |
|||
onClicked: labelContent.editmode = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Payment hash') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: lnpaymentdetails.payment_hash |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Payment hash'), 'text': lnpaymentdetails.payment_hash }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Preimage') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: lnpaymentdetails.preimage |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Preimage'), 'text': lnpaymentdetails.preimage }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Lightning invoice') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: lnpaymentdetails.invoice |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
maximumLineCount: 3 |
|||
elide: Text.ElideRight |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: enabled ? 'transparent' : constants.mutedForeground |
|||
enabled: lnpaymentdetails.invoice != '' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Lightning Invoice'), 'text': lnpaymentdetails.invoice }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
LnPaymentDetails { |
|||
id: lnpaymentdetails |
|||
wallet: Daemon.currentWallet |
|||
key: root.key |
|||
onLabelChanged: root.detailsChanged() |
|||
} |
|||
|
|||
Component { |
|||
id: share |
|||
GenericShareDialog {} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,130 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
|
|||
required property string invoice_key |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
title: qsTr('Paying Lightning Invoice...') |
|||
standardButtons: Dialog.Cancel |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
Item { |
|||
id: s |
|||
state: '' |
|||
states: [ |
|||
State { |
|||
name: '' |
|||
}, |
|||
State { |
|||
name: 'success' |
|||
PropertyChanges { target: spinner; running: false } |
|||
PropertyChanges { target: helpText; text: qsTr('Paid!') } |
|||
PropertyChanges { target: dialog; standardButtons: Dialog.Ok } |
|||
PropertyChanges { target: icon; source: '../../icons/confirmed.png' } |
|||
}, |
|||
State { |
|||
name: 'failed' |
|||
PropertyChanges { target: spinner; running: false } |
|||
PropertyChanges { target: helpText; text: qsTr('Payment failed') } |
|||
PropertyChanges { target: dialog; standardButtons: Dialog.Ok } |
|||
PropertyChanges { target: errorText; visible: true } |
|||
PropertyChanges { target: icon; source: '../../icons/warning.png' } |
|||
} |
|||
] |
|||
transitions: [ |
|||
Transition { |
|||
from: '' |
|||
to: 'success' |
|||
PropertyAnimation { target: helpText; properties: 'text'; duration: 0} |
|||
NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 200 } |
|||
NumberAnimation { target: icon; properties: 'scale'; from: 0; to: 1; duration: 500 |
|||
easing.type: Easing.OutBack |
|||
easing.overshoot: 10 |
|||
} |
|||
}, |
|||
Transition { |
|||
from: '' |
|||
to: 'failed' |
|||
PropertyAnimation { target: helpText; properties: 'text'; duration: 0} |
|||
NumberAnimation { target: icon; properties: 'opacity'; from: 0; to: 1; duration: 500 } |
|||
} |
|||
] |
|||
} |
|||
|
|||
ColumnLayout { |
|||
id: content |
|||
anchors.centerIn: parent |
|||
|
|||
Item { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.preferredWidth: constants.iconSizeXXLarge |
|||
Layout.preferredHeight: constants.iconSizeXXLarge |
|||
|
|||
BusyIndicator { |
|||
id: spinner |
|||
visible: s.state == '' |
|||
width: constants.iconSizeXXLarge |
|||
height: constants.iconSizeXXLarge |
|||
} |
|||
|
|||
Image { |
|||
id: icon |
|||
width: constants.iconSizeXXLarge |
|||
height: constants.iconSizeXXLarge |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
id: helpText |
|||
text: qsTr('Paying...') |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
Label { |
|||
id: errorText |
|||
font.pixelSize: constants.fontSizeLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onPaymentSucceeded(key) { |
|||
if (key != invoice_key) { |
|||
console.log('wrong invoice ' + key + ' != ' + invoice_key) |
|||
return |
|||
} |
|||
console.log('payment succeeded!') |
|||
s.state = 'success' |
|||
} |
|||
function onPaymentFailed(key, reason) { |
|||
if (key != invoice_key) { |
|||
console.log('wrong invoice ' + key + ' != ' + invoice_key) |
|||
return |
|||
} |
|||
console.log('payment failed: ' + reason) |
|||
s.state = 'failed' |
|||
errorText.text = reason |
|||
} |
|||
function onPaymentAuthRejected() { |
|||
dialog.close() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,90 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Pane { |
|||
property string title: qsTr('Network') |
|||
|
|||
GridLayout { |
|||
columns: 3 |
|||
|
|||
Label { |
|||
text: qsTr("Network: "); |
|||
color: Material.primaryHighlightedTextColor; |
|||
font.bold: true |
|||
} |
|||
Label { |
|||
text: Network.networkName |
|||
Layout.columnSpan: 2 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Server: "); |
|||
color: Material.primaryHighlightedTextColor; |
|||
font.bold: true |
|||
} |
|||
Label { |
|||
text: Network.server |
|||
Layout.columnSpan: 2 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Local Height: "); |
|||
color: Material.primaryHighlightedTextColor; |
|||
font.bold: true |
|||
|
|||
} |
|||
Label { |
|||
text: Network.height |
|||
Layout.columnSpan: 2 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Status: "); |
|||
color: Material.primaryHighlightedTextColor; |
|||
font.bold: true |
|||
} |
|||
Image { |
|||
Layout.preferredWidth: constants.iconSizeSmall |
|||
Layout.preferredHeight: constants.iconSizeSmall |
|||
source: Network.status == 'connecting' || Network.status == 'disconnected' |
|||
? '../../icons/status_disconnected.png' |
|||
: Network.status == 'connected' |
|||
? Daemon.currentWallet && !Daemon.currentWallet.isUptodate |
|||
? '../../icons/status_lagging.png' |
|||
: '../../icons/status_connected.png' |
|||
: '../../icons/status_connected.png' |
|||
} |
|||
Label { |
|||
text: Network.status |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Network fees: "); |
|||
color: Material.primaryHighlightedTextColor; |
|||
font.bold: true |
|||
} |
|||
Label { |
|||
id: feeHistogram |
|||
Layout.columnSpan: 2 |
|||
} |
|||
} |
|||
|
|||
function setFeeHistogram() { |
|||
var txt = '' |
|||
Network.feeHistogram.forEach(function(item) { |
|||
txt = txt + item[0] + ': ' + item[1] + '\n'; |
|||
}) |
|||
feeHistogram.text = txt.trim() |
|||
} |
|||
|
|||
Connections { |
|||
target: Network |
|||
function onFeeHistogramUpdated() { |
|||
setFeeHistogram() |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: setFeeHistogram() |
|||
} |
@ -0,0 +1,110 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "wizard" |
|||
|
|||
Wizard { |
|||
id: walletwizard |
|||
|
|||
title: qsTr('New Wallet') |
|||
|
|||
signal walletCreated |
|||
|
|||
property alias path: walletdb.path |
|||
|
|||
enter: null // disable transition |
|||
|
|||
// State transition functions. These functions are called when the 'Next' |
|||
// button is pressed. Depending on the data create the next page |
|||
// in the conversation. |
|||
|
|||
function walletnameDone(d) { |
|||
console.log('wallet name done') |
|||
var page = _loadNextComponent(components.wallettype, wizard_data) |
|||
page.next.connect(function() {wallettypeDone()}) |
|||
} |
|||
|
|||
function wallettypeDone(d) { |
|||
console.log('wallet type done') |
|||
var page = _loadNextComponent(components.keystore, wizard_data) |
|||
page.next.connect(function() {keystoretypeDone()}) |
|||
} |
|||
|
|||
function keystoretypeDone(d) { |
|||
console.log('keystore type done') |
|||
var page |
|||
switch(wizard_data['keystore_type']) { |
|||
case 'createseed': |
|||
page = _loadNextComponent(components.createseed, wizard_data) |
|||
page.next.connect(function() {createseedDone()}) |
|||
break |
|||
case 'haveseed': |
|||
page = _loadNextComponent(components.haveseed, wizard_data) |
|||
page.next.connect(function() {haveseedDone()}) |
|||
break |
|||
// case 'masterkey' |
|||
// case 'hardware' |
|||
} |
|||
} |
|||
|
|||
function createseedDone(d) { |
|||
console.log('create seed done') |
|||
var page = _loadNextComponent(components.confirmseed, wizard_data) |
|||
page.next.connect(function() {confirmseedDone()}) |
|||
} |
|||
|
|||
function confirmseedDone(d) { |
|||
console.log('confirm seed done') |
|||
var page = _loadNextComponent(components.walletpassword, wizard_data) |
|||
page.next.connect(function() {walletpasswordDone()}) |
|||
page.last = true |
|||
} |
|||
|
|||
function haveseedDone(d) { |
|||
console.log('have seed done') |
|||
if (wizard_data['seed_type'] == 'bip39') { |
|||
var page = _loadNextComponent(components.bip39refine, wizard_data) |
|||
page.next.connect(function() {bip39refineDone()}) |
|||
} else { |
|||
var page = _loadNextComponent(components.walletpassword, wizard_data) |
|||
page.next.connect(function() {walletpasswordDone()}) |
|||
page.last = true |
|||
} |
|||
} |
|||
|
|||
function bip39refineDone(d) { |
|||
console.log('bip39 refine done') |
|||
var page = _loadNextComponent(components.walletpassword, wizard_data) |
|||
page.next.connect(function() {walletpasswordDone()}) |
|||
page.last = true |
|||
} |
|||
|
|||
function walletpasswordDone(d) { |
|||
console.log('walletpassword done') |
|||
var page = _loadNextComponent(components.walletpassword, wizard_data) |
|||
} |
|||
|
|||
WizardComponents { |
|||
id: components |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
_setWizardData({}) |
|||
var start = _loadNextComponent(components.walletname) |
|||
start.next.connect(function() {walletnameDone()}) |
|||
} |
|||
|
|||
onAccepted: { |
|||
console.log('Finished new wallet wizard') |
|||
walletdb.create_storage(wizard_data) |
|||
} |
|||
|
|||
WalletDB { |
|||
id: walletdb |
|||
onCreateSuccess: walletwizard.walletCreated() |
|||
} |
|||
} |
|||
|
@ -0,0 +1,187 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
|
|||
property string title: qsTr("Open Lightning Channel") |
|||
|
|||
GridLayout { |
|||
id: form |
|||
width: parent.width |
|||
rowSpacing: constants.paddingSmall |
|||
columnSpacing: constants.paddingSmall |
|||
columns: 4 |
|||
|
|||
Label { |
|||
text: qsTr('Node') |
|||
} |
|||
|
|||
// gossip |
|||
TextArea { |
|||
id: node |
|||
visible: Config.useGossip |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
placeholderText: qsTr('Paste or scan node uri/pubkey') |
|||
onActiveFocusChanged: { |
|||
if (!activeFocus) |
|||
channelopener.nodeid = text |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
visible: Config.useGossip |
|||
spacing: 0 |
|||
ToolButton { |
|||
icon.source: '../../icons/paste.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
onClicked: { |
|||
channelopener.nodeid = AppController.clipboardToText() |
|||
node.text = channelopener.nodeid |
|||
} |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/qrcode.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
scale: 1.2 |
|||
onClicked: { |
|||
var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) |
|||
page.onFound.connect(function() { |
|||
channelopener.nodeid = page.scanData |
|||
node.text = channelopener.nodeid |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// trampoline |
|||
ComboBox { |
|||
id: tnode |
|||
visible: !Config.useGossip |
|||
Layout.columnSpan: 3 |
|||
Layout.fillWidth: true |
|||
model: channelopener.trampolineNodeNames |
|||
onCurrentValueChanged: { |
|||
channelopener.nodeid = tnode.currentValue |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Amount') |
|||
} |
|||
|
|||
BtcField { |
|||
id: amount |
|||
fiatfield: amountFiat |
|||
Layout.preferredWidth: parent.width /3 |
|||
onTextChanged: channelopener.amount = Config.unitsToSats(amount.text) |
|||
enabled: !is_max.checked |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
Switch { |
|||
id: is_max |
|||
text: qsTr('Max') |
|||
onCheckedChanged: { |
|||
channelopener.amount = checked ? MAX : Config.unitsToSats(amount.text) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { width: 1; height: 1; visible: Daemon.fx.enabled } |
|||
|
|||
FiatField { |
|||
id: amountFiat |
|||
btcfield: amount |
|||
visible: Daemon.fx.enabled |
|||
Layout.preferredWidth: parent.width /3 |
|||
enabled: !is_max.checked |
|||
} |
|||
|
|||
Label { |
|||
visible: Daemon.fx.enabled |
|||
text: Daemon.fx.fiatCurrency |
|||
color: Material.accentColor |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
Item { visible: Daemon.fx.enabled ; height: 1; width: 1 } |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 4 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
|
|||
Button { |
|||
text: qsTr('Open Channel') |
|||
enabled: channelopener.valid |
|||
onClicked: channelopener.open_channel() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: confirmOpenChannelDialog |
|||
ConfirmTxDialog { |
|||
title: qsTr('Confirm Open Channel') |
|||
amountLabelText: qsTr('Channel capacity') |
|||
sendButtonText: qsTr('Open Channel') |
|||
finalizer: channelopener.finalizer |
|||
} |
|||
} |
|||
|
|||
|
|||
ChannelOpener { |
|||
id: channelopener |
|||
wallet: Daemon.currentWallet |
|||
onValidationError: { |
|||
if (code == 'invalid_nodeid') { |
|||
var dialog = app.messageDialog.createObject(root, { 'text': message }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
onConflictingBackup: { |
|||
var dialog = app.messageDialog.createObject(root, { 'text': message, 'yesno': true }) |
|||
dialog.open() |
|||
dialog.yesClicked.connect(function() { |
|||
channelopener.open_channel(true) |
|||
}) |
|||
} |
|||
onFinalizerChanged: { |
|||
var dialog = confirmOpenChannelDialog.createObject(root, { |
|||
'satoshis': channelopener.amount |
|||
}) |
|||
dialog.open() |
|||
} |
|||
onChannelOpenError: { |
|||
var dialog = app.messageDialog.createObject(root, { 'text': message }) |
|||
dialog.open() |
|||
} |
|||
onChannelOpenSuccess: { |
|||
var message = 'success!' |
|||
if (!has_backup) |
|||
message = message + ' (but no backup. TODO: show QR)' |
|||
var dialog = app.messageDialog.createObject(root, { 'text': message }) |
|||
dialog.open() |
|||
channelopener.wallet.channelModel.new_channel(cid) |
|||
app.stack.pop() |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,137 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: openwalletdialog |
|||
|
|||
property string title: qsTr("Open Wallet") |
|||
|
|||
property string name |
|||
property string path |
|||
|
|||
property bool _unlockClicked: false |
|||
|
|||
GridLayout { |
|||
columns: 2 |
|||
width: parent.width |
|||
|
|||
Label { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: name |
|||
} |
|||
|
|||
MessagePane { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: qsTr("Wallet requires password to unlock") |
|||
visible: wallet_db.needsPassword |
|||
width: parent.width * 2/3 |
|||
warning: true |
|||
} |
|||
|
|||
MessagePane { |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: qsTr("Invalid Password") |
|||
visible: !wallet_db.validPassword && _unlockClicked |
|||
width: parent.width * 2/3 |
|||
error: true |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Password') |
|||
visible: wallet_db.needsPassword |
|||
} |
|||
|
|||
TextField { |
|||
id: password |
|||
visible: wallet_db.needsPassword |
|||
echoMode: TextInput.Password |
|||
inputMethodHints: Qt.ImhSensitiveData |
|||
onTextChanged: { |
|||
unlockButton.enabled = true |
|||
_unlockClicked = false |
|||
} |
|||
onAccepted: { |
|||
unlock() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
id: unlockButton |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
visible: wallet_db.needsPassword |
|||
text: qsTr("Unlock") |
|||
onClicked: { |
|||
unlock() |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Select HW device') |
|||
visible: wallet_db.needsHWDevice |
|||
} |
|||
|
|||
ComboBox { |
|||
id: hw_device |
|||
model: ['','Not implemented'] |
|||
visible: wallet_db.needsHWDevice |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Wallet requires splitting') |
|||
visible: wallet_db.requiresSplit |
|||
} |
|||
|
|||
Button { |
|||
visible: wallet_db.requiresSplit |
|||
text: qsTr('Split wallet') |
|||
onClicked: wallet_db.doSplit() |
|||
} |
|||
|
|||
BusyIndicator { |
|||
id: busy |
|||
running: false |
|||
Layout.columnSpan: 2 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
} |
|||
|
|||
function unlock() { |
|||
unlockButton.enabled = false |
|||
_unlockClicked = true |
|||
wallet_db.password = password.text |
|||
openwalletdialog.forceActiveFocus() |
|||
} |
|||
|
|||
WalletDB { |
|||
id: wallet_db |
|||
path: openwalletdialog.path |
|||
onSplitFinished: { |
|||
// if wallet needed splitting, we close the pane and refresh the wallet list |
|||
Daemon.availableWallets.reload() |
|||
app.stack.pop() |
|||
} |
|||
onReadyChanged: { |
|||
if (ready) { |
|||
busy.running = true |
|||
Daemon.load_wallet(openwalletdialog.path, password.text) |
|||
app.stack.pop(null) |
|||
} |
|||
} |
|||
onInvalidPassword: { |
|||
password.forceActiveFocus() |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
password.forceActiveFocus() |
|||
} |
|||
} |
@ -0,0 +1,90 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: root |
|||
|
|||
width: parent.width * 2/3 |
|||
height: parent.height * 1/3 |
|||
|
|||
x: (parent.width - width) / 2 |
|||
y: (parent.height - height) / 2 |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
focus: true |
|||
|
|||
standardButtons: Dialog.Cancel |
|||
|
|||
property string mode // [check, enter, change] |
|||
property string pincode // old one passed in when change, new one passed out |
|||
|
|||
property int _phase: mode == 'enter' ? 1 : 0 // 0 = existing pin, 1 = new pin, 2 = re-enter new pin |
|||
property string _pin |
|||
|
|||
function submit() { |
|||
if (_phase == 0) { |
|||
if (pin.text == pincode) { |
|||
pin.text = '' |
|||
if (mode == 'check') |
|||
accepted() |
|||
else |
|||
_phase = 1 |
|||
return |
|||
} |
|||
} |
|||
if (_phase == 1) { |
|||
_pin = pin.text |
|||
pin.text = '' |
|||
_phase = 2 |
|||
return |
|||
} |
|||
if (_phase == 2) { |
|||
if (_pin == pin.text) { |
|||
pincode = pin.text |
|||
accepted() |
|||
} |
|||
return |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
Label { |
|||
text: [qsTr('Enter PIN'), qsTr('Enter New PIN'), qsTr('Re-enter New PIN')][_phase] |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
TextField { |
|||
id: pin |
|||
Layout.preferredWidth: root.width *2/3 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
maximumLength: 6 |
|||
inputMethodHints: Qt.ImhDigitsOnly |
|||
echoMode: TextInput.Password |
|||
focus: true |
|||
onTextChanged: { |
|||
if (text.length == 6) { |
|||
submit() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 } |
|||
} |
|||
|
|||
} |
@ -0,0 +1,199 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Pane { |
|||
id: preferences |
|||
|
|||
property string title: qsTr("Preferences") |
|||
|
|||
ColumnLayout { |
|||
anchors.fill: parent |
|||
|
|||
Flickable { |
|||
Layout.fillHeight: true |
|||
Layout.fillWidth: true |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Language') |
|||
} |
|||
|
|||
ComboBox { |
|||
id: language |
|||
enabled: false |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Base unit') |
|||
} |
|||
|
|||
ComboBox { |
|||
id: baseUnit |
|||
model: ['BTC','mBTC','bits','sat'] |
|||
onCurrentValueChanged: { |
|||
if (activeFocus) |
|||
Config.baseUnit = currentValue |
|||
} |
|||
} |
|||
|
|||
Switch { |
|||
id: thousands |
|||
Layout.columnSpan: 2 |
|||
text: qsTr('Add thousands separators to bitcoin amounts') |
|||
onCheckedChanged: { |
|||
if (activeFocus) |
|||
Config.thousandsSeparator = checked |
|||
} |
|||
} |
|||
|
|||
Switch { |
|||
id: checkSoftware |
|||
Layout.columnSpan: 2 |
|||
text: qsTr('Automatically check for software updates') |
|||
enabled: false |
|||
} |
|||
|
|||
Switch { |
|||
id: fiatEnable |
|||
text: qsTr('Fiat Currency') |
|||
onCheckedChanged: { |
|||
if (activeFocus) |
|||
Daemon.fx.enabled = checked |
|||
} |
|||
} |
|||
|
|||
ComboBox { |
|||
id: currencies |
|||
model: Daemon.fx.currencies |
|||
enabled: Daemon.fx.enabled |
|||
onCurrentValueChanged: { |
|||
if (activeFocus) |
|||
Daemon.fx.fiatCurrency = currentValue |
|||
} |
|||
} |
|||
|
|||
Switch { |
|||
id: historicRates |
|||
text: qsTr('Historic rates') |
|||
enabled: Daemon.fx.enabled |
|||
Layout.columnSpan: 2 |
|||
onCheckedChanged: { |
|||
if (activeFocus) |
|||
Daemon.fx.historicRates = checked |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Source') |
|||
enabled: Daemon.fx.enabled |
|||
} |
|||
|
|||
ComboBox { |
|||
id: rateSources |
|||
enabled: Daemon.fx.enabled |
|||
model: Daemon.fx.rateSources |
|||
onModelChanged: { |
|||
currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource) |
|||
} |
|||
onCurrentValueChanged: { |
|||
if (activeFocus) |
|||
Daemon.fx.rateSource = currentValue |
|||
} |
|||
} |
|||
|
|||
Switch { |
|||
id: spendUnconfirmed |
|||
text: qsTr('Spend unconfirmed') |
|||
Layout.columnSpan: 2 |
|||
onCheckedChanged: { |
|||
if (activeFocus) |
|||
Config.spendUnconfirmed = checked |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('PIN') |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
text: Config.pinCode == '' ? qsTr('Off'): qsTr('On') |
|||
color: Material.accentColor |
|||
Layout.rightMargin: constants.paddingMedium |
|||
} |
|||
Button { |
|||
text: qsTr('Enable') |
|||
visible: Config.pinCode == '' |
|||
onClicked: { |
|||
var dialog = pinSetup.createObject(preferences, {mode: 'enter'}) |
|||
dialog.accepted.connect(function() { |
|||
Config.pinCode = dialog.pincode |
|||
dialog.close() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
Button { |
|||
text: qsTr('Change') |
|||
visible: Config.pinCode != '' |
|||
onClicked: { |
|||
var dialog = pinSetup.createObject(preferences, {mode: 'change', pincode: Config.pinCode}) |
|||
dialog.accepted.connect(function() { |
|||
Config.pinCode = dialog.pincode |
|||
dialog.close() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
Button { |
|||
text: qsTr('Remove') |
|||
visible: Config.pinCode != '' |
|||
onClicked: { |
|||
Config.pinCode = '' |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Lightning Routing') |
|||
} |
|||
|
|||
ComboBox { |
|||
id: lnRoutingType |
|||
valueRole: 'key' |
|||
textRole: 'label' |
|||
enabled: Daemon.currentWallet != null && Daemon.currentWallet.isLightning && false |
|||
model: ListModel { |
|||
ListElement { key: 'gossip'; label: qsTr('Gossip') } |
|||
ListElement { key: 'trampoline'; label: qsTr('Trampoline') } |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
|||
|
|||
Component { |
|||
id: pinSetup |
|||
Pin {} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
baseUnit.currentIndex = ['BTC','mBTC','bits','sat'].indexOf(Config.baseUnit) |
|||
thousands.checked = Config.thousandsSeparator |
|||
currencies.currentIndex = currencies.indexOfValue(Daemon.fx.fiatCurrency) |
|||
historicRates.checked = Daemon.fx.historicRates |
|||
rateSources.currentIndex = rateSources.indexOfValue(Daemon.fx.rateSource) |
|||
fiatEnable.checked = Daemon.fx.enabled |
|||
spendUnconfirmed.checked = Config.spendUnconfirmed |
|||
lnRoutingType.currentIndex = Config.useGossip ? 0 : 1 |
|||
} |
|||
} |
@ -0,0 +1,239 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
import QtQml.Models 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: rootItem |
|||
visible: Daemon.currentWallet !== undefined |
|||
|
|||
GridLayout { |
|||
id: form |
|||
width: parent.width |
|||
rowSpacing: constants.paddingSmall |
|||
columnSpacing: constants.paddingSmall |
|||
columns: 4 |
|||
|
|||
Label { |
|||
text: qsTr('Message') |
|||
} |
|||
|
|||
TextField { |
|||
id: message |
|||
placeholderText: qsTr('Description of payment request') |
|||
Layout.columnSpan: 3 |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Request') |
|||
wrapMode: Text.WordWrap |
|||
Layout.rightMargin: constants.paddingXLarge |
|||
} |
|||
|
|||
BtcField { |
|||
id: amount |
|||
fiatfield: amountFiat |
|||
Layout.preferredWidth: parent.width /3 |
|||
} |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Item { width: 1; height: 1; Layout.fillWidth: true } |
|||
|
|||
Item { visible: Daemon.fx.enabled; width: 1; height: 1 } |
|||
|
|||
FiatField { |
|||
id: amountFiat |
|||
btcfield: amount |
|||
visible: Daemon.fx.enabled |
|||
Layout.preferredWidth: parent.width /3 |
|||
} |
|||
|
|||
Label { |
|||
visible: Daemon.fx.enabled |
|||
text: Daemon.fx.fiatCurrency |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Item { visible: Daemon.fx.enabled; width: 1; height: 1; Layout.fillWidth: true } |
|||
|
|||
Label { |
|||
text: qsTr('Expires after') |
|||
Layout.fillWidth: false |
|||
} |
|||
|
|||
ComboBox { |
|||
id: expires |
|||
Layout.columnSpan: 2 |
|||
|
|||
textRole: 'text' |
|||
valueRole: 'value' |
|||
|
|||
model: ListModel { |
|||
id: expiresmodel |
|||
Component.onCompleted: { |
|||
// we need to fill the model like this, as ListElement can't evaluate script |
|||
expiresmodel.append({'text': qsTr('10 minutes'), 'value': 10*60}) |
|||
expiresmodel.append({'text': qsTr('1 hour'), 'value': 60*60}) |
|||
expiresmodel.append({'text': qsTr('1 day'), 'value': 24*60*60}) |
|||
expiresmodel.append({'text': qsTr('1 week'), 'value': 7*24*60*60}) |
|||
expiresmodel.append({'text': qsTr('1 month'), 'value': 31*24*60*60}) |
|||
expiresmodel.append({'text': qsTr('Never'), 'value': 0}) |
|||
expires.currentIndex = 0 |
|||
} |
|||
} |
|||
|
|||
// redefine contentItem, as the default crops the widest item |
|||
contentItem: Label { |
|||
text: expires.currentText |
|||
padding: constants.paddingLarge |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
} |
|||
|
|||
Item { width: 1; height: 1; Layout.fillWidth: true } |
|||
|
|||
Button { |
|||
Layout.columnSpan: 4 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: qsTr('Create Request') |
|||
icon.source: '../../icons/qrcode.png' |
|||
onClicked: { |
|||
createRequest() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Frame { |
|||
verticalPadding: 0 |
|||
horizontalPadding: 0 |
|||
|
|||
anchors { |
|||
top: form.bottom |
|||
topMargin: constants.paddingXLarge |
|||
left: parent.left |
|||
right: parent.right |
|||
bottom: parent.bottom |
|||
} |
|||
|
|||
background: PaneInsetBackground {} |
|||
|
|||
ColumnLayout { |
|||
spacing: 0 |
|||
anchors.fill: parent |
|||
|
|||
Item { |
|||
Layout.preferredHeight: hitem.height |
|||
Layout.preferredWidth: parent.width |
|||
Rectangle { |
|||
anchors.fill: parent |
|||
color: Qt.lighter(Material.background, 1.25) |
|||
} |
|||
RowLayout { |
|||
id: hitem |
|||
width: parent.width |
|||
Label { |
|||
text: qsTr('Receive queue') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
|
|||
ListView { |
|||
id: listview |
|||
Layout.fillHeight: true |
|||
Layout.fillWidth: true |
|||
clip: true |
|||
|
|||
model: DelegateModel { |
|||
id: delegateModel |
|||
model: Daemon.currentWallet.requestModel |
|||
delegate: InvoiceDelegate { |
|||
onClicked: { |
|||
var dialog = requestdialog.createObject(app, {'modelItem': model}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
|
|||
remove: Transition { |
|||
NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } |
|||
NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } |
|||
} |
|||
removeDisplaced: Transition { |
|||
SequentialAnimation { |
|||
PauseAnimation { duration: 200 } |
|||
SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } |
|||
} |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
} |
|||
} |
|||
} |
|||
|
|||
// make clicking the dialog background move the scope away from textedit fields |
|||
// so the keyboard goes away |
|||
MouseArea { |
|||
anchors.fill: parent |
|||
z: -1000 |
|||
onClicked: parkFocus.focus = true |
|||
FocusScope { id: parkFocus } |
|||
} |
|||
|
|||
Component { |
|||
id: requestdialog |
|||
RequestDialog { |
|||
onClosed: destroy() |
|||
} |
|||
} |
|||
|
|||
function createRequest(ignoreGaplimit = false) { |
|||
var qamt = Config.unitsToSats(amount.text) |
|||
if (qamt.satsInt > Daemon.currentWallet.lightningCanReceive.satsInt) { |
|||
console.log('Creating OnChain request') |
|||
Daemon.currentWallet.create_request(qamt, message.text, expires.currentValue, false, ignoreGaplimit) |
|||
} else { |
|||
console.log('Creating Lightning request') |
|||
Daemon.currentWallet.create_request(qamt, message.text, expires.currentValue, true) |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onRequestCreateSuccess() { |
|||
message.text = '' |
|||
amount.text = '' |
|||
var dialog = requestdialog.createObject(app, { |
|||
'modelItem': delegateModel.items.get(0).model |
|||
}) |
|||
dialog.open() |
|||
} |
|||
function onRequestCreateError(code, error) { |
|||
if (code == 'gaplimit') { |
|||
var dialog = app.messageDialog.createObject(app, {'text': error, 'yesno': true}) |
|||
dialog.yesClicked.connect(function() { |
|||
createRequest(true) |
|||
}) |
|||
} else { |
|||
console.log(error) |
|||
var dialog = app.messageDialog.createObject(app, {'text': error}) |
|||
} |
|||
dialog.open() |
|||
} |
|||
function onRequestStatusChanged(key, status) { |
|||
Daemon.currentWallet.requestModel.updateRequest(key, status) |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,228 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
title: qsTr('Payment Request') |
|||
|
|||
property var modelItem |
|||
|
|||
property string _bip21uri |
|||
|
|||
parent: Overlay.overlay |
|||
modal: true |
|||
standardButtons: Dialog.Close |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
header: RowLayout { |
|||
width: dialog.width |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: dialog.title |
|||
visible: dialog.title |
|||
elide: Label.ElideRight |
|||
padding: constants.paddingXLarge |
|||
bottomPadding: 0 |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
rowSpacing: constants.paddingMedium |
|||
columns: 5 |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 5 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Image { |
|||
id: qr |
|||
Layout.columnSpan: 5 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.topMargin: constants.paddingSmall |
|||
Layout.bottomMargin: constants.paddingSmall |
|||
|
|||
Rectangle { |
|||
property int size: 57 // should be qr pixel multiple |
|||
color: 'white' |
|||
x: (parent.width - size) / 2 |
|||
y: (parent.height - size) / 2 |
|||
width: size |
|||
height: size |
|||
|
|||
Image { |
|||
|
|||
source: '../../icons/electrum.png' |
|||
x: 1 |
|||
y: 1 |
|||
width: parent.width - 2 |
|||
height: parent.height - 2 |
|||
scale: 0.9 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 5 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 5 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Button { |
|||
icon.source: '../../icons/delete.png' |
|||
text: qsTr('Delete') |
|||
onClicked: { |
|||
Daemon.currentWallet.delete_request(modelItem.key) |
|||
dialog.close() |
|||
} |
|||
} |
|||
Button { |
|||
icon.source: '../../icons/copy_bw.png' |
|||
icon.color: 'transparent' |
|||
text: 'Copy' |
|||
onClicked: { |
|||
if (modelItem.is_lightning) |
|||
AppController.textToClipboard(modelItem.lightning_invoice) |
|||
else |
|||
AppController.textToClipboard(_bip21uri) |
|||
|
|||
} |
|||
} |
|||
Button { |
|||
icon.source: '../../icons/share.png' |
|||
text: 'Share' |
|||
onClicked: { |
|||
enabled = false |
|||
if (modelItem.is_lightning) |
|||
AppController.doShare(modelItem.lightning_invoice, qsTr('Payment Request')) |
|||
else |
|||
AppController.doShare(_bip21uri, qsTr('Payment Request')) |
|||
enabled = true |
|||
} |
|||
} |
|||
} |
|||
Label { |
|||
visible: modelItem.message != '' |
|||
text: qsTr('Description') |
|||
} |
|||
Label { |
|||
visible: modelItem.message != '' |
|||
Layout.columnSpan: 4 |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
text: modelItem.message |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
|
|||
Label { |
|||
visible: modelItem.amount.satsInt != 0 |
|||
text: qsTr('Amount') |
|||
} |
|||
Label { |
|||
visible: modelItem.amount.satsInt != 0 |
|||
text: Config.formatSats(modelItem.amount) |
|||
font.family: FixedFont |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.bold: true |
|||
} |
|||
Label { |
|||
visible: modelItem.amount.satsInt != 0 |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
|
|||
Label { |
|||
id: fiatValue |
|||
visible: modelItem.amount.satsInt != 0 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
text: Daemon.fx.enabled |
|||
? '(' + Daemon.fx.fiatValue(modelItem.amount, false) + ' ' + Daemon.fx.fiatCurrency + ')' |
|||
: '' |
|||
font.pixelSize: constants.fontSizeMedium |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Address') |
|||
visible: !modelItem.is_lightning |
|||
} |
|||
Label { |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 3 |
|||
visible: !modelItem.is_lightning |
|||
font.family: FixedFont |
|||
font.pixelSize: constants.fontSizeLarge |
|||
wrapMode: Text.WrapAnywhere |
|||
text: modelItem.address |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/copy_bw.png' |
|||
visible: !modelItem.is_lightning |
|||
onClicked: { |
|||
AppController.textToClipboard(modelItem.address) |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Status') |
|||
} |
|||
Label { |
|||
Layout.columnSpan: 4 |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
text: modelItem.status_str |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onRequestStatusChanged(key, status) { |
|||
if (key != modelItem.key) |
|||
return |
|||
modelItem = Daemon.currentWallet.get_request(key) |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
if (!modelItem.is_lightning) { |
|||
_bip21uri = bitcoin.create_bip21_uri(modelItem.address, modelItem.amount, modelItem.message, modelItem.timestamp, modelItem.expiration - modelItem.timestamp) |
|||
qr.source = 'image://qrgen/' + _bip21uri |
|||
} else { |
|||
qr.source = 'image://qrgen/' + modelItem.lightning_invoice |
|||
} |
|||
} |
|||
|
|||
Bitcoin { |
|||
id: bitcoin |
|||
} |
|||
} |
@ -0,0 +1,38 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Item { |
|||
id: scanPage |
|||
property string title: qsTr('Scan') |
|||
|
|||
property bool toolbar: false |
|||
|
|||
property string scanData |
|||
property string error |
|||
|
|||
signal found |
|||
|
|||
QRScan { |
|||
anchors.top: parent.top |
|||
anchors.bottom: parent.bottom |
|||
width: parent.width |
|||
|
|||
onFound: { |
|||
scanPage.scanData = scanData |
|||
scanPage.found() |
|||
app.stack.pop() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
anchors.horizontalCenter: parent.horizontalCenter |
|||
id: button |
|||
anchors.bottom: parent.bottom |
|||
text: 'Cancel' |
|||
onClicked: app.stack.pop() |
|||
} |
|||
} |
@ -0,0 +1,359 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
import QtQml.Models 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: rootItem |
|||
|
|||
function clear() { |
|||
recipient.text = '' |
|||
amount.text = '' |
|||
message.text = '' |
|||
is_max.checked = false |
|||
} |
|||
|
|||
GridLayout { |
|||
id: form |
|||
width: parent.width |
|||
rowSpacing: constants.paddingSmall |
|||
columnSpacing: constants.paddingSmall |
|||
columns: 3 |
|||
|
|||
BalanceSummary { |
|||
Layout.columnSpan: 3 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Recipient') |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
|
|||
TextArea { |
|||
id: recipient |
|||
Layout.fillWidth: true |
|||
font.family: FixedFont |
|||
wrapMode: Text.Wrap |
|||
placeholderText: qsTr('Paste address or invoice') |
|||
onTextChanged: { |
|||
//if (activeFocus) |
|||
//userEnteredPayment.recipient = text |
|||
userEnteredPayment.recipient = recipient.text |
|||
} |
|||
} |
|||
|
|||
spacing: 0 |
|||
ToolButton { |
|||
icon.source: '../../icons/paste.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
onClicked: invoice.recipient = AppController.clipboardToText() |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/qrcode.png' |
|||
icon.height: constants.iconSizeMedium |
|||
icon.width: constants.iconSizeMedium |
|||
scale: 1.2 |
|||
onClicked: { |
|||
var page = app.stack.push(Qt.resolvedUrl('Scan.qml')) |
|||
page.onFound.connect(function() { |
|||
invoice.recipient = page.scanData |
|||
}) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Amount') |
|||
} |
|||
|
|||
BtcField { |
|||
id: amount |
|||
fiatfield: amountFiat |
|||
enabled: !is_max.checked |
|||
Layout.preferredWidth: parent.width /3 |
|||
onTextChanged: { |
|||
userEnteredPayment.amount = is_max.checked ? MAX : Config.unitsToSats(amount.text) |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
Switch { |
|||
id: is_max |
|||
text: qsTr('Max') |
|||
onCheckedChanged: { |
|||
userEnteredPayment.amount = is_max.checked ? MAX : Config.unitsToSats(amount.text) |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { width: 1; height: 1; visible: Daemon.fx.enabled } |
|||
|
|||
FiatField { |
|||
id: amountFiat |
|||
btcfield: amount |
|||
visible: Daemon.fx.enabled |
|||
enabled: !is_max.checked |
|||
Layout.preferredWidth: parent.width /3 |
|||
} |
|||
|
|||
Label { |
|||
Layout.fillWidth: true |
|||
visible: Daemon.fx.enabled |
|||
text: Daemon.fx.fiatCurrency |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Description') |
|||
} |
|||
|
|||
TextField { |
|||
id: message |
|||
placeholderText: qsTr('Message') |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
onTextChanged: { |
|||
userEnteredPayment.message = message.text |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.columnSpan: 3 |
|||
Layout.alignment: Qt.AlignHCenter |
|||
spacing: constants.paddingMedium |
|||
|
|||
Button { |
|||
text: qsTr('Save') |
|||
enabled: userEnteredPayment.canSave |
|||
icon.source: '../../icons/save.png' |
|||
onClicked: { |
|||
userEnteredPayment.save_invoice() |
|||
userEnteredPayment.clear() |
|||
rootItem.clear() |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr('Pay now') |
|||
enabled: userEnteredPayment.canPay |
|||
icon.source: '../../icons/confirmed.png' |
|||
onClicked: { |
|||
var dialog = confirmPaymentDialog.createObject(app, { |
|||
'address': recipient.text, |
|||
'satoshis': is_max.checked ? MAX : Config.unitsToSats(amount.text), |
|||
'message': message.text |
|||
}) |
|||
dialog.txaccepted.connect(function() { |
|||
userEnteredPayment.clear() |
|||
rootItem.clear() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
Frame { |
|||
verticalPadding: 0 |
|||
horizontalPadding: 0 |
|||
|
|||
anchors { |
|||
top: form.bottom |
|||
topMargin: constants.paddingXLarge |
|||
left: parent.left |
|||
right: parent.right |
|||
bottom: parent.bottom |
|||
} |
|||
|
|||
background: PaneInsetBackground {} |
|||
|
|||
ColumnLayout { |
|||
spacing: 0 |
|||
anchors.fill: parent |
|||
|
|||
Item { |
|||
Layout.preferredHeight: hitem.height |
|||
Layout.preferredWidth: parent.width |
|||
Rectangle { |
|||
anchors.fill: parent |
|||
color: Qt.lighter(Material.background, 1.25) |
|||
} |
|||
RowLayout { |
|||
id: hitem |
|||
width: parent.width |
|||
Label { |
|||
text: qsTr('Send queue') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
|
|||
ListView { |
|||
id: listview |
|||
Layout.fillHeight: true |
|||
Layout.fillWidth: true |
|||
clip: true |
|||
|
|||
model: DelegateModel { |
|||
id: delegateModel |
|||
model: Daemon.currentWallet.invoiceModel |
|||
delegate: InvoiceDelegate { |
|||
onClicked: { |
|||
var dialog = invoiceDialog.createObject(app, {'invoice' : invoice, 'invoice_key': model.key}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
|
|||
remove: Transition { |
|||
NumberAnimation { properties: 'scale'; to: 0.75; duration: 300 } |
|||
NumberAnimation { properties: 'opacity'; to: 0; duration: 300 } |
|||
} |
|||
removeDisplaced: Transition { |
|||
SequentialAnimation { |
|||
PauseAnimation { duration: 200 } |
|||
SpringAnimation { properties: 'y'; duration: 100; spring: 5; damping: 0.5; mass: 2 } |
|||
} |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: confirmPaymentDialog |
|||
ConfirmTxDialog { |
|||
title: qsTr('Confirm Payment') |
|||
finalizer: TxFinalizer { |
|||
wallet: Daemon.currentWallet |
|||
canRbf: true |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: lightningPaymentProgressDialog |
|||
LightningPaymentProgressDialog {} |
|||
} |
|||
|
|||
Component { |
|||
id: invoiceDialog |
|||
InvoiceDialog { |
|||
onDoPay: { |
|||
if (invoice.invoiceType == Invoice.OnchainInvoice) { |
|||
var dialog = confirmPaymentDialog.createObject(rootItem, { |
|||
'address': invoice.address, |
|||
'satoshis': invoice.amount, |
|||
'message': invoice.message |
|||
}) |
|||
dialog.open() |
|||
} else if (invoice.invoiceType == Invoice.LightningInvoice) { |
|||
console.log('About to pay lightning invoice') |
|||
if (invoice.key == '') { |
|||
console.log('No invoice key, aborting') |
|||
return |
|||
} |
|||
var dialog = lightningPaymentProgressDialog.createObject(rootItem, { |
|||
invoice_key: invoice.key |
|||
}) |
|||
dialog.open() |
|||
Daemon.currentWallet.pay_lightning_invoice(invoice.key) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onInvoiceStatusChanged(key, status) { |
|||
Daemon.currentWallet.invoiceModel.updateInvoice(key, status) |
|||
} |
|||
} |
|||
|
|||
// make clicking the dialog background move the scope away from textedit fields |
|||
// so the keyboard goes away |
|||
MouseArea { |
|||
anchors.fill: parent |
|||
z: -1000 |
|||
onClicked: parkFocus.focus = true |
|||
FocusScope { id: parkFocus } |
|||
} |
|||
|
|||
|
|||
UserEnteredPayment { |
|||
id: userEnteredPayment |
|||
wallet: Daemon.currentWallet |
|||
|
|||
//onValidationError: { |
|||
//if (recipient.activeFocus) { |
|||
//// no popups when editing |
|||
//return |
|||
//} |
|||
//var dialog = app.messageDialog.createObject(app, {'text': message }) |
|||
//dialog.open() |
|||
//// rootItem.clear() |
|||
//} |
|||
|
|||
onInvoiceSaved: { |
|||
Daemon.currentWallet.invoiceModel.init_model() |
|||
} |
|||
} |
|||
|
|||
InvoiceParser { |
|||
id: invoice |
|||
wallet: Daemon.currentWallet |
|||
onValidationError: { |
|||
if (recipient.activeFocus) { |
|||
// no popups when editing |
|||
return |
|||
} |
|||
var dialog = app.messageDialog.createObject(app, {'text': message }) |
|||
dialog.open() |
|||
rootItem.clear() |
|||
} |
|||
onValidationWarning: { |
|||
if (code == 'no_channels') { |
|||
var dialog = app.messageDialog.createObject(app, {'text': message }) |
|||
dialog.open() |
|||
// TODO: ask user to open a channel, if funds allow |
|||
// and maybe store invoice if expiry allows |
|||
} |
|||
} |
|||
onValidationSuccess: { |
|||
// address only -> fill form fields and clear this instance |
|||
// else -> show invoice confirmation dialog |
|||
if (invoiceType == Invoice.OnchainOnlyAddress) { |
|||
recipient.text = invoice.recipient |
|||
invoice.clear() |
|||
} else { |
|||
var dialog = invoiceDialog.createObject(rootItem, {'invoice': invoice}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
onInvoiceCreateError: console.log(code + ' ' + message) |
|||
|
|||
onInvoiceSaved: { |
|||
Daemon.currentWallet.invoiceModel.init_model() |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,53 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
|
|||
import "wizard" |
|||
|
|||
Wizard { |
|||
id: serverconnectwizard |
|||
|
|||
title: qsTr('How do you want to connect to a server?') |
|||
|
|||
enter: null // disable transition |
|||
|
|||
onAccepted: { |
|||
var proxy = wizard_data['proxy'] |
|||
if (proxy && proxy['enabled'] == true) { |
|||
Network.proxy = proxy |
|||
} else { |
|||
Network.proxy = {'enabled': false} |
|||
} |
|||
Config.autoConnect = wizard_data['autoconnect'] |
|||
if (!wizard_data['autoconnect']) { |
|||
Network.server = wizard_data['server'] |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
var start = _loadNextComponent(autoconnect) |
|||
start.next.connect(function() {autoconnectDone()}) |
|||
} |
|||
|
|||
function autoconnectDone() { |
|||
var page = _loadNextComponent(proxyconfig, wizard_data) |
|||
page.next.connect(function() {proxyconfigDone()}) |
|||
} |
|||
|
|||
function proxyconfigDone() { |
|||
var page = _loadNextComponent(serverconfig, wizard_data) |
|||
} |
|||
|
|||
property Component autoconnect: Component { |
|||
WCAutoConnect {} |
|||
} |
|||
|
|||
property Component proxyconfig: Component { |
|||
WCProxyConfig {} |
|||
} |
|||
|
|||
property Component serverconfig: Component { |
|||
WCServerConfig {} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,16 @@ |
|||
import QtQuick 2.0 |
|||
|
|||
Item { |
|||
property bool toolbar: false |
|||
|
|||
Rectangle { |
|||
anchors.fill: parent |
|||
color: '#111144' |
|||
} |
|||
|
|||
Image { |
|||
anchors.horizontalCenter: parent.horizontalCenter |
|||
anchors.verticalCenter: parent.verticalCenter |
|||
source: "../../icons/electrum.png" |
|||
} |
|||
} |
@ -0,0 +1,205 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Dialog { |
|||
id: root |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
title: qsTr('Lightning Swap') |
|||
standardButtons: Dialog.Cancel |
|||
|
|||
modal: true |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
GridLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
columns: 2 |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('You send') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
id: tosend |
|||
text: Config.formatSats(swaphelper.tosend) |
|||
font.family: FixedFont |
|||
visible: swaphelper.valid |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
visible: swaphelper.valid |
|||
} |
|||
Label { |
|||
text: swaphelper.isReverse ? qsTr('(offchain)') : qsTr('(onchain)') |
|||
visible: swaphelper.valid |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('You receive') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
id: toreceive |
|||
text: Config.formatSats(swaphelper.toreceive) |
|||
font.family: FixedFont |
|||
visible: swaphelper.valid |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
visible: swaphelper.valid |
|||
} |
|||
Label { |
|||
text: swaphelper.isReverse ? qsTr('(onchain)') : qsTr('(offchain)') |
|||
visible: swaphelper.valid |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Server fee') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
text: swaphelper.serverfeeperc |
|||
} |
|||
Label { |
|||
text: Config.formatSats(swaphelper.serverfee) |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Mining fee') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
text: Config.formatSats(swaphelper.miningfee) |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Slider { |
|||
id: swapslider |
|||
Layout.columnSpan: 2 |
|||
Layout.preferredWidth: 2/3 * layout.width |
|||
Layout.alignment: Qt.AlignHCenter |
|||
|
|||
from: swaphelper.rangeMin |
|||
to: swaphelper.rangeMax |
|||
|
|||
onValueChanged: { |
|||
if (activeFocus) |
|||
swaphelper.sliderPos = value |
|||
} |
|||
Component.onCompleted: { |
|||
value = swaphelper.sliderPos |
|||
} |
|||
Connections { |
|||
target: swaphelper |
|||
function onSliderPosChanged() { |
|||
swapslider.value = swaphelper.sliderPos |
|||
} |
|||
} |
|||
} |
|||
|
|||
InfoTextArea { |
|||
Layout.columnSpan: 2 |
|||
visible: swaphelper.userinfo != '' |
|||
text: swaphelper.userinfo |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Button { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.columnSpan: 2 |
|||
text: qsTr('Ok') |
|||
enabled: swaphelper.valid |
|||
onClicked: swaphelper.executeSwap() |
|||
} |
|||
|
|||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1; Layout.columnSpan: 2 } |
|||
} |
|||
|
|||
SwapHelper { |
|||
id: swaphelper |
|||
wallet: Daemon.currentWallet |
|||
onError: { |
|||
var dialog = app.messageDialog.createObject(root, {'text': message}) |
|||
dialog.open() |
|||
} |
|||
onConfirm: { |
|||
var dialog = app.messageDialog.createObject(app, {'text': message, 'yesno': true}) |
|||
dialog.yesClicked.connect(function() { |
|||
dialog.close() |
|||
swaphelper.executeSwap(true) |
|||
root.close() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
onAuthRequired: { // TODO: don't replicate this code |
|||
if (swaphelper.wallet.verify_password('')) { |
|||
// wallet has no password |
|||
console.log('wallet has no password, proceeding') |
|||
swaphelper.authProceed() |
|||
} else { |
|||
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')}) |
|||
dialog.accepted.connect(function() { |
|||
if (swaphelper.wallet.verify_password(dialog.password)) { |
|||
swaphelper.wallet.authProceed() |
|||
} else { |
|||
swaphelper.wallet.authCancel() |
|||
} |
|||
}) |
|||
dialog.rejected.connect(function() { |
|||
swaphelper.wallet.authCancel() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,262 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: root |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property string title: qsTr("Transaction details") |
|||
|
|||
property string txid |
|||
|
|||
property alias label: txdetails.label |
|||
|
|||
signal detailsChanged |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Bump fee') |
|||
enabled: txdetails.canBump |
|||
//onTriggered: |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Cancel double-spend') |
|||
enabled: txdetails.canCancel |
|||
} |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip: true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Status') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: txdetails.status |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Mempool depth') |
|||
color: Material.accentColor |
|||
visible: !txdetails.isMined |
|||
} |
|||
|
|||
Label { |
|||
text: txdetails.mempoolDepth |
|||
visible: !txdetails.isMined |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Date') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Label { |
|||
text: txdetails.date |
|||
} |
|||
|
|||
Label { |
|||
text: txdetails.amount.satsInt > 0 |
|||
? qsTr('Amount received') |
|||
: qsTr('Amount sent') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
Label { |
|||
text: Config.formatSats(txdetails.amount) |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
visible: txdetails.amount.satsInt < 0 |
|||
text: qsTr('Transaction fee') |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
RowLayout { |
|||
visible: txdetails.amount.satsInt < 0 |
|||
Label { |
|||
text: Config.formatSats(txdetails.fee) |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Label') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
id: labelContent |
|||
|
|||
property bool editmode: false |
|||
|
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
visible: !labelContent.editmode |
|||
text: txdetails.label |
|||
wrapMode: Text.Wrap |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
ToolButton { |
|||
visible: !labelContent.editmode |
|||
icon.source: '../../icons/pen.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelEdit.text = txdetails.label |
|||
labelContent.editmode = true |
|||
labelEdit.focus = true |
|||
} |
|||
} |
|||
TextField { |
|||
id: labelEdit |
|||
visible: labelContent.editmode |
|||
text: txdetails.label |
|||
font.pixelSize: constants.fontSizeLarge |
|||
Layout.fillWidth: true |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/confirmed.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
labelContent.editmode = false |
|||
txdetails.set_label(labelEdit.text) |
|||
} |
|||
} |
|||
ToolButton { |
|||
visible: labelContent.editmode |
|||
icon.source: '../../icons/delete.png' |
|||
icon.color: 'transparent' |
|||
onClicked: labelContent.editmode = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Transaction ID') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: root.txid |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(root, { 'title': qsTr('Transaction ID'), 'text': root.txid }) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Outputs') |
|||
Layout.columnSpan: 2 |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Repeater { |
|||
model: txdetails.outputs |
|||
delegate: TextHighlightPane { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: modelData.address |
|||
Layout.fillWidth: true |
|||
wrapMode: Text.Wrap |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
color: modelData.is_mine ? constants.colorMine : Material.foreground |
|||
} |
|||
Label { |
|||
text: Config.formatSats(modelData.value) |
|||
font.pixelSize: constants.fontSizeMedium |
|||
font.family: FixedFont |
|||
} |
|||
Label { |
|||
text: Config.baseUnit |
|||
font.pixelSize: constants.fontSizeMedium |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
TxDetails { |
|||
id: txdetails |
|||
wallet: Daemon.currentWallet |
|||
txid: root.txid |
|||
onLabelChanged: root.detailsChanged() |
|||
} |
|||
|
|||
Component { |
|||
id: share |
|||
GenericShareDialog {} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,168 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQml 2.6 |
|||
|
|||
Item { |
|||
id: rootItem |
|||
|
|||
property string title: Daemon.currentWallet ? Daemon.currentWallet.name : '' |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Addresses'); |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('Addresses.qml')); |
|||
enabled: Daemon.currentWallet != null |
|||
icon.source: '../../icons/tab_addresses.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Wallets'); |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('Wallets.qml')) |
|||
icon.source: '../../icons/wallet.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Network'); |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('NetworkStats.qml')) |
|||
icon.source: '../../icons/network.png' |
|||
} |
|||
} |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Channels'); |
|||
enabled: Daemon.currentWallet != null && Daemon.currentWallet.isLightning |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml')) |
|||
icon.source: '../../icons/lightning.png' |
|||
} |
|||
} |
|||
|
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Preferences'); |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('Preferences.qml')) |
|||
icon.source: '../../icons/preferences.png' |
|||
} |
|||
} |
|||
|
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('About'); |
|||
onTriggered: menu.openPage(Qt.resolvedUrl('About.qml')) |
|||
icon.source: '../../icons/electrum.png' |
|||
} |
|||
} |
|||
|
|||
function openPage(url) { |
|||
stack.push(url) |
|||
currentIndex = -1 |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
anchors.centerIn: parent |
|||
width: parent.width |
|||
spacing: 2*constants.paddingXLarge |
|||
visible: Daemon.currentWallet == null |
|||
|
|||
Label { |
|||
text: qsTr('No wallet loaded') |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr('Open/Create Wallet') |
|||
Layout.alignment: Qt.AlignHCenter |
|||
onClicked: { |
|||
stack.push(Qt.resolvedUrl('Wallets.qml')) |
|||
} |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
anchors.fill: parent |
|||
visible: Daemon.currentWallet != null |
|||
|
|||
SwipeView { |
|||
id: swipeview |
|||
|
|||
Layout.fillHeight: true |
|||
Layout.fillWidth: true |
|||
currentIndex: tabbar.currentIndex |
|||
|
|||
Item { |
|||
Loader { |
|||
anchors.fill: parent |
|||
Receive { |
|||
id: receive |
|||
anchors.fill: parent |
|||
} |
|||
} |
|||
} |
|||
|
|||
Item { |
|||
Loader { |
|||
anchors.fill: parent |
|||
History { |
|||
id: history |
|||
anchors.fill: parent |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
Item { |
|||
enabled: !Daemon.currentWallet.isWatchOnly |
|||
Loader { |
|||
anchors.fill: parent |
|||
Send { |
|||
anchors.fill: parent |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
TabBar { |
|||
id: tabbar |
|||
position: TabBar.Footer |
|||
Layout.fillWidth: true |
|||
currentIndex: swipeview.currentIndex |
|||
TabButton { |
|||
text: qsTr('Receive') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
TabButton { |
|||
text: qsTr('History') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
TabButton { |
|||
enabled: !Daemon.currentWallet.isWatchOnly |
|||
text: qsTr('Send') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
} |
|||
Component.onCompleted: tabbar.setCurrentIndex(1) |
|||
} |
|||
|
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon |
|||
function onWalletLoaded() { |
|||
tabbar.setCurrentIndex(1) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,348 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "controls" |
|||
|
|||
Pane { |
|||
id: rootItem |
|||
|
|||
property string title: qsTr('Wallets') |
|||
|
|||
function createWallet() { |
|||
var dialog = app.newWalletWizard.createObject(rootItem) |
|||
dialog.open() |
|||
dialog.walletCreated.connect(function() { |
|||
Daemon.availableWallets.reload() |
|||
// and load the new wallet |
|||
Daemon.load_wallet(dialog.path, dialog.wizard_data['password']) |
|||
}) |
|||
} |
|||
|
|||
function enableLightning() { |
|||
var dialog = app.messageDialog.createObject(rootItem, |
|||
{'text': qsTr('Enable Lightning for this wallet?'), 'yesno': true}) |
|||
dialog.yesClicked.connect(function() { |
|||
Daemon.currentWallet.enableLightning() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
|
|||
function deleteWallet() { |
|||
var dialog = app.messageDialog.createObject(rootItem, |
|||
{'text': qsTr('Really delete this wallet?'), 'yesno': true}) |
|||
dialog.yesClicked.connect(function() { |
|||
Daemon.delete_wallet(Daemon.currentWallet) |
|||
}) |
|||
dialog.open() |
|||
} |
|||
|
|||
function changePassword() { |
|||
// trigger dialog via wallet (auth then signal) |
|||
Daemon.start_change_password() |
|||
} |
|||
|
|||
property QtObject menu: Menu { |
|||
id: menu |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Create Wallet'); |
|||
onTriggered: rootItem.createWallet() |
|||
icon.source: '../../icons/wallet.png' |
|||
} |
|||
} |
|||
Component { |
|||
id: changePasswordComp |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
enabled: Daemon.currentWallet // != null |
|||
action: Action { |
|||
text: qsTr('Change Password'); |
|||
onTriggered: rootItem.changePassword() |
|||
icon.source: '../../icons/lock.png' |
|||
} |
|||
} |
|||
} |
|||
Component { |
|||
id: deleteWalletComp |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
enabled: Daemon.currentWallet // != null |
|||
action: Action { |
|||
text: qsTr('Delete Wallet'); |
|||
onTriggered: rootItem.deleteWallet() |
|||
icon.source: '../../icons/delete.png' |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: enableLightningComp |
|||
MenuItem { |
|||
icon.color: 'transparent' |
|||
action: Action { |
|||
text: qsTr('Enable Lightning'); |
|||
onTriggered: rootItem.enableLightning() |
|||
enabled: Daemon.currentWallet != null && Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning |
|||
icon.source: '../../icons/lightning.png' |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: sepComp |
|||
MenuSeparator {} |
|||
} |
|||
|
|||
// add items dynamically, if using visible: false property the menu item isn't removed but empty |
|||
Component.onCompleted: { |
|||
if (Daemon.currentWallet != null) { |
|||
menu.insertItem(0, sepComp.createObject(menu)) |
|||
if (Daemon.currentWallet.canHaveLightning && !Daemon.currentWallet.isLightning) { |
|||
menu.insertItem(0, enableLightningComp.createObject(menu)) |
|||
} |
|||
menu.insertItem(0, deleteWalletComp.createObject(menu)) |
|||
menu.insertItem(0, changePasswordComp.createObject(menu)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
id: layout |
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
GridLayout { |
|||
id: detailsLayout |
|||
visible: Daemon.currentWallet != null |
|||
Layout.preferredWidth: parent.width |
|||
|
|||
columns: 4 |
|||
|
|||
Label { text: 'Wallet'; Layout.columnSpan: 2; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.name; font.bold: true /*pixelSize: constants.fontSizeLarge*/; Layout.columnSpan: 2 } |
|||
|
|||
Label { text: 'derivation prefix (BIP32)'; visible: Daemon.currentWallet.isDeterministic; color: Material.accentColor; Layout.columnSpan: 2 } |
|||
Label { text: Daemon.currentWallet.derivationPrefix; visible: Daemon.currentWallet.isDeterministic; Layout.columnSpan: 2 } |
|||
|
|||
Label { text: 'txinType'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.txinType } |
|||
|
|||
Label { text: 'is deterministic'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.isDeterministic } |
|||
|
|||
Label { text: 'is watch only'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.isWatchOnly } |
|||
|
|||
Label { text: 'is Encrypted'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.isEncrypted } |
|||
|
|||
Label { text: 'is Hardware'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.isHardware } |
|||
|
|||
Label { text: 'is Lightning'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.isLightning } |
|||
|
|||
Label { text: 'has Seed'; color: Material.accentColor } |
|||
Label { text: Daemon.currentWallet.hasSeed; Layout.columnSpan: 3 } |
|||
|
|||
Label { Layout.columnSpan:4; text: qsTr('Master Public Key'); color: Material.accentColor } |
|||
|
|||
TextHighlightPane { |
|||
Layout.columnSpan: 4 |
|||
Layout.fillWidth: true |
|||
padding: 0 |
|||
leftPadding: constants.paddingSmall |
|||
|
|||
RowLayout { |
|||
width: parent.width |
|||
Label { |
|||
text: Daemon.currentWallet.masterPubkey |
|||
wrapMode: Text.Wrap |
|||
Layout.fillWidth: true |
|||
font.family: FixedFont |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
ToolButton { |
|||
icon.source: '../../icons/share.png' |
|||
icon.color: 'transparent' |
|||
onClicked: { |
|||
var dialog = share.createObject(rootItem, { |
|||
'title': qsTr('Master Public Key'), |
|||
'text': Daemon.currentWallet.masterPubkey |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
visible: Daemon.currentWallet == null |
|||
|
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.bottomMargin: constants.paddingXXLarge |
|||
Layout.topMargin: constants.paddingXXLarge |
|||
spacing: 2*constants.paddingXLarge |
|||
|
|||
Label { |
|||
text: qsTr('No wallet loaded') |
|||
font.pixelSize: constants.fontSizeXXLarge |
|||
Layout.alignment: Qt.AlignHCenter |
|||
} |
|||
|
|||
} |
|||
|
|||
Frame { |
|||
id: detailsFrame |
|||
Layout.topMargin: constants.paddingXLarge |
|||
Layout.preferredWidth: parent.width |
|||
Layout.fillHeight: true |
|||
verticalPadding: 0 |
|||
horizontalPadding: 0 |
|||
background: PaneInsetBackground {} |
|||
|
|||
ColumnLayout { |
|||
spacing: 0 |
|||
anchors.fill: parent |
|||
|
|||
Item { |
|||
Layout.preferredHeight: hitem.height |
|||
Layout.preferredWidth: parent.width |
|||
Rectangle { |
|||
anchors.fill: parent |
|||
color: Qt.lighter(Material.background, 1.25) |
|||
} |
|||
RowLayout { |
|||
id: hitem |
|||
width: parent.width |
|||
Label { |
|||
text: qsTr('Available wallets') |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
} |
|||
|
|||
ListView { |
|||
id: listview |
|||
Layout.preferredWidth: parent.width |
|||
Layout.fillHeight: true |
|||
clip: true |
|||
model: Daemon.availableWallets |
|||
|
|||
delegate: ItemDelegate { |
|||
width: ListView.view.width |
|||
height: row.height |
|||
|
|||
onClicked: { |
|||
Daemon.load_wallet(model.path) |
|||
} |
|||
|
|||
RowLayout { |
|||
id: row |
|||
spacing: 10 |
|||
x: constants.paddingSmall |
|||
width: parent.width - 2 * constants.paddingSmall |
|||
|
|||
Image { |
|||
id: walleticon |
|||
source: "../../icons/wallet.png" |
|||
fillMode: Image.PreserveAspectFit |
|||
Layout.preferredWidth: constants.iconSizeLarge |
|||
Layout.preferredHeight: constants.iconSizeLarge |
|||
Layout.topMargin: constants.paddingSmall |
|||
Layout.bottomMargin: constants.paddingSmall |
|||
} |
|||
|
|||
Label { |
|||
font.pixelSize: constants.fontSizeLarge |
|||
text: model.name |
|||
color: model.active ? Material.foreground : Qt.darker(Material.foreground, 1.20) |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
Tag { |
|||
visible: Daemon.currentWallet && model.name == Daemon.currentWallet.name |
|||
text: qsTr('Current') |
|||
border.color: Material.foreground |
|||
font.bold: true |
|||
labelcolor: Material.foreground |
|||
} |
|||
Tag { |
|||
visible: model.active |
|||
text: qsTr('Active') |
|||
border.color: 'green' |
|||
labelcolor: 'green' |
|||
} |
|||
Tag { |
|||
visible: !model.active |
|||
text: qsTr('Not loaded') |
|||
border.color: 'grey' |
|||
labelcolor: 'grey' |
|||
} |
|||
} |
|||
} |
|||
|
|||
ScrollIndicator.vertical: ScrollIndicator { } |
|||
} |
|||
} |
|||
} |
|||
|
|||
Button { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
text: 'Create Wallet' |
|||
onClicked: rootItem.createWallet() |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon |
|||
function onWalletLoaded() { |
|||
Daemon.availableWallets.reload() |
|||
app.stack.pop() |
|||
} |
|||
function onRequestNewPassword() { // new unified password (all wallets) |
|||
var dialog = app.passwordDialog.createObject(app, |
|||
{ |
|||
'confirmPassword': true, |
|||
'title': qsTr('Enter new password'), |
|||
'infotext': qsTr('If you forget your password, you\'ll need to\ |
|||
restore from seed. Please make sure you have your seed stored safely') |
|||
} ) |
|||
dialog.accepted.connect(function() { |
|||
Daemon.set_password(dialog.password) |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onRequestNewPassword() { // new wallet password |
|||
var dialog = app.passwordDialog.createObject(app, |
|||
{ |
|||
'confirmPassword': true, |
|||
'title': qsTr('Enter new password'), |
|||
'infotext': qsTr('If you forget your password, you\'ll need to\ |
|||
restore from seed. Please make sure you have your seed stored safely') |
|||
} ) |
|||
dialog.accepted.connect(function() { |
|||
Daemon.currentWallet.set_password(dialog.password) |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
|
|||
Component { |
|||
id: share |
|||
GenericShareDialog { |
|||
onClosed: destroy() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,44 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import "wizard" |
|||
|
|||
Item { |
|||
property Component walletname: Component { |
|||
WCWalletName {} |
|||
} |
|||
|
|||
property Component wallettype: Component { |
|||
WCWalletType {} |
|||
} |
|||
|
|||
property Component keystore: Component { |
|||
WCKeystoreType {} |
|||
} |
|||
|
|||
property Component createseed: Component { |
|||
WCCreateSeed {} |
|||
} |
|||
|
|||
property Component haveseed: Component { |
|||
WCHaveSeed {} |
|||
} |
|||
|
|||
property Component confirmseed: Component { |
|||
WCConfirmSeed {} |
|||
} |
|||
|
|||
property Component bip39refine: Component { |
|||
WCBIP39Refine {} |
|||
} |
|||
|
|||
property Component walletpassword: Component { |
|||
WCWalletPassword {} |
|||
} |
|||
|
|||
|
|||
} |
@ -0,0 +1,30 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
TextField { |
|||
id: amount |
|||
|
|||
required property TextField fiatfield |
|||
|
|||
font.family: FixedFont |
|||
placeholderText: qsTr('Amount') |
|||
inputMethodHints: Qt.ImhPreferNumbers |
|||
property Amount textAsSats |
|||
onTextChanged: { |
|||
textAsSats = Config.unitsToSats(amount.text) |
|||
if (fiatfield.activeFocus) |
|||
return |
|||
fiatfield.text = text == '' ? '' : Daemon.fx.fiatValue(amount.textAsSats) |
|||
} |
|||
|
|||
Connections { |
|||
target: Config |
|||
function onBaseUnitChanged() { |
|||
amount.text = amount.textAsSats.satsInt != 0 |
|||
? Config.satsToUnits(amount.textAsSats) |
|||
: '' |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,120 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
ItemDelegate { |
|||
id: root |
|||
height: item.height |
|||
width: ListView.view.width |
|||
|
|||
font.pixelSize: constants.fontSizeSmall // set default font size for child controls |
|||
|
|||
GridLayout { |
|||
id: item |
|||
|
|||
anchors { |
|||
left: parent.left |
|||
right: parent.right |
|||
leftMargin: constants.paddingSmall |
|||
rightMargin: constants.paddingSmall |
|||
} |
|||
|
|||
columns: 2 |
|||
|
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
color: 'transparent' |
|||
} |
|||
|
|||
Image { |
|||
id: walleticon |
|||
source: "../../../icons/lightning.png" |
|||
fillMode: Image.PreserveAspectFit |
|||
Layout.rowSpan: 3 |
|||
Layout.preferredWidth: constants.iconSizeLarge |
|||
Layout.preferredHeight: constants.iconSizeLarge |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: model.node_alias |
|||
elide: Text.ElideRight |
|||
wrapMode: Text.Wrap |
|||
maximumLineCount: 2 |
|||
} |
|||
|
|||
Label { |
|||
text: model.state |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: model.short_cid |
|||
color: constants.mutedForeground |
|||
} |
|||
|
|||
Label { |
|||
text: Config.formatSats(model.capacity) |
|||
font.family: FixedFont |
|||
} |
|||
|
|||
Label { |
|||
text: Config.baseUnit |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Item { |
|||
id: chviz |
|||
Layout.fillWidth: true |
|||
height: 10 |
|||
onWidthChanged: { |
|||
var cap = model.capacity.satsInt * 1000 |
|||
var twocap = cap * 2 |
|||
b1.width = width * (cap - model.can_send.msatsInt) / twocap |
|||
b2.width = width * model.can_send.msatsInt / twocap |
|||
b3.width = width * model.can_receive.msatsInt / twocap |
|||
b4.width = width * (cap - model.can_receive.msatsInt) / twocap |
|||
} |
|||
Rectangle { |
|||
id: b1 |
|||
x: 0 |
|||
height: parent.height |
|||
color: 'gray' |
|||
} |
|||
Rectangle { |
|||
id: b2 |
|||
anchors.left: b1.right |
|||
height: parent.height |
|||
color: constants.colorLightningLocal |
|||
} |
|||
Rectangle { |
|||
id: b3 |
|||
anchors.left: b2.right |
|||
height: parent.height |
|||
color: constants.colorLightningRemote |
|||
} |
|||
Rectangle { |
|||
id: b4 |
|||
anchors.left: b3.right |
|||
height: parent.height |
|||
color: 'gray' |
|||
} |
|||
} |
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
color: 'transparent' |
|||
} |
|||
|
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
TextField { |
|||
id: amountFiat |
|||
|
|||
required property TextField btcfield |
|||
|
|||
font.family: FixedFont |
|||
placeholderText: qsTr('Amount') |
|||
inputMethodHints: Qt.ImhPreferNumbers |
|||
onTextChanged: { |
|||
if (amountFiat.activeFocus) |
|||
btcfield.text = text == '' |
|||
? '' |
|||
: Config.satsToUnits(Daemon.fx.satoshiValue(amountFiat.text)) |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.fx |
|||
function onQuotesUpdated() { |
|||
amountFiat.text = btcfield.text == '' |
|||
? '' |
|||
: Daemon.fx.fiatValue(Config.unitsToSats(btcfield.text)) |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,119 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.14 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
|
|||
property string text |
|||
|
|||
title: '' |
|||
parent: Overlay.overlay |
|||
modal: true |
|||
standardButtons: Dialog.Ok |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
header: RowLayout { |
|||
width: dialog.width |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: dialog.title |
|||
visible: dialog.title |
|||
elide: Label.ElideRight |
|||
padding: constants.paddingXLarge |
|||
bottomPadding: 0 |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: rootLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
ColumnLayout { |
|||
id: rootLayout |
|||
width: parent.width |
|||
spacing: constants.paddingMedium |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
Image { |
|||
id: qr |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.topMargin: constants.paddingSmall |
|||
Layout.bottomMargin: constants.paddingSmall |
|||
|
|||
Rectangle { |
|||
property int size: 57 // should be qr pixel multiple |
|||
color: 'white' |
|||
x: (parent.width - size) / 2 |
|||
y: (parent.height - size) / 2 |
|||
width: size |
|||
height: size |
|||
|
|||
Image { |
|||
source: '../../../icons/electrum.png' |
|||
x: 1 |
|||
y: 1 |
|||
width: parent.width - 2 |
|||
height: parent.height - 2 |
|||
scale: 0.9 |
|||
} |
|||
} |
|||
} |
|||
|
|||
Rectangle { |
|||
height: 1 |
|||
Layout.fillWidth: true |
|||
color: Material.accentColor |
|||
} |
|||
|
|||
TextHighlightPane { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
width: parent.width |
|||
text: dialog.text |
|||
wrapMode: Text.Wrap |
|||
font.pixelSize: constants.fontSizeLarge |
|||
font.family: FixedFont |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Button { |
|||
text: qsTr('Copy') |
|||
icon.source: '../../../icons/copy_bw.png' |
|||
onClicked: AppController.textToClipboard(dialog.text) |
|||
} |
|||
Button { |
|||
//enabled: false |
|||
text: qsTr('Share') |
|||
icon.source: '../../../icons/share.png' |
|||
onClicked: { |
|||
AppController.doShare(dialog.text, dialog.title) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
qr.source = 'image://qrgen/' + dialog.text |
|||
} |
|||
} |
@ -0,0 +1,58 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
GridLayout { |
|||
property alias text: infotext.text |
|||
|
|||
enum IconStyle { |
|||
None, |
|||
Info, |
|||
Warn, |
|||
Error |
|||
} |
|||
|
|||
property int iconStyle: InfoTextArea.IconStyle.Info |
|||
|
|||
columns: 1 |
|||
rowSpacing: 0 |
|||
|
|||
Rectangle { |
|||
height: 2 |
|||
Layout.fillWidth: true |
|||
color: Qt.rgba(1,1,1,0.25) |
|||
} |
|||
|
|||
TextArea { |
|||
id: infotext |
|||
Layout.fillWidth: true |
|||
Layout.minimumHeight: constants.iconSizeLarge + 2*constants.paddingLarge |
|||
readOnly: true |
|||
rightPadding: constants.paddingLarge |
|||
leftPadding: 2*constants.iconSizeLarge |
|||
wrapMode: TextInput.WordWrap |
|||
textFormat: TextEdit.RichText |
|||
background: Rectangle { |
|||
color: Qt.rgba(1,1,1,0.05) // whiten 5% |
|||
} |
|||
|
|||
Image { |
|||
source: iconStyle == InfoTextArea.IconStyle.Info ? "../../../icons/info.png" : InfoTextArea.IconStyle.Warn ? "../../../icons/warning.png" : InfoTextArea.IconStyle.Error ? "../../../icons/expired.png" : "" |
|||
anchors.left: parent.left |
|||
anchors.top: parent.top |
|||
anchors.leftMargin: constants.paddingLarge |
|||
anchors.topMargin: constants.paddingLarge |
|||
height: constants.iconSizeLarge |
|||
width: constants.iconSizeLarge |
|||
fillMode: Image.PreserveAspectCrop |
|||
} |
|||
|
|||
} |
|||
|
|||
Rectangle { |
|||
height: 2 |
|||
Layout.fillWidth: true |
|||
color: Qt.rgba(0,0,0,0.25) |
|||
} |
|||
} |
@ -0,0 +1,147 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
ItemDelegate { |
|||
id: root |
|||
height: item.height |
|||
width: ListView.view.width |
|||
|
|||
font.pixelSize: constants.fontSizeSmall // set default font size for child controls |
|||
|
|||
GridLayout { |
|||
id: item |
|||
|
|||
anchors { |
|||
left: parent.left |
|||
right: parent.right |
|||
leftMargin: constants.paddingSmall |
|||
rightMargin: constants.paddingSmall |
|||
} |
|||
|
|||
columns: 2 |
|||
|
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
color: 'transparent' |
|||
} |
|||
|
|||
Image { |
|||
Layout.rowSpan: 2 |
|||
Layout.preferredWidth: constants.iconSizeLarge |
|||
Layout.preferredHeight: constants.iconSizeLarge |
|||
source: model.is_lightning |
|||
? "../../../icons/lightning.png" |
|||
: "../../../icons/bitcoin.png" |
|||
|
|||
Image { |
|||
visible: model.onchain_fallback |
|||
z: -1 |
|||
source: "../../../icons/bitcoin.png" |
|||
anchors { |
|||
right: parent.right |
|||
bottom: parent.bottom |
|||
} |
|||
width: parent.width /2 |
|||
height: parent.height /2 |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
Layout.fillWidth: true |
|||
text: model.message |
|||
? model.message |
|||
: model.type == 'request' |
|||
? model.address |
|||
: '' |
|||
elide: Text.ElideRight |
|||
wrapMode: Text.Wrap |
|||
maximumLineCount: 2 |
|||
font.pixelSize: model.message ? constants.fontSizeMedium : constants.fontSizeSmall |
|||
} |
|||
|
|||
Label { |
|||
id: amount |
|||
text: model.amount.isEmpty ? '' : Config.formatSats(model.amount) |
|||
font.pixelSize: constants.fontSizeMedium |
|||
font.family: FixedFont |
|||
} |
|||
|
|||
Label { |
|||
text: model.amount.isEmpty ? '' : Config.baseUnit |
|||
font.pixelSize: constants.fontSizeMedium |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.fillWidth: true |
|||
Label { |
|||
text: model.status_str |
|||
color: Material.accentColor |
|||
} |
|||
Item { |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: status_icon.height |
|||
Image { |
|||
id: status_icon |
|||
source: model.status == 0 |
|||
? '../../../icons/unpaid.png' |
|||
: model.status == 1 |
|||
? '../../../icons/expired.png' |
|||
: model.status == 3 |
|||
? '../../../icons/confirmed.png' |
|||
: model.status == 7 |
|||
? '../../../icons/unconfirmed.png' |
|||
: '' |
|||
width: constants.iconSizeSmall |
|||
height: constants.iconSizeSmall |
|||
} |
|||
} |
|||
Label { |
|||
id: fiatValue |
|||
visible: Daemon.fx.enabled |
|||
Layout.alignment: Qt.AlignRight |
|||
text: model.amount.isEmpty ? '' : Daemon.fx.fiatValue(model.amount, false) |
|||
font.family: FixedFont |
|||
font.pixelSize: constants.fontSizeSmall |
|||
} |
|||
Label { |
|||
visible: Daemon.fx.enabled |
|||
Layout.alignment: Qt.AlignRight |
|||
text: model.amount.isEmpty ? '' : Daemon.fx.fiatCurrency |
|||
font.pixelSize: constants.fontSizeSmall |
|||
color: Material.accentColor |
|||
} |
|||
} |
|||
|
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.preferredHeight: constants.paddingTiny |
|||
color: 'transparent' |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Config |
|||
function onBaseUnitChanged() { |
|||
amount.text = model.amount.isEmpty ? '' : Config.formatSats(model.amount) |
|||
} |
|||
function onThousandsSeparatorChanged() { |
|||
amount.text = model.amount.isEmpty ? '' : Config.formatSats(model.amount) |
|||
} |
|||
} |
|||
Connections { |
|||
target: Daemon.fx |
|||
function onQuotesUpdated() { |
|||
fiatValue.text = model.amount.isEmpty ? '' : Daemon.fx.fiatValue(model.amount, false) |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,61 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Dialog { |
|||
id: dialog |
|||
title: qsTr("Message") |
|||
|
|||
property bool yesno: false |
|||
property alias text: message.text |
|||
|
|||
signal yesClicked |
|||
signal noClicked |
|||
|
|||
parent: Overlay.overlay |
|||
modal: true |
|||
x: (parent.width - width) / 2 |
|||
y: (parent.height - height) / 2 |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
ColumnLayout { |
|||
TextArea { |
|||
id: message |
|||
Layout.preferredWidth: Overlay.overlay.width *2/3 |
|||
readOnly: true |
|||
wrapMode: TextInput.WordWrap |
|||
//textFormat: TextEdit.RichText // existing translations not richtext yet |
|||
background: Rectangle { |
|||
color: 'transparent' |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Button { |
|||
text: qsTr('Ok') |
|||
visible: !yesno |
|||
onClicked: dialog.close() |
|||
} |
|||
Button { |
|||
text: qsTr('Yes') |
|||
visible: yesno |
|||
onClicked: { |
|||
yesClicked() |
|||
dialog.close() |
|||
} |
|||
} |
|||
Button { |
|||
text: qsTr('No') |
|||
visible: yesno |
|||
onClicked: { |
|||
noClicked() |
|||
dialog.close() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,30 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Rectangle { |
|||
id: item |
|||
|
|||
property bool warning |
|||
property bool error |
|||
property string text |
|||
|
|||
color: "transparent" |
|||
border.color: error ? "red" : warning ? "yellow" : Material.accentColor |
|||
border.width: 1 |
|||
height: text.height + 2* 16 |
|||
radius: 8 |
|||
|
|||
Text { |
|||
id: text |
|||
width: item.width - 2* 16 |
|||
x: 16 |
|||
y: 16 |
|||
|
|||
color: item.border.color |
|||
text: item.text |
|||
wrapMode: Text.Wrap |
|||
} |
|||
|
|||
} |
@ -0,0 +1,61 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Rectangle { |
|||
id: root |
|||
|
|||
property alias text: textItem.text |
|||
|
|||
property bool hide: true |
|||
|
|||
color: Qt.lighter(Material.background, 1.5) |
|||
radius: constants.paddingXLarge |
|||
|
|||
width: root.parent.width * 2/3 |
|||
height: layout.height |
|||
x: (root.parent.width - width) / 2 |
|||
y: -height |
|||
|
|||
states: [ |
|||
State { |
|||
name: 'expanded'; when: !hide |
|||
PropertyChanges { target: root; y: 100 } |
|||
} |
|||
] |
|||
|
|||
transitions: [ |
|||
Transition { |
|||
from: ''; to: 'expanded'; reversible: true |
|||
NumberAnimation { properties: 'y'; duration: 300; easing.type: Easing.InOutQuad } |
|||
} |
|||
] |
|||
|
|||
function show(message) { |
|||
root.text = message |
|||
root.hide = false |
|||
closetimer.start() |
|||
} |
|||
|
|||
RowLayout { |
|||
id: layout |
|||
width: parent.width |
|||
Text { |
|||
id: textItem |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
color: Material.foreground |
|||
wrapMode: Text.Wrap |
|||
} |
|||
} |
|||
|
|||
Timer { |
|||
id: closetimer |
|||
interval: 5000 |
|||
repeat: false |
|||
onTriggered: hide = true |
|||
} |
|||
|
|||
} |
@ -0,0 +1,26 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Rectangle { |
|||
Rectangle { |
|||
anchors { left: parent.left; top: parent.top; right: parent.right } |
|||
height: 1 |
|||
color: Qt.darker(Material.background, 1.50) |
|||
} |
|||
Rectangle { |
|||
anchors { left: parent.left; top: parent.top; bottom: parent.bottom } |
|||
width: 1 |
|||
color: Qt.darker(Material.background, 1.50) |
|||
} |
|||
Rectangle { |
|||
anchors { left: parent.left; bottom: parent.bottom; right: parent.right } |
|||
height: 1 |
|||
color: Qt.lighter(Material.background, 1.50) |
|||
} |
|||
Rectangle { |
|||
anchors { right: parent.right; top: parent.top; bottom: parent.bottom } |
|||
width: 1 |
|||
color: Qt.lighter(Material.background, 1.50) |
|||
} |
|||
color: Qt.darker(Material.background, 1.15) |
|||
} |
@ -0,0 +1,115 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Dialog { |
|||
id: passworddialog |
|||
|
|||
title: qsTr("Enter Password") |
|||
|
|||
property bool confirmPassword: false |
|||
property string password |
|||
property string infotext |
|||
|
|||
parent: Overlay.overlay |
|||
modal: true |
|||
x: (parent.width - width) / 2 |
|||
y: (parent.height - height) / 2 |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
|
|||
header: GridLayout { |
|||
columns: 2 |
|||
rowSpacing: 0 |
|||
|
|||
Image { |
|||
source: "../../../icons/lock.png" |
|||
Layout.preferredWidth: constants.iconSizeXLarge |
|||
Layout.preferredHeight: constants.iconSizeXLarge |
|||
Layout.leftMargin: constants.paddingMedium |
|||
Layout.topMargin: constants.paddingMedium |
|||
Layout.bottomMargin: constants.paddingMedium |
|||
} |
|||
|
|||
Label { |
|||
text: title |
|||
elide: Label.ElideRight |
|||
Layout.fillWidth: true |
|||
topPadding: constants.paddingXLarge |
|||
bottomPadding: constants.paddingXLarge |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
|
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.leftMargin: constants.paddingXXSmall |
|||
Layout.rightMargin: constants.paddingXXSmall |
|||
height: 1 |
|||
color: Qt.rgba(0,0,0,0.5) |
|||
} |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
InfoTextArea { |
|||
visible: infotext |
|||
text: infotext |
|||
Layout.preferredWidth: password_layout.width |
|||
} |
|||
|
|||
GridLayout { |
|||
id: password_layout |
|||
columns: 2 |
|||
Layout.fillWidth: true |
|||
Layout.margins: constants.paddingXXLarge |
|||
|
|||
Label { |
|||
text: qsTr('Password') |
|||
} |
|||
|
|||
TextField { |
|||
id: pw_1 |
|||
echoMode: TextInput.Password |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr('Password (again)') |
|||
visible: confirmPassword |
|||
} |
|||
|
|||
TextField { |
|||
id: pw_2 |
|||
echoMode: TextInput.Password |
|||
visible: confirmPassword |
|||
} |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.topMargin: constants.paddingXXLarge |
|||
|
|||
Button { |
|||
text: qsTr("Ok") |
|||
enabled: confirmPassword ? pw_1.text == pw_2.text : true |
|||
onClicked: { |
|||
password = pw_1.text |
|||
passworddialog.accept() |
|||
} |
|||
} |
|||
Button { |
|||
text: qsTr("Cancel") |
|||
onClicked: { |
|||
passworddialog.reject() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,164 @@ |
|||
import QtQuick 2.12 |
|||
import QtQuick.Controls 2.0 |
|||
import QtMultimedia 5.6 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
Item { |
|||
id: scanner |
|||
|
|||
property bool active: false |
|||
property string url |
|||
property string scanData |
|||
|
|||
property bool _pointsVisible |
|||
|
|||
signal found |
|||
|
|||
VideoOutput { |
|||
id: vo |
|||
anchors.fill: parent |
|||
source: camera |
|||
fillMode: VideoOutput.PreserveAspectCrop |
|||
|
|||
Rectangle { |
|||
width: parent.width |
|||
height: (parent.height - parent.width) / 2 |
|||
visible: camera.cameraStatus == Camera.ActiveStatus |
|||
anchors.top: parent.top |
|||
color: Qt.rgba(0,0,0,0.5) |
|||
} |
|||
Rectangle { |
|||
width: parent.width |
|||
height: (parent.height - parent.width) / 2 |
|||
visible: camera.cameraStatus == Camera.ActiveStatus |
|||
anchors.bottom: parent.bottom |
|||
color: Qt.rgba(0,0,0,0.5) |
|||
} |
|||
} |
|||
|
|||
Image { |
|||
id: still |
|||
anchors.fill: vo |
|||
} |
|||
|
|||
SequentialAnimation { |
|||
id: foundAnimation |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: true} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: false} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: true} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: false} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: true} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: false} |
|||
PauseAnimation { duration: 80 } |
|||
PropertyAction { target: scanner; property: '_pointsVisible'; value: true} |
|||
onFinished: found() |
|||
} |
|||
|
|||
Component { |
|||
id: r |
|||
Rectangle { |
|||
property int cx |
|||
property int cy |
|||
width: 15 |
|||
height: 15 |
|||
x: cx - width/2 |
|||
y: cy - height/2 |
|||
radius: 5 |
|||
visible: scanner._pointsVisible |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: qr |
|||
function onDataChanged() { |
|||
console.log(qr.data) |
|||
scanner.active = false |
|||
scanner.scanData = qr.data |
|||
still.source = scanner.url |
|||
|
|||
var sx = still.width/still.sourceSize.width |
|||
var sy = still.height/still.sourceSize.height |
|||
r.createObject(scanner, {cx: qr.points[0].x * sx, cy: qr.points[0].y * sy, color: 'yellow'}) |
|||
r.createObject(scanner, {cx: qr.points[1].x * sx, cy: qr.points[1].y * sy, color: 'yellow'}) |
|||
r.createObject(scanner, {cx: qr.points[2].x * sx, cy: qr.points[2].y * sy, color: 'yellow'}) |
|||
r.createObject(scanner, {cx: qr.points[3].x * sx, cy: qr.points[3].y * sy, color: 'yellow'}) |
|||
|
|||
foundAnimation.start() |
|||
} |
|||
} |
|||
|
|||
Camera { |
|||
id: camera |
|||
deviceId: QtMultimedia.defaultCamera.deviceId |
|||
viewfinder.resolution: "640x480" |
|||
|
|||
focus { |
|||
focusMode: Camera.FocusContinuous |
|||
focusPointMode: Camera.FocusPointCustom |
|||
customFocusPoint: Qt.point(0.5, 0.5) |
|||
} |
|||
|
|||
function dumpstats() { |
|||
console.log(camera.viewfinder.resolution) |
|||
console.log(camera.viewfinder.minimumFrameRate) |
|||
console.log(camera.viewfinder.maximumFrameRate) |
|||
var resolutions = camera.supportedViewfinderResolutions() |
|||
resolutions.forEach(function(item, i) { |
|||
console.log('' + item.width + 'x' + item.height) |
|||
}) |
|||
// TODO |
|||
// pick a suitable resolution from the available resolutions |
|||
// problem: some cameras have no supportedViewfinderResolutions |
|||
// but still error out when an invalid resolution is set. |
|||
// 640x480 seems to be universally available, but this needs to |
|||
// be checked across a range of phone models. |
|||
} |
|||
} |
|||
|
|||
Timer { |
|||
id: scanTimer |
|||
interval: 200 |
|||
repeat: true |
|||
running: scanner.active |
|||
onTriggered: { |
|||
if (qr.busy) |
|||
return |
|||
vo.grabToImage(function(result) { |
|||
if (result.image !== undefined) { |
|||
scanner.url = result.url |
|||
qr.scanImage(result.image) |
|||
} else { |
|||
console.log('image grab returned null') |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
QRParser { |
|||
id: qr |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
console.log('Scan page initialized') |
|||
QtMultimedia.availableCameras.forEach(function(item) { |
|||
console.log('cam found') |
|||
console.log(item.deviceId) |
|||
console.log(item.displayName) |
|||
console.log(item.position) |
|||
console.log(item.orientation) |
|||
if (QtMultimedia.defaultCamera.deviceId == item.deviceId) { |
|||
vo.orientation = item.orientation |
|||
} |
|||
|
|||
camera.dumpstats() |
|||
}) |
|||
|
|||
active = true |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
TextArea { |
|||
id: seedtext |
|||
Layout.fillWidth: true |
|||
Layout.minimumHeight: 80 |
|||
rightPadding: constants.paddingLarge |
|||
leftPadding: constants.paddingLarge |
|||
wrapMode: TextInput.WordWrap |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeLarge |
|||
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhPreferLowercase | Qt.ImhNoPredictiveText |
|||
background: Rectangle { |
|||
color: "transparent" |
|||
border.color: Material.accentColor |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Rectangle { |
|||
radius: constants.paddingXSmall |
|||
width: layout.width |
|||
height: layout.height |
|||
color: 'transparent' |
|||
border.color: Material.accentColor |
|||
|
|||
property alias text: label.text |
|||
property alias font: label.font |
|||
property alias labelcolor: label.color |
|||
|
|||
RowLayout { |
|||
id: layout |
|||
|
|||
Label { |
|||
id: label |
|||
Layout.leftMargin: constants.paddingSmall |
|||
Layout.rightMargin: constants.paddingSmall |
|||
Layout.topMargin: constants.paddingXXSmall |
|||
Layout.bottomMargin: constants.paddingXXSmall |
|||
font.pixelSize: constants.fontSizeXSmall |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,13 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.0 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
Pane { |
|||
topPadding: constants.paddingSmall |
|||
bottomPadding: constants.paddingSmall |
|||
background: Rectangle { |
|||
color: Qt.lighter(Material.background, 1.15) |
|||
radius: constants.paddingSmall |
|||
} |
|||
} |
@ -0,0 +1,315 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.3 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import QtQml 2.6 |
|||
import QtMultimedia 5.6 |
|||
|
|||
import "controls" |
|||
|
|||
ApplicationWindow |
|||
{ |
|||
id: app |
|||
visible: true |
|||
|
|||
// dimensions ignored on android |
|||
width: 480 |
|||
height: 800 |
|||
|
|||
Material.theme: Material.Dark |
|||
Material.primary: Material.Indigo |
|||
Material.accent: Material.LightBlue |
|||
font.pixelSize: constants.fontSizeMedium |
|||
|
|||
property Item constants: appconstants |
|||
Constants { id: appconstants } |
|||
|
|||
property alias stack: mainStackView |
|||
|
|||
header: ToolBar { |
|||
id: toolbar |
|||
|
|||
RowLayout { |
|||
anchors.fill: parent |
|||
|
|||
ToolButton { |
|||
text: qsTr("‹") |
|||
enabled: stack.depth > 1 |
|||
onClicked: stack.pop() |
|||
} |
|||
|
|||
Label { |
|||
text: stack.currentItem.title |
|||
elide: Label.ElideRight |
|||
horizontalAlignment: Qt.AlignHCenter |
|||
verticalAlignment: Qt.AlignVCenter |
|||
Layout.fillWidth: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
font.bold: true |
|||
} |
|||
|
|||
Item { |
|||
visible: Network.isTestNet |
|||
width: column.width |
|||
height: column.height |
|||
|
|||
ColumnLayout { |
|||
id: column |
|||
spacing: 0 |
|||
Image { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Layout.preferredWidth: constants.iconSizeSmall |
|||
Layout.preferredHeight: constants.iconSizeSmall |
|||
source: "../../icons/info.png" |
|||
} |
|||
|
|||
Label { |
|||
id: networkNameLabel |
|||
text: Network.networkName |
|||
color: Material.accentColor |
|||
font.pixelSize: constants.fontSizeXSmall |
|||
} |
|||
} |
|||
} |
|||
|
|||
Image { |
|||
Layout.preferredWidth: constants.iconSizeSmall |
|||
Layout.preferredHeight: constants.iconSizeSmall |
|||
visible: Daemon.currentWallet && Daemon.currentWallet.isWatchOnly |
|||
source: '../../icons/eye1.png' |
|||
scale: 1.5 |
|||
} |
|||
|
|||
Image { |
|||
Layout.preferredWidth: constants.iconSizeSmall |
|||
Layout.preferredHeight: constants.iconSizeSmall |
|||
source: Network.status == 'connecting' || Network.status == 'disconnected' |
|||
? '../../icons/status_disconnected.png' |
|||
: Network.status == 'connected' |
|||
? Daemon.currentWallet && !Daemon.currentWallet.isUptodate |
|||
? '../../icons/status_lagging.png' |
|||
: '../../icons/status_connected.png' |
|||
: '../../icons/status_connected.png' |
|||
} |
|||
|
|||
Rectangle { |
|||
color: 'transparent' |
|||
Layout.preferredWidth: constants.paddingSmall |
|||
height: 1 |
|||
visible: !menuButton.visible |
|||
} |
|||
|
|||
ToolButton { |
|||
id: menuButton |
|||
enabled: stack.currentItem && stack.currentItem.menu ? stack.currentItem.menu.count > 0 : false |
|||
text: enabled ? qsTr("≡") : '' |
|||
font.pixelSize: constants.fontSizeXLarge |
|||
onClicked: { |
|||
stack.currentItem.menu.open() |
|||
// position the menu to the right |
|||
stack.currentItem.menu.x = toolbar.width - stack.currentItem.menu.width |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
StackView { |
|||
id: mainStackView |
|||
anchors.fill: parent |
|||
|
|||
initialItem: Qt.resolvedUrl('WalletMainView.qml') |
|||
} |
|||
|
|||
Timer { |
|||
id: splashTimer |
|||
interval: 10 |
|||
onTriggered: { |
|||
splash.opacity = 0 |
|||
} |
|||
} |
|||
|
|||
Splash { |
|||
id: splash |
|||
anchors.top: header.top |
|||
anchors.bottom: app.contentItem.bottom |
|||
width: app.width |
|||
z: 1000 |
|||
|
|||
Behavior on opacity { |
|||
NumberAnimation { duration: 300 } |
|||
} |
|||
} |
|||
|
|||
property alias newWalletWizard: _newWalletWizard |
|||
Component { |
|||
id: _newWalletWizard |
|||
NewWalletWizard { |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
} |
|||
} |
|||
|
|||
property alias serverConnectWizard: _serverConnectWizard |
|||
Component { |
|||
id: _serverConnectWizard |
|||
ServerConnectWizard { |
|||
parent: Overlay.overlay |
|||
Overlay.modal: Rectangle { |
|||
color: "#aa000000" |
|||
} |
|||
} |
|||
} |
|||
|
|||
property alias messageDialog: _messageDialog |
|||
Component { |
|||
id: _messageDialog |
|||
MessageDialog { |
|||
onClosed: destroy() |
|||
} |
|||
} |
|||
|
|||
property alias passwordDialog: _passwordDialog |
|||
Component { |
|||
id: _passwordDialog |
|||
PasswordDialog { |
|||
onClosed: destroy() |
|||
} |
|||
} |
|||
|
|||
property alias pinDialog: _pinDialog |
|||
Component { |
|||
id: _pinDialog |
|||
Pin { |
|||
onClosed: destroy() |
|||
} |
|||
} |
|||
|
|||
NotificationPopup { |
|||
id: notificationPopup |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
splashTimer.start() |
|||
|
|||
if (!Config.autoConnectDefined) { |
|||
var dialog = serverConnectWizard.createObject(app) |
|||
// without completed serverConnectWizard we can't start |
|||
dialog.rejected.connect(function() { |
|||
app.visible = false |
|||
Qt.callLater(Qt.quit) |
|||
}) |
|||
dialog.open() |
|||
} else { |
|||
Daemon.load_wallet() |
|||
} |
|||
} |
|||
|
|||
onClosing: { |
|||
if (stack.depth > 1) { |
|||
close.accepted = false |
|||
stack.pop() |
|||
} else { |
|||
// destroy most GUI components so that we don't dump so many null reference warnings on exit |
|||
if (closeMsgTimer.running) { |
|||
app.header.visible = false |
|||
mainStackView.clear() |
|||
} else { |
|||
notificationPopup.show('Press Back again to exit') |
|||
closeMsgTimer.start() |
|||
close.accepted = false |
|||
} |
|||
} |
|||
} |
|||
|
|||
Timer { |
|||
id: closeMsgTimer |
|||
interval: 5000 |
|||
repeat: false |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon |
|||
function onWalletRequiresPassword() { |
|||
console.log('wallet requires password') |
|||
app.stack.push(Qt.resolvedUrl("OpenWallet.qml"), {"path": Daemon.path}) |
|||
} |
|||
function onWalletOpenError(error) { |
|||
console.log('wallet open error') |
|||
var dialog = app.messageDialog.createObject(app, {'text': error}) |
|||
dialog.open() |
|||
} |
|||
function onAuthRequired(method) { |
|||
handleAuthRequired(Daemon, method) |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: AppController |
|||
function onUserNotify(message) { |
|||
notificationPopup.show(message) |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Daemon.currentWallet |
|||
function onAuthRequired(method) { |
|||
handleAuthRequired(Daemon.currentWallet, method) |
|||
} |
|||
// TODO: add to notification queue instead of barging through |
|||
function onPaymentSucceeded(key) { |
|||
notificationPopup.show(qsTr('Payment Succeeded')) |
|||
} |
|||
function onPaymentFailed(key, reason) { |
|||
notificationPopup.show(qsTr('Payment Failed') + ': ' + reason) |
|||
} |
|||
} |
|||
|
|||
Connections { |
|||
target: Config |
|||
function onAuthRequired(method) { |
|||
handleAuthRequired(Config, method) |
|||
} |
|||
} |
|||
|
|||
function handleAuthRequired(qtobject, method) { |
|||
console.log('AUTHENTICATING USING METHOD ' + method) |
|||
if (method == 'wallet') { |
|||
if (Daemon.currentWallet.verify_password('')) { |
|||
// wallet has no password |
|||
qtobject.authProceed() |
|||
} else { |
|||
var dialog = app.passwordDialog.createObject(app, {'title': qsTr('Enter current password')}) |
|||
dialog.accepted.connect(function() { |
|||
if (Daemon.currentWallet.verify_password(dialog.password)) { |
|||
qtobject.authProceed() |
|||
} else { |
|||
qtobject.authCancel() |
|||
} |
|||
}) |
|||
dialog.rejected.connect(function() { |
|||
qtobject.authCancel() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} else if (method == 'pin') { |
|||
if (Config.pinCode == '') { |
|||
// no PIN configured |
|||
qtobject.authProceed() |
|||
} else { |
|||
var dialog = app.pinDialog.createObject(app, {mode: 'check', pincode: Config.pinCode}) |
|||
dialog.accepted.connect(function() { |
|||
qtobject.authProceed() |
|||
dialog.close() |
|||
}) |
|||
dialog.rejected.connect(function() { |
|||
qtobject.authCancel() |
|||
}) |
|||
dialog.open() |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,41 @@ |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import ".." |
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
valid: true |
|||
last: serverconnectgroup.checkedButton.connecttype === 'auto' |
|||
|
|||
onAccept: { |
|||
wizard_data['autoconnect'] = serverconnectgroup.checkedButton.connecttype === 'auto' |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
InfoTextArea { |
|||
text: qsTr('Electrum communicates with remote servers to get information about your transactions and addresses. The servers all fulfill the same purpose only differing in hardware. In most cases you simply want to let Electrum pick one at random. However if you prefer feel free to select a server manually.') |
|||
Layout.fillWidth: true |
|||
} |
|||
|
|||
ButtonGroup { |
|||
id: serverconnectgroup |
|||
} |
|||
|
|||
RadioButton { |
|||
ButtonGroup.group: serverconnectgroup |
|||
property string connecttype: 'auto' |
|||
text: qsTr('Auto connect') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: serverconnectgroup |
|||
property string connecttype: 'manual' |
|||
checked: true |
|||
text: qsTr('Select servers manually') |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,99 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import ".." |
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
valid: false |
|||
|
|||
onAccept: { |
|||
wizard_data['script_type'] = scripttypegroup.checkedButton.scripttype |
|||
wizard_data['derivation_path'] = derivationpathtext.text |
|||
} |
|||
function getScriptTypePurposeDict() { |
|||
return { |
|||
'p2pkh': 44, |
|||
'p2wpkh-p2sh': 49, |
|||
'p2wpkh': 84 |
|||
} |
|||
} |
|||
|
|||
function validate() { |
|||
valid = false |
|||
if (!scripttypegroup.checkedButton.scripttype in getScriptTypePurposeDict()) |
|||
return |
|||
if (!bitcoin.verify_derivation_path(derivationpathtext.text)) |
|||
return |
|||
valid = true |
|||
} |
|||
|
|||
function setDerivationPath() { |
|||
var p = getScriptTypePurposeDict() |
|||
derivationpathtext.text = |
|||
"m/" + p[scripttypegroup.checkedButton.scripttype] + "'/" |
|||
+ (Network.isTestNet ? 1 : 0) + "'/0'" |
|||
} |
|||
|
|||
ButtonGroup { |
|||
id: scripttypegroup |
|||
onCheckedButtonChanged: { |
|||
setDerivationPath() |
|||
} |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: mainLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: mainLayout |
|||
width: parent.width |
|||
columns: 1 |
|||
|
|||
Label { text: qsTr('Script type and Derivation path') } |
|||
Button { |
|||
text: qsTr('Detect Existing Accounts') |
|||
enabled: false |
|||
} |
|||
Label { text: qsTr('Choose the type of addresses in your wallet.') } |
|||
RadioButton { |
|||
ButtonGroup.group: scripttypegroup |
|||
property string scripttype: 'p2pkh' |
|||
text: qsTr('legacy (p2pkh)') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: scripttypegroup |
|||
property string scripttype: 'p2wpkh-p2sh' |
|||
text: qsTr('wrapped segwit (p2wpkh-p2sh)') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: scripttypegroup |
|||
property string scripttype: 'p2wpkh' |
|||
checked: true |
|||
text: qsTr('native segwit (p2wpkh)') |
|||
} |
|||
InfoTextArea { |
|||
text: qsTr('You can override the suggested derivation path.') + ' ' + |
|||
qsTr('If you are not sure what this is, leave this field unchanged.') |
|||
} |
|||
TextField { |
|||
id: derivationpathtext |
|||
Layout.fillWidth: true |
|||
placeholderText: qsTr('Derivation path') |
|||
onTextChanged: validate() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Bitcoin { |
|||
id: bitcoin |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,59 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import ".." |
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
valid: false |
|||
|
|||
function checkValid() { |
|||
var seedvalid = confirm.text == wizard_data['seed'] |
|||
var customwordsvalid = customwordstext.text == wizard_data['seed_extra_words'] |
|||
valid = seedvalid && (wizard_data['seed_extend'] ? customwordsvalid : true) |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: mainLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: mainLayout |
|||
width: parent.width |
|||
columns: 1 |
|||
|
|||
InfoTextArea { |
|||
Layout.fillWidth: true |
|||
text: qsTr('Your seed is important!') + ' ' + |
|||
qsTr('If you lose your seed, your money will be permanently lost.') + ' ' + |
|||
qsTr('To make sure that you have properly saved your seed, please retype it here.') |
|||
} |
|||
Label { text: qsTr('Confirm your seed (re-enter)') } |
|||
SeedTextArea { |
|||
id: confirm |
|||
Layout.fillWidth: true |
|||
onTextChanged: { |
|||
checkValid() |
|||
} |
|||
} |
|||
TextField { |
|||
id: customwordstext |
|||
Layout.fillWidth: true |
|||
placeholderText: qsTr('Enter your custom word(s)') |
|||
onTextChanged: { |
|||
checkValid() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
onReadyChanged: { |
|||
if (ready) |
|||
customwordstext.visible = wizard_data['seed_extend'] |
|||
} |
|||
} |
@ -0,0 +1,88 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import ".." |
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
valid: seedtext.text != '' |
|||
|
|||
onAccept: { |
|||
wizard_data['seed'] = seedtext.text |
|||
wizard_data['seed_type'] = 'segwit' |
|||
wizard_data['seed_extend'] = extendcb.checked |
|||
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' |
|||
} |
|||
|
|||
function setWarningText(numwords) { |
|||
var t = [ |
|||
'<p>', |
|||
qsTr('Please save these %1 words on paper (order is important).').arg(numwords), |
|||
qsTr('This seed will allow you to recover your wallet in case of computer failure.'), |
|||
'</p>', |
|||
'<b>' + qsTr('WARNING') + ':</b>', |
|||
'<ul>', |
|||
'<li>' + qsTr('Never disclose your seed.') + '</li>', |
|||
'<li>' + qsTr('Never type it on a website.') + '</li>', |
|||
'<li>' + qsTr('Do not store it electronically.') + '</li>', |
|||
'</ul>' |
|||
] |
|||
warningtext.text = t.join(' ') |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: mainLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: mainLayout |
|||
width: parent.width |
|||
columns: 1 |
|||
|
|||
InfoTextArea { |
|||
id: warningtext |
|||
Layout.fillWidth: true |
|||
iconStyle: InfoTextArea.IconStyle.Warn |
|||
} |
|||
Label { text: qsTr('Your wallet generation seed is:') } |
|||
SeedTextArea { |
|||
id: seedtext |
|||
readOnly: true |
|||
Layout.fillWidth: true |
|||
|
|||
BusyIndicator { |
|||
anchors.centerIn: parent |
|||
height: parent.height * 2/3 |
|||
visible: seedtext.text == '' |
|||
} |
|||
} |
|||
CheckBox { |
|||
id: extendcb |
|||
text: qsTr('Extend seed with custom words') |
|||
} |
|||
TextField { |
|||
id: customwordstext |
|||
visible: extendcb.checked |
|||
Layout.fillWidth: true |
|||
placeholderText: qsTr('Enter your custom word(s)') |
|||
} |
|||
Component.onCompleted : { |
|||
setWarningText(12) |
|||
bitcoin.generate_seed() |
|||
} |
|||
} |
|||
} |
|||
|
|||
Bitcoin { |
|||
id: bitcoin |
|||
onGeneratedSeedChanged: { |
|||
seedtext.text = generated_seed |
|||
setWarningText(generated_seed.split(' ').length) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,151 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
import QtQuick.Controls.Material 2.0 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
import ".." |
|||
import "../controls" |
|||
|
|||
WizardComponent { |
|||
id: root |
|||
valid: false |
|||
|
|||
onAccept: { |
|||
wizard_data['seed'] = seedtext.text |
|||
wizard_data['seed_type'] = bitcoin.seed_type |
|||
wizard_data['seed_extend'] = extendcb.checked |
|||
wizard_data['seed_extra_words'] = extendcb.checked ? customwordstext.text : '' |
|||
wizard_data['seed_bip39'] = seed_type.getTypeCode() == 'BIP39' |
|||
wizard_data['seed_slip39'] = seed_type.getTypeCode() == 'SLIP39' |
|||
} |
|||
|
|||
function setSeedTypeHelpText() { |
|||
var t = { |
|||
'Electrum': [ |
|||
qsTr('Electrum seeds are the default seed type.'), |
|||
qsTr('If you are restoring from a seed previously created by Electrum, choose this option') |
|||
].join(' '), |
|||
'BIP39': [ |
|||
qsTr('BIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'), |
|||
'<br/><br/>', |
|||
qsTr('However, we do not generate BIP39 seeds, because they do not meet our safety standard.'), |
|||
qsTr('BIP39 seeds do not include a version number, which compromises compatibility with future software.') |
|||
].join(' '), |
|||
'SLIP39': [ |
|||
qsTr('SLIP39 seeds can be imported in Electrum, so that users can access funds locked in other wallets.'), |
|||
'<br/><br/>', |
|||
qsTr('However, we do not generate SLIP39 seeds.') |
|||
].join(' ') |
|||
} |
|||
infotext.text = t[seed_type.currentText] |
|||
} |
|||
|
|||
function checkValid() { |
|||
bitcoin.verify_seed(seedtext.text, seed_type.getTypeCode() == 'BIP39', seed_type.getTypeCode() == 'SLIP39') |
|||
} |
|||
|
|||
Flickable { |
|||
anchors.fill: parent |
|||
contentHeight: mainLayout.height |
|||
clip:true |
|||
interactive: height < contentHeight |
|||
|
|||
GridLayout { |
|||
id: mainLayout |
|||
width: parent.width |
|||
columns: 2 |
|||
|
|||
Label { |
|||
text: qsTr('Seed Type') |
|||
Layout.fillWidth: true |
|||
} |
|||
ComboBox { |
|||
id: seed_type |
|||
model: ['Electrum', 'BIP39', 'SLIP39'] |
|||
onActivated: { |
|||
setSeedTypeHelpText() |
|||
checkValid() |
|||
} |
|||
function getTypeCode() { |
|||
return currentText |
|||
} |
|||
} |
|||
InfoTextArea { |
|||
id: infotext |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
} |
|||
Label { |
|||
text: qsTr('Enter your seed') |
|||
Layout.columnSpan: 2 |
|||
} |
|||
SeedTextArea { |
|||
id: seedtext |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
onTextChanged: { |
|||
validationTimer.restart() |
|||
} |
|||
|
|||
Rectangle { |
|||
anchors.fill: contentText |
|||
color: 'green' |
|||
border.color: Material.accentColor |
|||
radius: 2 |
|||
} |
|||
Label { |
|||
id: contentText |
|||
anchors.right: parent.right |
|||
anchors.bottom: parent.bottom |
|||
leftPadding: text != '' ? constants.paddingLarge : 0 |
|||
rightPadding: text != '' ? constants.paddingLarge : 0 |
|||
font.bold: false |
|||
font.pixelSize: constants.fontSizeSmall |
|||
} |
|||
} |
|||
TextArea { |
|||
id: validationtext |
|||
visible: text != '' |
|||
Layout.fillWidth: true |
|||
readOnly: true |
|||
wrapMode: TextInput.WordWrap |
|||
background: Rectangle { |
|||
color: 'transparent' |
|||
} |
|||
} |
|||
|
|||
CheckBox { |
|||
id: extendcb |
|||
Layout.columnSpan: 2 |
|||
text: qsTr('Extend seed with custom words') |
|||
} |
|||
TextField { |
|||
id: customwordstext |
|||
visible: extendcb.checked |
|||
Layout.fillWidth: true |
|||
Layout.columnSpan: 2 |
|||
placeholderText: qsTr('Enter your custom word(s)') |
|||
} |
|||
} |
|||
} |
|||
|
|||
Bitcoin { |
|||
id: bitcoin |
|||
onSeedTypeChanged: contentText.text = bitcoin.seed_type |
|||
onSeedValidChanged: root.valid = bitcoin.seed_valid |
|||
onValidationMessageChanged: validationtext.text = bitcoin.validation_message |
|||
} |
|||
|
|||
Timer { |
|||
id: validationTimer |
|||
interval: 500 |
|||
repeat: false |
|||
onTriggered: checkValid() |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
setSeedTypeHelpText() |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
WizardComponent { |
|||
valid: keystoregroup.checkedButton !== null |
|||
|
|||
onAccept: { |
|||
wizard_data['keystore_type'] = keystoregroup.checkedButton.keystoretype |
|||
} |
|||
|
|||
ButtonGroup { |
|||
id: keystoregroup |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 1 |
|||
Label { text: qsTr('What kind of wallet do you want to create?') } |
|||
RadioButton { |
|||
ButtonGroup.group: keystoregroup |
|||
property string keystoretype: 'createseed' |
|||
checked: true |
|||
text: qsTr('Create a new seed') |
|||
} |
|||
RadioButton { |
|||
ButtonGroup.group: keystoregroup |
|||
property string keystoretype: 'haveseed' |
|||
text: qsTr('I already have a seed') |
|||
} |
|||
RadioButton { |
|||
enabled: false |
|||
ButtonGroup.group: keystoregroup |
|||
property string keystoretype: 'masterkey' |
|||
text: qsTr('Use a master key') |
|||
} |
|||
RadioButton { |
|||
enabled: false |
|||
ButtonGroup.group: keystoregroup |
|||
property string keystoretype: 'hardware' |
|||
text: qsTr('Use a hardware device') |
|||
} |
|||
} |
|||
} |
|||
|
@ -0,0 +1,94 @@ |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
WizardComponent { |
|||
valid: true |
|||
|
|||
onAccept: { |
|||
var p = {} |
|||
p['enabled'] = proxy_enabled.checked |
|||
if (proxy_enabled.checked) { |
|||
var type = proxytype.currentValue.toLowerCase() |
|||
if (type == 'tor') |
|||
type = 'socks5' |
|||
p['mode'] = type |
|||
p['host'] = address.text |
|||
p['port'] = port.text |
|||
p['user'] = username.text |
|||
p['password'] = password.text |
|||
} |
|||
wizard_data['proxy'] = p |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
Label { |
|||
text: qsTr('Proxy settings') |
|||
} |
|||
|
|||
CheckBox { |
|||
id: proxy_enabled |
|||
text: qsTr('Enable Proxy') |
|||
} |
|||
|
|||
ComboBox { |
|||
id: proxytype |
|||
enabled: proxy_enabled.checked |
|||
model: ['TOR', 'SOCKS5', 'SOCKS4'] |
|||
onCurrentIndexChanged: { |
|||
if (currentIndex == 0) { |
|||
address.text = "127.0.0.1" |
|||
port.text = "9050" |
|||
} |
|||
} |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 4 |
|||
Layout.fillWidth: true |
|||
|
|||
Label { |
|||
text: qsTr("Address") |
|||
enabled: address.enabled |
|||
} |
|||
|
|||
TextField { |
|||
id: address |
|||
enabled: proxytype.enabled && proxytype.currentIndex > 0 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Port") |
|||
enabled: port.enabled |
|||
} |
|||
|
|||
TextField { |
|||
id: port |
|||
enabled: proxytype.enabled && proxytype.currentIndex > 0 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Username") |
|||
enabled: username.enabled |
|||
} |
|||
|
|||
TextField { |
|||
id: username |
|||
enabled: proxytype.enabled && proxytype.currentIndex > 0 |
|||
} |
|||
|
|||
Label { |
|||
text: qsTr("Password") |
|||
enabled: password.enabled |
|||
} |
|||
|
|||
TextField { |
|||
id: password |
|||
enabled: proxytype.enabled && proxytype.currentIndex > 0 |
|||
echoMode: TextInput.Password |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,42 @@ |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
WizardComponent { |
|||
valid: true |
|||
last: true |
|||
|
|||
onAccept: { |
|||
wizard_data['oneserver'] = !auto_server.checked |
|||
wizard_data['server'] = address.text |
|||
} |
|||
|
|||
ColumnLayout { |
|||
width: parent.width |
|||
|
|||
Label { |
|||
text: qsTr('Server settings') |
|||
} |
|||
|
|||
CheckBox { |
|||
id: auto_server |
|||
text: qsTr('Select server automatically') |
|||
checked: true |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 2 |
|||
Layout.fillWidth: true |
|||
|
|||
Label { |
|||
text: qsTr("Server") |
|||
enabled: address.enabled |
|||
} |
|||
|
|||
TextField { |
|||
id: address |
|||
enabled: !auto_server.checked |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,27 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
import org.electrum 1.0 |
|||
|
|||
WizardComponent { |
|||
valid: wallet_name.text.length > 0 |
|||
|
|||
onAccept: { |
|||
wizard_data['wallet_name'] = wallet_name.text |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 1 |
|||
Label { text: qsTr('Wallet name') } |
|||
TextField { |
|||
id: wallet_name |
|||
focus: true |
|||
text: Daemon.suggestWalletName() |
|||
} |
|||
} |
|||
|
|||
Component.onCompleted: { |
|||
wallet_name.selectAll() |
|||
} |
|||
} |
@ -0,0 +1,25 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
WizardComponent { |
|||
valid: password1.text === password2.text |
|||
|
|||
onAccept: { |
|||
wizard_data['password'] = password1.text |
|||
wizard_data['encrypt'] = password1.text != '' |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 1 |
|||
Label { text: qsTr('Password protect wallet?') } |
|||
TextField { |
|||
id: password1 |
|||
echoMode: TextInput.Password |
|||
} |
|||
TextField { |
|||
id: password2 |
|||
echoMode: TextInput.Password |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
WizardComponent { |
|||
valid: wallettypegroup.checkedButton !== null |
|||
|
|||
onAccept: { |
|||
wizard_data['wallet_type'] = wallettypegroup.checkedButton.wallettype |
|||
} |
|||
|
|||
ButtonGroup { |
|||
id: wallettypegroup |
|||
} |
|||
|
|||
GridLayout { |
|||
columns: 1 |
|||
Label { text: qsTr('What kind of wallet do you want to create?') } |
|||
RadioButton { |
|||
ButtonGroup.group: wallettypegroup |
|||
property string wallettype: 'standard' |
|||
checked: true |
|||
text: qsTr('Standard Wallet') |
|||
} |
|||
RadioButton { |
|||
enabled: false |
|||
ButtonGroup.group: wallettypegroup |
|||
property string wallettype: '2fa' |
|||
text: qsTr('Wallet with two-factor authentication') |
|||
} |
|||
RadioButton { |
|||
enabled: false |
|||
ButtonGroup.group: wallettypegroup |
|||
property string wallettype: 'multisig' |
|||
text: qsTr('Multi-signature wallet') |
|||
} |
|||
RadioButton { |
|||
enabled: false |
|||
ButtonGroup.group: wallettypegroup |
|||
property string wallettype: 'import' |
|||
text: qsTr('Import Bitcoin addresses or private keys') |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,175 @@ |
|||
import QtQuick 2.6 |
|||
import QtQuick.Layouts 1.0 |
|||
import QtQuick.Controls 2.1 |
|||
|
|||
Dialog { |
|||
id: wizard |
|||
modal: true |
|||
|
|||
width: parent.width |
|||
height: parent.height |
|||
|
|||
property var wizard_data |
|||
property alias pages : pages |
|||
|
|||
function _setWizardData(wdata) { |
|||
wizard_data = {} |
|||
Object.assign(wizard_data, wdata) // deep copy |
|||
console.log('wizard data is now :' + JSON.stringify(wizard_data)) |
|||
} |
|||
|
|||
// helper function to dynamically load wizard page components |
|||
// and add them to the SwipeView |
|||
// Here we do some manual binding of page.valid -> pages.pagevalid and |
|||
// page.last -> pages.lastpage to propagate the state without the binding |
|||
// going stale. |
|||
function _loadNextComponent(comp, wdata={}) { |
|||
// remove any existing pages after current page |
|||
while (pages.contentChildren[pages.currentIndex+1]) { |
|||
pages.takeItem(pages.currentIndex+1).destroy() |
|||
} |
|||
|
|||
var page = comp.createObject(pages) |
|||
page.validChanged.connect(function() { |
|||
pages.pagevalid = page.valid |
|||
} ) |
|||
page.lastChanged.connect(function() { |
|||
pages.lastpage = page.last |
|||
} ) |
|||
Object.assign(page.wizard_data, wdata) // deep copy |
|||
page.ready = true // signal page it can access wizard_data |
|||
pages.pagevalid = page.valid |
|||
pages.lastpage = page.last |
|||
|
|||
return page |
|||
} |
|||
|
|||
ColumnLayout { |
|||
anchors.fill: parent |
|||
spacing: 0 |
|||
|
|||
SwipeView { |
|||
id: pages |
|||
Layout.fillWidth: true |
|||
Layout.fillHeight: true |
|||
interactive: false |
|||
|
|||
clip:true |
|||
|
|||
function prev() { |
|||
currentIndex = currentIndex - 1 |
|||
_setWizardData(pages.contentChildren[currentIndex].wizard_data) |
|||
pages.pagevalid = pages.contentChildren[currentIndex].valid |
|||
pages.lastpage = pages.contentChildren[currentIndex].last |
|||
} |
|||
|
|||
function next() { |
|||
currentItem.accept() |
|||
_setWizardData(pages.contentChildren[currentIndex].wizard_data) |
|||
currentItem.next() |
|||
currentIndex = currentIndex + 1 |
|||
} |
|||
|
|||
function finish() { |
|||
currentItem.accept() |
|||
_setWizardData(pages.contentChildren[currentIndex].wizard_data) |
|||
wizard.accept() |
|||
} |
|||
|
|||
property bool pagevalid: false |
|||
property bool lastpage: false |
|||
|
|||
Component.onCompleted: { |
|||
_setWizardData({}) |
|||
} |
|||
|
|||
} |
|||
|
|||
ColumnLayout { |
|||
Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom |
|||
|
|||
PageIndicator { |
|||
id: indicator |
|||
|
|||
Layout.alignment: Qt.AlignHCenter |
|||
|
|||
count: pages.count |
|||
currentIndex: pages.currentIndex |
|||
} |
|||
|
|||
RowLayout { |
|||
Layout.alignment: Qt.AlignHCenter |
|||
Button { |
|||
visible: pages.currentIndex == 0 |
|||
text: qsTr("Cancel") |
|||
onClicked: wizard.reject() |
|||
} |
|||
|
|||
Button { |
|||
visible: pages.currentIndex > 0 |
|||
text: qsTr('Back') |
|||
onClicked: pages.prev() |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr("Next") |
|||
visible: !pages.lastpage |
|||
enabled: pages.pagevalid |
|||
onClicked: pages.next() |
|||
} |
|||
|
|||
Button { |
|||
text: qsTr("Finish") |
|||
visible: pages.lastpage |
|||
enabled: pages.pagevalid |
|||
onClicked: pages.finish() |
|||
} |
|||
|
|||
} |
|||
} |
|||
} |
|||
|
|||
header: GridLayout { |
|||
columns: 2 |
|||
rowSpacing: 0 |
|||
|
|||
Image { |
|||
source: "../../../icons/electrum.png" |
|||
Layout.preferredWidth: constants.iconSizeXLarge |
|||
Layout.preferredHeight: constants.iconSizeXLarge |
|||
Layout.leftMargin: constants.paddingMedium |
|||
Layout.topMargin: constants.paddingMedium |
|||
Layout.bottomMargin: constants.paddingMedium |
|||
} |
|||
|
|||
Label { |
|||
text: title |
|||
elide: Label.ElideRight |
|||
Layout.fillWidth: true |
|||
topPadding: constants.paddingXLarge |
|||
bottomPadding: constants.paddingXLarge |
|||
font.bold: true |
|||
font.pixelSize: constants.fontSizeMedium |
|||
} |
|||
|
|||
Rectangle { |
|||
Layout.columnSpan: 2 |
|||
Layout.fillWidth: true |
|||
Layout.leftMargin: constants.paddingTiny |
|||
Layout.rightMargin: constants.paddingTiny |
|||
height: 1 |
|||
color: Qt.rgba(0,0,0,0.5) |
|||
} |
|||
} |
|||
|
|||
// make clicking the dialog background move the scope away from textedit fields |
|||
// so the keyboard goes away |
|||
// TODO: here it works on desktop, but not android. hmm. |
|||
MouseArea { |
|||
anchors.fill: parent |
|||
z: -1000 |
|||
onClicked: { parkFocus.focus = true } |
|||
FocusScope { id: parkFocus } |
|||
} |
|||
|
|||
} |
@ -0,0 +1,10 @@ |
|||
import QtQuick 2.0 |
|||
|
|||
Item { |
|||
signal next |
|||
signal accept |
|||
property var wizard_data : ({}) |
|||
property bool valid |
|||
property bool last: false |
|||
property bool ready: false |
|||
} |
@ -0,0 +1,94 @@ |
|||
Copyright (c) 2011, ParaType Ltd. (http://www.paratype.com/public), |
|||
with Reserved Font Names "PT Sans", "PT Serif", "PT Mono" and "ParaType". |
|||
|
|||
This Font Software is licensed under the SIL Open Font License, Version 1.1. |
|||
This license is copied below, and is also available with a FAQ at: |
|||
http://scripts.sil.org/OFL |
|||
|
|||
|
|||
----------------------------------------------------------- |
|||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 |
|||
----------------------------------------------------------- |
|||
|
|||
PREAMBLE |
|||
The goals of the Open Font License (OFL) are to stimulate worldwide |
|||
development of collaborative font projects, to support the font creation |
|||
efforts of academic and linguistic communities, and to provide a free and |
|||
open framework in which fonts may be shared and improved in partnership |
|||
with others. |
|||
|
|||
The OFL allows the licensed fonts to be used, studied, modified and |
|||
redistributed freely as long as they are not sold by themselves. The |
|||
fonts, including any derivative works, can be bundled, embedded, |
|||
redistributed and/or sold with any software provided that any reserved |
|||
names are not used by derivative works. The fonts and derivatives, |
|||
however, cannot be released under any other type of license. The |
|||
requirement for fonts to remain under this license does not apply |
|||
to any document created using the fonts or their derivatives. |
|||
|
|||
DEFINITIONS |
|||
"Font Software" refers to the set of files released by the Copyright |
|||
Holder(s) under this license and clearly marked as such. This may |
|||
include source files, build scripts and documentation. |
|||
|
|||
"Reserved Font Name" refers to any names specified as such after the |
|||
copyright statement(s). |
|||
|
|||
"Original Version" refers to the collection of Font Software components as |
|||
distributed by the Copyright Holder(s). |
|||
|
|||
"Modified Version" refers to any derivative made by adding to, deleting, |
|||
or substituting -- in part or in whole -- any of the components of the |
|||
Original Version, by changing formats or by porting the Font Software to a |
|||
new environment. |
|||
|
|||
"Author" refers to any designer, engineer, programmer, technical |
|||
writer or other person who contributed to the Font Software. |
|||
|
|||
PERMISSION & CONDITIONS |
|||
Permission is hereby granted, free of charge, to any person obtaining |
|||
a copy of the Font Software, to use, study, copy, merge, embed, modify, |
|||
redistribute, and sell modified and unmodified copies of the Font |
|||
Software, subject to the following conditions: |
|||
|
|||
1) Neither the Font Software nor any of its individual components, |
|||
in Original or Modified Versions, may be sold by itself. |
|||
|
|||
2) Original or Modified Versions of the Font Software may be bundled, |
|||
redistributed and/or sold with any software, provided that each copy |
|||
contains the above copyright notice and this license. These can be |
|||
included either as stand-alone text files, human-readable headers or |
|||
in the appropriate machine-readable metadata fields within text or |
|||
binary files as long as those fields can be easily viewed by the user. |
|||
|
|||
3) No Modified Version of the Font Software may use the Reserved Font |
|||
Name(s) unless explicit written permission is granted by the corresponding |
|||
Copyright Holder. This restriction only applies to the primary font name as |
|||
presented to the users. |
|||
|
|||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font |
|||
Software shall not be used to promote, endorse or advertise any |
|||
Modified Version, except to acknowledge the contribution(s) of the |
|||
Copyright Holder(s) and the Author(s) or with their explicit written |
|||
permission. |
|||
|
|||
5) The Font Software, modified or unmodified, in part or in whole, |
|||
must be distributed entirely under this license, and must not be |
|||
distributed under any other license. The requirement for fonts to |
|||
remain under this license does not apply to any document created |
|||
using the Font Software. |
|||
|
|||
TERMINATION |
|||
This license becomes null and void if any of the above conditions are |
|||
not met. |
|||
|
|||
DISCLAIMER |
|||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF |
|||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT |
|||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE |
|||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
|||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL |
|||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM |
|||
OTHER DEALINGS IN THE FONT SOFTWARE. |
@ -0,0 +1,130 @@ |
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from decimal import Decimal |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import DECIMAL_POINT_DEFAULT |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qetypes import QEAmount |
|||
from .qetransactionlistmodel import QETransactionListModel |
|||
|
|||
class QEAddressDetails(QObject): |
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
_wallet = None |
|||
_address = None |
|||
|
|||
_label = None |
|||
_frozen = False |
|||
_scriptType = None |
|||
_status = None |
|||
_balance = QEAmount() |
|||
_pubkeys = None |
|||
_privkey = None |
|||
_derivationPath = None |
|||
_numtx = 0 |
|||
|
|||
_historyModel = None |
|||
|
|||
detailsChanged = pyqtSignal() |
|||
|
|||
walletChanged = pyqtSignal() |
|||
@pyqtProperty(QEWallet, notify=walletChanged) |
|||
def wallet(self): |
|||
return self._wallet |
|||
|
|||
@wallet.setter |
|||
def wallet(self, wallet: QEWallet): |
|||
if self._wallet != wallet: |
|||
self._wallet = wallet |
|||
self.walletChanged.emit() |
|||
|
|||
addressChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=addressChanged) |
|||
def address(self): |
|||
return self._address |
|||
|
|||
@address.setter |
|||
def address(self, address: str): |
|||
if self._address != address: |
|||
self._logger.debug('address changed') |
|||
self._address = address |
|||
self.addressChanged.emit() |
|||
self.update() |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def scriptType(self): |
|||
return self._scriptType |
|||
|
|||
@pyqtProperty(QEAmount, notify=detailsChanged) |
|||
def balance(self): |
|||
return self._balance |
|||
|
|||
@pyqtProperty('QStringList', notify=detailsChanged) |
|||
def pubkeys(self): |
|||
return self._pubkeys |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def derivationPath(self): |
|||
return self._derivationPath |
|||
|
|||
@pyqtProperty(int, notify=detailsChanged) |
|||
def numTx(self): |
|||
return self._numtx |
|||
|
|||
|
|||
frozenChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=frozenChanged) |
|||
def isFrozen(self): |
|||
return self._frozen |
|||
|
|||
labelChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=labelChanged) |
|||
def label(self): |
|||
return self._label |
|||
|
|||
@pyqtSlot(bool) |
|||
def freeze(self, freeze: bool): |
|||
if freeze != self._frozen: |
|||
self._wallet.wallet.set_frozen_state_of_addresses([self._address], freeze=freeze) |
|||
self._frozen = freeze |
|||
self.frozenChanged.emit() |
|||
self._wallet.balanceChanged.emit() |
|||
|
|||
@pyqtSlot(str) |
|||
def set_label(self, label: str): |
|||
if label != self._label: |
|||
self._wallet.wallet.set_label(self._address, label) |
|||
self._label = label |
|||
self.labelChanged.emit() |
|||
|
|||
historyModelChanged = pyqtSignal() |
|||
@pyqtProperty(QETransactionListModel, notify=historyModelChanged) |
|||
def historyModel(self): |
|||
if self._historyModel is None: |
|||
self._historyModel = QETransactionListModel(self._wallet.wallet, |
|||
onchain_domain=[self._address], include_lightning=False) |
|||
return self._historyModel |
|||
|
|||
def update(self): |
|||
if self._wallet is None: |
|||
self._logger.error('wallet undefined') |
|||
return |
|||
|
|||
self._frozen = self._wallet.wallet.is_frozen_address(self._address) |
|||
self.frozenChanged.emit() |
|||
|
|||
self._scriptType = self._wallet.wallet.get_txin_type(self._address) |
|||
self._label = self._wallet.wallet.get_label(self._address) |
|||
c, u, x = self._wallet.wallet.get_addr_balance(self._address) |
|||
self._balance = QEAmount(amount_sat=c + u + x) |
|||
self._pubkeys = self._wallet.wallet.get_public_keys(self._address) |
|||
self._derivationPath = self._wallet.wallet.get_address_path_str(self._address) |
|||
self._derivationPath = self._derivationPath.replace('m', self._wallet.derivationPrefix) |
|||
self._numtx = self._wallet.wallet.adb.get_address_history_len(self._address) |
|||
assert(self._numtx == self.historyModel.rowCount(0)) |
|||
self.detailsChanged.emit() |
@ -0,0 +1,101 @@ |
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import Satoshis |
|||
|
|||
from .qetypes import QEAmount |
|||
|
|||
class QEAddressListModel(QAbstractListModel): |
|||
def __init__(self, wallet, parent=None): |
|||
super().__init__(parent) |
|||
self.wallet = wallet |
|||
self.init_model() |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
# define listmodel rolemap |
|||
_ROLE_NAMES=('type','iaddr','address','label','balance','numtx', 'held') |
|||
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) |
|||
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) |
|||
|
|||
def rowCount(self, index): |
|||
return len(self.receive_addresses) + len(self.change_addresses) |
|||
|
|||
def roleNames(self): |
|||
return self._ROLE_MAP |
|||
|
|||
def data(self, index, role): |
|||
if index.row() > len(self.receive_addresses) - 1: |
|||
address = self.change_addresses[index.row() - len(self.receive_addresses)] |
|||
else: |
|||
address = self.receive_addresses[index.row()] |
|||
role_index = role - Qt.UserRole |
|||
value = address[self._ROLE_NAMES[role_index]] |
|||
if isinstance(value, (bool, list, int, str, QEAmount)) or value is None: |
|||
return value |
|||
if isinstance(value, Satoshis): |
|||
return value.value |
|||
return str(value) |
|||
|
|||
def clear(self): |
|||
self.beginResetModel() |
|||
self.receive_addresses = [] |
|||
self.change_addresses = [] |
|||
self.endResetModel() |
|||
|
|||
def addr_to_model(self, address): |
|||
item = {} |
|||
item['address'] = address |
|||
item['numtx'] = self.wallet.adb.get_address_history_len(address) |
|||
item['label'] = self.wallet.get_label(address) |
|||
c, u, x = self.wallet.get_addr_balance(address) |
|||
item['balance'] = QEAmount(amount_sat=c + u + x) |
|||
item['held'] = self.wallet.is_frozen_address(address) |
|||
return item |
|||
|
|||
# initial model data |
|||
@pyqtSlot() |
|||
def init_model(self): |
|||
r_addresses = self.wallet.get_receiving_addresses() |
|||
c_addresses = self.wallet.get_change_addresses() |
|||
n_addresses = len(r_addresses) + len(c_addresses) |
|||
|
|||
def insert_row(atype, alist, address, iaddr): |
|||
item = self.addr_to_model(address) |
|||
item['type'] = atype |
|||
item['iaddr'] = iaddr |
|||
alist.append(item) |
|||
|
|||
self.clear() |
|||
self.beginInsertRows(QModelIndex(), 0, n_addresses - 1) |
|||
i = 0 |
|||
for address in r_addresses: |
|||
insert_row('receive', self.receive_addresses, address, i) |
|||
i = i + 1 |
|||
i = 0 |
|||
for address in c_addresses: |
|||
insert_row('change', self.change_addresses, address, i) |
|||
i = i + 1 |
|||
self.endInsertRows() |
|||
|
|||
@pyqtSlot(str) |
|||
def update_address(self, address): |
|||
i = 0 |
|||
for a in self.receive_addresses: |
|||
if a['address'] == address: |
|||
self.do_update(i,a) |
|||
return |
|||
i = i + 1 |
|||
for a in self.change_addresses: |
|||
if a['address'] == address: |
|||
self.do_update(i,a) |
|||
return |
|||
i = i + 1 |
|||
|
|||
def do_update(self, modelindex, modelitem): |
|||
mi = self.createIndex(modelindex, 0) |
|||
self._logger.debug(repr(modelitem)) |
|||
modelitem.update(self.addr_to_model(modelitem['address'])) |
|||
self._logger.debug(repr(modelitem)) |
|||
self.dataChanged.emit(mi, mi, self._ROLE_KEYS) |
@ -0,0 +1,211 @@ |
|||
import re |
|||
import queue |
|||
import time |
|||
import os |
|||
|
|||
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject, QUrl, QLocale, qInstallMessageHandler, QTimer |
|||
from PyQt5.QtGui import QGuiApplication, QFontDatabase |
|||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType, QQmlApplicationEngine |
|||
|
|||
from electrum.logging import Logger, get_logger |
|||
from electrum import version |
|||
|
|||
from .qeconfig import QEConfig |
|||
from .qedaemon import QEDaemon, QEWalletListModel |
|||
from .qenetwork import QENetwork |
|||
from .qewallet import QEWallet |
|||
from .qeqr import QEQRParser, QEQRImageProvider |
|||
from .qewalletdb import QEWalletDB |
|||
from .qebitcoin import QEBitcoin |
|||
from .qefx import QEFX |
|||
from .qetxfinalizer import QETxFinalizer |
|||
from .qeinvoice import QEInvoice, QEInvoiceParser, QEUserEnteredPayment |
|||
from .qetypes import QEAmount |
|||
from .qeaddressdetails import QEAddressDetails |
|||
from .qetxdetails import QETxDetails |
|||
from .qechannelopener import QEChannelOpener |
|||
from .qelnpaymentdetails import QELnPaymentDetails |
|||
from .qechanneldetails import QEChannelDetails |
|||
from .qeswaphelper import QESwapHelper |
|||
|
|||
notification = None |
|||
|
|||
class QEAppController(QObject): |
|||
userNotify = pyqtSignal(str) |
|||
|
|||
def __init__(self, qedaemon): |
|||
super().__init__() |
|||
self.logger = get_logger(__name__) |
|||
|
|||
self._qedaemon = qedaemon |
|||
|
|||
# set up notification queue and notification_timer |
|||
self.user_notification_queue = queue.Queue() |
|||
self.user_notification_last_time = 0 |
|||
|
|||
self.notification_timer = QTimer(self) |
|||
self.notification_timer.setSingleShot(False) |
|||
self.notification_timer.setInterval(500) # msec |
|||
self.notification_timer.timeout.connect(self.on_notification_timer) |
|||
|
|||
self._qedaemon.walletLoaded.connect(self.on_wallet_loaded) |
|||
|
|||
self.userNotify.connect(self.notifyAndroid) |
|||
|
|||
def on_wallet_loaded(self): |
|||
qewallet = self._qedaemon.currentWallet |
|||
if not qewallet: |
|||
return |
|||
# attach to the wallet user notification events |
|||
# connect only once |
|||
try: |
|||
qewallet.userNotify.disconnect(self.on_wallet_usernotify) |
|||
except: |
|||
pass |
|||
qewallet.userNotify.connect(self.on_wallet_usernotify) |
|||
|
|||
def on_wallet_usernotify(self, wallet, message): |
|||
self.logger.debug(message) |
|||
self.user_notification_queue.put(message) |
|||
if not self.notification_timer.isActive(): |
|||
self.logger.debug('starting app notification timer') |
|||
self.notification_timer.start() |
|||
|
|||
def on_notification_timer(self): |
|||
if self.user_notification_queue.qsize() == 0: |
|||
self.logger.debug('queue empty, stopping app notification timer') |
|||
self.notification_timer.stop() |
|||
return |
|||
now = time.time() |
|||
rate_limit = 20 # seconds |
|||
if self.user_notification_last_time + rate_limit > now: |
|||
return |
|||
self.user_notification_last_time = now |
|||
self.logger.info("Notifying GUI about new user notifications") |
|||
try: |
|||
self.userNotify.emit(self.user_notification_queue.get_nowait()) |
|||
except queue.Empty: |
|||
pass |
|||
|
|||
def notifyAndroid(self, message): |
|||
try: |
|||
# TODO: lazy load not in UI thread please |
|||
global notification |
|||
if not notification: |
|||
from plyer import notification |
|||
icon = (os.path.dirname(os.path.realpath(__file__)) |
|||
+ '/../icons/electrum.png') |
|||
notification.notify('Electrum', message, app_icon=icon, app_name='Electrum') |
|||
except ImportError: |
|||
self.logger.error('Notification: needs plyer; `sudo python3 -m pip install plyer`') |
|||
|
|||
@pyqtSlot(str, str) |
|||
def doShare(self, data, title): |
|||
#if platform != 'android': |
|||
#return |
|||
try: |
|||
from jnius import autoclass, cast |
|||
except ImportError: |
|||
self.logger.error('Share: needs jnius. Platform not Android?') |
|||
return |
|||
|
|||
JS = autoclass('java.lang.String') |
|||
Intent = autoclass('android.content.Intent') |
|||
sendIntent = Intent() |
|||
sendIntent.setAction(Intent.ACTION_SEND) |
|||
sendIntent.setType("text/plain") |
|||
sendIntent.putExtra(Intent.EXTRA_TEXT, JS(data)) |
|||
pythonActivity = autoclass('org.kivy.android.PythonActivity') |
|||
currentActivity = cast('android.app.Activity', pythonActivity.mActivity) |
|||
it = Intent.createChooser(sendIntent, cast('java.lang.CharSequence', JS(title))) |
|||
currentActivity.startActivity(it) |
|||
|
|||
@pyqtSlot('QString') |
|||
def textToClipboard(self, text): |
|||
QGuiApplication.clipboard().setText(text) |
|||
|
|||
@pyqtSlot(result='QString') |
|||
def clipboardToText(self): |
|||
return QGuiApplication.clipboard().text() |
|||
|
|||
class ElectrumQmlApplication(QGuiApplication): |
|||
|
|||
_valid = True |
|||
|
|||
def __init__(self, args, config, daemon): |
|||
super().__init__(args) |
|||
|
|||
self.logger = get_logger(__name__) |
|||
|
|||
ElectrumQmlApplication._daemon = daemon |
|||
|
|||
qmlRegisterType(QEWalletListModel, 'org.electrum', 1, 0, 'WalletListModel') |
|||
qmlRegisterType(QEWallet, 'org.electrum', 1, 0, 'Wallet') |
|||
qmlRegisterType(QEWalletDB, 'org.electrum', 1, 0, 'WalletDB') |
|||
qmlRegisterType(QEBitcoin, 'org.electrum', 1, 0, 'Bitcoin') |
|||
qmlRegisterType(QEQRParser, 'org.electrum', 1, 0, 'QRParser') |
|||
qmlRegisterType(QEFX, 'org.electrum', 1, 0, 'FX') |
|||
qmlRegisterType(QETxFinalizer, 'org.electrum', 1, 0, 'TxFinalizer') |
|||
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice') |
|||
qmlRegisterType(QEInvoiceParser, 'org.electrum', 1, 0, 'InvoiceParser') |
|||
qmlRegisterType(QEUserEnteredPayment, 'org.electrum', 1, 0, 'UserEnteredPayment') |
|||
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails') |
|||
qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') |
|||
qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener') |
|||
qmlRegisterType(QELnPaymentDetails, 'org.electrum', 1, 0, 'LnPaymentDetails') |
|||
qmlRegisterType(QEChannelDetails, 'org.electrum', 1, 0, 'ChannelDetails') |
|||
qmlRegisterType(QESwapHelper, 'org.electrum', 1, 0, 'SwapHelper') |
|||
|
|||
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') |
|||
|
|||
self.engine = QQmlApplicationEngine(parent=self) |
|||
self.engine.addImportPath('./qml') |
|||
|
|||
screensize = self.primaryScreen().size() |
|||
|
|||
self.qr_ip = QEQRImageProvider((7/8)*min(screensize.width(), screensize.height())) |
|||
self.engine.addImageProvider('qrgen', self.qr_ip) |
|||
|
|||
# add a monospace font as we can't rely on device having one |
|||
self.fixedFont = 'PT Mono' |
|||
not_loaded = QFontDatabase.addApplicationFont('electrum/gui/qml/fonts/PTMono-Regular.ttf') < 0 |
|||
not_loaded = QFontDatabase.addApplicationFont('electrum/gui/qml/fonts/PTMono-Bold.ttf') < 0 and not_loaded |
|||
if not_loaded: |
|||
self.logger.warning('Could not load font PT Mono') |
|||
self.fixedFont = 'Monospace' # hope for the best |
|||
|
|||
self.context = self.engine.rootContext() |
|||
self._qeconfig = QEConfig(config) |
|||
self._qenetwork = QENetwork(daemon.network) |
|||
self._qedaemon = QEDaemon(daemon) |
|||
self._appController = QEAppController(self._qedaemon) |
|||
self._maxAmount = QEAmount(is_max=True) |
|||
self.context.setContextProperty('AppController', self._appController) |
|||
self.context.setContextProperty('Config', self._qeconfig) |
|||
self.context.setContextProperty('Network', self._qenetwork) |
|||
self.context.setContextProperty('Daemon', self._qedaemon) |
|||
self.context.setContextProperty('FixedFont', self.fixedFont) |
|||
self.context.setContextProperty('MAX', self._maxAmount) |
|||
self.context.setContextProperty('BUILD', { |
|||
'electrum_version': version.ELECTRUM_VERSION, |
|||
'apk_version': version.APK_VERSION, |
|||
'protocol_version': version.PROTOCOL_VERSION |
|||
}) |
|||
|
|||
qInstallMessageHandler(self.message_handler) |
|||
|
|||
# get notified whether root QML document loads or not |
|||
self.engine.objectCreated.connect(self.objectCreated) |
|||
|
|||
# slot is called after loading root QML. If object is None, it has failed. |
|||
@pyqtSlot('QObject*', 'QUrl') |
|||
def objectCreated(self, object, url): |
|||
if object is None: |
|||
self._valid = False |
|||
self.engine.objectCreated.disconnect(self.objectCreated) |
|||
|
|||
def message_handler(self, line, funct, file): |
|||
# filter out common harmless messages |
|||
if re.search('file:///.*TypeError: Cannot read property.*null$', file): |
|||
return |
|||
self.logger.warning(file) |
@ -0,0 +1,132 @@ |
|||
import asyncio |
|||
from datetime import datetime |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.keystore import bip39_is_checksum_valid |
|||
from electrum.bip32 import is_bip32_derivation |
|||
from electrum.slip39 import decode_mnemonic, Slip39Error |
|||
from electrum import mnemonic |
|||
from electrum.util import parse_URI, create_bip21_uri, InvalidBitcoinURI, get_asyncio_loop |
|||
|
|||
from .qetypes import QEAmount |
|||
|
|||
class QEBitcoin(QObject): |
|||
def __init__(self, config, parent=None): |
|||
super().__init__(parent) |
|||
self.config = config |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
generatedSeedChanged = pyqtSignal() |
|||
generatedSeed = '' |
|||
|
|||
seedValidChanged = pyqtSignal() |
|||
seedValid = False |
|||
|
|||
seedTypeChanged = pyqtSignal() |
|||
seedType = '' |
|||
|
|||
validationMessageChanged = pyqtSignal() |
|||
validationMessage = '' |
|||
|
|||
@pyqtProperty('QString', notify=generatedSeedChanged) |
|||
def generated_seed(self): |
|||
return self.generatedSeed |
|||
|
|||
@pyqtProperty(bool, notify=seedValidChanged) |
|||
def seed_valid(self): |
|||
return self.seedValid |
|||
|
|||
@pyqtProperty('QString', notify=seedTypeChanged) |
|||
def seed_type(self): |
|||
return self.seedType |
|||
|
|||
@pyqtProperty('QString', notify=validationMessageChanged) |
|||
def validation_message(self): |
|||
return self.validationMessage |
|||
|
|||
@pyqtSlot() |
|||
@pyqtSlot(str) |
|||
@pyqtSlot(str,str) |
|||
def generate_seed(self, seed_type='segwit', language='en'): |
|||
self._logger.debug('generating seed of type ' + str(seed_type)) |
|||
|
|||
async def co_gen_seed(seed_type, language): |
|||
self.generatedSeed = mnemonic.Mnemonic(language).make_seed(seed_type=seed_type) |
|||
self._logger.debug('seed generated') |
|||
self.generatedSeedChanged.emit() |
|||
|
|||
asyncio.run_coroutine_threadsafe(co_gen_seed(seed_type, language), get_asyncio_loop()) |
|||
|
|||
@pyqtSlot(str) |
|||
@pyqtSlot(str,bool,bool) |
|||
@pyqtSlot(str,bool,bool,str,str,str) |
|||
def verify_seed(self, seed, bip39=False, slip39=False, wallet_type='standard', language='en'): |
|||
self._logger.debug('bip39 ' + str(bip39)) |
|||
self._logger.debug('slip39 ' + str(slip39)) |
|||
|
|||
seed_type = '' |
|||
seed_valid = False |
|||
validation_message = '' |
|||
|
|||
if not (bip39 or slip39): |
|||
seed_type = mnemonic.seed_type(seed) |
|||
if seed_type != '': |
|||
seed_valid = True |
|||
elif bip39: |
|||
is_checksum, is_wordlist = bip39_is_checksum_valid(seed) |
|||
status = ('checksum: ' + ('ok' if is_checksum else 'failed')) if is_wordlist else 'unknown wordlist' |
|||
validation_message = 'BIP39 (%s)' % status |
|||
|
|||
if is_checksum: |
|||
seed_type = 'bip39' |
|||
seed_valid = True |
|||
|
|||
elif slip39: # TODO: incomplete impl, this code only validates a single share. |
|||
try: |
|||
share = decode_mnemonic(seed) |
|||
seed_type = 'slip39' |
|||
validation_message = 'SLIP39: share #%d in %dof%d scheme' % (share.group_index, share.group_threshold, share.group_count) |
|||
except Slip39Error as e: |
|||
validation_message = 'SLIP39: %s' % str(e) |
|||
seed_valid = False # for now |
|||
|
|||
# cosigning seed |
|||
if wallet_type != 'standard' and seed_type not in ['standard', 'segwit']: |
|||
seed_type = '' |
|||
seed_valid = False |
|||
|
|||
self.seedType = seed_type |
|||
self.seedTypeChanged.emit() |
|||
|
|||
if self.validationMessage != validation_message: |
|||
self.validationMessage = validation_message |
|||
self.validationMessageChanged.emit() |
|||
|
|||
if self.seedValid != seed_valid: |
|||
self.seedValid = seed_valid |
|||
self.seedValidChanged.emit() |
|||
|
|||
self._logger.debug('seed verified: ' + str(seed_valid)) |
|||
|
|||
@pyqtSlot(str, result=bool) |
|||
def verify_derivation_path(self, path): |
|||
return is_bip32_derivation(path) |
|||
|
|||
@pyqtSlot(str, result='QVariantMap') |
|||
def parse_uri(self, uri: str) -> dict: |
|||
try: |
|||
return parse_URI(uri) |
|||
except InvalidBitcoinURI as e: |
|||
return { 'error': str(e) } |
|||
|
|||
@pyqtSlot(str, QEAmount, str, int, int, result=str) |
|||
def create_bip21_uri(self, address, satoshis, message, timestamp, expiry): |
|||
extra_params = {} |
|||
if expiry: |
|||
extra_params['time'] = str(timestamp) |
|||
extra_params['exp'] = str(expiry) |
|||
|
|||
return create_bip21_uri(address, satoshis.satsInt, message, extra_query_params=extra_params) |
@ -0,0 +1,182 @@ |
|||
import asyncio |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS |
|||
|
|||
from electrum.i18n import _ |
|||
from electrum.gui import messages |
|||
from electrum.logging import get_logger |
|||
from electrum.lnutil import LOCAL, REMOTE |
|||
from electrum.lnchannel import ChanCloseOption |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qetypes import QEAmount |
|||
from .util import QtEventListener, qt_event_listener |
|||
|
|||
class QEChannelDetails(QObject, QtEventListener): |
|||
|
|||
_logger = get_logger(__name__) |
|||
_wallet = None |
|||
_channelid = None |
|||
_channel = None |
|||
|
|||
channelChanged = pyqtSignal() |
|||
channelCloseSuccess = pyqtSignal() |
|||
channelCloseFailed = pyqtSignal([str], arguments=['message']) |
|||
|
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
self.register_callbacks() |
|||
self.destroyed.connect(lambda: self.on_destroy()) |
|||
|
|||
@qt_event_listener |
|||
def on_event_channel(self, wallet, channel): |
|||
if wallet == self._wallet.wallet and self._channelid == channel.channel_id.hex(): |
|||
self.channelChanged.emit() |
|||
|
|||
def on_destroy(self): |
|||
self.unregister_callbacks() |
|||
|
|||
walletChanged = pyqtSignal() |
|||
@pyqtProperty(QEWallet, notify=walletChanged) |
|||
def wallet(self): |
|||
return self._wallet |
|||
|
|||
@wallet.setter |
|||
def wallet(self, wallet: QEWallet): |
|||
if self._wallet != wallet: |
|||
self._wallet = wallet |
|||
self.walletChanged.emit() |
|||
|
|||
channelidChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=channelidChanged) |
|||
def channelid(self): |
|||
return self._channelid |
|||
|
|||
@channelid.setter |
|||
def channelid(self, channelid: str): |
|||
if self._channelid != channelid: |
|||
self._channelid = channelid |
|||
if channelid: |
|||
self.load() |
|||
self.channelidChanged.emit() |
|||
|
|||
def load(self): |
|||
lnchannels = self._wallet.wallet.lnworker.channels |
|||
for channel in lnchannels.values(): |
|||
#self._logger.debug('%s == %s ?' % (self._channelid, channel.channel_id)) |
|||
if self._channelid == channel.channel_id.hex(): |
|||
self._channel = channel |
|||
self.channelChanged.emit() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def name(self): |
|||
if not self._channel: |
|||
return |
|||
return self._wallet.wallet.lnworker.get_node_alias(self._channel.node_id) or self._channel.node_id.hex() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def pubkey(self): |
|||
return self._channel.node_id.hex() #if self._channel else '' |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def short_cid(self): |
|||
return self._channel.short_id_for_GUI() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def state(self): |
|||
return self._channel.get_state_for_GUI() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def initiator(self): |
|||
return 'Local' if self._channel.constraints.is_initiator else 'Remote' |
|||
|
|||
@pyqtProperty(QEAmount, notify=channelChanged) |
|||
def capacity(self): |
|||
self._capacity = QEAmount(amount_sat=self._channel.get_capacity()) |
|||
return self._capacity |
|||
|
|||
@pyqtProperty(QEAmount, notify=channelChanged) |
|||
def canSend(self): |
|||
self._can_send = QEAmount(amount_sat=self._channel.available_to_spend(LOCAL)/1000) |
|||
return self._can_send |
|||
|
|||
@pyqtProperty(QEAmount, notify=channelChanged) |
|||
def canReceive(self): |
|||
self._can_receive = QEAmount(amount_sat=self._channel.available_to_spend(REMOTE)/1000) |
|||
return self._can_receive |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def frozenForSending(self): |
|||
return self._channel.is_frozen_for_sending() |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def frozenForReceiving(self): |
|||
return self._channel.is_frozen_for_receiving() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def channelType(self): |
|||
return self._channel.storage['channel_type'].name_minimal |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def isOpen(self): |
|||
return self._channel.is_open() |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def canClose(self): |
|||
return self.canCoopClose or self.canForceClose |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def canCoopClose(self): |
|||
return ChanCloseOption.COOP_CLOSE in self._channel.get_close_options() |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def canForceClose(self): |
|||
return ChanCloseOption.LOCAL_FCLOSE in self._channel.get_close_options() |
|||
|
|||
@pyqtProperty(bool, notify=channelChanged) |
|||
def canDelete(self): |
|||
return self._channel.can_be_deleted() |
|||
|
|||
@pyqtProperty(str, notify=channelChanged) |
|||
def message_force_close(self, notify=channelChanged): |
|||
return _(messages.MSG_REQUEST_FORCE_CLOSE) |
|||
|
|||
@pyqtSlot() |
|||
def freezeForSending(self): |
|||
lnworker = self._channel.lnworker |
|||
if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id): |
|||
self._channel.set_frozen_for_sending(not self.frozenForSending) |
|||
self.channelChanged.emit() |
|||
else: |
|||
self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) |
|||
|
|||
@pyqtSlot() |
|||
def freezeForReceiving(self): |
|||
lnworker = self._channel.lnworker |
|||
if lnworker.channel_db or lnworker.is_trampoline_peer(self._channel.node_id): |
|||
self._channel.set_frozen_for_receiving(not self.frozenForReceiving) |
|||
self.channelChanged.emit() |
|||
else: |
|||
self._logger.debug(messages.MSG_NON_TRAMPOLINE_CHANNEL_FROZEN_WITHOUT_GOSSIP) |
|||
|
|||
# this method assumes the qobject is not destroyed before the close either fails or succeeds |
|||
@pyqtSlot(str) |
|||
def close_channel(self, closetype): |
|||
async def do_close(closetype, channel_id): |
|||
try: |
|||
if closetype == 'force': |
|||
await self._wallet.wallet.lnworker.request_force_close(channel_id) |
|||
else: |
|||
await self._wallet.wallet.lnworker.close_channel(channel_id) |
|||
self.channelCloseSuccess.emit() |
|||
except Exception as e: |
|||
self._logger.exception("Could not close channel: " + repr(e)) |
|||
self.channelCloseFailed.emit(_('Could not close channel: ') + repr(e)) |
|||
|
|||
loop = self._wallet.wallet.network.asyncio_loop |
|||
coro = do_close(closetype, self._channel.channel_id) |
|||
asyncio.run_coroutine_threadsafe(coro, loop) |
|||
|
|||
@pyqtSlot() |
|||
def deleteChannel(self): |
|||
self._wallet.wallet.lnworker.remove_channel(self._channel.channel_id) |
@ -0,0 +1,154 @@ |
|||
from datetime import datetime, timedelta |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import Satoshis |
|||
from electrum.lnutil import LOCAL, REMOTE |
|||
from electrum.lnchannel import ChannelState |
|||
|
|||
from .qetypes import QEAmount |
|||
from .util import QtEventListener, qt_event_listener |
|||
|
|||
class QEChannelListModel(QAbstractListModel, QtEventListener): |
|||
_logger = get_logger(__name__) |
|||
|
|||
# define listmodel rolemap |
|||
_ROLE_NAMES=('cid','state','initiator','capacity','can_send','can_receive', |
|||
'l_csv_delay','r_csv_delay','send_frozen','receive_frozen', |
|||
'type','node_id','node_alias','short_cid','funding_tx') |
|||
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) |
|||
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) |
|||
_ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) |
|||
|
|||
_network_signal = pyqtSignal(str, object) |
|||
|
|||
def __init__(self, wallet, parent=None): |
|||
super().__init__(parent) |
|||
self.wallet = wallet |
|||
self.init_model() |
|||
|
|||
# To avoid leaking references to "self" that prevent the |
|||
# window from being GC-ed when closed, callbacks should be |
|||
# methods of this class only, and specifically not be |
|||
# partials, lambdas or methods of subobjects. Hence... |
|||
self.register_callbacks() |
|||
self.destroyed.connect(lambda: self.on_destroy()) |
|||
|
|||
@qt_event_listener |
|||
def on_event_channel(self, wallet, channel): |
|||
if wallet == self.wallet: |
|||
self.on_channel_updated(channel) |
|||
|
|||
# elif event == 'channels_updated': |
|||
@qt_event_listener |
|||
def on_event_channels_updated(self, wallet): |
|||
if wallet == self.wallet: |
|||
self.init_model() # TODO: remove/add less crude than full re-init |
|||
|
|||
def on_destroy(self): |
|||
self.unregister_callbacks() |
|||
|
|||
def rowCount(self, index): |
|||
return len(self.channels) |
|||
|
|||
def roleNames(self): |
|||
return self._ROLE_MAP |
|||
|
|||
def data(self, index, role): |
|||
tx = self.channels[index.row()] |
|||
role_index = role - Qt.UserRole |
|||
value = tx[self._ROLE_NAMES[role_index]] |
|||
if isinstance(value, (bool, list, int, str, QEAmount)) or value is None: |
|||
return value |
|||
if isinstance(value, Satoshis): |
|||
return value.value |
|||
return str(value) |
|||
|
|||
def clear(self): |
|||
self.beginResetModel() |
|||
self.channels = [] |
|||
self.endResetModel() |
|||
|
|||
def channel_to_model(self, lnc): |
|||
lnworker = self.wallet.lnworker |
|||
item = {} |
|||
item['cid'] = lnc.channel_id.hex() |
|||
item['node_alias'] = lnworker.get_node_alias(lnc.node_id) or lnc.node_id.hex() |
|||
item['short_cid'] = lnc.short_id_for_GUI() |
|||
item['state'] = lnc.get_state_for_GUI() |
|||
item['state_code'] = lnc.get_state() |
|||
item['capacity'] = QEAmount(amount_sat=lnc.get_capacity()) |
|||
item['can_send'] = QEAmount(amount_msat=lnc.available_to_spend(LOCAL)) |
|||
item['can_receive'] = QEAmount(amount_msat=lnc.available_to_spend(REMOTE)) |
|||
self._logger.debug(repr(item)) |
|||
return item |
|||
|
|||
numOpenChannelsChanged = pyqtSignal() |
|||
@pyqtProperty(int, notify=numOpenChannelsChanged) |
|||
def numOpenChannels(self): |
|||
return sum([1 if x['state_code'] == ChannelState.OPEN else 0 for x in self.channels]) |
|||
|
|||
@pyqtSlot() |
|||
def init_model(self): |
|||
self._logger.debug('init_model') |
|||
if not self.wallet.lnworker: |
|||
self._logger.warning('lnworker should be defined') |
|||
return |
|||
|
|||
channels = [] |
|||
|
|||
lnchannels = self.wallet.lnworker.channels |
|||
for channel in lnchannels.values(): |
|||
self._logger.debug(repr(channel)) |
|||
item = self.channel_to_model(channel) |
|||
channels.append(item) |
|||
|
|||
self.clear() |
|||
self.beginInsertRows(QModelIndex(), 0, len(channels) - 1) |
|||
self.channels = channels |
|||
self.endInsertRows() |
|||
|
|||
def on_channel_updated(self, channel): |
|||
i = 0 |
|||
for c in self.channels: |
|||
if c['cid'] == channel.channel_id.hex(): |
|||
self.do_update(i,channel) |
|||
break |
|||
i = i + 1 |
|||
|
|||
def do_update(self, modelindex, channel): |
|||
modelitem = self.channels[modelindex] |
|||
#self._logger.debug(repr(modelitem)) |
|||
modelitem.update(self.channel_to_model(channel)) |
|||
|
|||
mi = self.createIndex(modelindex, 0) |
|||
self.dataChanged.emit(mi, mi, self._ROLE_KEYS) |
|||
self.numOpenChannelsChanged.emit() |
|||
|
|||
@pyqtSlot(str) |
|||
def new_channel(self, cid): |
|||
self._logger.debug('new channel with cid %s' % cid) |
|||
lnchannels = self.wallet.lnworker.channels |
|||
for channel in lnchannels.values(): |
|||
self._logger.debug(repr(channel)) |
|||
if cid == channel.channel_id.hex(): |
|||
item = self.channel_to_model(channel) |
|||
self._logger.debug(item) |
|||
self.beginInsertRows(QModelIndex(), 0, 0) |
|||
self.channels.insert(0,item) |
|||
self.endInsertRows() |
|||
|
|||
@pyqtSlot(str) |
|||
def remove_channel(self, cid): |
|||
self._logger.debug('remove channel with cid %s' % cid) |
|||
i = 0 |
|||
for channel in self.channels: |
|||
if cid == channel['cid']: |
|||
self._logger.debug(cid) |
|||
self.beginRemoveRows(QModelIndex(), i, i) |
|||
self.channels.remove(channel) |
|||
self.endRemoveRows() |
|||
return |
|||
i = i + 1 |
@ -0,0 +1,182 @@ |
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from electrum.i18n import _ |
|||
from electrum.logging import get_logger |
|||
from electrum.lnutil import extract_nodeid, ConnStringFormatError, LNPeerAddr, ln_dummy_address |
|||
from electrum.lnworker import hardcoded_trampoline_nodes |
|||
from electrum.gui import messages |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qetypes import QEAmount |
|||
from .qetxfinalizer import QETxFinalizer |
|||
|
|||
class QEChannelOpener(QObject): |
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
_wallet = None |
|||
_nodeid = None |
|||
_amount = QEAmount() |
|||
_valid = False |
|||
_opentx = None |
|||
|
|||
validationError = pyqtSignal([str,str], arguments=['code','message']) |
|||
conflictingBackup = pyqtSignal([str], arguments=['message']) |
|||
channelOpenError = pyqtSignal([str], arguments=['message']) |
|||
channelOpenSuccess = pyqtSignal([str,bool], arguments=['cid','has_backup']) |
|||
|
|||
dataChanged = pyqtSignal() # generic notify signal |
|||
|
|||
walletChanged = pyqtSignal() |
|||
@pyqtProperty(QEWallet, notify=walletChanged) |
|||
def wallet(self): |
|||
return self._wallet |
|||
|
|||
@wallet.setter |
|||
def wallet(self, wallet: QEWallet): |
|||
if self._wallet != wallet: |
|||
self._wallet = wallet |
|||
self.walletChanged.emit() |
|||
|
|||
nodeidChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=nodeidChanged) |
|||
def nodeid(self): |
|||
return self._nodeid |
|||
|
|||
@nodeid.setter |
|||
def nodeid(self, nodeid: str): |
|||
if self._nodeid != nodeid: |
|||
self._logger.debug('nodeid set -> %s' % nodeid) |
|||
self._nodeid = nodeid |
|||
self.nodeidChanged.emit() |
|||
self.validate() |
|||
|
|||
amountChanged = pyqtSignal() |
|||
@pyqtProperty(QEAmount, notify=amountChanged) |
|||
def amount(self): |
|||
return self._amount |
|||
|
|||
@amount.setter |
|||
def amount(self, amount: QEAmount): |
|||
if self._amount != amount: |
|||
self._amount = amount |
|||
self.amountChanged.emit() |
|||
self.validate() |
|||
|
|||
validChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=validChanged) |
|||
def valid(self): |
|||
return self._valid |
|||
|
|||
finalizerChanged = pyqtSignal() |
|||
@pyqtProperty(QETxFinalizer, notify=finalizerChanged) |
|||
def finalizer(self): |
|||
return self._finalizer |
|||
|
|||
@pyqtProperty(list, notify=dataChanged) |
|||
def trampolineNodeNames(self): |
|||
return list(hardcoded_trampoline_nodes().keys()) |
|||
|
|||
# FIXME min channel funding amount |
|||
# FIXME have requested funding amount |
|||
def validate(self): |
|||
nodeid_valid = False |
|||
if self._nodeid: |
|||
if not self._wallet.wallet.config.get('use_gossip', False): |
|||
self._peer = hardcoded_trampoline_nodes()[self._nodeid] |
|||
nodeid_valid = True |
|||
else: |
|||
try: |
|||
node_pubkey, host_port = extract_nodeid(self._nodeid) |
|||
host, port = host_port.split(':',1) |
|||
self._peer = LNPeerAddr(host, int(port), node_pubkey) |
|||
nodeid_valid = True |
|||
except ConnStringFormatError as e: |
|||
self.validationError.emit('invalid_nodeid', repr(e)) |
|||
except ValueError as e: |
|||
self.validationError.emit('invalid_nodeid', repr(e)) |
|||
|
|||
if not nodeid_valid: |
|||
self._valid = False |
|||
self.validChanged.emit() |
|||
return |
|||
|
|||
self._logger.debug('amount=%s' % str(self._amount)) |
|||
if not self._amount or not (self._amount.satsInt > 0 or self._amount.isMax): |
|||
self._valid = False |
|||
self.validChanged.emit() |
|||
return |
|||
|
|||
self._valid = True |
|||
self.validChanged.emit() |
|||
|
|||
# FIXME "max" button in amount_dialog should enforce LN_MAX_FUNDING_SAT |
|||
@pyqtSlot() |
|||
@pyqtSlot(bool) |
|||
def open_channel(self, confirm_backup_conflict=False): |
|||
if not self.valid: |
|||
return |
|||
|
|||
self._logger.debug('Connect String: %s' % str(self._peer)) |
|||
|
|||
lnworker = self._wallet.wallet.lnworker |
|||
if lnworker.has_conflicting_backup_with(self._peer.pubkey) and not confirm_backup_conflict: |
|||
self.conflictingBackup.emit(messages.MGS_CONFLICTING_BACKUP_INSTANCE) |
|||
return |
|||
|
|||
amount = '!' if self._amount.isMax else self._amount.satsInt |
|||
self._logger.debug('amount = %s' % str(amount)) |
|||
|
|||
coins = self._wallet.wallet.get_spendable_coins(None, nonlocal_only=True) |
|||
|
|||
mktx = lambda amt: lnworker.mktx_for_open_channel( |
|||
coins=coins, |
|||
funding_sat=amt, |
|||
node_id=self._peer.pubkey, |
|||
fee_est=None) |
|||
|
|||
acpt = lambda tx: self.do_open_channel(tx, str(self._peer), None) |
|||
|
|||
self._finalizer = QETxFinalizer(self, make_tx=mktx, accept=acpt) |
|||
self._finalizer.canRbf = False |
|||
self._finalizer.amount = self._amount |
|||
self._finalizer.wallet = self._wallet |
|||
self.finalizerChanged.emit() |
|||
|
|||
def do_open_channel(self, funding_tx, conn_str, password): |
|||
self._logger.debug('opening channel') |
|||
# read funding_sat from tx; converts '!' to int value |
|||
funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) |
|||
lnworker = self._wallet.wallet.lnworker |
|||
try: |
|||
chan, funding_tx = lnworker.open_channel( |
|||
connect_str=conn_str, |
|||
funding_tx=funding_tx, |
|||
funding_sat=funding_sat, |
|||
push_amt_sat=0, |
|||
password=password) |
|||
except Exception as e: |
|||
self._logger.exception("Problem opening channel") |
|||
self.channelOpenError.emit(_('Problem opening channel: ') + '\n' + repr(e)) |
|||
return |
|||
|
|||
self._logger.debug('opening channel succeeded') |
|||
self.channelOpenSuccess.emit(chan.channel_id.hex(), chan.has_onchain_backup()) |
|||
|
|||
# TODO: it would be nice to show this before broadcasting |
|||
#if chan.has_onchain_backup(): |
|||
#self.maybe_show_funding_tx(chan, funding_tx) |
|||
#else: |
|||
#title = _('Save backup') |
|||
#help_text = _(messages.MSG_CREATED_NON_RECOVERABLE_CHANNEL) |
|||
#data = lnworker.export_channel_backup(chan.channel_id) |
|||
#popup = QRDialog( |
|||
#title, data, |
|||
#show_text=False, |
|||
#text_for_clipboard=data, |
|||
#help_text=help_text, |
|||
#close_button_text=_('OK'), |
|||
#on_close=lambda: self.maybe_show_funding_tx(chan, funding_tx)) |
|||
#popup.open() |
@ -0,0 +1,168 @@ |
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from decimal import Decimal |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import DECIMAL_POINT_DEFAULT, format_satoshis |
|||
|
|||
from .qetypes import QEAmount |
|||
from .auth import AuthMixin, auth_protect |
|||
|
|||
class QEConfig(AuthMixin, QObject): |
|||
def __init__(self, config, parent=None): |
|||
super().__init__(parent) |
|||
self.config = config |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
autoConnectChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=autoConnectChanged) |
|||
def autoConnect(self): |
|||
return self.config.get('auto_connect') |
|||
|
|||
@autoConnect.setter |
|||
def autoConnect(self, auto_connect): |
|||
self.config.set_key('auto_connect', auto_connect, True) |
|||
self.autoConnectChanged.emit() |
|||
|
|||
# auto_connect is actually a tri-state, expose the undefined case |
|||
@pyqtProperty(bool, notify=autoConnectChanged) |
|||
def autoConnectDefined(self): |
|||
return self.config.get('auto_connect') is not None |
|||
|
|||
serverStringChanged = pyqtSignal() |
|||
@pyqtProperty('QString', notify=serverStringChanged) |
|||
def serverString(self): |
|||
return self.config.get('server') |
|||
|
|||
@serverString.setter |
|||
def serverString(self, server): |
|||
self.config.set_key('server', server, True) |
|||
self.serverStringChanged.emit() |
|||
|
|||
manualServerChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=manualServerChanged) |
|||
def manualServer(self): |
|||
return self.config.get('oneserver') |
|||
|
|||
@manualServer.setter |
|||
def manualServer(self, oneserver): |
|||
self.config.set_key('oneserver', oneserver, True) |
|||
self.manualServerChanged.emit() |
|||
|
|||
baseUnitChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=baseUnitChanged) |
|||
def baseUnit(self): |
|||
return self.config.get_base_unit() |
|||
|
|||
@baseUnit.setter |
|||
def baseUnit(self, unit): |
|||
self.config.set_base_unit(unit) |
|||
self.baseUnitChanged.emit() |
|||
|
|||
thousandsSeparatorChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=thousandsSeparatorChanged) |
|||
def thousandsSeparator(self): |
|||
return self.config.get('amt_add_thousands_sep', False) |
|||
|
|||
@thousandsSeparator.setter |
|||
def thousandsSeparator(self, checked): |
|||
self.config.set_key('amt_add_thousands_sep', checked) |
|||
self.config.amt_add_thousands_sep = checked |
|||
self.thousandsSeparatorChanged.emit() |
|||
|
|||
spendUnconfirmedChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=spendUnconfirmedChanged) |
|||
def spendUnconfirmed(self): |
|||
return not self.config.get('confirmed_only', False) |
|||
|
|||
@spendUnconfirmed.setter |
|||
def spendUnconfirmed(self, checked): |
|||
self.config.set_key('confirmed_only', not checked, True) |
|||
self.spendUnconfirmedChanged.emit() |
|||
|
|||
pinCodeChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=pinCodeChanged) |
|||
def pinCode(self): |
|||
return self.config.get('pin_code', '') |
|||
|
|||
@pinCode.setter |
|||
def pinCode(self, pin_code): |
|||
if pin_code == '': |
|||
self.pinCodeRemoveAuth() |
|||
self.config.set_key('pin_code', pin_code, True) |
|||
self.pinCodeChanged.emit() |
|||
|
|||
@auth_protect(method='wallet') |
|||
def pinCodeRemoveAuth(self): |
|||
pass # no-op |
|||
|
|||
useGossipChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=useGossipChanged) |
|||
def useGossip(self): |
|||
return self.config.get('use_gossip', False) |
|||
|
|||
@useGossip.setter |
|||
def useGossip(self, gossip): |
|||
self.config.set_key('use_gossip', gossip) |
|||
self.useGossipChanged.emit() |
|||
|
|||
@pyqtSlot('qint64', result=str) |
|||
@pyqtSlot('qint64', bool, result=str) |
|||
@pyqtSlot(QEAmount, result=str) |
|||
@pyqtSlot(QEAmount, bool, result=str) |
|||
def formatSats(self, satoshis, with_unit=False): |
|||
if isinstance(satoshis, QEAmount): |
|||
satoshis = satoshis.satsInt |
|||
if with_unit: |
|||
return self.config.format_amount_and_units(satoshis) |
|||
else: |
|||
return self.config.format_amount(satoshis) |
|||
|
|||
@pyqtSlot(QEAmount, result=str) |
|||
@pyqtSlot(QEAmount, bool, result=str) |
|||
def formatMilliSats(self, amount, with_unit=False): |
|||
if isinstance(amount, QEAmount): |
|||
msats = amount.msatsInt |
|||
else: |
|||
return '---' |
|||
|
|||
s = format_satoshis(msats/1000, |
|||
decimal_point=self.decimal_point(), |
|||
precision=3) |
|||
return s |
|||
#if with_unit: |
|||
#return self.config.format_amount_and_units(msats) |
|||
#else: |
|||
#return self.config.format_amount(satoshis) |
|||
|
|||
# TODO delegate all this to config.py/util.py |
|||
def decimal_point(self): |
|||
return self.config.get('decimal_point', DECIMAL_POINT_DEFAULT) |
|||
|
|||
def max_precision(self): |
|||
return self.decimal_point() + 0 #self.extra_precision |
|||
|
|||
@pyqtSlot(str, result=QEAmount) |
|||
def unitsToSats(self, unitAmount): |
|||
self._amount = QEAmount() |
|||
try: |
|||
x = Decimal(unitAmount) |
|||
except: |
|||
return self._amount |
|||
|
|||
# scale it to max allowed precision, make it an int |
|||
max_prec_amount = int(pow(10, self.max_precision()) * x) |
|||
# if the max precision is simply what unit conversion allows, just return |
|||
if self.max_precision() == self.decimal_point(): |
|||
self._amount = QEAmount(amount_sat=max_prec_amount) |
|||
return self._amount |
|||
self._logger.debug('fallthrough') |
|||
# otherwise, scale it back to the expected unit |
|||
#amount = Decimal(max_prec_amount) / Decimal(pow(10, self.max_precision()-self.decimal_point())) |
|||
#return int(amount) #Decimal(amount) if not self.is_int else int(amount) |
|||
return self._amount |
|||
|
|||
@pyqtSlot('quint64', result=float) |
|||
def satsToUnits(self, satoshis): |
|||
return satoshis / pow(10,self.config.decimal_point) |
@ -0,0 +1,218 @@ |
|||
import os |
|||
from decimal import Decimal |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl |
|||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex |
|||
|
|||
from electrum.util import register_callback, get_new_wallet_name, WalletFileException |
|||
from electrum.logging import get_logger |
|||
from electrum.wallet import Wallet, Abstract_Wallet |
|||
from electrum.storage import WalletStorage, StorageReadWriteError |
|||
from electrum.wallet_db import WalletDB |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qewalletdb import QEWalletDB |
|||
from .qefx import QEFX |
|||
from .auth import AuthMixin, auth_protect |
|||
|
|||
# wallet list model. supports both wallet basenames (wallet file basenames) |
|||
# and whole Wallet instances (loaded wallets) |
|||
class QEWalletListModel(QAbstractListModel): |
|||
_logger = get_logger(__name__) |
|||
def __init__(self, parent=None): |
|||
QAbstractListModel.__init__(self, parent) |
|||
self.wallets = [] |
|||
|
|||
# define listmodel rolemap |
|||
_ROLE_NAMES= ('name','path','active') |
|||
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) |
|||
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) |
|||
|
|||
def rowCount(self, index): |
|||
return len(self.wallets) |
|||
|
|||
def roleNames(self): |
|||
return self._ROLE_MAP |
|||
|
|||
def data(self, index, role): |
|||
(wallet_name, wallet_path, wallet) = self.wallets[index.row()] |
|||
role_index = role - Qt.UserRole |
|||
role_name = self._ROLE_NAMES[role_index] |
|||
if role_name == 'name': |
|||
return wallet_name |
|||
if role_name == 'path': |
|||
return wallet_path |
|||
if role_name == 'active': |
|||
return wallet != None |
|||
|
|||
def add_wallet(self, wallet_path = None, wallet: Abstract_Wallet = None): |
|||
if wallet_path == None and wallet == None: |
|||
return |
|||
# only add wallet instance if instance not yet in model |
|||
if wallet: |
|||
for name,path,w in self.wallets: |
|||
if w == wallet: |
|||
return |
|||
self.beginInsertRows(QModelIndex(), len(self.wallets), len(self.wallets)); |
|||
if wallet == None: |
|||
wallet_name = os.path.basename(wallet_path) |
|||
else: |
|||
wallet_name = wallet.basename() |
|||
item = (wallet_name, wallet_path, wallet) |
|||
self.wallets.append(item); |
|||
self.endInsertRows(); |
|||
|
|||
class QEAvailableWalletListModel(QEWalletListModel): |
|||
def __init__(self, daemon, parent=None): |
|||
QEWalletListModel.__init__(self, parent) |
|||
self.daemon = daemon |
|||
self.reload() |
|||
|
|||
@pyqtSlot() |
|||
def reload(self): |
|||
if len(self.wallets) > 0: |
|||
self.beginRemoveRows(QModelIndex(), 0, len(self.wallets) - 1) |
|||
self.wallets = [] |
|||
self.endRemoveRows() |
|||
|
|||
available = [] |
|||
wallet_folder = os.path.dirname(self.daemon.config.get_wallet_path()) |
|||
with os.scandir(wallet_folder) as it: |
|||
for i in it: |
|||
if i.is_file() and not i.name.startswith('.'): |
|||
available.append(i.path) |
|||
for path in sorted(available): |
|||
wallet = self.daemon.get_wallet(path) |
|||
self.add_wallet(wallet_path = path, wallet = wallet) |
|||
|
|||
def wallet_name_exists(self, name): |
|||
for wallet_name, wallet_path, wallet in self.wallets: |
|||
if name == wallet_name: |
|||
return True |
|||
return False |
|||
|
|||
class QEDaemon(AuthMixin, QObject): |
|||
def __init__(self, daemon, parent=None): |
|||
super().__init__(parent) |
|||
self.daemon = daemon |
|||
self.qefx = QEFX(daemon.fx, daemon.config) |
|||
self._walletdb = QEWalletDB() |
|||
self._walletdb.validPasswordChanged.connect(self.passwordValidityCheck) |
|||
|
|||
_logger = get_logger(__name__) |
|||
_loaded_wallets = QEWalletListModel() |
|||
_available_wallets = None |
|||
_current_wallet = None |
|||
_path = None |
|||
_use_single_password = False |
|||
_password = None |
|||
|
|||
walletLoaded = pyqtSignal() |
|||
walletRequiresPassword = pyqtSignal() |
|||
activeWalletsChanged = pyqtSignal() |
|||
availableWalletsChanged = pyqtSignal() |
|||
walletOpenError = pyqtSignal([str], arguments=["error"]) |
|||
fxChanged = pyqtSignal() |
|||
|
|||
@pyqtSlot() |
|||
def passwordValidityCheck(self): |
|||
if not self._walletdb._validPassword: |
|||
self.walletRequiresPassword.emit() |
|||
|
|||
@pyqtSlot() |
|||
@pyqtSlot(str) |
|||
@pyqtSlot(str, str) |
|||
def load_wallet(self, path=None, password=None): |
|||
if path == None: |
|||
self._path = self.daemon.config.get('gui_last_wallet') |
|||
else: |
|||
self._path = path |
|||
if self._path is None: |
|||
return |
|||
|
|||
self._logger.debug('load wallet ' + str(self._path)) |
|||
|
|||
if path not in self.daemon._wallets: |
|||
# pre-checks, let walletdb trigger any necessary user interactions |
|||
self._walletdb.path = self._path |
|||
self._walletdb.password = password |
|||
if not self._walletdb.ready: |
|||
return |
|||
|
|||
try: |
|||
wallet = self.daemon.load_wallet(self._path, password) |
|||
if wallet != None: |
|||
self._loaded_wallets.add_wallet(wallet=wallet) |
|||
self._current_wallet = QEWallet.getInstanceFor(wallet) |
|||
self.walletLoaded.emit() |
|||
|
|||
if self.daemon.config.get('single_password'): |
|||
self._use_single_password = self.daemon.update_password_for_directory(old_password=password, new_password=password) |
|||
self._password = password |
|||
self._logger.info(f'use single password: {self._use_single_password}') |
|||
|
|||
self.daemon.config.save_last_wallet(wallet) |
|||
else: |
|||
self._logger.info('could not open wallet') |
|||
self.walletOpenError.emit('could not open wallet') |
|||
except WalletFileException as e: |
|||
self._logger.error(str(e)) |
|||
self.walletOpenError.emit(str(e)) |
|||
|
|||
@pyqtSlot(QEWallet) |
|||
@auth_protect |
|||
def delete_wallet(self, wallet): |
|||
path = wallet.wallet.storage.path |
|||
self._logger.debug('Ok to delete wallet with path %s' % path) |
|||
# TODO checks, e.g. existing LN channels, unpaid requests, etc |
|||
self._logger.debug('Not deleting yet, just unloading for now') |
|||
# TODO actually delete |
|||
# TODO walletLoaded signal is confusing |
|||
self.daemon.stop_wallet(path) |
|||
self._current_wallet = None |
|||
self.walletLoaded.emit() |
|||
|
|||
@pyqtProperty('QString') |
|||
def path(self): |
|||
return self._path |
|||
|
|||
@pyqtProperty(QEWallet, notify=walletLoaded) |
|||
def currentWallet(self): |
|||
return self._current_wallet |
|||
|
|||
@pyqtProperty(QEWalletListModel, notify=activeWalletsChanged) |
|||
def activeWallets(self): |
|||
return self._loaded_wallets |
|||
|
|||
@pyqtProperty(QEAvailableWalletListModel, notify=availableWalletsChanged) |
|||
def availableWallets(self): |
|||
if not self._available_wallets: |
|||
self._available_wallets = QEAvailableWalletListModel(self.daemon) |
|||
|
|||
return self._available_wallets |
|||
|
|||
@pyqtProperty(QEFX, notify=fxChanged) |
|||
def fx(self): |
|||
return self.qefx |
|||
|
|||
@pyqtSlot(result=str) |
|||
def suggestWalletName(self): |
|||
i = 1 |
|||
while self.availableWallets.wallet_name_exists(f'wallet_{i}'): |
|||
i = i + 1 |
|||
return f'wallet_{i}' |
|||
|
|||
requestNewPassword = pyqtSignal() |
|||
@pyqtSlot() |
|||
@auth_protect |
|||
def start_change_password(self): |
|||
if self._use_single_password: |
|||
self.requestNewPassword.emit() |
|||
else: |
|||
self.currentWallet.requestNewPassword.emit() |
|||
|
|||
@pyqtSlot(str) |
|||
def set_password(self, password): |
|||
assert self._use_single_password |
|||
self._logger.debug('about to set password to %s for ALL wallets' % password) |
|||
update_password_for_directory(self.daemon.config, self._password, password) |
@ -0,0 +1,154 @@ |
|||
from decimal import Decimal |
|||
from datetime import datetime |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.exchange_rate import FxThread |
|||
from electrum.simple_config import SimpleConfig |
|||
from electrum.bitcoin import COIN |
|||
|
|||
from .qetypes import QEAmount |
|||
from .util import QtEventListener, qt_event_listener |
|||
|
|||
class QEFX(QObject, QtEventListener): |
|||
def __init__(self, fxthread: FxThread, config: SimpleConfig, parent=None): |
|||
super().__init__(parent) |
|||
self.fx = fxthread |
|||
self.config = config |
|||
self.register_callbacks() |
|||
self.destroyed.connect(lambda: self.on_destroy()) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
quotesUpdated = pyqtSignal() |
|||
|
|||
def on_destroy(self): |
|||
self.unregister_callbacks() |
|||
|
|||
@qt_event_listener |
|||
def on_event_on_quotes(self, *args): |
|||
self._logger.debug('new quotes') |
|||
self.quotesUpdated.emit() |
|||
|
|||
historyUpdated = pyqtSignal() |
|||
@qt_event_listener |
|||
def on_event_on_history(self, *args): |
|||
self._logger.debug('new history') |
|||
self.historyUpdated.emit() |
|||
|
|||
currenciesChanged = pyqtSignal() |
|||
@pyqtProperty('QVariantList', notify=currenciesChanged) |
|||
def currencies(self): |
|||
return self.fx.get_currencies(self.historicRates) |
|||
|
|||
rateSourcesChanged = pyqtSignal() |
|||
@pyqtProperty('QVariantList', notify=rateSourcesChanged) |
|||
def rateSources(self): |
|||
return self.fx.get_exchanges_by_ccy(self.fiatCurrency, self.historicRates) |
|||
|
|||
fiatCurrencyChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=fiatCurrencyChanged) |
|||
def fiatCurrency(self): |
|||
return self.fx.get_currency() |
|||
|
|||
@fiatCurrency.setter |
|||
def fiatCurrency(self, currency): |
|||
if currency != self.fiatCurrency: |
|||
self.fx.set_currency(currency) |
|||
self.enabled = self.enabled and currency != '' |
|||
self.fiatCurrencyChanged.emit() |
|||
self.rateSourcesChanged.emit() |
|||
|
|||
historicRatesChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=historicRatesChanged) |
|||
def historicRates(self): |
|||
return self.fx.get_history_config() |
|||
|
|||
@historicRates.setter |
|||
def historicRates(self, checked): |
|||
if checked != self.historicRates: |
|||
self.fx.set_history_config(checked) |
|||
self.historicRatesChanged.emit() |
|||
self.rateSourcesChanged.emit() |
|||
|
|||
rateSourceChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=rateSourceChanged) |
|||
def rateSource(self): |
|||
return self.fx.config_exchange() |
|||
|
|||
@rateSource.setter |
|||
def rateSource(self, source): |
|||
if source != self.rateSource: |
|||
self.fx.set_exchange(source) |
|||
self.rateSourceChanged.emit() |
|||
|
|||
enabledUpdated = pyqtSignal() # curiously, enabledChanged is clashing, so name it enabledUpdated |
|||
@pyqtProperty(bool, notify=enabledUpdated) |
|||
def enabled(self): |
|||
return self.fx.is_enabled() |
|||
|
|||
@enabled.setter |
|||
def enabled(self, enable): |
|||
if enable != self.enabled: |
|||
self.fx.set_enabled(enable) |
|||
self.enabledUpdated.emit() |
|||
|
|||
@pyqtSlot(str, result=str) |
|||
@pyqtSlot(str, bool, result=str) |
|||
@pyqtSlot(QEAmount, result=str) |
|||
@pyqtSlot(QEAmount, bool, result=str) |
|||
def fiatValue(self, satoshis, plain=True): |
|||
rate = self.fx.exchange_rate() |
|||
if isinstance(satoshis, QEAmount): |
|||
satoshis = satoshis.satsInt |
|||
else: |
|||
try: |
|||
sd = Decimal(satoshis) |
|||
except: |
|||
return '' |
|||
if plain: |
|||
return self.fx.ccy_amount_str(self.fx.fiat_value(satoshis, rate), False) |
|||
else: |
|||
return self.fx.value_str(satoshis, rate) |
|||
|
|||
@pyqtSlot(str, str, result=str) |
|||
@pyqtSlot(str, str, bool, result=str) |
|||
@pyqtSlot(QEAmount, str, result=str) |
|||
@pyqtSlot(QEAmount, str, bool, result=str) |
|||
def fiatValueHistoric(self, satoshis, timestamp, plain=True): |
|||
if isinstance(satoshis, QEAmount): |
|||
satoshis = satoshis.satsInt |
|||
else: |
|||
try: |
|||
sd = Decimal(satoshis) |
|||
except: |
|||
return '' |
|||
|
|||
try: |
|||
td = Decimal(timestamp) |
|||
if td == 0: |
|||
return '' |
|||
except: |
|||
return '' |
|||
dt = datetime.fromtimestamp(int(td)) |
|||
if plain: |
|||
return self.fx.ccy_amount_str(self.fx.historical_value(satoshis, dt), False) |
|||
else: |
|||
return self.fx.historical_value_str(satoshis, dt) |
|||
|
|||
@pyqtSlot(str, result=str) |
|||
@pyqtSlot(str, bool, result=str) |
|||
def satoshiValue(self, fiat, plain=True): |
|||
rate = self.fx.exchange_rate() |
|||
try: |
|||
fd = Decimal(fiat) |
|||
except: |
|||
return '' |
|||
v = fd / Decimal(rate) * COIN |
|||
if v.is_nan(): |
|||
return '' |
|||
if plain: |
|||
return str(v.to_integral_value()) |
|||
else: |
|||
return self.config.format_amount(v) |
@ -0,0 +1,491 @@ |
|||
import asyncio |
|||
from datetime import datetime |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, Q_ENUMS |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.i18n import _ |
|||
from electrum.util import (parse_URI, create_bip21_uri, InvalidBitcoinURI, InvoiceError, |
|||
maybe_extract_lightning_payment_identifier) |
|||
from electrum.invoices import Invoice |
|||
from electrum.invoices import (PR_UNPAID,PR_EXPIRED,PR_UNKNOWN,PR_PAID,PR_INFLIGHT, |
|||
PR_FAILED,PR_ROUTING,PR_UNCONFIRMED,LN_EXPIRY_NEVER) |
|||
from electrum.transaction import PartialTxOutput |
|||
from electrum.lnaddr import lndecode |
|||
from electrum import bitcoin |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qetypes import QEAmount |
|||
|
|||
class QEInvoice(QObject): |
|||
class Type: |
|||
Invalid = -1 |
|||
OnchainOnlyAddress = 0 |
|||
OnchainInvoice = 1 |
|||
LightningInvoice = 2 |
|||
LightningAndOnchainInvoice = 3 |
|||
|
|||
class Status: |
|||
Unpaid = PR_UNPAID |
|||
Expired = PR_EXPIRED |
|||
Unknown = PR_UNKNOWN |
|||
Paid = PR_PAID |
|||
Inflight = PR_INFLIGHT |
|||
Failed = PR_FAILED |
|||
Routing = PR_ROUTING |
|||
Unconfirmed = PR_UNCONFIRMED |
|||
|
|||
Q_ENUMS(Type) |
|||
Q_ENUMS(Status) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
_wallet = None |
|||
_canSave = False |
|||
_canPay = False |
|||
_key = None |
|||
|
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
|
|||
walletChanged = pyqtSignal() |
|||
@pyqtProperty(QEWallet, notify=walletChanged) |
|||
def wallet(self): |
|||
return self._wallet |
|||
|
|||
@wallet.setter |
|||
def wallet(self, wallet: QEWallet): |
|||
if self._wallet != wallet: |
|||
self._wallet = wallet |
|||
self.walletChanged.emit() |
|||
|
|||
canSaveChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=canSaveChanged) |
|||
def canSave(self): |
|||
return self._canSave |
|||
|
|||
@canSave.setter |
|||
def canSave(self, canSave): |
|||
if self._canSave != canSave: |
|||
self._canSave = canSave |
|||
self.canSaveChanged.emit() |
|||
|
|||
canPayChanged = pyqtSignal() |
|||
@pyqtProperty(bool, notify=canPayChanged) |
|||
def canPay(self): |
|||
return self._canPay |
|||
|
|||
@canPay.setter |
|||
def canPay(self, canPay): |
|||
if self._canPay != canPay: |
|||
self._canPay = canPay |
|||
self.canPayChanged.emit() |
|||
|
|||
keyChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=keyChanged) |
|||
def key(self): |
|||
return self._key |
|||
|
|||
@key.setter |
|||
def key(self, key): |
|||
if self._key != key: |
|||
self._key = key |
|||
self.keyChanged.emit() |
|||
|
|||
userinfoChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=userinfoChanged) |
|||
def userinfo(self): |
|||
return self._userinfo |
|||
|
|||
@userinfo.setter |
|||
def userinfo(self, userinfo): |
|||
if self._userinfo != userinfo: |
|||
self._userinfo = userinfo |
|||
self.userinfoChanged.emit() |
|||
|
|||
def get_max_spendable_onchain(self): |
|||
c, u, x = self._wallet.wallet.get_balance() |
|||
#TODO determine real max |
|||
return c |
|||
|
|||
|
|||
class QEInvoiceParser(QEInvoice): |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
_invoiceType = QEInvoice.Type.Invalid |
|||
_recipient = '' |
|||
_effectiveInvoice = None |
|||
_amount = QEAmount() |
|||
_userinfo = '' |
|||
|
|||
invoiceChanged = pyqtSignal() |
|||
invoiceSaved = pyqtSignal() |
|||
|
|||
validationSuccess = pyqtSignal() |
|||
validationWarning = pyqtSignal([str,str], arguments=['code', 'message']) |
|||
validationError = pyqtSignal([str,str], arguments=['code', 'message']) |
|||
|
|||
invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) |
|||
|
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
self.clear() |
|||
|
|||
@pyqtProperty(int, notify=invoiceChanged) |
|||
def invoiceType(self): |
|||
return self._invoiceType |
|||
|
|||
# not a qt setter, don't let outside set state |
|||
def setInvoiceType(self, invoiceType: QEInvoice.Type): |
|||
self._invoiceType = invoiceType |
|||
|
|||
recipientChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=recipientChanged) |
|||
def recipient(self): |
|||
return self._recipient |
|||
|
|||
@recipient.setter |
|||
def recipient(self, recipient: str): |
|||
#if self._recipient != recipient: |
|||
self._recipient = recipient |
|||
if recipient: |
|||
self.validateRecipient(recipient) |
|||
self.recipientChanged.emit() |
|||
|
|||
@pyqtProperty(str, notify=invoiceChanged) |
|||
def message(self): |
|||
return self._effectiveInvoice.message if self._effectiveInvoice else '' |
|||
|
|||
@pyqtProperty(QEAmount, notify=invoiceChanged) |
|||
def amount(self): |
|||
# store ref to QEAmount on instance, otherwise we get destroyed when going out of scope |
|||
self._amount = QEAmount() |
|||
if not self._effectiveInvoice: |
|||
return self._amount |
|||
self._amount = QEAmount(from_invoice=self._effectiveInvoice) |
|||
return self._amount |
|||
|
|||
@pyqtProperty('quint64', notify=invoiceChanged) |
|||
def expiration(self): |
|||
return self._effectiveInvoice.exp if self._effectiveInvoice else 0 |
|||
|
|||
@pyqtProperty('quint64', notify=invoiceChanged) |
|||
def time(self): |
|||
return self._effectiveInvoice.time if self._effectiveInvoice else 0 |
|||
|
|||
statusChanged = pyqtSignal() |
|||
@pyqtProperty(int, notify=statusChanged) |
|||
def status(self): |
|||
if not self._effectiveInvoice: |
|||
return PR_UNKNOWN |
|||
return self._wallet.wallet.get_invoice_status(self._effectiveInvoice) |
|||
|
|||
@pyqtProperty(str, notify=statusChanged) |
|||
def status_str(self): |
|||
if not self._effectiveInvoice: |
|||
return '' |
|||
status = self._wallet.wallet.get_invoice_status(self._effectiveInvoice) |
|||
return self._effectiveInvoice.get_status_str(status) |
|||
|
|||
# single address only, TODO: n outputs |
|||
@pyqtProperty(str, notify=invoiceChanged) |
|||
def address(self): |
|||
return self._effectiveInvoice.get_address() if self._effectiveInvoice else '' |
|||
|
|||
@pyqtProperty('QVariantMap', notify=invoiceChanged) |
|||
def lnprops(self): |
|||
if not self.invoiceType == QEInvoice.Type.LightningInvoice: |
|||
return {} |
|||
lnaddr = self._effectiveInvoice._lnaddr |
|||
self._logger.debug(str(lnaddr)) |
|||
self._logger.debug(str(lnaddr.get_routing_info('t'))) |
|||
return { |
|||
'pubkey': lnaddr.pubkey.serialize().hex(), |
|||
't': '', #lnaddr.get_routing_info('t')[0][0].hex(), |
|||
'r': '' #lnaddr.get_routing_info('r')[0][0][0].hex() |
|||
} |
|||
|
|||
@pyqtSlot() |
|||
def clear(self): |
|||
self.recipient = '' |
|||
self.setInvoiceType(QEInvoice.Type.Invalid) |
|||
self._bip21 = None |
|||
self.canSave = False |
|||
self.canPay = False |
|||
self.userinfo = '' |
|||
self.invoiceChanged.emit() |
|||
|
|||
# don't parse the recipient string, but init qeinvoice from an invoice key |
|||
# this should not emit validation signals |
|||
@pyqtSlot(str) |
|||
def initFromKey(self, key): |
|||
self.clear() |
|||
invoice = self._wallet.wallet.get_invoice(key) |
|||
self._logger.debug(repr(invoice)) |
|||
if invoice: |
|||
self.set_effective_invoice(invoice) |
|||
self.key = key |
|||
|
|||
def set_effective_invoice(self, invoice: Invoice): |
|||
self._effectiveInvoice = invoice |
|||
|
|||
if invoice.is_lightning(): |
|||
self.setInvoiceType(QEInvoice.Type.LightningInvoice) |
|||
else: |
|||
self.setInvoiceType(QEInvoice.Type.OnchainInvoice) |
|||
|
|||
self.canSave = True |
|||
|
|||
self.determine_can_pay() |
|||
|
|||
self.invoiceChanged.emit() |
|||
self.statusChanged.emit() |
|||
|
|||
def determine_can_pay(self): |
|||
if self.invoiceType == QEInvoice.Type.LightningInvoice: |
|||
if self.status in [PR_UNPAID, PR_FAILED]: |
|||
if self.get_max_spendable_lightning() >= self.amount.satsInt: |
|||
self.canPay = True |
|||
else: |
|||
self.userinfo = _('Can\'t pay, insufficient balance') |
|||
else: |
|||
self.userinfo = { |
|||
PR_EXPIRED: _('Can\'t pay, invoice is expired'), |
|||
PR_PAID: _('Can\'t pay, invoice is already paid'), |
|||
PR_INFLIGHT: _('Can\'t pay, invoice is already being paid'), |
|||
PR_ROUTING: _('Can\'t pay, invoice is already being paid'), |
|||
PR_UNKNOWN: _('Can\'t pay, invoice has unknown status'), |
|||
}[self.status] |
|||
elif self.invoiceType == QEInvoice.Type.OnchainInvoice: |
|||
if self.status in [PR_UNPAID, PR_FAILED]: |
|||
if self.get_max_spendable_onchain() >= self.amount.satsInt: |
|||
self.canPay = True |
|||
else: |
|||
self.userinfo = _('Can\'t pay, insufficient balance') |
|||
else: |
|||
self.userinfo = { |
|||
PR_EXPIRED: _('Can\'t pay, invoice is expired'), |
|||
PR_PAID: _('Can\'t pay, invoice is already paid'), |
|||
PR_UNCONFIRMED: _('Can\'t pay, invoice is already paid'), |
|||
PR_UNKNOWN: _('Can\'t pay, invoice has unknown status'), |
|||
}[self.status] |
|||
|
|||
|
|||
def get_max_spendable_lightning(self): |
|||
return self._wallet.wallet.lnworker.num_sats_can_send() |
|||
|
|||
def setValidAddressOnly(self): |
|||
self._logger.debug('setValidAddressOnly') |
|||
self.setInvoiceType(QEInvoice.Type.OnchainOnlyAddress) |
|||
self._effectiveInvoice = None |
|||
self.invoiceChanged.emit() |
|||
|
|||
def setValidOnchainInvoice(self, invoice: Invoice): |
|||
self._logger.debug('setValidOnchainInvoice') |
|||
if invoice.is_lightning(): |
|||
raise Exception('unexpected LN invoice') |
|||
self.set_effective_invoice(invoice) |
|||
|
|||
def setValidLightningInvoice(self, invoice: Invoice): |
|||
self._logger.debug('setValidLightningInvoice') |
|||
if not invoice.is_lightning(): |
|||
raise Exception('unexpected Onchain invoice') |
|||
self.set_effective_invoice(invoice) |
|||
|
|||
def create_onchain_invoice(self, outputs, message, payment_request, uri): |
|||
return self._wallet.wallet.create_invoice( |
|||
outputs=outputs, |
|||
message=message, |
|||
pr=payment_request, |
|||
URI=uri |
|||
) |
|||
|
|||
def validateRecipient(self, recipient): |
|||
if not recipient: |
|||
self.setInvoiceType(QEInvoice.Type.Invalid) |
|||
return |
|||
|
|||
maybe_lightning_invoice = recipient |
|||
|
|||
def _payment_request_resolved(request): |
|||
self._logger.debug('resolved payment request') |
|||
outputs = request.get_outputs() |
|||
invoice = self.create_onchain_invoice(outputs, None, request, None) |
|||
self.setValidOnchainInvoice(invoice) |
|||
|
|||
try: |
|||
self._bip21 = parse_URI(recipient, _payment_request_resolved) |
|||
if self._bip21: |
|||
if 'r' in self._bip21 or ('name' in self._bip21 and 'sig' in self._bip21): # TODO set flag in util? |
|||
# let callback handle state |
|||
return |
|||
if ':' not in recipient: |
|||
# address only |
|||
self.setValidAddressOnly() |
|||
self.validationSuccess.emit() |
|||
return |
|||
else: |
|||
# fallback lightning invoice? |
|||
if 'lightning' in self._bip21: |
|||
maybe_lightning_invoice = self._bip21['lightning'] |
|||
except InvalidBitcoinURI as e: |
|||
self._bip21 = None |
|||
self._logger.debug(repr(e)) |
|||
|
|||
lninvoice = None |
|||
try: |
|||
maybe_lightning_invoice = maybe_extract_lightning_payment_identifier(maybe_lightning_invoice) |
|||
lninvoice = Invoice.from_bech32(maybe_lightning_invoice) |
|||
except InvoiceError as e: |
|||
pass |
|||
|
|||
if not lninvoice and not self._bip21: |
|||
self.validationError.emit('unknown',_('Unknown invoice')) |
|||
self.clear() |
|||
return |
|||
|
|||
if lninvoice: |
|||
if not self._wallet.wallet.has_lightning(): |
|||
if not self._bip21: |
|||
# TODO: lightning onchain fallback in ln invoice |
|||
#self.validationError.emit('no_lightning',_('Detected valid Lightning invoice, but Lightning not enabled for wallet')) |
|||
self.setValidLightningInvoice(lninvoice) |
|||
self.clear() |
|||
return |
|||
else: |
|||
self._logger.debug('flow with LN but not LN enabled AND having bip21 uri') |
|||
self.setValidOnchainInvoice(self._bip21['address']) |
|||
else: |
|||
self.setValidLightningInvoice(lninvoice) |
|||
if not self._wallet.wallet.lnworker.channels: |
|||
self.validationWarning.emit('no_channels',_('Detected valid Lightning invoice, but there are no open channels')) |
|||
else: |
|||
self.validationSuccess.emit() |
|||
else: |
|||
self._logger.debug('flow without LN but having bip21 uri') |
|||
if 'amount' not in self._bip21: #TODO can we have amount-less invoices? |
|||
self.validationError.emit('no_amount', 'no amount in uri') |
|||
return |
|||
outputs = [PartialTxOutput.from_address_and_value(self._bip21['address'], self._bip21['amount'])] |
|||
self._logger.debug(outputs) |
|||
message = self._bip21['message'] if 'message' in self._bip21 else '' |
|||
invoice = self.create_onchain_invoice(outputs, message, None, self._bip21) |
|||
self._logger.debug(repr(invoice)) |
|||
self.setValidOnchainInvoice(invoice) |
|||
self.validationSuccess.emit() |
|||
|
|||
@pyqtSlot() |
|||
def save_invoice(self): |
|||
self.canSave = False |
|||
if not self._effectiveInvoice: |
|||
return |
|||
# TODO detect duplicate? |
|||
self.key = self._wallet.wallet.get_key_for_outgoing_invoice(self._effectiveInvoice) |
|||
self._wallet.wallet.save_invoice(self._effectiveInvoice) |
|||
self.invoiceSaved.emit() |
|||
|
|||
|
|||
class QEUserEnteredPayment(QEInvoice): |
|||
_logger = get_logger(__name__) |
|||
|
|||
_recipient = None |
|||
_message = None |
|||
_amount = QEAmount() |
|||
|
|||
validationError = pyqtSignal([str,str], arguments=['code','message']) |
|||
invoiceCreateError = pyqtSignal([str,str], arguments=['code', 'message']) |
|||
invoiceSaved = pyqtSignal() |
|||
|
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
self.clear() |
|||
|
|||
recipientChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=recipientChanged) |
|||
def recipient(self): |
|||
return self._recipient |
|||
|
|||
@recipient.setter |
|||
def recipient(self, recipient: str): |
|||
if self._recipient != recipient: |
|||
self._recipient = recipient |
|||
self.validate() |
|||
self.recipientChanged.emit() |
|||
|
|||
messageChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=messageChanged) |
|||
def message(self): |
|||
return self._message |
|||
|
|||
@message.setter |
|||
def message(self, message): |
|||
if self._message != message: |
|||
self._message = message |
|||
self.messageChanged.emit() |
|||
|
|||
amountChanged = pyqtSignal() |
|||
@pyqtProperty(QEAmount, notify=amountChanged) |
|||
def amount(self): |
|||
return self._amount |
|||
|
|||
@amount.setter |
|||
def amount(self, amount): |
|||
if self._amount != amount: |
|||
self._amount = amount |
|||
self.validate() |
|||
self.amountChanged.emit() |
|||
|
|||
|
|||
def validate(self): |
|||
self.canPay = False |
|||
self.canSave = False |
|||
self._logger.debug('validate') |
|||
|
|||
if not self._recipient: |
|||
self.validationError.emit('recipient', _('Recipient not specified.')) |
|||
return |
|||
|
|||
if not bitcoin.is_address(self._recipient): |
|||
self.validationError.emit('recipient', _('Invalid Bitcoin address')) |
|||
return |
|||
|
|||
if self._amount.isEmpty: |
|||
self.validationError.emit('amount', _('Invalid amount')) |
|||
return |
|||
|
|||
if self._amount.isMax: |
|||
self.canPay = True |
|||
else: |
|||
self.canSave = True |
|||
if self.get_max_spendable_onchain() >= self._amount.satsInt: |
|||
self.canPay = True |
|||
|
|||
@pyqtSlot() |
|||
def save_invoice(self): |
|||
assert self.canSave |
|||
assert not self._amount.isMax |
|||
|
|||
self._logger.debug('saving invoice to %s, amount=%s, message=%s' % (self._recipient, repr(self._amount), self._message)) |
|||
|
|||
inv_amt = self._amount.satsInt |
|||
|
|||
try: |
|||
outputs = [PartialTxOutput.from_address_and_value(self._recipient, inv_amt)] |
|||
self._logger.debug(repr(outputs)) |
|||
invoice = self._wallet.wallet.create_invoice(outputs=outputs, message=self._message, pr=None, URI=None) |
|||
except InvoiceError as e: |
|||
self.invoiceCreateError.emit('fatal', _('Error creating payment') + ':\n' + str(e)) |
|||
return |
|||
|
|||
self.key = self._wallet.wallet.get_key_for_outgoing_invoice(invoice) |
|||
self._wallet.wallet.save_invoice(invoice) |
|||
self.invoiceSaved.emit() |
|||
|
|||
@pyqtSlot() |
|||
def clear(self): |
|||
self._recipient = None |
|||
self._amount = QEAmount() |
|||
self._message = None |
|||
self.canSave = False |
|||
self.canPay = False |
@ -0,0 +1,173 @@ |
|||
from abc import abstractmethod |
|||
|
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
from PyQt5.QtCore import Qt, QAbstractListModel, QModelIndex |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import Satoshis, format_time |
|||
from electrum.invoices import Invoice |
|||
|
|||
from .qetypes import QEAmount |
|||
|
|||
class QEAbstractInvoiceListModel(QAbstractListModel): |
|||
_logger = get_logger(__name__) |
|||
|
|||
def __init__(self, wallet, parent=None): |
|||
super().__init__(parent) |
|||
self.wallet = wallet |
|||
self.init_model() |
|||
|
|||
# define listmodel rolemap |
|||
_ROLE_NAMES=('key', 'is_lightning', 'timestamp', 'date', 'message', 'amount', |
|||
'status', 'status_str', 'address', 'expiration', 'type', 'onchain_fallback', |
|||
'lightning_invoice') |
|||
_ROLE_KEYS = range(Qt.UserRole, Qt.UserRole + len(_ROLE_NAMES)) |
|||
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES])) |
|||
_ROLE_RMAP = dict(zip(_ROLE_NAMES, _ROLE_KEYS)) |
|||
|
|||
def rowCount(self, index): |
|||
return len(self.invoices) |
|||
|
|||
def roleNames(self): |
|||
return self._ROLE_MAP |
|||
|
|||
def data(self, index, role): |
|||
invoice = self.invoices[index.row()] |
|||
role_index = role - Qt.UserRole |
|||
value = invoice[self._ROLE_NAMES[role_index]] |
|||
|
|||
if isinstance(value, (bool, list, int, str, QEAmount)) or value is None: |
|||
return value |
|||
if isinstance(value, Satoshis): |
|||
return value.value |
|||
return str(value) |
|||
|
|||
def clear(self): |
|||
self.beginResetModel() |
|||
self.invoices = [] |
|||
self.endResetModel() |
|||
|
|||
@pyqtSlot() |
|||
def init_model(self): |
|||
invoices = [] |
|||
for invoice in self.get_invoice_list(): |
|||
item = self.invoice_to_model(invoice) |
|||
#self._logger.debug(str(item)) |
|||
invoices.append(item) |
|||
|
|||
self.clear() |
|||
self.beginInsertRows(QModelIndex(), 0, len(invoices) - 1) |
|||
self.invoices = invoices |
|||
self.endInsertRows() |
|||
|
|||
def add_invoice(self, invoice: Invoice): |
|||
item = self.invoice_to_model(invoice) |
|||
self._logger.debug(str(item)) |
|||
|
|||
self.beginInsertRows(QModelIndex(), 0, 0) |
|||
self.invoices.insert(0, item) |
|||
self.endInsertRows() |
|||
|
|||
def delete_invoice(self, key: str): |
|||
i = 0 |
|||
for invoice in self.invoices: |
|||
if invoice['key'] == key: |
|||
self.beginRemoveRows(QModelIndex(), i, i) |
|||
self.invoices.pop(i) |
|||
self.endRemoveRows() |
|||
break |
|||
i = i + 1 |
|||
|
|||
def get_model_invoice(self, key: str): |
|||
for invoice in self.invoices: |
|||
if invoice['key'] == key: |
|||
return invoice |
|||
return None |
|||
|
|||
@pyqtSlot(str, int) |
|||
def updateInvoice(self, key, status): |
|||
self._logger.debug('updating invoice for %s to %d' % (key,status)) |
|||
i = 0 |
|||
for item in self.invoices: |
|||
if item['key'] == key: |
|||
invoice = self.get_invoice_for_key(key) |
|||
item['status'] = status |
|||
item['status_str'] = invoice.get_status_str(status) |
|||
index = self.index(i,0) |
|||
self.dataChanged.emit(index, index, [self._ROLE_RMAP['status'], self._ROLE_RMAP['status_str']]) |
|||
return |
|||
i = i + 1 |
|||
|
|||
def invoice_to_model(self, invoice: Invoice): |
|||
item = self.get_invoice_as_dict(invoice) |
|||
#item['key'] = invoice.get_id() |
|||
item['is_lightning'] = invoice.is_lightning() |
|||
if invoice.is_lightning() and 'address' not in item: |
|||
item['address'] = '' |
|||
item['date'] = format_time(item['timestamp']) |
|||
item['amount'] = QEAmount(from_invoice=invoice) |
|||
item['onchain_fallback'] = invoice.is_lightning() and invoice._lnaddr.get_fallback_address() |
|||
item['type'] = 'invoice' |
|||
|
|||
return item |
|||
|
|||
@abstractmethod |
|||
def get_invoice_for_key(self, key: str): |
|||
raise Exception('provide impl') |
|||
|
|||
@abstractmethod |
|||
def get_invoice_list(self): |
|||
raise Exception('provide impl') |
|||
|
|||
@abstractmethod |
|||
def get_invoice_as_dict(self, invoice: Invoice): |
|||
raise Exception('provide impl') |
|||
|
|||
|
|||
class QEInvoiceListModel(QEAbstractInvoiceListModel): |
|||
def __init__(self, wallet, parent=None): |
|||
super().__init__(wallet, parent) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
def invoice_to_model(self, invoice: Invoice): |
|||
item = super().invoice_to_model(invoice) |
|||
item['type'] = 'invoice' |
|||
item['key'] = invoice.get_id() |
|||
|
|||
return item |
|||
|
|||
def get_invoice_list(self): |
|||
return self.wallet.get_unpaid_invoices() |
|||
|
|||
def get_invoice_for_key(self, key: str): |
|||
return self.wallet.get_invoice(key) |
|||
|
|||
def get_invoice_as_dict(self, invoice: Invoice): |
|||
return self.wallet.export_invoice(invoice) |
|||
|
|||
class QERequestListModel(QEAbstractInvoiceListModel): |
|||
def __init__(self, wallet, parent=None): |
|||
super().__init__(wallet, parent) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
def invoice_to_model(self, invoice: Invoice): |
|||
item = super().invoice_to_model(invoice) |
|||
item['type'] = 'request' |
|||
item['key'] = invoice.get_id() if invoice.is_lightning() else invoice.get_address() |
|||
|
|||
return item |
|||
|
|||
def get_invoice_list(self): |
|||
return self.wallet.get_unpaid_requests() |
|||
|
|||
def get_invoice_for_key(self, key: str): |
|||
return self.wallet.get_request(key) |
|||
|
|||
def get_invoice_as_dict(self, invoice: Invoice): |
|||
return self.wallet.export_request(invoice) |
|||
|
|||
@pyqtSlot(str, int) |
|||
def updateRequest(self, key, status): |
|||
self.updateInvoice(key, status) |
@ -0,0 +1,114 @@ |
|||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject |
|||
|
|||
from electrum.logging import get_logger |
|||
from electrum.util import format_time, bfh, format_time |
|||
|
|||
from .qewallet import QEWallet |
|||
from .qetypes import QEAmount |
|||
|
|||
class QELnPaymentDetails(QObject): |
|||
def __init__(self, parent=None): |
|||
super().__init__(parent) |
|||
|
|||
_logger = get_logger(__name__) |
|||
|
|||
_wallet = None |
|||
_key = None |
|||
_date = None |
|||
|
|||
detailsChanged = pyqtSignal() |
|||
|
|||
walletChanged = pyqtSignal() |
|||
@pyqtProperty(QEWallet, notify=walletChanged) |
|||
def wallet(self): |
|||
return self._wallet |
|||
|
|||
@wallet.setter |
|||
def wallet(self, wallet: QEWallet): |
|||
if self._wallet != wallet: |
|||
self._wallet = wallet |
|||
self.walletChanged.emit() |
|||
|
|||
keyChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=keyChanged) |
|||
def key(self): |
|||
return self._key |
|||
|
|||
@key.setter |
|||
def key(self, key: str): |
|||
if self._key != key: |
|||
self._logger.debug('key set -> %s' % key) |
|||
self._key = key |
|||
self.keyChanged.emit() |
|||
self.update() |
|||
|
|||
labelChanged = pyqtSignal() |
|||
@pyqtProperty(str, notify=labelChanged) |
|||
def label(self): |
|||
return self._label |
|||
|
|||
@pyqtSlot(str) |
|||
def set_label(self, label: str): |
|||
if label != self._label: |
|||
self._wallet.wallet.set_label(self._key, label) |
|||
self._label = label |
|||
self.labelChanged.emit() |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def status(self): |
|||
return self._status |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def date(self): |
|||
return self._date |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def payment_hash(self): |
|||
return self._phash |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def preimage(self): |
|||
return self._preimage |
|||
|
|||
@pyqtProperty(str, notify=detailsChanged) |
|||
def invoice(self): |
|||
return self._invoice |
|||
|
|||
@pyqtProperty(QEAmount, notify=detailsChanged) |
|||
def amount(self): |
|||
return self._amount |
|||
|
|||
@pyqtProperty(QEAmount, notify=detailsChanged) |
|||
def fee(self): |
|||
return self._fee |
|||
|
|||
def update(self): |
|||
if self._wallet is None: |
|||
self._logger.error('wallet undefined') |
|||
return |
|||
|
|||
if self._key not in self._wallet.wallet.lnworker.payment_info: |
|||
self._logger.error('payment_hash not found') |
|||
return |
|||
|
|||
# TODO this is horribly inefficient. need a payment getter/query method |
|||
tx = self._wallet.wallet.lnworker.get_lightning_history()[bfh(self._key)] |
|||
self._logger.debug(str(tx)) |
|||
|
|||
self._fee = QEAmount() if not tx['fee_msat'] else QEAmount(amount_msat=tx['fee_msat']) |
|||
self._amount = QEAmount(amount_msat=tx['amount_msat']) |
|||
self._label = tx['label'] |
|||
self._date = format_time(tx['timestamp']) |
|||
self._status = 'settled' # TODO: other states? get_lightning_history is deciding the filter for us :( |
|||
self._phash = tx['payment_hash'] |
|||
self._preimage = tx['preimage'] |
|||
|
|||
invoice = (self._wallet.wallet.get_invoice(self._key) |
|||
or self._wallet.wallet.get_request(self._key)) |
|||
self._logger.debug(str(invoice)) |
|||
if invoice: |
|||
self._invoice = invoice.lightning_invoice or '' |
|||
else: |
|||
self._invoice = '' |
|||
|
|||
self.detailsChanged.emit() |