From 67494688660adad8583c4b692707fbf7700e5acf Mon Sep 17 00:00:00 2001 From: SomberNight Date: Fri, 14 Feb 2025 14:10:24 +0000 Subject: [PATCH] swaps: add base class for transports. move "get_recent_offers" logic the "get_recent_offers" logic is now shared between the GUIs --- electrum/gui/qml/qeswaphelper.py | 8 ++--- electrum/gui/qt/main_window.py | 26 +++++++------- electrum/gui/qt/swap_dialog.py | 3 +- electrum/submarine_swaps.py | 61 ++++++++++++++++++++++---------- 4 files changed, 62 insertions(+), 36 deletions(-) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index b798506a1..1e27955a2 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -12,7 +12,7 @@ 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, age -from electrum.submarine_swaps import NostrTransport +from electrum.submarine_swaps import NostrTransport, SwapServerTransport from electrum.gui import messages @@ -354,7 +354,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): swap_transport = swap_manager.create_transport() - def query_task(transport): + def query_task(transport: SwapServerTransport): with transport: try: async def wait_initialized(): @@ -388,7 +388,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): ]) self.state = QESwapHelper.State.NoService return - self.recent_offers = [x for x in transport.offers.values()] + self.recent_offers = [x for x in transport.get_recent_offers()] if not self.recent_offers: self.userinfo = _('Could not find a swap provider.') self.state = QESwapHelper.State.NoService @@ -398,7 +398,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): self.undefinedNPub.emit() return else: - self.recent_offers = [x for x in transport.offers.values()] + self.recent_offers = [x for x in transport.get_recent_offers()] if not self.recent_offers: self.userinfo = _('Could not find a swap provider.') self.state = QESwapHelper.State.NoService diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 2b866b4bb..f2682e276 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -71,6 +71,7 @@ from electrum.simple_config import SimpleConfig from electrum.logging import Logger from electrum.lntransport import extract_nodeid, ConnStringFormatError from electrum.lnaddr import lndecode +from electrum.submarine_swaps import SwapServerTransport, NostrTransport from .rate_limiter import rate_limited from .exception_window import Exception_Hook @@ -1187,25 +1188,25 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): except UserCancelled: return - def create_sm_transport(self): + def create_sm_transport(self) -> Optional['SwapServerTransport']: sm = self.wallet.lnworker.swap_manager if sm.is_server: self.show_error(_('Swap server is active')) - return False + return None if self.network is None: - return False + return None if not self.config.SWAPSERVER_URL and not self.config.SWAPSERVER_NPUB: if not self.question('\n'.join([ _('Electrum uses Nostr in order to find liquidity providers.'), _('Do you want to enable Nostr?'), ])): - return False + return None return sm.create_transport() - def initialize_swap_manager(self, transport): + def initialize_swap_manager(self, transport: 'SwapServerTransport'): sm = self.wallet.lnworker.swap_manager if not sm.is_initialized.is_set(): async def wait_until_initialized(): @@ -1226,7 +1227,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): assert sm.is_initialized.is_set() return True - def choose_swapserver_dialog(self, transport): + def choose_swapserver_dialog(self, transport: NostrTransport) -> bool: + assert isinstance(transport, NostrTransport) if not transport.is_connected.is_set(): self.show_message( '\n'.join([ @@ -1234,8 +1236,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): _('Please check your relays and network connection'), ])) return False - now = int(time.time()) - recent_offers = [x for x in transport.offers.values() if now - x['timestamp'] < 3600] + recent_offers = transport.get_recent_offers() if not recent_offers: self.show_message( '\n'.join([ @@ -1245,9 +1246,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): sm = self.wallet.lnworker.swap_manager def descr(x): last_seen = util.age(x['timestamp']) - return f"pubkey={x['pubkey'][0:10]}, fee={x['percentage_fee']}% + {x['reverse_mining_fee']} sats" - pow_sorted_offers = sorted(recent_offers, key=lambda x: x['pow_bits'], reverse=True) - server_keys = [(x['pubkey'], descr(x)) for x in pow_sorted_offers] + return (f"pubkey={x['pubkey'][0:10]}, " + f"fee={x['percentage_fee']}% + {x['reverse_mining_fee']} sats, " + f"last_seen: {last_seen}") + server_keys = [(x['pubkey'], descr(x)) for x in recent_offers] msg = '\n'.join([ _("Please choose a server from this list."), _("Note that fees may be updated frequently.") @@ -1258,7 +1260,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): title = _("Choose Swap Server"), default_choice = self.config.SWAPSERVER_NPUB ) - if choice not in transport.offers: + if choice is None: return False self.config.SWAPSERVER_NPUB = choice pairs = transport.get_offer(choice) diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 83325dba1..473f6c69b 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -19,6 +19,7 @@ from .my_treeview import create_toolbar_with_menu if TYPE_CHECKING: from .main_window import ElectrumWindow + from electrum.submarine_swaps import SwapServerTransport CANNOT_RECEIVE_WARNING = _( """The requested amount is higher than what you can receive in your currently open channels. @@ -33,7 +34,7 @@ class InvalidSwapParameters(Exception): pass class SwapDialog(WindowModalDialog, QtEventListener): - def __init__(self, window: 'ElectrumWindow', transport, is_reverse=None, recv_amount_sat=None, channels=None): + def __init__(self, window: 'ElectrumWindow', transport: 'SwapServerTransport', is_reverse=None, recv_amount_sat=None, channels=None): WindowModalDialog.__init__(self, window, _('Submarine Swap')) self.window = window self.config = window.config diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 4cbee6880..e3cb4965e 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -261,7 +261,7 @@ class SwapManager(Logger): async def stop(self): await self.taskgroup.cancel_remaining() - def create_transport(self): + def create_transport(self) -> 'SwapServerTransport': from .lnutil import generate_random_keypair if self.config.SWAPSERVER_URL: return HttpTransport(self.config, self) @@ -1266,16 +1266,33 @@ class SwapManager(Logger): return swap.funding_txid +class SwapServerTransport(Logger): -class HttpTransport(Logger): - - def __init__(self, config, sm): + def __init__(self, *, config: 'SimpleConfig', sm: 'SwapManager'): Logger.__init__(self) self.sm = sm self.network = sm.network - self.api_url = config.SWAPSERVER_URL self.config = config self.is_connected = asyncio.Event() + + def __enter__(self): + pass + + def __exit__(self, ex_type, ex, tb): + pass + + async def send_request_to_server(self, method: str, request_data: Optional[dict]) -> dict: + pass + + async def get_pairs(self) -> None: + pass + + +class HttpTransport(SwapServerTransport): + + def __init__(self, config, sm): + SwapServerTransport.__init__(self, config=config, sm=sm) + self.api_url = config.SWAPSERVER_URL self.is_connected.set() def __enter__(self): @@ -1314,7 +1331,7 @@ class HttpTransport(Logger): self.sm.update_pairs(pairs) -class NostrTransport(Logger): +class NostrTransport(SwapServerTransport): # uses nostr: # - to advertise servers # - for client-server RPCs (using DMs) @@ -1326,11 +1343,8 @@ class NostrTransport(Logger): OFFER_UPDATE_INTERVAL_SEC = 60 * 10 def __init__(self, config, sm, keypair): - Logger.__init__(self) - self.config = config - self.network = sm.network - self.sm = sm - self.offers = {} + SwapServerTransport.__init__(self, config=config, sm=sm) + self._offers = {} # type: Dict[str, Dict] self.private_key = keypair.privkey self.nostr_private_key = to_nip19('nsec', keypair.privkey.hex()) self.nostr_pubkey = keypair.pubkey.hex()[2:] @@ -1338,7 +1352,6 @@ class NostrTransport(Logger): ssl_context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH, cafile=ca_path) self.relay_manager = aionostr.Manager(self.relays, private_key=self.nostr_private_key, log=self.logger, ssl_context=ssl_context) self.taskgroup = OldTaskGroup() - self.is_connected = asyncio.Event() self.server_relays = None def __enter__(self): @@ -1390,9 +1403,19 @@ class NostrTransport(Logger): return self.network.config.NOSTR_RELAYS.split(',') def get_offer(self, pubkey): - offer = self.offers.get(pubkey) + offer = self._offers.get(pubkey) return self._parse_offer(offer) + def get_recent_offers(self) -> Sequence[Dict]: + # filter to fresh timestamps + now = int(time.time()) + recent_offers = [x for x in self._offers.values() if now - x['timestamp'] < 3600] + # sort by proof-of-work + recent_offers = sorted(recent_offers, key=lambda x: x['pow_bits'], reverse=True) + # cap list size + recent_offers = recent_offers[:20] + return recent_offers + def _parse_offer(self, offer): return SwapFees( percentage = offer['percentage_fee'], @@ -1439,11 +1462,11 @@ class NostrTransport(Logger): return event_id @log_exceptions - async def send_request_to_server(self, method: str, request: dict) -> dict: - request['method'] = method - request['relays'] = self.config.NOSTR_RELAYS + async def send_request_to_server(self, method: str, request_data: dict) -> dict: + request_data['method'] = method + request_data['relays'] = self.config.NOSTR_RELAYS server_pubkey = self.config.SWAPSERVER_NPUB - event_id = await self.send_direct_message(server_pubkey, self.server_relays, json.dumps(request)) + event_id = await self.send_direct_message(server_pubkey, self.server_relays, json.dumps(request_data)) response = await self.dm_replies[event_id] return response @@ -1468,7 +1491,7 @@ class NostrTransport(Logger): continue # check if this is the most recent event for this pubkey pubkey = event.pubkey - ts = self.offers.get(pubkey, {}).get('timestamp', 0) + ts = self._offers.get(pubkey, {}).get('timestamp', 0) if event.created_at <= ts: #print('skipping old event', pubkey[0:10], event.id) continue @@ -1485,7 +1508,7 @@ class NostrTransport(Logger): content['pow_bits'] = pow_bits content['pubkey'] = pubkey content['timestamp'] = event.created_at - self.offers[pubkey] = content + self._offers[pubkey] = content # mirror event to other relays server_relays = content['relays'].split(',') if 'relays' in content else [] await self.taskgroup.spawn(self.rebroadcast_event(event, server_relays))