1
0

submarine_swaps: implement swaps-over-nostr for qml

This commit is contained in:
Sander van Grieken
2024-11-15 02:13:38 +01:00
committed by ThomasV
parent 22995b4a34
commit 993a575b7f
7 changed files with 495 additions and 110 deletions

View File

@@ -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"

View 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)
}
}
}

View File

@@ -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 {

View File

@@ -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()
}
} }
} }
} }

View File

@@ -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):

View File

@@ -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()

View File

@@ -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)