Browse Source

wip lightning

master
Sander van Grieken 4 years ago
parent
commit
c55aa7bb48
  1. 132
      electrum/gui/qml/components/Channels.qml
  2. 145
      electrum/gui/qml/components/OpenChannel.qml
  3. 15
      electrum/gui/qml/components/Scan.qml
  4. 10
      electrum/gui/qml/components/WalletMainView.qml
  5. 4
      electrum/gui/qml/qeapp.py
  6. 53
      electrum/gui/qml/qechannellistmodel.py
  7. 160
      electrum/gui/qml/qechannelopener.py
  8. 9
      electrum/gui/qml/qewallet.py

132
electrum/gui/qml/components/Channels.qml

@ -0,0 +1,132 @@
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 {
property string title: qsTr("Lightning Channels")
ColumnLayout {
id: layout
width: parent.width
height: parent.height
GridLayout {
id: summaryLayout
Layout.preferredWidth: parent.width
columns: 2
Label {
Layout.columnSpan: 2
text: ''
}
Label {
text: qsTr('You can send:')
color: Material.accentColor
}
Label {
text: ''
}
Label {
text: qsTr('You can receive:')
color: Material.accentColor
}
Label {
text: ''
}
RowLayout {
Layout.columnSpan: 2
Button {
text: qsTr('Open Channel')
onClicked: app.stack.push(Qt.resolvedUrl('OpenChannel.qml'))
}
}
}
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: 3 //Daemon.currentWallet.channelsModel
delegate: ItemDelegate {
width: ListView.view.width
height: row.height
highlighted: ListView.isCurrentItem
font.pixelSize: constants.fontSizeMedium // set default font size for child controls
RowLayout {
id: row
spacing: 10
x: constants.paddingSmall
width: parent.width - 2 * constants.paddingSmall
Image {
id: walleticon
source: "../../icons/lightning.png"
fillMode: Image.PreserveAspectFit
Layout.preferredWidth: constants.iconSizeLarge
Layout.preferredHeight: constants.iconSizeLarge
}
Label {
font.pixelSize: constants.fontSizeLarge
text: index
Layout.fillWidth: true
}
}
}
ScrollIndicator.vertical: ScrollIndicator { }
}
}
}
}
Component.onCompleted: Daemon.currentWallet.channelModel.init_model()
}

145
electrum/gui/qml/components/OpenChannel.qml

@ -0,0 +1,145 @@
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')
}
TextArea {
id: node
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 {
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
})
}
}
}
Label {
text: qsTr('Amount')
}
BtcField {
id: amount
fiatfield: amountFiat
Layout.preferredWidth: parent.width /2
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: {
if (checked) {
channelopener.amount = MAX
}
}
}
}
Item { width: 1; height: 1; visible: Daemon.fx.enabled }
FiatField {
id: amountFiat
btcfield: amount
visible: Daemon.fx.enabled
Layout.preferredWidth: parent.width /2
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()
}
}
}
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 })
dialog.open()
dialog.yesClicked.connect(function() {
channelopener.open_channel(true)
})
}
}
}

15
electrum/gui/qml/components/Scan.qml

@ -12,7 +12,6 @@ Item {
property bool toolbar: false property bool toolbar: false
property string scanData property string scanData
property var invoiceData: undefined
property string error property string error
signal found signal found
@ -24,16 +23,6 @@ Item {
onFound: { onFound: {
scanPage.scanData = scanData scanPage.scanData = scanData
var invoice = bitcoin.parse_uri(scanData)
if (invoice['error']) {
error = invoice['error']
console.log(error)
app.stack.pop()
return
}
invoiceData = invoice
console.log(invoiceData['address'])
scanPage.found() scanPage.found()
app.stack.pop() app.stack.pop()
} }
@ -46,8 +35,4 @@ Item {
text: 'Cancel' text: 'Cancel'
onClicked: app.stack.pop() onClicked: app.stack.pop()
} }
Bitcoin {
id: bitcoin
}
} }

10
electrum/gui/qml/components/WalletMainView.qml

@ -35,6 +35,16 @@ Item {
icon.source: '../../icons/network.png' icon.source: '../../icons/network.png'
} }
} }
MenuItem {
icon.color: 'transparent'
action: Action {
text: qsTr('Channels');
enabled: Daemon.currentWallet.isLightning
onTriggered: menu.openPage(Qt.resolvedUrl('Channels.qml'))
icon.source: '../../icons/lightning.png'
}
}
MenuItem { MenuItem {
icon.color: 'transparent' icon.color: 'transparent'
action: Action { action: Action {

4
electrum/gui/qml/qeapp.py

@ -23,6 +23,7 @@ from .qeinvoice import QEInvoice
from .qetypes import QEAmount from .qetypes import QEAmount
from .qeaddressdetails import QEAddressDetails from .qeaddressdetails import QEAddressDetails
from .qetxdetails import QETxDetails from .qetxdetails import QETxDetails
from .qechannelopener import QEChannelOpener
notification = None notification = None
@ -143,6 +144,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice') qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails') qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')
qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails') qmlRegisterType(QETxDetails, 'org.electrum', 1, 0, 'TxDetails')
qmlRegisterType(QEChannelOpener, 'org.electrum', 1, 0, 'ChannelOpener')
qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property') qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
@ -165,11 +167,13 @@ class ElectrumQmlApplication(QGuiApplication):
self._qenetwork = QENetwork(daemon.network) self._qenetwork = QENetwork(daemon.network)
self._qedaemon = QEDaemon(daemon) self._qedaemon = QEDaemon(daemon)
self._appController = QEAppController(self._qedaemon) self._appController = QEAppController(self._qedaemon)
self._maxAmount = QEAmount(is_max=True)
self.context.setContextProperty('AppController', self._appController) self.context.setContextProperty('AppController', self._appController)
self.context.setContextProperty('Config', self._qeconfig) self.context.setContextProperty('Config', self._qeconfig)
self.context.setContextProperty('Network', self._qenetwork) self.context.setContextProperty('Network', self._qenetwork)
self.context.setContextProperty('Daemon', self._qedaemon) self.context.setContextProperty('Daemon', self._qedaemon)
self.context.setContextProperty('FixedFont', self.fixedFont) self.context.setContextProperty('FixedFont', self.fixedFont)
self.context.setContextProperty('MAX', self._maxAmount)
self.context.setContextProperty('BUILD', { self.context.setContextProperty('BUILD', {
'electrum_version': version.ELECTRUM_VERSION, 'electrum_version': version.ELECTRUM_VERSION,
'apk_version': version.APK_VERSION, 'apk_version': version.APK_VERSION,

53
electrum/gui/qml/qechannellistmodel.py

@ -0,0 +1,53 @@
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, TxMinedInfo
from .qetypes import QEAmount
class QEChannelListModel(QAbstractListModel):
def __init__(self, wallet, parent=None):
super().__init__(parent)
self.wallet = wallet
self.channels = []
_logger = get_logger(__name__)
# define listmodel rolemap
_ROLE_NAMES=('cid','state','initiator','capacity','can_send','can_receive',
'l_csv_delat','r_csv_delay','send_frozen','receive_frozen',
'type','node_id','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))
def rowCount(self, index):
return len(self.tx_history)
def roleNames(self):
return self._ROLE_MAP
def data(self, index, role):
tx = self.tx_history[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
if isinstance(value, QEAmount):
return value
return str(value)
@pyqtSlot()
def init_model(self):
if not self.wallet.lnworker:
self._logger.warning('lnworker should be defined')
return
channels = self.wallet.lnworker.channels
self._logger.debug(repr(channels))
#channels = list(lnworker.channels.values()) if lnworker else []

160
electrum/gui/qml/qechannelopener.py

@ -0,0 +1,160 @@
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.logging import get_logger
from electrum.util import format_time
from electrum.lnutil import extract_nodeid, ConnStringFormatError
from electrum.gui import messages
from .qewallet import QEWallet
from .qetypes import QEAmount
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'])
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
openTxChanged = pyqtSignal()
@pyqtProperty(bool, notify=openTxChanged)
def openTx(self):
return self._opentx
def validate(self):
nodeid_valid = False
if self._nodeid:
try:
self._node_pubkey, self._host_port = extract_nodeid(self._nodeid)
nodeid_valid = True
except ConnStringFormatError 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
#if self.use_gossip:
#conn_str = self.pubkey
#if self.ipport:
#conn_str += '@' + self.ipport.strip()
#else:
#conn_str = str(self.trampolines[self.pubkey])
amount = '!' if self._amount.isMax else self._amount.satsInt
lnworker = self._wallet.wallet.lnworker
if lnworker.has_conflicting_backup_with(node_pubkey) and not confirm_backup_conflict:
self.conflictingBackup.emit(messages.MGS_CONFLICTING_BACKUP_INSTANCE)
return
coins = self._wallet.wallet.get_spendable_coins(None, nonlocal_only=True)
#node_id, rest = extract_nodeid(conn_str)
make_tx = lambda rbf: lnworker.mktx_for_open_channel(
coins=coins,
funding_sat=amount,
node_id=self._node_pubkey,
fee_est=None)
#on_pay = lambda tx: self.app.protected('Create a new channel?', self.do_open_channel, (tx, conn_str))
#d = ConfirmTxDialog(
#self.app,
#amount = amount,
#make_tx=make_tx,
#on_pay=on_pay,
#show_final=False)
#d.open()
#def do_open_channel(self, funding_tx, conn_str, password):
## read funding_sat from tx; converts '!' to int value
#funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
#lnworker = self.app.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.app.logger.exception("Problem opening channel")
#self.app.show_error(_('Problem opening channel: ') + '\n' + repr(e))
#return
## 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()

9
electrum/gui/qml/qewallet.py

@ -17,6 +17,7 @@ from electrum.invoices import (Invoice, InvoiceError,
from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel
from .qetransactionlistmodel import QETransactionListModel from .qetransactionlistmodel import QETransactionListModel
from .qeaddresslistmodel import QEAddressListModel from .qeaddresslistmodel import QEAddressListModel
from .qechannellistmodel import QEChannelListModel
from .qetypes import QEAmount from .qetypes import QEAmount
class QEWallet(QObject): class QEWallet(QObject):
@ -60,6 +61,7 @@ class QEWallet(QObject):
self._addressModel = QEAddressListModel(wallet) self._addressModel = QEAddressListModel(wallet)
self._requestModel = QERequestListModel(wallet) self._requestModel = QERequestListModel(wallet)
self._invoiceModel = QEInvoiceListModel(wallet) self._invoiceModel = QEInvoiceListModel(wallet)
self._channelModel = None
self._historyModel.init_model() self._historyModel.init_model()
self._requestModel.init_model() self._requestModel.init_model()
@ -192,6 +194,13 @@ class QEWallet(QObject):
def invoiceModel(self): def invoiceModel(self):
return self._invoiceModel return self._invoiceModel
channelModelChanged = pyqtSignal()
@pyqtProperty(QEChannelListModel, notify=channelModelChanged)
def channelModel(self):
if self._channelModel is None:
self._channelModel = QEChannelListModel(self.wallet)
return self._channelModel
nameChanged = pyqtSignal() nameChanged = pyqtSignal()
@pyqtProperty('QString', notify=nameChanged) @pyqtProperty('QString', notify=nameChanged)
def name(self): def name(self):

Loading…
Cancel
Save