diff --git a/electrum/crypto.py b/electrum/crypto.py index 84d42340c..e3bfe6a3b 100644 --- a/electrum/crypto.py +++ b/electrum/crypto.py @@ -330,7 +330,7 @@ def sha256d(x: Union[bytes, str]) -> bytes: def hash_160(x: bytes) -> bytes: return ripemd(sha256(x)) -def ripemd(x): +def ripemd(x: bytes) -> bytes: try: md = hashlib.new('ripemd160') md.update(x) diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index 78c4b0f04..a85030e39 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -2,7 +2,7 @@ import asyncio import concurrent import threading from enum import IntEnum -from typing import Union +from typing import Union, Optional from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum @@ -39,7 +39,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): def __init__(self, parent=None): super().__init__(parent) - self._wallet = None + self._wallet = None # type: Optional[QEWallet] self._sliderPos = 0 self._rangeMin = 0 self._rangeMax = 0 @@ -377,8 +377,8 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): self.state = QESwapHelper.State.Started self._swap, invoice = fut.result() - tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, self._wallet.password) - coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(self._swap, invoice, tx) + 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) self.canCancel = True diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 06b701859..85ed0271a 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -734,7 +734,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): 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, tx.swap_invoice, tx) + 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=self.window.on_swap_result, diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 1d5ef7e16..7a35a2e40 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -330,8 +330,8 @@ class SwapDialog(WindowModalDialog, QtEventListener): except Exception as e: self.window.show_error(str(e)) return - tx = sm.create_funding_tx(swap, dummy_tx, password) - coro2 = sm.wait_for_htlcs_and_broadcast(swap, invoice, tx) + tx = sm.create_funding_tx(swap, dummy_tx, password=password) + coro2 = sm.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx) self.window.run_coroutine_dialog( coro2, _('Awaiting swap payment...'), on_result=self.window.on_swap_result, diff --git a/electrum/lnworker.py b/electrum/lnworker.py index 4e4835aed..d422ae2dd 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -1473,7 +1473,7 @@ class LNWallet(LNWorker): async def pay_invoice( self, invoice: str, *, amount_msat: int = None, - attempts: int = None, # used only in unit tests + attempts: int = None, # used only in unit tests (and for swaps?!) full_path: LNPaymentPath = None, channels: Optional[Sequence[Channel]] = None, ) -> Tuple[bool, List[HtlcLog]]: diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py index 2cb6e1522..c3b6cc20d 100644 --- a/electrum/plugins/swapserver/server.py +++ b/electrum/plugins/swapserver/server.py @@ -106,7 +106,7 @@ class SwapServer(Logger, EventListener): assert len(their_pubkey) == 33 swap = self.sm.create_reverse_swap( lightning_amount_sat=lightning_amount_sat, - their_pubkey=their_pubkey + their_pubkey=their_pubkey, ) response = { "id": swap.payment_hash.hex(), diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 9d9c403be..a5dd98e82 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -813,6 +813,7 @@ class SimpleConfig(Logger): @classmethod def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal], size: Union[int, float, Decimal]) -> int: + # note: 'size' is in vbytes size = Decimal(size) fee_per_kb = Decimal(fee_per_kb) fee_per_byte = fee_per_kb / 1000 diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index bb48f4acf..96d92a90a 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -1,7 +1,7 @@ import asyncio import json import os -from typing import TYPE_CHECKING, Optional, Dict, Union +from typing import TYPE_CHECKING, Optional, Dict, Union, Sequence, Tuple from decimal import Decimal import math import time @@ -9,6 +9,7 @@ import time import attr import aiohttp +from . import lnutil from .crypto import sha256, hash_160 from .ecc import ECPrivkey from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script, @@ -38,6 +39,7 @@ if TYPE_CHECKING: from .wallet import Abstract_Wallet from .lnwatcher import LNWalletWatcher from .lnworker import LNWallet + from .lnchannel import Channel from .simple_config import SimpleConfig @@ -76,7 +78,16 @@ WITNESS_TEMPLATE_REVERSE_SWAP = [ opcodes.OP_CHECKSIG ] -def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, *, refund_pubkey=None, claim_pubkey=None): + +def check_reverse_redeem_script( + *, + redeem_script: str, + lockup_address: str, + payment_hash: bytes, + locktime: int, + refund_pubkey: bytes = None, + claim_pubkey: bytes = None, +) -> None: redeem_script = bytes.fromhex(redeem_script) parsed_script = [x for x in script_GetOp(redeem_script)] if not match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP): @@ -91,7 +102,6 @@ def check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, loc raise Exception("rswap check failed: our pubkey not in script") if locktime != int.from_bytes(parsed_script[10][1], byteorder='little'): raise Exception("rswap check failed: inconsistent locktime and script") - return parsed_script[7][1], parsed_script[13][1] class SwapServerError(Exception): @@ -109,7 +119,7 @@ class SwapData(StoredObject): onchain_amount = attr.ib(type=int) # in sats lightning_amount = attr.ib(type=int) # in sats redeem_script = attr.ib(type=bytes, converter=hex_to_bytes) - preimage = attr.ib(type=bytes, converter=hex_to_bytes) + preimage = attr.ib(type=Optional[bytes], converter=hex_to_bytes) prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes) privkey = attr.ib(type=bytes, converter=hex_to_bytes) lockup_address = attr.ib(type=str) @@ -349,6 +359,7 @@ class SwapManager(Logger): return self.get_fee(CLAIM_FEE_SIZE) def get_fee(self, size): + # note: 'size' is in vbytes return self._get_fee(size=size, config=self.wallet.config) @classmethod @@ -376,15 +387,16 @@ class SwapManager(Logger): if swap.funding_txid is None: password = self.wallet.get_unlocked_password() for batch_rbf in [True, False]: - tx = self.create_funding_tx(swap, None, password, batch_rbf=batch_rbf) + tx = self.create_funding_tx(swap, None, password=password, batch_rbf=batch_rbf) try: await self.broadcast_funding_tx(swap, tx) except TxBroadcastServerReturnedError: continue break - def create_normal_swap(self, *, lightning_amount_sat=None, payment_hash: bytes=None, their_pubkey=None): + def create_normal_swap(self, *, lightning_amount_sat: int, payment_hash: bytes, their_pubkey: bytes = None): """ server method """ + assert lightning_amount_sat locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND our_privkey = os.urandom(32) our_pubkey = ECPrivkey(our_privkey).get_public_key_bytes(compressed=True) @@ -400,8 +412,6 @@ class SwapManager(Logger): lightning_amount_sat=lightning_amount_sat, payment_hash=payment_hash, our_privkey=our_privkey, - their_pubkey=their_pubkey, - invoice=None, prepay=True, ) self.lnworker.register_hold_invoice(payment_hash, self.hold_invoice_callback) @@ -409,35 +419,32 @@ class SwapManager(Logger): def add_normal_swap( self, *, - redeem_script=None, - locktime=None, - onchain_amount_sat=None, - lightning_amount_sat=None, - payment_hash=None, - our_privkey=None, - their_pubkey=None, - invoice=None, - prepay=None, - channels=None, - ): - """ if invoice is None, create a hold invoice """ + redeem_script: str, + locktime: int, # onchain + onchain_amount_sat: int, + lightning_amount_sat: int, + payment_hash: bytes, + our_privkey: bytes, + prepay: bool, + channels: Optional[Sequence['Channel']] = None, + ) -> Tuple[SwapData, str, str]: + """creates a hold invoice""" if prepay: prepay_amount_sat = self.get_claim_fee() * 2 invoice_amount_sat = lightning_amount_sat - prepay_amount_sat else: invoice_amount_sat = lightning_amount_sat - if not invoice: - _, invoice = self.lnworker.get_bolt11_invoice( - payment_hash=payment_hash, - amount_msat=invoice_amount_sat * 1000, - message='Submarine swap', - expiry=300, - fallback_address=None, - channels=channels, - ) - # add payment info to lnworker - self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) + _, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=invoice_amount_sat * 1000, + message='Submarine swap', + expiry=300, + fallback_address=None, + channels=channels, + ) + # add payment info to lnworker + self.lnworker.add_payment_info_for_hold_invoice(payment_hash, invoice_amount_sat) if prepay: prepay_hash = self.lnworker.create_payment_info(amount_msat=prepay_amount_sat*1000) @@ -477,14 +484,14 @@ class SwapManager(Logger): self.add_lnwatcher_callback(swap) return swap, invoice, prepay_invoice - def create_reverse_swap(self, *, lightning_amount_sat=None, their_pubkey=None): + def create_reverse_swap(self, *, lightning_amount_sat: int, their_pubkey: bytes) -> SwapData: """ server method. """ + assert lightning_amount_sat is not None locktime = self.network.get_local_height() + LOCKTIME_DELTA_REFUND privkey = os.urandom(32) our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) preimage = os.urandom(32) - assert lightning_amount_sat is not None payment_hash = sha256(preimage) redeem_script = construct_script( WITNESS_TEMPLATE_REVERSE_SWAP, @@ -501,7 +508,18 @@ class SwapManager(Logger): lightning_amount_sat=lightning_amount_sat) return swap - def add_reverse_swap(self, *, redeem_script=None, locktime=None, privkey=None, lightning_amount_sat=None, onchain_amount_sat=None, preimage=None, payment_hash=None, prepay_hash=None): + def add_reverse_swap( + self, + *, + redeem_script: str, + locktime: int, # onchain + privkey: bytes, + lightning_amount_sat: int, + onchain_amount_sat: int, + preimage: bytes, + payment_hash: bytes, + prepay_hash: Optional[bytes] = None, + ) -> SwapData: lockup_address = script_to_p2wsh(redeem_script) receive_address = self.wallet.get_receiving_address() swap = SwapData( @@ -526,7 +544,7 @@ class SwapManager(Logger): self.add_lnwatcher_callback(swap) return swap - def add_invoice(self, invoice, pay_now=False): + def add_invoice(self, invoice: str, pay_now: bool = False) -> None: invoice = Invoice.from_bech32(invoice) key = invoice.rhash payment_hash = bytes.fromhex(key) @@ -548,28 +566,41 @@ class SwapManager(Logger): password, tx: PartialTransaction = None, channels = None, - ) -> str: + ) -> Optional[str]: """send on-chain BTC, receive on Lightning + Old (removed) flow: - User generates an LN invoice with RHASH, and knows preimage. - User creates on-chain output locked to RHASH. - Server pays LN invoice. User reveals preimage. - Server spends the on-chain output using preimage. New flow: - - user requests swap - - server creates preimage, sends RHASH to user - - user creates hold invoice, sends it to server - + - User requests swap + - Server creates preimage, sends RHASH to user + - User creates hold invoice, sends it to server + - Server sends HTLC, user holds it + - User creates on-chain output locked to RHASH + - Server spends the on-chain output using preimage (revealing the preimage) + - User fulfills HTLC using preimage """ assert self.network assert self.lnwatcher - swap, invoice = await self.request_normal_swap(lightning_amount_sat, expected_onchain_amount_sat, channels=channels) - tx = self.create_funding_tx(swap, tx, password) - return await self.wait_for_htlcs_and_broadcast(swap, invoice, tx) + swap, invoice = await self.request_normal_swap( + lightning_amount_sat=lightning_amount_sat, + expected_onchain_amount_sat=expected_onchain_amount_sat, + channels=channels, + ) + tx = self.create_funding_tx(swap, tx, password=password) + return await self.wait_for_htlcs_and_broadcast(swap=swap, invoice=invoice, tx=tx) - async def request_normal_swap(self, lightning_amount_sat, expected_onchain_amount_sat, channels=None): - amount_msat = lightning_amount_sat * 1000 + async def request_normal_swap( + self, + *, + lightning_amount_sat: int, + expected_onchain_amount_sat: int, + channels: Optional[Sequence['Channel']] = None, + ) -> Tuple[SwapData, str]: refund_privkey = os.urandom(32) refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) @@ -585,8 +616,6 @@ class SwapManager(Logger): timeout=30) data = json.loads(response) payment_hash = bytes.fromhex(data["preimageHash"]) - preimage = None - invoice = None zeroconf = data["acceptZeroConf"] onchain_amount = data["expectedAmount"] @@ -594,7 +623,13 @@ class SwapManager(Logger): lockup_address = data["address"] redeem_script = data["redeemScript"] # verify redeem_script is built with our pubkey and preimage - claim_pubkey, _ = check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=refund_pubkey) + check_reverse_redeem_script( + redeem_script=redeem_script, + lockup_address=lockup_address, + payment_hash=payment_hash, + locktime=locktime, + refund_pubkey=refund_pubkey, + ) # check that onchain_amount is not more than what we estimated if onchain_amount > expected_onchain_amount_sat: @@ -611,14 +646,18 @@ class SwapManager(Logger): onchain_amount_sat=onchain_amount, payment_hash=payment_hash, our_privkey=refund_privkey, - their_pubkey=claim_pubkey, - invoice=invoice, prepay=False, channels=channels, ) return swap, invoice - async def wait_for_htlcs_and_broadcast(self, swap, invoice, tx): + async def wait_for_htlcs_and_broadcast( + self, + *, + swap: SwapData, + invoice: str, + tx: Transaction, + ) -> Optional[str]: payment_hash = swap.payment_hash refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True) async def callback(payment_hash): @@ -644,7 +683,14 @@ class SwapManager(Logger): await asyncio.sleep(0.1) return swap.funding_txid - def create_funding_tx(self, swap, tx, password, *, batch_rbf: Optional[bool] = None): + def create_funding_tx( + self, + swap: SwapData, + tx: Optional[PartialTransaction], + *, + password, + batch_rbf: Optional[bool] = None, + ) -> PartialTransaction: # create funding tx # note: rbf must not decrease payment # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output @@ -663,7 +709,7 @@ class SwapManager(Logger): return tx @log_exceptions - async def request_swap_for_tx(self, tx: 'PartialTransaction'): + async def request_swap_for_tx(self, tx: 'PartialTransaction') -> Optional[Tuple[SwapData, str, PartialTransaction]]: for o in tx.outputs(): if o.address == self.dummy_address: change_amount = o.value @@ -679,7 +725,7 @@ class SwapManager(Logger): return swap, invoice, tx @log_exceptions - async def broadcast_funding_tx(self, swap, tx): + async def broadcast_funding_tx(self, swap: SwapData, tx: Transaction) -> None: swap.funding_txid = tx.txid() await self.network.broadcast_transaction(tx) @@ -688,7 +734,7 @@ class SwapManager(Logger): *, lightning_amount_sat: int, expected_onchain_amount_sat: int, - channels = None, + channels: Optional[Sequence['Channel']] = None, ) -> Optional[str]: """send on Lightning, receive on-chain @@ -729,7 +775,14 @@ class SwapManager(Logger): onchain_amount = data["onchainAmount"] response_id = data['id'] # verify redeem_script is built with our pubkey and preimage - check_reverse_redeem_script(redeem_script, lockup_address, payment_hash, locktime, refund_pubkey=None, claim_pubkey=our_pubkey) + check_reverse_redeem_script( + redeem_script=redeem_script, + lockup_address=lockup_address, + payment_hash=payment_hash, + locktime=locktime, + refund_pubkey=None, + claim_pubkey=our_pubkey, + ) # check that the onchain amount is what we expected if onchain_amount < expected_onchain_amount_sat: raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "