1
0

swaps: add base class for transports. move "get_recent_offers" logic

the "get_recent_offers" logic is now shared between the GUIs
This commit is contained in:
SomberNight
2025-02-14 14:10:24 +00:00
parent 6ddd0b95ca
commit 6749468866
4 changed files with 62 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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