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 colorInvalidBackground: '#ff800000'
property color colorAcceptable: '#ff8080ff'
property color colorOk: colorDone
property color colorLightningLocal: "#6060ff"
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
visible: swaphelper.userinfo != ''
text: swaphelper.userinfo
iconStyle: swaphelper.state == SwapHelper.Started
iconStyle: swaphelper.state == SwapHelper.Started || swaphelper.state == SwapHelper.Initializing
? InfoTextArea.IconStyle.Spinner
: swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled
? InfoTextArea.IconStyle.Error
: swaphelper.state == SwapHelper.Success
? InfoTextArea.IconStyle.Done
: InfoTextArea.IconStyle.Info
: swaphelper.state == SwapHelper.NoService
? InfoTextArea.IconStyle.Warn
: InfoTextArea.IconStyle.Info
}
GridLayout {
@@ -170,7 +172,7 @@ ElDialog {
Layout.leftMargin: constants.paddingXXLarge + (parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
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
@@ -193,21 +195,21 @@ ElDialog {
x: swapslider.visualPosition > swapslider.scenter
? swapslider.scenter * parent.rangeWidth
: swapslider.visualPosition * parent.rangeWidth
y: enabled ? -1 : 0
width: swapslider.visualPosition > swapslider.scenter
? (swapslider.visualPosition-swapslider.scenter) * parent.rangeWidth
: (swapslider.scenter-swapslider.visualPosition) * parent.rangeWidth
height: parent.height
height: enabled ? parent.height + 2 : parent.height
color: enabled
? Material.accentColor
? constants.colorOk
: Material.sliderDisabledColor
radius: 2
}
Rectangle {
x: - (swapslider.parent.width - 2 * constants.paddingXXLarge) * swaphelper.leftVoid
z: -1
// 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
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 }
ButtonContainer {

View File

@@ -436,9 +436,18 @@ ApplicationWindow
}
}
property alias nostrSwapServersDialog: _nostrSwapServersDialog
Component {
id: _nostrSwapServersDialog
NostrSwapServersDialog {
onClosed: destroy()
}
}
Component {
id: swapDialog
SwapDialog {
id: _swapdialog
onClosed: destroy()
swaphelper: SwapHelper {
id: _swaphelper
@@ -454,6 +463,20 @@ ApplicationWindow
})
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.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(QEAmount, result=str)
def formatSatsForEditing(self, satoshis):

View File

@@ -1,16 +1,19 @@
import asyncio
import concurrent
import threading
import time
from enum import IntEnum
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.bitcoin import DummyAddress
from electrum.logging import get_logger
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
@@ -23,35 +26,106 @@ from .util import QtEventListener, qt_event_listener
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):
_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
class State(IntEnum):
Initialized = 0
ServiceReady = 1
Started = 2
Failed = 3
Success = 4
Cancelled = 5
Initializing = 0
Initialized = 1
NoService = 2
ServiceReady = 3
Started = 4
Failed = 5
Success = 6
Cancelled = 7
confirm = pyqtSignal([str], arguments=['message'])
error = pyqtSignal([str], arguments=['message'])
undefinedNPub = pyqtSignal()
offersUpdated = pyqtSignal()
requestTxUpdate = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._wallet = None # type: Optional[QEWallet]
self._sliderPos = 0
self._rangeMin = 0
self._rangeMax = 0
self._rangeMin = -1
self._rangeMax = 1
self._tx = None
self._valid = False
self._state = QESwapHelper.State.Initialized
self._userinfo = ' '.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.'),
])
self._userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO
self._tosend = QEAmount()
self._toreceive = QEAmount()
self._serverfeeperc = ''
@@ -69,13 +143,17 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._leftVoid = 0
self._rightVoid = 0
self._available_swapservers = None
self.register_callbacks()
self.destroyed.connect(lambda: self.on_destroy())
self._fwd_swap_updatetx_timer = QTimer(self)
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.requestTxUpdate.connect(self.tx_update_pushback_timer)
self.offersUpdated.connect(self.on_offers_updated)
def on_destroy(self):
self.unregister_callbacks()
@@ -89,7 +167,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
def wallet(self, wallet: QEWallet):
if self._wallet != wallet:
self._wallet = wallet
self.init_swap_slider_range()
self.init_swap_manager()
self.walletChanged.emit()
sliderPosChanged = pyqtSignal()
@@ -246,18 +324,108 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._canCancel = canCancel
self.canCancelChanged.emit()
def init_swap_slider_range(self):
lnworker = self._wallet.wallet.lnworker
if not lnworker:
availableSwapServersChanged = pyqtSignal()
@pyqtProperty(QESwapServerNPubListModel, notify=availableSwapServersChanged)
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
swap_manager = lnworker.swap_manager
try:
asyncio.run(swap_manager.get_pairs())
self.state = QESwapHelper.State.ServiceReady
except Exception as e:
self.error.emit(_('Swap service unavailable'))
self._logger.error(f'could not get pairs for swap: {repr(e)}')
return
assert not swap_manager.is_server, 'running as swap server not supported'
# if not self._wallet.wallet.config.SWAPSERVER_URL and not self._wallet.wallet.config.SWAPSERVER_NPUB: # TODO enable nostr
# self._logger.debug('nostr is preferred but swapserver npub still undefined')
# 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
slider."""
@@ -322,7 +490,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self.swap_slider_moved()
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
position = int(self._sliderPos)
@@ -345,7 +513,11 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
else:
# update tx only if slider isn't moved for a while
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):
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:
return
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():
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()
with self._wallet.wallet.lnworker.swap_manager.create_transport() as transport:
coro = self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount,
)
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)
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(swap=self._swap, invoice=invoice, tx=tx)
self._fut_htlc_wait = fut = asyncio.run_coroutine_threadsafe(coro2, loop)
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(transport, swap=self._swap, invoice=invoice, tx=tx)
self._fut_htlc_wait = fut = asyncio.run_coroutine_threadsafe(coro2, loop)
self.canCancel = True
txid = fut.result()
try: # swaphelper might be destroyed at this point
if txid:
self.userinfo = ' '.join([
_('Success!'),
messages.MSG_FORWARD_SWAP_FUNDING_MEMPOOL,
])
self.state = QESwapHelper.State.Success
else:
self.userinfo = _('Swap failed!')
self.canCancel = True
txid = fut.result()
try: # swaphelper might be destroyed at this point
if txid:
self.userinfo = ' '.join([
_('Success!'),
messages.MSG_FORWARD_SWAP_FUNDING_MEMPOOL,
])
self.state = QESwapHelper.State.Success
else:
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
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.userinfo = _('Error') + ': ' + str(e)
self._logger.error(str(e))
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
self.userinfo = _('Error') + ': ' + str(e)
self._logger.error(str(e))
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()
@@ -446,38 +620,42 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
def do_reverse_swap(self, lightning_amount, onchain_amount):
if lightning_amount is None or onchain_amount is None:
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():
try:
fut = asyncio.run_coroutine_threadsafe(coro, loop)
self.userinfo = _('Performing swap...')
self.state = QESwapHelper.State.Started
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!')
swap_manager = self._wallet.wallet.lnworker.swap_manager
loop = get_asyncio_loop()
with self._wallet.wallet.lnworker.swap_manager.create_transport() as transport:
coro = swap_manager.reverse_swap(
transport,
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + swap_manager.get_claim_fee(),
)
try:
time.sleep(1) # FIXME: this is needed because transport hasn't finished initializing yet.
fut = asyncio.run_coroutine_threadsafe(coro, loop)
self.userinfo = _('Performing swap...')
self.state = QESwapHelper.State.Started
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
except RuntimeError:
pass
except Exception as e:
try: # swaphelper might be destroyed at this point
self.state = QESwapHelper.State.Failed
self.userinfo = _('Error') + ': ' + str(e)
self._logger.error(str(e))
except RuntimeError:
pass
msg = _('Timeout') if isinstance(e, TimeoutError) else str(e)
self.userinfo = _('Error') + ': ' + msg
self._logger.error(str(e))
except RuntimeError:
pass
threading.Thread(target=swap_task, daemon=True).start()

View File

@@ -877,6 +877,7 @@ class SwapManager(Logger):
"preimageHash": payment_hash.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)
invoice = data['invoice']
fee_invoice = data.get('minerFeeInvoice')
@@ -885,6 +886,7 @@ class SwapManager(Logger):
locktime = data['timeoutBlockHeight']
onchain_amount = data["onchainAmount"]
response_id = data['id']
self.logger.debug(f'rswap: {response_id=}')
# verify redeem_script is built with our pubkey and preimage
check_reverse_redeem_script(
redeem_script=redeem_script,
@@ -1312,7 +1314,6 @@ class HttpTransport(Logger):
self.sm.update_pairs(pairs)
class NostrTransport(Logger):
# uses nostr:
# - to advertise servers
@@ -1382,6 +1383,7 @@ class NostrTransport(Logger):
self.sm.is_initialized.clear()
await self.taskgroup.cancel_remaining()
await self.relay_manager.close()
self.logger.info("nostr transport shut down")
@property
def relays(self):
@@ -1559,7 +1561,7 @@ class NostrTransport(Logger):
method = request.pop('method')
event_id = request.pop('event_id')
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(',')
if method == 'addswapinvoice':
r = self.sm.server_add_swap_invoice(request)