swaps: replace offers dict with class, fix incorrect naming
introduces a class SwapOffer which is used instead of passing around offers in dicts. Also fixes incorrect variable naming of swapserver npubs / public keys by assigning the npub instead of the hex pubkey to config.SWAPSERVER_NPUB
This commit is contained in:
@@ -2,7 +2,7 @@ import asyncio
|
||||
import concurrent
|
||||
import threading
|
||||
from enum import IntEnum
|
||||
from typing import Union, Optional
|
||||
from typing import Union, Optional, TYPE_CHECKING, Sequence
|
||||
|
||||
from PyQt6.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum, QAbstractListModel, Qt,
|
||||
QModelIndex)
|
||||
@@ -22,6 +22,9 @@ from .qetypes import QEAmount
|
||||
from .qewallet import QEWallet
|
||||
from .util import QtEventListener, qt_event_listener
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.submarine_swaps import SwapOffer
|
||||
|
||||
|
||||
class InvalidSwapParameters(Exception): pass
|
||||
|
||||
@@ -64,16 +67,16 @@ class QESwapServerNPubListModel(QAbstractListModel):
|
||||
self._services = []
|
||||
self.endResetModel()
|
||||
|
||||
def initModel(self, items):
|
||||
def initModel(self, items: Sequence['SwapOffer']):
|
||||
self.beginInsertRows(QModelIndex(), len(items), len(items))
|
||||
self._services = [{
|
||||
'npub': x['pubkey'],
|
||||
'percentage_fee': x['percentage_fee'],
|
||||
'mining_fee': x['mining_fee'],
|
||||
'min_amount': x['min_amount'],
|
||||
'max_forward_amount': x['max_forward_amount'],
|
||||
'max_reverse_amount': x['max_reverse_amount'],
|
||||
'timestamp': age(x['timestamp']),
|
||||
'npub': x.server_npub,
|
||||
'percentage_fee': x.pairs.percentage,
|
||||
'mining_fee': x.pairs.mining_fee,
|
||||
'min_amount': x.pairs.min_amount,
|
||||
'max_forward_amount': x.pairs.max_forward,
|
||||
'max_reverse_amount': x.pairs.max_reverse,
|
||||
'timestamp': age(x.timestamp),
|
||||
} for x in items]
|
||||
self.endInsertRows()
|
||||
self.countChanged.emit()
|
||||
|
||||
@@ -106,6 +106,7 @@ from electrum.gui.common_qt.util import TaskThread
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ElectrumGui
|
||||
from electrum.submarine_swaps import SwapOffer
|
||||
|
||||
|
||||
class StatusBarButton(QToolButton):
|
||||
@@ -1333,8 +1334,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
if choice is None:
|
||||
return False
|
||||
self.config.SWAPSERVER_NPUB = choice
|
||||
pairs = transport.get_offer(choice)
|
||||
sm.update_pairs(pairs)
|
||||
offer = transport.get_offer(choice)
|
||||
sm.update_pairs(offer.pairs)
|
||||
return True
|
||||
|
||||
@qt_event_listener
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING, Optional, Union, Tuple
|
||||
from typing import TYPE_CHECKING, Optional, Union, Tuple, Sequence
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, Qt
|
||||
from PyQt6.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton
|
||||
@@ -21,7 +21,7 @@ from .my_treeview import create_toolbar_with_menu
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .main_window import ElectrumWindow
|
||||
from electrum.submarine_swaps import SwapServerTransport
|
||||
from electrum.submarine_swaps import SwapServerTransport, SwapOffer
|
||||
|
||||
CANNOT_RECEIVE_WARNING = _(
|
||||
"""The requested amount is higher than what you can receive in your currently open channels.
|
||||
@@ -31,7 +31,7 @@ Do you want to continue?"""
|
||||
)
|
||||
|
||||
|
||||
ROLE_PUBKEY = Qt.ItemDataRole.UserRole + 1000
|
||||
ROLE_NPUB = Qt.ItemDataRole.UserRole + 1000
|
||||
|
||||
class InvalidSwapParameters(Exception): pass
|
||||
|
||||
@@ -51,7 +51,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
||||
toolbar, menu = create_toolbar_with_menu(self.config, '')
|
||||
menu.addAction(
|
||||
_('Choose swap provider'),
|
||||
lambda: self.window.choose_swapserver_dialog(transport),
|
||||
lambda: self.choose_swap_server(transport),
|
||||
).setEnabled(not self.config.SWAPSERVER_URL)
|
||||
vbox.addLayout(toolbar)
|
||||
self.description_label = WWLabel(self.get_description())
|
||||
@@ -395,6 +395,10 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
||||
capacityType="receiving" if self.is_reverse else "sending",
|
||||
)
|
||||
|
||||
def choose_swap_server(self, transport: 'SwapServerTransport') -> None:
|
||||
self.window.choose_swapserver_dialog(transport) # type: ignore
|
||||
self.update()
|
||||
|
||||
|
||||
class SwapServerDialog(WindowModalDialog, QtEventListener):
|
||||
|
||||
@@ -425,21 +429,21 @@ class SwapServerDialog(WindowModalDialog, QtEventListener):
|
||||
|
||||
def run(self):
|
||||
if self.exec() != 1:
|
||||
return
|
||||
return None
|
||||
if item := self.servers_list.currentItem():
|
||||
return item.data(0, ROLE_PUBKEY)
|
||||
return item.data(0, ROLE_NPUB)
|
||||
return None
|
||||
|
||||
def update_servers_list(self, servers):
|
||||
def update_servers_list(self, servers: Sequence['SwapOffer']):
|
||||
self.servers_list.clear()
|
||||
from electrum.util import age
|
||||
items = []
|
||||
for x in servers:
|
||||
# fixme: these fields have not been sanitized yet
|
||||
last_seen = age(x['timestamp'])
|
||||
fee = f"{x['percentage_fee']}% + {x['mining_fee']} sats"
|
||||
max_forward = self.window.format_amount(x['max_forward_amount']) + ' ' + self.window.base_unit()
|
||||
max_reverse = self.window.format_amount(x['max_reverse_amount']) + ' ' + self.window.base_unit()
|
||||
item = QTreeWidgetItem([x['pubkey'][0:10], fee, max_forward, max_reverse, last_seen])
|
||||
item.setData(0, ROLE_PUBKEY, x['pubkey'])
|
||||
last_seen = age(x.timestamp)
|
||||
fee = f"{x.pairs.percentage}% + {x.pairs.mining_fee} sats"
|
||||
max_forward = self.window.format_amount(x.pairs.max_forward) + ' ' + self.window.base_unit()
|
||||
max_reverse = self.window.format_amount(x.pairs.max_reverse) + ' ' + self.window.base_unit()
|
||||
item = QTreeWidgetItem([x.server_pubkey[0:10], fee, max_forward, max_reverse, last_seen])
|
||||
item.setData(0, ROLE_NPUB, x.server_npub)
|
||||
items.append(item)
|
||||
self.servers_list.insertTopLevelItems(0, items)
|
||||
|
||||
@@ -135,7 +135,7 @@ class SwapServerError(Exception):
|
||||
def now():
|
||||
return int(time.time())
|
||||
|
||||
@attr.s
|
||||
@attr.s(frozen=True)
|
||||
class SwapFees:
|
||||
percentage = attr.ib(type=int)
|
||||
mining_fee = attr.ib(type=int)
|
||||
@@ -143,6 +143,18 @@ class SwapFees:
|
||||
max_forward = attr.ib(type=int)
|
||||
max_reverse = attr.ib(type=int)
|
||||
|
||||
@attr.frozen
|
||||
class SwapOffer:
|
||||
pairs = attr.ib(type=SwapFees)
|
||||
relays = attr.ib(type=list[str])
|
||||
pow_bits = attr.ib(type=int)
|
||||
server_pubkey = attr.ib(type=str)
|
||||
timestamp = attr.ib(type=int)
|
||||
|
||||
@property
|
||||
def server_npub(self):
|
||||
return to_nip19('npub', self.server_pubkey)
|
||||
|
||||
@stored_in('submarine_swaps')
|
||||
@attr.s
|
||||
class SwapData(StoredObject):
|
||||
@@ -962,7 +974,7 @@ class SwapManager(Logger):
|
||||
zeroed_num_str = f"{num_str[:digits]}{(len(num_str[digits:])) * '0'}"
|
||||
return int(zeroed_num_str)
|
||||
|
||||
def update_pairs(self, pairs):
|
||||
def update_pairs(self, pairs: SwapFees):
|
||||
self.logger.info(f'updating fees {pairs}')
|
||||
self.mining_fee = pairs.mining_fee
|
||||
self.percentage = pairs.percentage
|
||||
@@ -1365,7 +1377,7 @@ class NostrTransport(SwapServerTransport):
|
||||
|
||||
def __init__(self, config, sm, keypair):
|
||||
SwapServerTransport.__init__(self, config=config, sm=sm)
|
||||
self._offers = {} # type: Dict[str, Dict]
|
||||
self._offers = {} # type: Dict[str, SwapOffer]
|
||||
self.private_key = keypair.privkey
|
||||
self.nostr_private_key = to_nip19('nsec', keypair.privkey.hex())
|
||||
self.nostr_pubkey = keypair.pubkey.hex()[2:]
|
||||
@@ -1447,29 +1459,19 @@ class NostrTransport(SwapServerTransport):
|
||||
)
|
||||
return self.relay_manager
|
||||
|
||||
def get_offer(self, pubkey):
|
||||
offer = self._offers.get(pubkey)
|
||||
return self._parse_offer(offer)
|
||||
def get_offer(self, pubkey) -> Optional[SwapOffer]:
|
||||
return self._offers.get(pubkey)
|
||||
|
||||
def get_recent_offers(self) -> Sequence[Dict]:
|
||||
def get_recent_offers(self) -> Sequence[SwapOffer]:
|
||||
# filter to fresh timestamps
|
||||
now = int(time.time())
|
||||
recent_offers = [x for x in self._offers.values() if now - x['timestamp'] < 3600]
|
||||
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)
|
||||
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'],
|
||||
mining_fee=offer['mining_fee'],
|
||||
min_amount=offer['min_amount'],
|
||||
max_forward=offer['max_forward_amount'],
|
||||
max_reverse=offer['max_reverse_amount'],
|
||||
)
|
||||
|
||||
@ignore_exceptions
|
||||
@log_exceptions
|
||||
async def publish_offer(self, sm) -> None:
|
||||
@@ -1515,8 +1517,8 @@ class NostrTransport(SwapServerTransport):
|
||||
async def send_request_to_server(self, method: str, request_data: dict) -> dict:
|
||||
self.logger.debug(f"swapserver req: method: {method} relays: {self.relays}")
|
||||
request_data['method'] = method
|
||||
server_pubkey = self.config.SWAPSERVER_NPUB
|
||||
event_id = await self.send_direct_message(server_pubkey, json.dumps(request_data))
|
||||
server_npub = self.config.SWAPSERVER_NPUB
|
||||
event_id = await self.send_direct_message(server_npub, json.dumps(request_data))
|
||||
response = await self.dm_replies[event_id]
|
||||
if 'error' in response:
|
||||
self.logger.warning(f"error from swap server [DO NOT TRUST THIS MESSAGE]: {response['error']}")
|
||||
@@ -1544,12 +1546,13 @@ class NostrTransport(SwapServerTransport):
|
||||
continue
|
||||
if tags.get('r') != f"net:{constants.net.NET_NAME}":
|
||||
continue
|
||||
if (event.created_at > time.time() + 60 * 60
|
||||
or event.created_at < time.time() - 60 * 60):
|
||||
continue
|
||||
# check if this is the most recent event for this pubkey
|
||||
pubkey = event.pubkey
|
||||
ts = self._offers.get(pubkey, {}).get('timestamp', 0)
|
||||
if (event.created_at <= ts
|
||||
or event.created_at > time.time() + 60 * 60
|
||||
or event.created_at < time.time() - 60 * 60):
|
||||
prev_offer = self._offers.get(to_nip19('npub', pubkey))
|
||||
if prev_offer and event.created_at <= prev_offer.timestamp:
|
||||
continue
|
||||
try:
|
||||
pow_bits = get_nostr_ann_pow_amount(
|
||||
@@ -1561,15 +1564,28 @@ class NostrTransport(SwapServerTransport):
|
||||
if pow_bits < self.config.SWAPSERVER_POW_TARGET:
|
||||
self.logger.debug(f"too low pow: {pubkey}: pow: {pow_bits} nonce: {content.get('pow_nonce', 0)}")
|
||||
continue
|
||||
content['pow_bits'] = pow_bits
|
||||
content['pubkey'] = pubkey
|
||||
content['timestamp'] = event.created_at
|
||||
server_relays = content['relays'].split(',') if 'relays' in content else []
|
||||
content['relays'] = server_relays[:10] # limit to 10 relays
|
||||
self._offers[pubkey] = content
|
||||
if self.config.SWAPSERVER_NPUB == pubkey:
|
||||
pairs = self._parse_offer(content)
|
||||
try:
|
||||
pairs = SwapFees(
|
||||
percentage=content['percentage_fee'],
|
||||
mining_fee=content['mining_fee'],
|
||||
min_amount=content['min_amount'],
|
||||
max_forward=content['max_forward_amount'],
|
||||
max_reverse=content['max_reverse_amount'],
|
||||
)
|
||||
except Exception:
|
||||
self.logger.debug(f"swap fees couldn't be parsed", exc_info=True)
|
||||
continue
|
||||
offer = SwapOffer(
|
||||
pairs=pairs,
|
||||
relays=server_relays[:10],
|
||||
timestamp=event.created_at,
|
||||
server_pubkey=pubkey,
|
||||
pow_bits=pow_bits,
|
||||
)
|
||||
if self.config.SWAPSERVER_NPUB == offer.server_npub:
|
||||
self.sm.update_pairs(pairs)
|
||||
self._offers[offer.server_npub] = offer
|
||||
# mirror event to other relays
|
||||
await self.taskgroup.spawn(self.rebroadcast_event(event, server_relays))
|
||||
|
||||
@@ -1581,7 +1597,7 @@ class NostrTransport(SwapServerTransport):
|
||||
while True:
|
||||
previous_relays = self._last_swapserver_relays
|
||||
await self.sm.pairs_updated.wait()
|
||||
latest_known_relays = self._offers[self.config.SWAPSERVER_NPUB]['relays']
|
||||
latest_known_relays = self._offers[self.config.SWAPSERVER_NPUB].relays
|
||||
if latest_known_relays != previous_relays:
|
||||
self.logger.debug(f"swapserver relays changed, updating relay list.")
|
||||
# store the latest known relays to a file
|
||||
|
||||
Reference in New Issue
Block a user