Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swaps over nostr #9260

Merged
merged 2 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions contrib/requirements/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ certifi
attrs>=20.1.0
jsonpatch
electrum_ecc
electrum_aionostr>=0.0.6

# Note that we also need the dnspython[DNSSEC] extra which pulls in cryptography,
# but as that is not pure-python it cannot be listed in this file!
Expand Down
75 changes: 39 additions & 36 deletions electrum/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -1320,24 +1320,26 @@ async def normal_swap(self, onchain_amount, lightning_amount, password=None, wal
Normal submarine swap: send on-chain BTC, receive on Lightning
"""
sm = wallet.lnworker.swap_manager
if lightning_amount == 'dryrun':
await sm.get_pairs()
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
txid = None
elif onchain_amount == 'dryrun':
await sm.get_pairs()
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = satoshis(onchain_amount)
txid = await wallet.lnworker.swap_manager.normal_swap(
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
password=password,
)
with sm.create_transport() as transport:
await sm.is_initialized.wait()
if lightning_amount == 'dryrun':
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_recv_amount(onchain_amount_sat, is_reverse=False)
txid = None
elif onchain_amount == 'dryrun':
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_send_amount(lightning_amount_sat, is_reverse=False)
txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = satoshis(onchain_amount)
txid = await wallet.lnworker.swap_manager.normal_swap(
transport,
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
password=password,
)

return {
'txid': txid,
'lightning_amount': format_satoshis(lightning_amount_sat),
Expand All @@ -1349,24 +1351,25 @@ async def reverse_swap(self, lightning_amount, onchain_amount, password=None, wa
"""Reverse submarine swap: send on Lightning, receive on-chain
"""
sm = wallet.lnworker.swap_manager
if onchain_amount == 'dryrun':
await sm.get_pairs()
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
funding_txid = None
elif lightning_amount == 'dryrun':
await sm.get_pairs()
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
funding_txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
claim_fee = sm.get_claim_fee()
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
)
with sm.create_transport() as transport:
await sm.is_initialized.wait()
if onchain_amount == 'dryrun':
lightning_amount_sat = satoshis(lightning_amount)
onchain_amount_sat = sm.get_recv_amount(lightning_amount_sat, is_reverse=True)
funding_txid = None
elif lightning_amount == 'dryrun':
onchain_amount_sat = satoshis(onchain_amount)
lightning_amount_sat = sm.get_send_amount(onchain_amount_sat, is_reverse=True)
funding_txid = None
else:
lightning_amount_sat = satoshis(lightning_amount)
claim_fee = sm.get_claim_fee()
onchain_amount_sat = satoshis(onchain_amount) + claim_fee
funding_txid = await wallet.lnworker.swap_manager.reverse_swap(
transport,
lightning_amount_sat=lightning_amount_sat,
expected_onchain_amount_sat=onchain_amount_sat,
)
return {
'funding_txid': funding_txid,
'lightning_amount': format_satoshis(lightning_amount_sat),
Expand Down
115 changes: 102 additions & 13 deletions electrum/gui/qt/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -1160,19 +1160,98 @@ def run_swap_dialog(self, is_reverse=None, recv_amount_sat=None, channels=None):
if not self.wallet.lnworker.num_sats_can_send() and not self.wallet.lnworker.num_sats_can_receive():
self.show_error(_("You do not have liquidity in your active channels."))
return
try:
self.run_coroutine_dialog(
self.wallet.lnworker.swap_manager.get_pairs(), _('Please wait...'))
except SwapServerError as e:
self.show_error(str(e))
return
d = SwapDialog(self, is_reverse=is_reverse, recv_amount_sat=recv_amount_sat, channels=channels)
try:
return d.run()
except InvalidSwapParameters as e:
self.show_error(str(e))

transport = self.create_sm_transport()
if not transport:
return

with transport:
if not self.initialize_swap_manager(transport):
return
d = SwapDialog(self, transport, is_reverse=is_reverse, recv_amount_sat=recv_amount_sat, channels=channels)
try:
return d.run(transport)
except InvalidSwapParameters as e:
self.show_error(str(e))
return

def create_sm_transport(self):
sm = self.wallet.lnworker.swap_manager
if sm.is_server:
self.show_error(_('Swap server is active'))
return False

if self.network is None:
return False

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 sm.create_transport()

def initialize_swap_manager(self, transport):
sm = self.wallet.lnworker.swap_manager
if not sm.is_initialized.is_set():
async def wait_until_initialized():
try:
await asyncio.wait_for(sm.is_initialized.wait(), timeout=5)
except asyncio.TimeoutError:
return
try:
self.run_coroutine_dialog(wait_until_initialized(), _('Please wait...'))
except Exception as e:
self.show_error(str(e))
return False

if not self.config.SWAPSERVER_URL and not sm.is_initialized.is_set():
if not self.choose_swapserver_dialog(transport):
return False

assert sm.is_initialized.is_set()
return True

def choose_swapserver_dialog(self, transport):
if not transport.is_connected.is_set():
self.show_message(
'\n'.join([
_('Could not connect to a Nostr relay.'),
_('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]
if not recent_offers:
self.show_message(
'\n'.join([
_('Could not find a swap provider.'),
]))
return False
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"
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.")
])
choice = self.query_choice(
msg = msg,
choices = server_keys,
title = _("Choose Swap Server"),
default_choice = self.config.SWAPSERVER_NPUB
)
if choice not in transport.offers:
return False
self.config.SWAPSERVER_NPUB = choice
pairs = transport.get_offer(choice)
sm.update_pairs(pairs)
return True

@qt_event_listener
def on_event_request_status(self, wallet, key, status):
if wallet != self.wallet:
Expand Down Expand Up @@ -1309,12 +1388,22 @@ def open_channel(self, connect_str, funding_sat, push_amt):
return
# we need to know the fee before we broadcast, because the txid is required
make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id)
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=funding_sat, allow_preview=False)
funding_tx = d.run()
funding_tx, _ = self.confirm_tx_dialog(make_tx, funding_sat, allow_preview=False)
if not funding_tx:
return
self._open_channel(connect_str, funding_sat, push_amt, funding_tx)

def confirm_tx_dialog(self, make_tx, output_value, allow_preview=True):
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, allow_preview=allow_preview)
if d.not_enough_funds:
# note: use confirmed_only=False here, regardless of config setting,
# as the user needs to get to ConfirmTxDialog to change the config setting
if not d.can_pay_assuming_zero_fees(confirmed_only=False):
text = self.send_tab.get_text_not_enough_funds_mentioning_frozen()
self.show_message(text)
return
return d.run(), d.is_preview

@protected
def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):
# read funding_sat from tx; converts '!' to int value
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/rbf_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from .main_window import ElectrumWindow


from .confirm_tx_dialog import ConfirmTxDialog, TxEditor, TxSizeLabel, HelpLabel
from .confirm_tx_dialog import TxEditor, TxSizeLabel, HelpLabel

class _BaseRBFDialog(TxEditor):

Expand Down
50 changes: 23 additions & 27 deletions electrum/gui/qt/send_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from electrum.logging import Logger
from electrum.bitcoin import DummyAddress
from electrum.plugin import run_hook
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, parse_max_spend, UserCancelled
from electrum.invoices import PR_PAID, Invoice, PR_BROADCASTING, PR_BROADCAST
from electrum.transaction import Transaction, PartialTxInput, PartialTxOutput
from electrum.network import TxBroadcastError, BestEffortRequestFailed
Expand All @@ -26,7 +26,6 @@
from .paytoedit import InvalidPaymentIdentifier
from .util import (WaitingDialog, HelpLabel, MessageBoxMixin, EnterButton, char_width_in_lineedit,
get_iconname_camera, read_QIcon, ColorScheme, icon_path)
from .confirm_tx_dialog import ConfirmTxDialog
from .invoice_list import InvoiceList

if TYPE_CHECKING:
Expand Down Expand Up @@ -321,31 +320,26 @@ def pay_onchain_dialog(
output_values = [x.value for x in outputs]
is_max = any(parse_max_spend(outval) for outval in output_values)
output_value = '!' if is_max else sum(output_values)
conf_dlg = ConfirmTxDialog(window=self.window, make_tx=make_tx, output_value=output_value)
if conf_dlg.not_enough_funds:
# note: use confirmed_only=False here, regardless of config setting,
# as the user needs to get to ConfirmTxDialog to change the config setting
if not conf_dlg.can_pay_assuming_zero_fees(confirmed_only=False):
text = self.get_text_not_enough_funds_mentioning_frozen()
self.show_message(text)
return
tx = conf_dlg.run()

tx, is_preview = self.window.confirm_tx_dialog(make_tx, output_value)
if tx is None:
# user cancelled
return
is_preview = conf_dlg.is_preview

if tx.has_dummy_output(DummyAddress.SWAP):
sm = self.wallet.lnworker.swap_manager
coro = sm.request_swap_for_tx(tx)
try:
swap, invoice, tx = self.network.run_from_another_thread(coro)
except SwapServerError as e:
self.show_error(str(e))
return
assert not tx.has_dummy_output(DummyAddress.SWAP)
tx.swap_invoice = invoice
tx.swap_payment_hash = swap.payment_hash
with self.window.create_sm_transport() as transport:
if not self.window.initialize_swap_manager(transport):
return
coro = sm.request_swap_for_tx(transport, tx)
try:
swap, invoice, tx = self.window.run_coroutine_dialog(coro, _('Requesting swap invoice...'))
except SwapServerError as e:
self.show_error(str(e))
return
assert not tx.has_dummy_output(DummyAddress.SWAP)
tx.swap_invoice = invoice
tx.swap_payment_hash = swap.payment_hash

if is_preview:
self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier)
Expand Down Expand Up @@ -744,12 +738,14 @@ def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentI
if hasattr(tx, 'swap_payment_hash'):
sm = self.wallet.lnworker.swap_manager
swap = sm.get_swap(tx.swap_payment_hash)
coro = sm.wait_for_htlcs_and_broadcast(swap=swap, invoice=tx.swap_invoice, tx=tx)
self.window.run_coroutine_dialog(
coro, _('Awaiting swap payment...'),
on_result=lambda funding_txid: self.window.on_swap_result(funding_txid, is_reverse=False),
on_cancelled=lambda: sm.cancel_normal_swap(swap))
return
with sm.create_transport() as transport:
coro = sm.wait_for_htlcs_and_broadcast(transport, swap=swap, invoice=tx.swap_invoice, tx=tx)
try:
funding_txid = self.window.run_coroutine_dialog(coro, _('Awaiting lightning payment...'))
except UserCancelled:
sm.cancel_normal_swap(swap)
return
self.window.on_swap_result(funding_txid, is_reverse=False)

def broadcast_thread():
# non-GUI thread
Expand Down
8 changes: 8 additions & 0 deletions electrum/gui/qt/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ def lnfee_slider_released():
self.set_alias_color()
self.alias_e.editingFinished.connect(self.on_alias_edit)

nostr_relays_label = HelpLabel.from_configvar(self.config.cv.NOSTR_RELAYS)
nostr_relays = self.config.NOSTR_RELAYS
self.nostr_relays_e = QLineEdit(nostr_relays)
def on_nostr_edit():
self.config.NOSTR_RELAYS = str(self.nostr_relays_e.text())
self.nostr_relays_e.editingFinished.connect(on_nostr_edit)

msat_cb = checkbox_from_configvar(self.config.cv.BTC_AMOUNTS_PREC_POST_SAT)
msat_cb.setChecked(self.config.BTC_AMOUNTS_PREC_POST_SAT > 0)
def on_msat_checked(_x):
Expand Down Expand Up @@ -392,6 +399,7 @@ def on_history_rates(_x):
misc_widgets = []
misc_widgets.append((updatecheck_cb, None))
misc_widgets.append((filelogging_cb, None))
misc_widgets.append((nostr_relays_label, self.nostr_relays_e))
misc_widgets.append((alias_label, self.alias_e))
misc_widgets.append((qr_label, qr_combo))

Expand Down
Loading