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