submarine_swaps: implement swaps-over-nostr for qml
This commit is contained in:
committed by
ThomasV
parent
22995b4a34
commit
993a575b7f
@@ -44,6 +44,7 @@ Item {
|
|||||||
property color colorValidBackground: '#ff008000'
|
property color colorValidBackground: '#ff008000'
|
||||||
property color colorInvalidBackground: '#ff800000'
|
property color colorInvalidBackground: '#ff800000'
|
||||||
property color colorAcceptable: '#ff8080ff'
|
property color colorAcceptable: '#ff8080ff'
|
||||||
|
property color colorOk: colorDone
|
||||||
|
|
||||||
property color colorLightningLocal: "#6060ff"
|
property color colorLightningLocal: "#6060ff"
|
||||||
property color colorLightningLocalReserve: "#0000a0"
|
property color colorLightningLocalReserve: "#0000a0"
|
||||||
|
|||||||
140
electrum/gui/qml/components/NostrSwapServersDialog.qml
Normal file
140
electrum/gui/qml/components/NostrSwapServersDialog.qml
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Controls.Material
|
||||||
|
|
||||||
|
import org.electrum 1.0
|
||||||
|
|
||||||
|
import "controls"
|
||||||
|
|
||||||
|
ElDialog {
|
||||||
|
id: dialog
|
||||||
|
title: qsTr("Select Swap Server")
|
||||||
|
|
||||||
|
property QtObject swaphelper
|
||||||
|
|
||||||
|
property string selectedPubkey
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
width: parent.width * 4/5
|
||||||
|
height: parent.height * 4/5
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: rootLayout
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Frame {
|
||||||
|
id: accountsFrame
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.topMargin: constants.paddingLarge
|
||||||
|
Layout.bottomMargin: constants.paddingLarge
|
||||||
|
Layout.leftMargin: constants.paddingMedium
|
||||||
|
Layout.rightMargin: constants.paddingMedium
|
||||||
|
|
||||||
|
verticalPadding: 0
|
||||||
|
horizontalPadding: 0
|
||||||
|
background: PaneInsetBackground {}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: 0
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: listview
|
||||||
|
Layout.preferredWidth: parent.width
|
||||||
|
Layout.fillHeight: true
|
||||||
|
clip: true
|
||||||
|
model: swaphelper.availableSwapServers
|
||||||
|
|
||||||
|
delegate: ItemDelegate {
|
||||||
|
width: ListView.view.width
|
||||||
|
height: itemLayout.height
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
dialog.selectedPubkey = model.npub
|
||||||
|
dialog.doAccept()
|
||||||
|
}
|
||||||
|
|
||||||
|
GridLayout {
|
||||||
|
id: itemLayout
|
||||||
|
columns: 3
|
||||||
|
rowSpacing: 0
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
leftMargin: constants.paddingMedium
|
||||||
|
rightMargin: constants.paddingMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.columnSpan: 3
|
||||||
|
Layout.preferredHeight: constants.paddingLarge
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
Layout.rowSpan: 3
|
||||||
|
source: Qt.resolvedUrl('../../icons/network.png')
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr('npub')
|
||||||
|
color: Material.accentColor
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: model.npub.substring(0,10)
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr('fee')
|
||||||
|
color: Material.accentColor
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: model.percentage_fee + '%'
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
text: qsTr('last seen')
|
||||||
|
color: Material.accentColor
|
||||||
|
}
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
text: model.timestamp
|
||||||
|
}
|
||||||
|
Item {
|
||||||
|
Layout.columnSpan: 3
|
||||||
|
Layout.preferredHeight: constants.paddingLarge
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollIndicator.vertical: ScrollIndicator { }
|
||||||
|
|
||||||
|
Label {
|
||||||
|
visible: swaphelper.availableSwapServers.count == 0
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: listview.width * 4/5
|
||||||
|
font.pixelSize: constants.fontSizeXXLarge
|
||||||
|
color: constants.mutedForeground
|
||||||
|
text: qsTr('No swap servers found')
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (dialog.selectedPubkey) {
|
||||||
|
listview.currentIndex = swaphelper.availableSwapServers.indexFor(dialog.selectedPubkey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,13 +33,15 @@ ElDialog {
|
|||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
visible: swaphelper.userinfo != ''
|
visible: swaphelper.userinfo != ''
|
||||||
text: swaphelper.userinfo
|
text: swaphelper.userinfo
|
||||||
iconStyle: swaphelper.state == SwapHelper.Started
|
iconStyle: swaphelper.state == SwapHelper.Started || swaphelper.state == SwapHelper.Initializing
|
||||||
? InfoTextArea.IconStyle.Spinner
|
? InfoTextArea.IconStyle.Spinner
|
||||||
: swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled
|
: swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled
|
||||||
? InfoTextArea.IconStyle.Error
|
? InfoTextArea.IconStyle.Error
|
||||||
: swaphelper.state == SwapHelper.Success
|
: swaphelper.state == SwapHelper.Success
|
||||||
? InfoTextArea.IconStyle.Done
|
? InfoTextArea.IconStyle.Done
|
||||||
: InfoTextArea.IconStyle.Info
|
: swaphelper.state == SwapHelper.NoService
|
||||||
|
? InfoTextArea.IconStyle.Warn
|
||||||
|
: InfoTextArea.IconStyle.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
GridLayout {
|
GridLayout {
|
||||||
@@ -170,7 +172,7 @@ ElDialog {
|
|||||||
Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
|
Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
|
||||||
Layout.rightMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid
|
Layout.rightMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid
|
||||||
|
|
||||||
property real scenter: -swapslider.from/(swapslider.to-swapslider.from)
|
property real scenter: -swapslider.from / (swapslider.to - swapslider.from)
|
||||||
|
|
||||||
enabled: swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed
|
enabled: swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed
|
||||||
|
|
||||||
@@ -193,21 +195,21 @@ ElDialog {
|
|||||||
x: swapslider.visualPosition > swapslider.scenter
|
x: swapslider.visualPosition > swapslider.scenter
|
||||||
? swapslider.scenter * parent.rangeWidth
|
? swapslider.scenter * parent.rangeWidth
|
||||||
: swapslider.visualPosition * parent.rangeWidth
|
: swapslider.visualPosition * parent.rangeWidth
|
||||||
|
y: enabled ? -1 : 0
|
||||||
width: swapslider.visualPosition > swapslider.scenter
|
width: swapslider.visualPosition > swapslider.scenter
|
||||||
? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth
|
? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth
|
||||||
: (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth
|
: (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth
|
||||||
height: parent.height
|
height: enabled ? parent.height + 2 : parent.height
|
||||||
color: enabled
|
color: enabled
|
||||||
? Material.accentColor
|
? constants.colorOk
|
||||||
: Material.sliderDisabledColor
|
: Material.sliderDisabledColor
|
||||||
radius: 2
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
x: - (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
|
x: - (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
|
||||||
z: -1
|
z: -1
|
||||||
// width makes rectangle go outside the control, into the Layout margins
|
// width makes rectangle go outside the control, into the Layout margins
|
||||||
width: parent.width + (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.rightVoid
|
width: swapslider.parent.width - 2 * constants.paddingXXLarge - swapslider.leftPadding - swapslider.rightPadding
|
||||||
height: parent.height
|
height: parent.height
|
||||||
color: Material.sliderDisabledColor
|
color: Material.sliderDisabledColor
|
||||||
}
|
}
|
||||||
@@ -249,6 +251,34 @@ ElDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Pane {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
visible: _swaphelper.isNostr()
|
||||||
|
background: Rectangle { color: constants.darkerDialogBackground }
|
||||||
|
padding: 0
|
||||||
|
|
||||||
|
FlatButton {
|
||||||
|
text: qsTr('Choose swap provider')
|
||||||
|
enabled: _swaphelper.state != SwapHelper.Initializing
|
||||||
|
&& _swaphelper.state != SwapHelper.Success
|
||||||
|
&& _swaphelper.availableSwapServers.count
|
||||||
|
onClicked: {
|
||||||
|
var dialog = app.nostrSwapServersDialog.createObject(app, {
|
||||||
|
swaphelper: _swaphelper,
|
||||||
|
selectedPubkey: Config.swapServerNPub
|
||||||
|
})
|
||||||
|
dialog.accepted.connect(function() {
|
||||||
|
if (Config.swapServerNPub != dialog.selectedPubkey) {
|
||||||
|
Config.swapServerNPub = dialog.selectedPubkey
|
||||||
|
_swaphelper.init_swap_manager()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
dialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
|
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
|
||||||
|
|
||||||
ButtonContainer {
|
ButtonContainer {
|
||||||
|
|||||||
@@ -436,9 +436,18 @@ ApplicationWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
property alias nostrSwapServersDialog: _nostrSwapServersDialog
|
||||||
|
Component {
|
||||||
|
id: _nostrSwapServersDialog
|
||||||
|
NostrSwapServersDialog {
|
||||||
|
onClosed: destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Component {
|
Component {
|
||||||
id: swapDialog
|
id: swapDialog
|
||||||
SwapDialog {
|
SwapDialog {
|
||||||
|
id: _swapdialog
|
||||||
onClosed: destroy()
|
onClosed: destroy()
|
||||||
swaphelper: SwapHelper {
|
swaphelper: SwapHelper {
|
||||||
id: _swaphelper
|
id: _swaphelper
|
||||||
@@ -454,6 +463,20 @@ ApplicationWindow
|
|||||||
})
|
})
|
||||||
dialog.open()
|
dialog.open()
|
||||||
}
|
}
|
||||||
|
onUndefinedNPub: {
|
||||||
|
var dialog = app.nostrSwapServersDialog.createObject(app, {
|
||||||
|
swaphelper: _swaphelper,
|
||||||
|
selectedPubkey: Config.swapServerNPub
|
||||||
|
})
|
||||||
|
dialog.accepted.connect(function() {
|
||||||
|
Config.swapServerNPub = dialog.selectedPubkey
|
||||||
|
_swaphelper.init_swap_manager()
|
||||||
|
})
|
||||||
|
dialog.rejected.connect(function() {
|
||||||
|
_swaphelper.npubSelectionCancelled()
|
||||||
|
})
|
||||||
|
dialog.open()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,6 +285,17 @@ class QEConfig(AuthMixin, QObject):
|
|||||||
self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = lightningPaymentFeeMaxMillionths
|
self.config.LIGHTNING_PAYMENT_FEE_MAX_MILLIONTHS = lightningPaymentFeeMaxMillionths
|
||||||
self.lightningPaymentFeeMaxMillionthsChanged.emit()
|
self.lightningPaymentFeeMaxMillionthsChanged.emit()
|
||||||
|
|
||||||
|
swapServerNPubChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(str, notify=swapServerNPubChanged)
|
||||||
|
def swapServerNPub(self):
|
||||||
|
return self.config.SWAPSERVER_NPUB
|
||||||
|
|
||||||
|
@swapServerNPub.setter
|
||||||
|
def swapServerNPub(self, swapserver_npub):
|
||||||
|
if swapserver_npub != self.config.SWAPSERVER_NPUB:
|
||||||
|
self.config.SWAPSERVER_NPUB = swapserver_npub
|
||||||
|
self.swapServerNPubChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot('qint64', result=str)
|
@pyqtSlot('qint64', result=str)
|
||||||
@pyqtSlot(QEAmount, result=str)
|
@pyqtSlot(QEAmount, result=str)
|
||||||
def formatSatsForEditing(self, satoshis):
|
def formatSatsForEditing(self, satoshis):
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import concurrent
|
import concurrent
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Union, Optional
|
from typing import Union, Optional
|
||||||
|
|
||||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum
|
from PyQt6.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum, QAbstractListModel, Qt,
|
||||||
|
QModelIndex)
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.bitcoin import DummyAddress
|
from electrum.bitcoin import DummyAddress
|
||||||
from electrum.logging import get_logger
|
from electrum.logging import get_logger
|
||||||
from electrum.transaction import PartialTxOutput, PartialTransaction
|
from electrum.transaction import PartialTxOutput, PartialTransaction
|
||||||
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop
|
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop, age
|
||||||
|
from electrum.submarine_swaps import NostrTransport
|
||||||
|
|
||||||
from electrum.gui import messages
|
from electrum.gui import messages
|
||||||
|
|
||||||
@@ -23,35 +26,106 @@ from .util import QtEventListener, qt_event_listener
|
|||||||
class InvalidSwapParameters(Exception): pass
|
class InvalidSwapParameters(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
|
class QESwapServerNPubListModel(QAbstractListModel):
|
||||||
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# define listmodel rolemap
|
||||||
|
_ROLE_NAMES= ('npub', 'timestamp', 'percentage_fee', 'normal_mining_fee', 'reverse_mining_fee', 'claim_mining_fee',
|
||||||
|
'min_amount', 'max_amount')
|
||||||
|
_ROLE_KEYS = range(Qt.ItemDataRole.UserRole, Qt.ItemDataRole.UserRole + len(_ROLE_NAMES))
|
||||||
|
_ROLE_MAP = dict(zip(_ROLE_KEYS, [bytearray(x.encode()) for x in _ROLE_NAMES]))
|
||||||
|
|
||||||
|
def __init__(self, config, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.config = config
|
||||||
|
self._services = []
|
||||||
|
|
||||||
|
def rowCount(self, index):
|
||||||
|
return len(self._services)
|
||||||
|
|
||||||
|
# also expose rowCount as a property
|
||||||
|
countChanged = pyqtSignal()
|
||||||
|
@pyqtProperty(int, notify=countChanged)
|
||||||
|
def count(self):
|
||||||
|
return len(self._services)
|
||||||
|
|
||||||
|
def roleNames(self):
|
||||||
|
return self._ROLE_MAP
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
service = self._services[index.row()]
|
||||||
|
role_index = role - Qt.ItemDataRole.UserRole
|
||||||
|
value = service[self._ROLE_NAMES[role_index]]
|
||||||
|
if isinstance(value, (bool, list, int, str)) or value is None:
|
||||||
|
return value
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
self.beginResetModel()
|
||||||
|
self._services = []
|
||||||
|
self.endResetModel()
|
||||||
|
|
||||||
|
def initModel(self, items):
|
||||||
|
self.beginInsertRows(QModelIndex(), len(items), len(items))
|
||||||
|
self._services = [{
|
||||||
|
'npub': x['pubkey'],
|
||||||
|
'percentage_fee': x['percentage_fee'],
|
||||||
|
'normal_mining_fee': x['normal_mining_fee'],
|
||||||
|
'reverse_mining_fee': x['reverse_mining_fee'],
|
||||||
|
'claim_mining_fee': x['claim_mining_fee'],
|
||||||
|
'min_amount': x['min_amount'],
|
||||||
|
'max_amount': x['max_amount'],
|
||||||
|
'timestamp': age(x['timestamp']),
|
||||||
|
}
|
||||||
|
for x in items
|
||||||
|
]
|
||||||
|
self.endInsertRows()
|
||||||
|
self.countChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot(str, result=int)
|
||||||
|
def indexFor(self, npub: str):
|
||||||
|
for i, item in enumerate(self._services):
|
||||||
|
if npub == item['npub']:
|
||||||
|
return i
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
|
MESSAGE_SWAP_HOWTO = ' '.join([
|
||||||
|
_('Move the slider to set the amount and direction of the swap.'),
|
||||||
|
_('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'),
|
||||||
|
])
|
||||||
|
|
||||||
@pyqtEnum
|
@pyqtEnum
|
||||||
class State(IntEnum):
|
class State(IntEnum):
|
||||||
Initialized = 0
|
Initializing = 0
|
||||||
ServiceReady = 1
|
Initialized = 1
|
||||||
Started = 2
|
NoService = 2
|
||||||
Failed = 3
|
ServiceReady = 3
|
||||||
Success = 4
|
Started = 4
|
||||||
Cancelled = 5
|
Failed = 5
|
||||||
|
Success = 6
|
||||||
|
Cancelled = 7
|
||||||
|
|
||||||
confirm = pyqtSignal([str], arguments=['message'])
|
confirm = pyqtSignal([str], arguments=['message'])
|
||||||
error = pyqtSignal([str], arguments=['message'])
|
error = pyqtSignal([str], arguments=['message'])
|
||||||
|
undefinedNPub = pyqtSignal()
|
||||||
|
offersUpdated = pyqtSignal()
|
||||||
|
requestTxUpdate = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._wallet = None # type: Optional[QEWallet]
|
self._wallet = None # type: Optional[QEWallet]
|
||||||
self._sliderPos = 0
|
self._sliderPos = 0
|
||||||
self._rangeMin = 0
|
self._rangeMin = -1
|
||||||
self._rangeMax = 0
|
self._rangeMax = 1
|
||||||
self._tx = None
|
self._tx = None
|
||||||
self._valid = False
|
self._valid = False
|
||||||
self._state = QESwapHelper.State.Initialized
|
self._state = QESwapHelper.State.Initialized
|
||||||
self._userinfo = ' '.join([
|
self._userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO
|
||||||
_('Move the slider to set the amount and direction of the swap.'),
|
|
||||||
_('Swapping lightning funds for onchain funds will increase your capacity to receive lightning payments.'),
|
|
||||||
])
|
|
||||||
self._tosend = QEAmount()
|
self._tosend = QEAmount()
|
||||||
self._toreceive = QEAmount()
|
self._toreceive = QEAmount()
|
||||||
self._serverfeeperc = ''
|
self._serverfeeperc = ''
|
||||||
@@ -69,13 +143,17 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self._leftVoid = 0
|
self._leftVoid = 0
|
||||||
self._rightVoid = 0
|
self._rightVoid = 0
|
||||||
|
|
||||||
|
self._available_swapservers = None
|
||||||
|
|
||||||
self.register_callbacks()
|
self.register_callbacks()
|
||||||
self.destroyed.connect(lambda: self.on_destroy())
|
self.destroyed.connect(lambda: self.on_destroy())
|
||||||
|
|
||||||
self._fwd_swap_updatetx_timer = QTimer(self)
|
self._fwd_swap_updatetx_timer = QTimer(self)
|
||||||
self._fwd_swap_updatetx_timer.setSingleShot(True)
|
self._fwd_swap_updatetx_timer.setSingleShot(True)
|
||||||
# self._fwd_swap_updatetx_timer.setInterval(500)
|
|
||||||
self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx)
|
self._fwd_swap_updatetx_timer.timeout.connect(self.fwd_swap_updatetx)
|
||||||
|
self.requestTxUpdate.connect(self.tx_update_pushback_timer)
|
||||||
|
|
||||||
|
self.offersUpdated.connect(self.on_offers_updated)
|
||||||
|
|
||||||
def on_destroy(self):
|
def on_destroy(self):
|
||||||
self.unregister_callbacks()
|
self.unregister_callbacks()
|
||||||
@@ -89,7 +167,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
def wallet(self, wallet: QEWallet):
|
def wallet(self, wallet: QEWallet):
|
||||||
if self._wallet != wallet:
|
if self._wallet != wallet:
|
||||||
self._wallet = wallet
|
self._wallet = wallet
|
||||||
self.init_swap_slider_range()
|
self.init_swap_manager()
|
||||||
self.walletChanged.emit()
|
self.walletChanged.emit()
|
||||||
|
|
||||||
sliderPosChanged = pyqtSignal()
|
sliderPosChanged = pyqtSignal()
|
||||||
@@ -246,18 +324,108 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self._canCancel = canCancel
|
self._canCancel = canCancel
|
||||||
self.canCancelChanged.emit()
|
self.canCancelChanged.emit()
|
||||||
|
|
||||||
def init_swap_slider_range(self):
|
availableSwapServersChanged = pyqtSignal()
|
||||||
lnworker = self._wallet.wallet.lnworker
|
@pyqtProperty(QESwapServerNPubListModel, notify=availableSwapServersChanged)
|
||||||
if not lnworker:
|
def availableSwapServers(self):
|
||||||
|
if not self._available_swapservers:
|
||||||
|
self._available_swapservers = QESwapServerNPubListModel(self._wallet.wallet.config)
|
||||||
|
|
||||||
|
return self._available_swapservers
|
||||||
|
|
||||||
|
def on_offers_updated(self):
|
||||||
|
self.availableSwapServers.initModel(self.recent_offers)
|
||||||
|
|
||||||
|
@pyqtSlot(result=bool)
|
||||||
|
def isNostr(self):
|
||||||
|
return True # TODO
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def init_swap_manager(self):
|
||||||
|
self._logger.debug('init_swap_manager')
|
||||||
|
if (lnworker := self._wallet.wallet.lnworker) is None:
|
||||||
return
|
return
|
||||||
swap_manager = lnworker.swap_manager
|
swap_manager = lnworker.swap_manager
|
||||||
try:
|
|
||||||
asyncio.run(swap_manager.get_pairs())
|
assert not swap_manager.is_server, 'running as swap server not supported'
|
||||||
self.state = QESwapHelper.State.ServiceReady
|
|
||||||
except Exception as e:
|
# if not self._wallet.wallet.config.SWAPSERVER_URL and not self._wallet.wallet.config.SWAPSERVER_NPUB: # TODO enable nostr
|
||||||
self.error.emit(_('Swap service unavailable'))
|
# self._logger.debug('nostr is preferred but swapserver npub still undefined')
|
||||||
self._logger.error(f'could not get pairs for swap: {repr(e)}')
|
|
||||||
return
|
# FIXME: clearing is_initialized, we might be called because the npub was changed
|
||||||
|
swap_manager.is_initialized.clear()
|
||||||
|
self.state = QESwapHelper.State.Initialized if swap_manager.is_initialized.is_set() else QESwapHelper.State.Initializing
|
||||||
|
|
||||||
|
swap_transport = swap_manager.create_transport()
|
||||||
|
|
||||||
|
def query_task(transport):
|
||||||
|
with transport:
|
||||||
|
try:
|
||||||
|
async def wait_initialized():
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(swap_manager.is_initialized.wait(), timeout=15)
|
||||||
|
self._logger.debug('swapmanager initialized')
|
||||||
|
self.state = QESwapHelper.State.Initialized
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
self._logger.debug('swapmanager init timeout')
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
return
|
||||||
|
|
||||||
|
if not swap_manager.is_initialized.is_set():
|
||||||
|
self.userinfo = _('Initializing...')
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(wait_initialized(), get_asyncio_loop())
|
||||||
|
fut.result()
|
||||||
|
except Exception as e:
|
||||||
|
try: # swaphelper might be destroyed at this point
|
||||||
|
self.userinfo = _('Error') + ': ' + str(e)
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
self._logger.error(str(e))
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if isinstance(transport, NostrTransport):
|
||||||
|
now = int(time.time())
|
||||||
|
if not swap_manager.is_initialized.is_set():
|
||||||
|
if not transport.is_connected.is_set():
|
||||||
|
self.userinfo = _('Error') + ': ' + '\n'.join([
|
||||||
|
_('Could not connect to a Nostr relay.'),
|
||||||
|
_('Please check your relays and network connection')
|
||||||
|
])
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
return
|
||||||
|
self.recent_offers = [x for x in transport.offers.values() if now - x['timestamp'] < NostrTransport.NOSTR_EVENT_TIMEOUT]
|
||||||
|
if not self.recent_offers:
|
||||||
|
self.userinfo = _('Could not find a swap provider.')
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
return
|
||||||
|
|
||||||
|
self.offersUpdated.emit()
|
||||||
|
self.undefinedNPub.emit()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
self.recent_offers = [x for x in transport.offers.values() if now - x['timestamp'] < NostrTransport.NOSTR_EVENT_TIMEOUT]
|
||||||
|
if not self.recent_offers:
|
||||||
|
self.userinfo = _('Could not find a swap provider.')
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
return
|
||||||
|
|
||||||
|
self.offersUpdated.emit()
|
||||||
|
|
||||||
|
self.state = QESwapHelper.State.ServiceReady
|
||||||
|
self.userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO
|
||||||
|
self.init_swap_slider_range()
|
||||||
|
|
||||||
|
threading.Thread(target=query_task, args=(swap_transport,), daemon=True).start()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def npubSelectionCancelled(self):
|
||||||
|
if not self._wallet.wallet.config.SWAPSERVER_NPUB:
|
||||||
|
self._logger.debug('nostr is preferred but swapserver npub still undefined')
|
||||||
|
self.userinfo = _('No swap provider selected.')
|
||||||
|
self.state = QESwapHelper.State.NoService
|
||||||
|
|
||||||
|
def init_swap_slider_range(self):
|
||||||
|
lnworker = self._wallet.wallet.lnworker
|
||||||
|
swap_manager = lnworker.swap_manager
|
||||||
|
|
||||||
"""Sets the minimal and maximal amount that can be swapped for the swap
|
"""Sets the minimal and maximal amount that can be swapped for the swap
|
||||||
slider."""
|
slider."""
|
||||||
@@ -322,7 +490,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self.swap_slider_moved()
|
self.swap_slider_moved()
|
||||||
|
|
||||||
def swap_slider_moved(self):
|
def swap_slider_moved(self):
|
||||||
if self._state == QESwapHelper.State.Initialized:
|
if self._state in [QESwapHelper.State.Initializing, QESwapHelper.State.Initialized, QESwapHelper.State.NoService]:
|
||||||
return
|
return
|
||||||
|
|
||||||
position = int(self._sliderPos)
|
position = int(self._sliderPos)
|
||||||
@@ -345,7 +513,11 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
else:
|
else:
|
||||||
# update tx only if slider isn't moved for a while
|
# update tx only if slider isn't moved for a while
|
||||||
self.valid = False
|
self.valid = False
|
||||||
self._fwd_swap_updatetx_timer.start(250)
|
# trigger tx_update_pushback_timer through signal, as this might be called from other thread
|
||||||
|
self.requestTxUpdate.emit()
|
||||||
|
|
||||||
|
def tx_update_pushback_timer(self):
|
||||||
|
self._fwd_swap_updatetx_timer.start(250)
|
||||||
|
|
||||||
def check_valid(self, send_amount, receive_amount):
|
def check_valid(self, send_amount, receive_amount):
|
||||||
if send_amount and receive_amount:
|
if send_amount and receive_amount:
|
||||||
@@ -366,55 +538,57 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
if lightning_amount is None or onchain_amount is None:
|
if lightning_amount is None or onchain_amount is None:
|
||||||
return
|
return
|
||||||
loop = get_asyncio_loop()
|
loop = get_asyncio_loop()
|
||||||
coro = self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
|
|
||||||
lightning_amount_sat=lightning_amount,
|
|
||||||
expected_onchain_amount_sat=onchain_amount,
|
|
||||||
)
|
|
||||||
|
|
||||||
def swap_task():
|
def swap_task():
|
||||||
try:
|
with self._wallet.wallet.lnworker.swap_manager.create_transport() as transport:
|
||||||
dummy_tx = self._create_tx(onchain_amount)
|
coro = self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
transport,
|
||||||
self.userinfo = _('Performing swap...')
|
lightning_amount_sat=lightning_amount,
|
||||||
self.state = QESwapHelper.State.Started
|
expected_onchain_amount_sat=onchain_amount,
|
||||||
self._swap, invoice = fut.result()
|
)
|
||||||
|
try:
|
||||||
|
dummy_tx = self._create_tx(onchain_amount)
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
|
self.userinfo = _('Performing swap...')
|
||||||
|
self.state = QESwapHelper.State.Started
|
||||||
|
self._swap, invoice = fut.result()
|
||||||
|
|
||||||
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, password=self._wallet.password)
|
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, password=self._wallet.password)
|
||||||
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(swap=self._swap, invoice=invoice, tx=tx)
|
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(transport, swap=self._swap, invoice=invoice, tx=tx)
|
||||||
self._fut_htlc_wait = fut = asyncio.run_coroutine_threadsafe(coro2, loop)
|
self._fut_htlc_wait = fut = asyncio.run_coroutine_threadsafe(coro2, loop)
|
||||||
|
|
||||||
self.canCancel = True
|
self.canCancel = True
|
||||||
txid = fut.result()
|
txid = fut.result()
|
||||||
try: # swaphelper might be destroyed at this point
|
try: # swaphelper might be destroyed at this point
|
||||||
if txid:
|
if txid:
|
||||||
self.userinfo = ' '.join([
|
self.userinfo = ' '.join([
|
||||||
_('Success!'),
|
_('Success!'),
|
||||||
messages.MSG_FORWARD_SWAP_FUNDING_MEMPOOL,
|
messages.MSG_FORWARD_SWAP_FUNDING_MEMPOOL,
|
||||||
])
|
])
|
||||||
self.state = QESwapHelper.State.Success
|
self.state = QESwapHelper.State.Success
|
||||||
else:
|
else:
|
||||||
self.userinfo = _('Swap failed!')
|
self.userinfo = _('Swap failed!')
|
||||||
|
self.state = QESwapHelper.State.Failed
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
except concurrent.futures.CancelledError:
|
||||||
|
self._wallet.wallet.lnworker.swap_manager.cancel_normal_swap(self._swap)
|
||||||
|
self.userinfo = _('Swap cancelled')
|
||||||
|
self.state = QESwapHelper.State.Cancelled
|
||||||
|
except Exception as e:
|
||||||
|
try: # swaphelper might be destroyed at this point
|
||||||
self.state = QESwapHelper.State.Failed
|
self.state = QESwapHelper.State.Failed
|
||||||
except RuntimeError:
|
self.userinfo = _('Error') + ': ' + str(e)
|
||||||
pass
|
self._logger.error(str(e))
|
||||||
except concurrent.futures.CancelledError:
|
except RuntimeError:
|
||||||
self._wallet.wallet.lnworker.swap_manager.cancel_normal_swap(self._swap)
|
pass
|
||||||
self.userinfo = _('Swap cancelled')
|
finally:
|
||||||
self.state = QESwapHelper.State.Cancelled
|
try: # swaphelper might be destroyed at this point
|
||||||
except Exception as e:
|
self.canCancel = False
|
||||||
try: # swaphelper might be destroyed at this point
|
self._swap = None
|
||||||
self.state = QESwapHelper.State.Failed
|
self._fut_htlc_wait = None
|
||||||
self.userinfo = _('Error') + ': ' + str(e)
|
except RuntimeError:
|
||||||
self._logger.error(str(e))
|
pass
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
finally:
|
|
||||||
try: # swaphelper might be destroyed at this point
|
|
||||||
self.canCancel = False
|
|
||||||
self._swap = None
|
|
||||||
self._fut_htlc_wait = None
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
threading.Thread(target=swap_task, daemon=True).start()
|
threading.Thread(target=swap_task, daemon=True).start()
|
||||||
|
|
||||||
@@ -446,38 +620,42 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
def do_reverse_swap(self, lightning_amount, onchain_amount):
|
def do_reverse_swap(self, lightning_amount, onchain_amount):
|
||||||
if lightning_amount is None or onchain_amount is None:
|
if lightning_amount is None or onchain_amount is None:
|
||||||
return
|
return
|
||||||
swap_manager = self._wallet.wallet.lnworker.swap_manager
|
|
||||||
loop = get_asyncio_loop()
|
|
||||||
coro = swap_manager.reverse_swap(
|
|
||||||
lightning_amount_sat=lightning_amount,
|
|
||||||
expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def swap_task():
|
def swap_task():
|
||||||
try:
|
swap_manager = self._wallet.wallet.lnworker.swap_manager
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
loop = get_asyncio_loop()
|
||||||
self.userinfo = _('Performing swap...')
|
with self._wallet.wallet.lnworker.swap_manager.create_transport() as transport:
|
||||||
self.state = QESwapHelper.State.Started
|
coro = swap_manager.reverse_swap(
|
||||||
txid = fut.result()
|
transport,
|
||||||
try: # swaphelper might be destroyed at this point
|
lightning_amount_sat=lightning_amount,
|
||||||
if txid:
|
expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(),
|
||||||
self.userinfo = ' '.join([
|
)
|
||||||
_('Success!'),
|
try:
|
||||||
messages.MSG_REVERSE_SWAP_FUNDING_MEMPOOL,
|
time.sleep(1) # FIXME: this is needed because transport hasn't finished initializing yet.
|
||||||
])
|
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
self.state = QESwapHelper.State.Success
|
self.userinfo = _('Performing swap...')
|
||||||
else:
|
self.state = QESwapHelper.State.Started
|
||||||
self.userinfo = _('Swap failed!')
|
txid = fut.result()
|
||||||
|
try: # swaphelper might be destroyed at this point
|
||||||
|
if txid:
|
||||||
|
self.userinfo = ' '.join([
|
||||||
|
_('Success!'),
|
||||||
|
messages.MSG_REVERSE_SWAP_FUNDING_MEMPOOL,
|
||||||
|
])
|
||||||
|
self.state = QESwapHelper.State.Success
|
||||||
|
else:
|
||||||
|
self.userinfo = _('Swap failed!')
|
||||||
|
self.state = QESwapHelper.State.Failed
|
||||||
|
except RuntimeError:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
try: # swaphelper might be destroyed at this point
|
||||||
self.state = QESwapHelper.State.Failed
|
self.state = QESwapHelper.State.Failed
|
||||||
except RuntimeError:
|
msg = _('Timeout') if isinstance(e, TimeoutError) else str(e)
|
||||||
pass
|
self.userinfo = _('Error') + ': ' + msg
|
||||||
except Exception as e:
|
self._logger.error(str(e))
|
||||||
try: # swaphelper might be destroyed at this point
|
except RuntimeError:
|
||||||
self.state = QESwapHelper.State.Failed
|
pass
|
||||||
self.userinfo = _('Error') + ': ' + str(e)
|
|
||||||
self._logger.error(str(e))
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
threading.Thread(target=swap_task, daemon=True).start()
|
threading.Thread(target=swap_task, daemon=True).start()
|
||||||
|
|
||||||
|
|||||||
@@ -877,6 +877,7 @@ class SwapManager(Logger):
|
|||||||
"preimageHash": payment_hash.hex(),
|
"preimageHash": payment_hash.hex(),
|
||||||
"claimPublicKey": our_pubkey.hex()
|
"claimPublicKey": our_pubkey.hex()
|
||||||
}
|
}
|
||||||
|
self.logger.debug(f'rswap: sending request for {lightning_amount_sat}')
|
||||||
data = await transport.send_request_to_server('createswap', request_data)
|
data = await transport.send_request_to_server('createswap', request_data)
|
||||||
invoice = data['invoice']
|
invoice = data['invoice']
|
||||||
fee_invoice = data.get('minerFeeInvoice')
|
fee_invoice = data.get('minerFeeInvoice')
|
||||||
@@ -885,6 +886,7 @@ class SwapManager(Logger):
|
|||||||
locktime = data['timeoutBlockHeight']
|
locktime = data['timeoutBlockHeight']
|
||||||
onchain_amount = data["onchainAmount"]
|
onchain_amount = data["onchainAmount"]
|
||||||
response_id = data['id']
|
response_id = data['id']
|
||||||
|
self.logger.debug(f'rswap: {response_id=}')
|
||||||
# verify redeem_script is built with our pubkey and preimage
|
# verify redeem_script is built with our pubkey and preimage
|
||||||
check_reverse_redeem_script(
|
check_reverse_redeem_script(
|
||||||
redeem_script=redeem_script,
|
redeem_script=redeem_script,
|
||||||
@@ -1312,7 +1314,6 @@ class HttpTransport(Logger):
|
|||||||
self.sm.update_pairs(pairs)
|
self.sm.update_pairs(pairs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class NostrTransport(Logger):
|
class NostrTransport(Logger):
|
||||||
# uses nostr:
|
# uses nostr:
|
||||||
# - to advertise servers
|
# - to advertise servers
|
||||||
@@ -1382,6 +1383,7 @@ class NostrTransport(Logger):
|
|||||||
self.sm.is_initialized.clear()
|
self.sm.is_initialized.clear()
|
||||||
await self.taskgroup.cancel_remaining()
|
await self.taskgroup.cancel_remaining()
|
||||||
await self.relay_manager.close()
|
await self.relay_manager.close()
|
||||||
|
self.logger.info("nostr transport shut down")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def relays(self):
|
def relays(self):
|
||||||
@@ -1559,7 +1561,7 @@ class NostrTransport(Logger):
|
|||||||
method = request.pop('method')
|
method = request.pop('method')
|
||||||
event_id = request.pop('event_id')
|
event_id = request.pop('event_id')
|
||||||
event_pubkey = request.pop('event_pubkey')
|
event_pubkey = request.pop('event_pubkey')
|
||||||
print(f'handle_request: id={event_id} {method} {request}')
|
self.logger.info(f'handle_request: id={event_id} {method} {request}')
|
||||||
relays = request.pop('relays').split(',')
|
relays = request.pop('relays').split(',')
|
||||||
if method == 'addswapinvoice':
|
if method == 'addswapinvoice':
|
||||||
r = self.sm.server_add_swap_invoice(request)
|
r = self.sm.server_add_swap_invoice(request)
|
||||||
|
|||||||
Reference in New Issue
Block a user