Sander van Grieken
3 years ago
8 changed files with 513 additions and 15 deletions
@ -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() |
|||
} |
@ -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) |
|||
}) |
|||
} |
|||
} |
|||
|
|||
} |
@ -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 [] |
@ -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() |
Loading…
Reference in new issue