Browse Source

Merge branch 'android-qml'

patch-4
SomberNight 3 years ago
parent
commit
60a0fcb6e5
No known key found for this signature in database GPG Key ID: B33B5F232C6271E9
  1. 4
      contrib/android/Dockerfile
  2. 15
      contrib/android/buildozer_qml.spec
  3. 18
      contrib/android/p4a_recipes/Pillow/__init__.py
  4. 18
      contrib/android/p4a_recipes/freetype/__init__.py
  5. 18
      contrib/android/p4a_recipes/jpeg/__init__.py
  6. 18
      contrib/android/p4a_recipes/libiconv/__init__.py
  7. 18
      contrib/android/p4a_recipes/libzbar/__init__.py
  8. 18
      contrib/android/p4a_recipes/png/__init__.py
  9. 18
      contrib/android/p4a_recipes/pyqt5/__init__.py
  10. 18
      contrib/android/p4a_recipes/pyqt5sip/__init__.py
  11. 8
      contrib/android/p4a_recipes/qt5/__init__.py
  12. 2
      electrum/commands.py
  13. BIN
      electrum/gui/icons/closebutton.png
  14. BIN
      electrum/gui/icons/confirmed_bw.png
  15. BIN
      electrum/gui/icons/copy_bw.png
  16. BIN
      electrum/gui/icons/delete.png
  17. BIN
      electrum/gui/icons/globe.png
  18. BIN
      electrum/gui/icons/mail_icon.png
  19. BIN
      electrum/gui/icons/paste.png
  20. BIN
      electrum/gui/icons/pen.png
  21. BIN
      electrum/gui/icons/save.png
  22. BIN
      electrum/gui/icons/share.png
  23. BIN
      electrum/gui/icons/wallet.png
  24. 117
      electrum/gui/qml/__init__.py
  25. 58
      electrum/gui/qml/auth.py
  26. 90
      electrum/gui/qml/components/About.qml
  27. 268
      electrum/gui/qml/components/AddressDetails.qml
  28. 172
      electrum/gui/qml/components/Addresses.qml
  29. 160
      electrum/gui/qml/components/BalanceSummary.qml
  30. 270
      electrum/gui/qml/components/ChannelDetails.qml
  31. 162
      electrum/gui/qml/components/Channels.qml
  32. 130
      electrum/gui/qml/components/CloseChannelDialog.qml
  33. 262
      electrum/gui/qml/components/ConfirmTxDialog.qml
  34. 35
      electrum/gui/qml/components/Constants.qml
  35. 211
      electrum/gui/qml/components/History.qml
  36. 233
      electrum/gui/qml/components/InvoiceDialog.qml
  37. 261
      electrum/gui/qml/components/LightningPaymentDetails.qml
  38. 130
      electrum/gui/qml/components/LightningPaymentProgressDialog.qml
  39. 90
      electrum/gui/qml/components/NetworkStats.qml
  40. 110
      electrum/gui/qml/components/NewWalletWizard.qml
  41. 187
      electrum/gui/qml/components/OpenChannel.qml
  42. 137
      electrum/gui/qml/components/OpenWallet.qml
  43. 90
      electrum/gui/qml/components/Pin.qml
  44. 199
      electrum/gui/qml/components/Preferences.qml
  45. 239
      electrum/gui/qml/components/Receive.qml
  46. 228
      electrum/gui/qml/components/RequestDialog.qml
  47. 38
      electrum/gui/qml/components/Scan.qml
  48. 359
      electrum/gui/qml/components/Send.qml
  49. 53
      electrum/gui/qml/components/ServerConnectWizard.qml
  50. 16
      electrum/gui/qml/components/Splash.qml
  51. 205
      electrum/gui/qml/components/Swap.qml
  52. 262
      electrum/gui/qml/components/TxDetails.qml
  53. 168
      electrum/gui/qml/components/WalletMainView.qml
  54. 348
      electrum/gui/qml/components/Wallets.qml
  55. 44
      electrum/gui/qml/components/WizardComponents.qml
  56. 30
      electrum/gui/qml/components/controls/BtcField.qml
  57. 120
      electrum/gui/qml/components/controls/ChannelDelegate.qml
  58. 30
      electrum/gui/qml/components/controls/FiatField.qml
  59. 119
      electrum/gui/qml/components/controls/GenericShareDialog.qml
  60. 58
      electrum/gui/qml/components/controls/InfoTextArea.qml
  61. 147
      electrum/gui/qml/components/controls/InvoiceDelegate.qml
  62. 61
      electrum/gui/qml/components/controls/MessageDialog.qml
  63. 30
      electrum/gui/qml/components/controls/MessagePane.qml
  64. 61
      electrum/gui/qml/components/controls/NotificationPopup.qml
  65. 26
      electrum/gui/qml/components/controls/PaneInsetBackground.qml
  66. 115
      electrum/gui/qml/components/controls/PasswordDialog.qml
  67. 164
      electrum/gui/qml/components/controls/QRScan.qml
  68. 20
      electrum/gui/qml/components/controls/SeedTextArea.qml
  69. 29
      electrum/gui/qml/components/controls/Tag.qml
  70. 13
      electrum/gui/qml/components/controls/TextHighlightPane.qml
  71. 315
      electrum/gui/qml/components/main.qml
  72. 41
      electrum/gui/qml/components/wizard/WCAutoConnect.qml
  73. 99
      electrum/gui/qml/components/wizard/WCBIP39Refine.qml
  74. 59
      electrum/gui/qml/components/wizard/WCConfirmSeed.qml
  75. 88
      electrum/gui/qml/components/wizard/WCCreateSeed.qml
  76. 151
      electrum/gui/qml/components/wizard/WCHaveSeed.qml
  77. 43
      electrum/gui/qml/components/wizard/WCKeystoreType.qml
  78. 94
      electrum/gui/qml/components/wizard/WCProxyConfig.qml
  79. 42
      electrum/gui/qml/components/wizard/WCServerConfig.qml
  80. 27
      electrum/gui/qml/components/wizard/WCWalletName.qml
  81. 25
      electrum/gui/qml/components/wizard/WCWalletPassword.qml
  82. 43
      electrum/gui/qml/components/wizard/WCWalletType.qml
  83. 175
      electrum/gui/qml/components/wizard/Wizard.qml
  84. 10
      electrum/gui/qml/components/wizard/WizardComponent.qml
  85. BIN
      electrum/gui/qml/fonts/PTMono-Bold.ttf
  86. BIN
      electrum/gui/qml/fonts/PTMono-Regular.ttf
  87. 94
      electrum/gui/qml/fonts/PTMono.LICENSE
  88. 130
      electrum/gui/qml/qeaddressdetails.py
  89. 101
      electrum/gui/qml/qeaddresslistmodel.py
  90. 211
      electrum/gui/qml/qeapp.py
  91. 132
      electrum/gui/qml/qebitcoin.py
  92. 182
      electrum/gui/qml/qechanneldetails.py
  93. 154
      electrum/gui/qml/qechannellistmodel.py
  94. 182
      electrum/gui/qml/qechannelopener.py
  95. 168
      electrum/gui/qml/qeconfig.py
  96. 218
      electrum/gui/qml/qedaemon.py
  97. 154
      electrum/gui/qml/qefx.py
  98. 491
      electrum/gui/qml/qeinvoice.py
  99. 173
      electrum/gui/qml/qeinvoicelistmodel.py
  100. 114
      electrum/gui/qml/qelnpaymentdetails.py

4
contrib/android/Dockerfile

@ -169,8 +169,8 @@ RUN cd /opt \
&& git remote add sombernight https://github.com/SomberNight/python-for-android \
&& git remote add accumulator https://github.com/accumulator/python-for-android \
&& git fetch --all \
# commit: from branch accumulator/qt5-wip
&& git checkout "64490fc4a7b1f727f1f07c86e1bdc6b291ffc6da^{commit}" \
# commit: from branch sombernight/qt5-wip
&& git checkout "c6e39ae1fb4eb8d547eb70b26b89beda7e6ff4b6^{commit}" \
&& python3 -m pip install --no-build-isolation --no-dependencies --user -e .
# build env vars

15
contrib/android/buildozer_qml.spec

@ -13,18 +13,23 @@ package.domain = org.electrum
source.dir = .
# (list) Source files to include (let empty to include all the files)
source.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,vs,fs,json,csv,so
source.include_exts = py,png,jpg,qml,qmltypes,ttf,txt,gif,pem,mo,json,csv,so
# (list) Source files to exclude (let empty to not exclude anything)
source.exclude_exts = spec
# (list) List of directory to exclude (let empty to not exclude anything)
source.exclude_dirs = bin, build, dist, contrib,
source.exclude_dirs = bin, build, dist, contrib, env,
electrum/tests,
electrum/www,
electrum/gui/qt,
electrum/gui/kivy,
packages/qdarkstyle,
packages/qtpy
packages/qtpy,
packages/bin,
packages/share,
packages/pkg_resources,
packages/setuptools
# (list) List of exclusions using pattern matching
source.exclude_patterns = Makefile,setup*,
@ -51,7 +56,9 @@ requirements =
libsecp256k1,
cryptography,
pyqt5sip,
pyqt5
pyqt5,
pillow,
libzbar
# (str) Presplash of the application
#presplash.filename = %(source.dir)s/gui/kivy/theming/splash.png

18
contrib/android/p4a_recipes/Pillow/__init__.py

@ -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()

18
contrib/android/p4a_recipes/freetype/__init__.py

@ -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()

18
contrib/android/p4a_recipes/jpeg/__init__.py

@ -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()

18
contrib/android/p4a_recipes/libiconv/__init__.py

@ -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()

18
contrib/android/p4a_recipes/libzbar/__init__.py

@ -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()

18
contrib/android/p4a_recipes/png/__init__.py

@ -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()

18
contrib/android/p4a_recipes/pyqt5/__init__.py

@ -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()

18
contrib/android/p4a_recipes/pyqt5sip/__init__.py

@ -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()

8
contrib/android/p4a_recipes/qt5/__init__.py

@ -0,0 +1,8 @@
from pythonforandroid.recipes.qt5 import Qt5Recipe
assert Qt5Recipe._version == "9b43a43ee96198674060c6b9591e515e2d27c28f"
assert Qt5Recipe.depends == ['python3']
assert Qt5Recipe.python_depends == []
recipe = Qt5Recipe()

2
electrum/commands.py

@ -1502,7 +1502,7 @@ def get_parser():
# gui
parser_gui = subparsers.add_parser('gui', description="Run Electrum's Graphical User Interface.", help="Run GUI (default)")
parser_gui.add_argument("url", nargs='?', default=None, help="bitcoin URI (or bip70 file)")
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio'])
parser_gui.add_argument("-g", "--gui", dest="gui", help="select graphical user interface", choices=['qt', 'kivy', 'text', 'stdio', 'qml'])
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")

BIN
electrum/gui/icons/closebutton.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
electrum/gui/icons/confirmed_bw.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
electrum/gui/icons/copy_bw.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 880 B

BIN
electrum/gui/icons/delete.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

BIN
electrum/gui/icons/globe.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

BIN
electrum/gui/icons/mail_icon.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
electrum/gui/icons/paste.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
electrum/gui/icons/pen.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
electrum/gui/icons/save.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 B

BIN
electrum/gui/icons/share.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
electrum/gui/icons/wallet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

117
electrum/gui/qml/__init__.py

@ -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'

58
electrum/gui/qml/auth.py

@ -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')

90
electrum/gui/qml/components/About.qml

@ -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
}
}
}
}

268
electrum/gui/qml/components/AddressDetails.qml

@ -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 {}
}
}

172
electrum/gui/qml/components/Addresses.qml

@ -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
}
}
}
}
}

160
electrum/gui/qml/components/BalanceSummary.qml

@ -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()
}

270
electrum/gui/qml/components/ChannelDetails.qml

@ -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 {}
}
}

162
electrum/gui/qml/components/Channels.qml

@ -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 {}
}
}

130
electrum/gui/qml/components/CloseChannelDialog.qml

@ -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
}
}
}

262
electrum/gui/qml/components/ConfirmTxDialog.qml

@ -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()
}
}
}
}
}

35
electrum/gui/qml/components/Constants.qml

@ -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"
}

211
electrum/gui/qml/components/History.qml

@ -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)
}
}
}

233
electrum/gui/qml/components/InvoiceDialog.qml

@ -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)
}
}
}

261
electrum/gui/qml/components/LightningPaymentDetails.qml

@ -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 {}
}
}

130
electrum/gui/qml/components/LightningPaymentProgressDialog.qml

@ -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()
}
}
}

90
electrum/gui/qml/components/NetworkStats.qml

@ -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()
}

110
electrum/gui/qml/components/NewWalletWizard.qml

@ -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()
}
}

187
electrum/gui/qml/components/OpenChannel.qml

@ -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()
}
}
}

137
electrum/gui/qml/components/OpenWallet.qml

@ -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()
}
}

90
electrum/gui/qml/components/Pin.qml

@ -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 }
}
}

199
electrum/gui/qml/components/Preferences.qml

@ -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
}
}

239
electrum/gui/qml/components/Receive.qml

@ -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)
}
}
}

228
electrum/gui/qml/components/RequestDialog.qml

@ -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
}
}

38
electrum/gui/qml/components/Scan.qml

@ -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()
}
}

359
electrum/gui/qml/components/Send.qml

@ -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()
}
}
}

53
electrum/gui/qml/components/ServerConnectWizard.qml

@ -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 {}
}
}

16
electrum/gui/qml/components/Splash.qml

@ -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"
}
}

205
electrum/gui/qml/components/Swap.qml

@ -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()
}
}
}
}

262
electrum/gui/qml/components/TxDetails.qml

@ -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 {}
}
}

168
electrum/gui/qml/components/WalletMainView.qml

@ -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)
}
}
}

348
electrum/gui/qml/components/Wallets.qml

@ -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()
}
}
}

44
electrum/gui/qml/components/WizardComponents.qml

@ -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 {}
}
}

30
electrum/gui/qml/components/controls/BtcField.qml

@ -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)
: ''
}
}
}

120
electrum/gui/qml/components/controls/ChannelDelegate.qml

@ -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'
}
}
}

30
electrum/gui/qml/components/controls/FiatField.qml

@ -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))
}
}
}

119
electrum/gui/qml/components/controls/GenericShareDialog.qml

@ -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
}
}

58
electrum/gui/qml/components/controls/InfoTextArea.qml

@ -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)
}
}

147
electrum/gui/qml/components/controls/InvoiceDelegate.qml

@ -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)
}
}
}

61
electrum/gui/qml/components/controls/MessageDialog.qml

@ -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()
}
}
}
}
}

30
electrum/gui/qml/components/controls/MessagePane.qml

@ -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
}
}

61
electrum/gui/qml/components/controls/NotificationPopup.qml

@ -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
}
}

26
electrum/gui/qml/components/controls/PaneInsetBackground.qml

@ -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)
}

115
electrum/gui/qml/components/controls/PasswordDialog.qml

@ -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()
}
}
}
}
}

164
electrum/gui/qml/components/controls/QRScan.qml

@ -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
}
}

20
electrum/gui/qml/components/controls/SeedTextArea.qml

@ -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
}
}

29
electrum/gui/qml/components/controls/Tag.qml

@ -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
}
}
}

13
electrum/gui/qml/components/controls/TextHighlightPane.qml

@ -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
}
}

315
electrum/gui/qml/components/main.qml

@ -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()
}
}
}
}

41
electrum/gui/qml/components/wizard/WCAutoConnect.qml

@ -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')
}
}
}

99
electrum/gui/qml/components/wizard/WCBIP39Refine.qml

@ -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
}
}

59
electrum/gui/qml/components/wizard/WCConfirmSeed.qml

@ -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']
}
}

88
electrum/gui/qml/components/wizard/WCCreateSeed.qml

@ -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)
}
}
}

151
electrum/gui/qml/components/wizard/WCHaveSeed.qml

@ -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()
}
}

43
electrum/gui/qml/components/wizard/WCKeystoreType.qml

@ -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')
}
}
}

94
electrum/gui/qml/components/wizard/WCProxyConfig.qml

@ -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
}
}
}
}

42
electrum/gui/qml/components/wizard/WCServerConfig.qml

@ -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
}
}
}
}

27
electrum/gui/qml/components/wizard/WCWalletName.qml

@ -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()
}
}

25
electrum/gui/qml/components/wizard/WCWalletPassword.qml

@ -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
}
}
}

43
electrum/gui/qml/components/wizard/WCWalletType.qml

@ -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')
}
}
}

175
electrum/gui/qml/components/wizard/Wizard.qml

@ -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 }
}
}

10
electrum/gui/qml/components/wizard/WizardComponent.qml

@ -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
}

BIN
electrum/gui/qml/fonts/PTMono-Bold.ttf

Binary file not shown.

BIN
electrum/gui/qml/fonts/PTMono-Regular.ttf

Binary file not shown.

94
electrum/gui/qml/fonts/PTMono.LICENSE

@ -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.

130
electrum/gui/qml/qeaddressdetails.py

@ -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()

101
electrum/gui/qml/qeaddresslistmodel.py

@ -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)

211
electrum/gui/qml/qeapp.py

@ -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)

132
electrum/gui/qml/qebitcoin.py

@ -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)

182
electrum/gui/qml/qechanneldetails.py

@ -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)

154
electrum/gui/qml/qechannellistmodel.py

@ -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

182
electrum/gui/qml/qechannelopener.py

@ -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()

168
electrum/gui/qml/qeconfig.py

@ -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)

218
electrum/gui/qml/qedaemon.py

@ -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)

154
electrum/gui/qml/qefx.py

@ -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)

491
electrum/gui/qml/qeinvoice.py

@ -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

173
electrum/gui/qml/qeinvoicelistmodel.py

@ -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)

114
electrum/gui/qml/qelnpaymentdetails.py

@ -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()

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save