wip lightning
This commit is contained in:
132
electrum/gui/qml/components/Channels.qml
Normal file
132
electrum/gui/qml/components/Channels.qml
Normal file
@@ -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
Normal file
145
electrum/gui/qml/components/OpenChannel.qml
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,7 +12,6 @@ Item {
|
||||
property bool toolbar: false
|
||||
|
||||
property string scanData
|
||||
property var invoiceData: undefined
|
||||
property string error
|
||||
|
||||
signal found
|
||||
@@ -24,16 +23,6 @@ Item {
|
||||
|
||||
onFound: {
|
||||
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()
|
||||
app.stack.pop()
|
||||
}
|
||||
@@ -46,8 +35,4 @@ Item {
|
||||
text: 'Cancel'
|
||||
onClicked: app.stack.pop()
|
||||
}
|
||||
|
||||
Bitcoin {
|
||||
id: bitcoin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,16 @@ Item {
|
||||
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 {
|
||||
icon.color: 'transparent'
|
||||
action: Action {
|
||||
|
||||
@@ -23,6 +23,7 @@ from .qeinvoice import QEInvoice
|
||||
from .qetypes import QEAmount
|
||||
from .qeaddressdetails import QEAddressDetails
|
||||
from .qetxdetails import QETxDetails
|
||||
from .qechannelopener import QEChannelOpener
|
||||
|
||||
notification = None
|
||||
|
||||
@@ -143,6 +144,7 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
qmlRegisterType(QEInvoice, 'org.electrum', 1, 0, 'Invoice')
|
||||
qmlRegisterType(QEAddressDetails, 'org.electrum', 1, 0, 'AddressDetails')
|
||||
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')
|
||||
|
||||
@@ -165,11 +167,13 @@ class ElectrumQmlApplication(QGuiApplication):
|
||||
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,
|
||||
|
||||
53
electrum/gui/qml/qechannellistmodel.py
Normal file
53
electrum/gui/qml/qechannellistmodel.py
Normal file
@@ -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
Normal file
160
electrum/gui/qml/qechannelopener.py
Normal file
@@ -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()
|
||||
@@ -17,6 +17,7 @@ from electrum.invoices import (Invoice, InvoiceError,
|
||||
from .qeinvoicelistmodel import QEInvoiceListModel, QERequestListModel
|
||||
from .qetransactionlistmodel import QETransactionListModel
|
||||
from .qeaddresslistmodel import QEAddressListModel
|
||||
from .qechannellistmodel import QEChannelListModel
|
||||
from .qetypes import QEAmount
|
||||
|
||||
class QEWallet(QObject):
|
||||
@@ -60,6 +61,7 @@ class QEWallet(QObject):
|
||||
self._addressModel = QEAddressListModel(wallet)
|
||||
self._requestModel = QERequestListModel(wallet)
|
||||
self._invoiceModel = QEInvoiceListModel(wallet)
|
||||
self._channelModel = None
|
||||
|
||||
self._historyModel.init_model()
|
||||
self._requestModel.init_model()
|
||||
@@ -192,6 +194,13 @@ class QEWallet(QObject):
|
||||
def invoiceModel(self):
|
||||
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()
|
||||
@pyqtProperty('QString', notify=nameChanged)
|
||||
def name(self):
|
||||
|
||||
Reference in New Issue
Block a user