commit 17ff6ffa0883d5b73845c3800fe5f2e04889e576
parent b26ad81e69b23cadebd9e2efb8d2861927d94543
Author: ThomasV <thomasv@electrum.org>
Date: Sun, 24 May 2020 13:42:36 +0200
submarine_swaps: add SwapManager
Diffstat:
4 files changed, 175 insertions(+), 197 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -1103,23 +1103,12 @@ class Commands:
@command('wnp')
async def submarine_swap(self, amount, password=None, wallet: Abstract_Wallet = None):
- return await submarine_swaps.normal_swap(satoshis(amount), wallet, self.network, password)
+ return await wallet.lnworker.swap_manager.normal_swap(satoshis(amount), password)
@command('wn')
async def reverse_swap(self, amount, wallet: Abstract_Wallet = None):
- return await submarine_swaps.reverse_swap(satoshis(amount), wallet, self.network)
+ return await wallet.lnworker.swap_manager.reverse_swap(satoshis(amount))
- @command('n')
- async def get_pairs(self):
- return await submarine_swaps.get_pairs(self.network)
-
- @command('wn')
- async def claim_swap(self, key, wallet: Abstract_Wallet = None):
- return await submarine_swaps.claim_swap(key, wallet)
-
- @command('wn')
- async def refund_swap(self, key, wallet: Abstract_Wallet = None):
- return await submarine_swaps.refund_swap(key, wallet)
def eval_bool(x: str) -> bool:
if x == 'false': return False
diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py
@@ -16,7 +16,6 @@ from .util import (MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButto
from .amountedit import BTCAmountEdit, FreezableLineEdit
-from electrum import submarine_swaps as ss
import asyncio
from .util import read_QIcon
@@ -26,6 +25,7 @@ class SwapDialog(WindowModalDialog):
def __init__(self, window):
WindowModalDialog.__init__(self, window, _('Submarine Swap'))
self.window = window
+ self.swap_manager = self.window.wallet.lnworker.swap_manager
self.network = window.network
self.normal_fee = 0
self.lockup_fee = 0
@@ -85,7 +85,7 @@ class SwapDialog(WindowModalDialog):
self.send_amount_e.follows = False
def get_pairs(self):
- fut = asyncio.run_coroutine_threadsafe(ss.get_pairs(self.network), self.network.asyncio_loop)
+ fut = asyncio.run_coroutine_threadsafe(self.swap_manager.get_pairs(), self.network.asyncio_loop)
pairs = fut.result()
print(pairs)
fees = pairs['pairs']['BTC/BTC']['fees']
@@ -125,9 +125,9 @@ class SwapDialog(WindowModalDialog):
return
if self.is_reverse:
amount_sat = self.send_amount_e.get_amount()
- coro = ss.reverse_swap(amount_sat, self.window.wallet, self.network)
+ coro = self.swap_manager.reverse_swap(amount_sat)
else:
amount_sat = self.recv_amount_e.get_amount()
password = self.window.protect(lambda x: x, [])
- coro = ss.normal_swap(amount_sat, self.window.wallet, self.network, password)
+ coro = self.swap_manager.normal_swap(amount_sat, password)
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
@@ -70,6 +70,7 @@ from .crypto import pw_encode_bytes, pw_decode_bytes, PW_HASH_VERSION_LATEST
from .lnutil import ChannelBackupStorage
from .lnchannel import ChannelBackup
from .channel_db import UpdateStatus
+from .submarine_swaps import SwapManager
if TYPE_CHECKING:
from .network import Network
@@ -556,6 +557,7 @@ class LNWallet(LNWorker):
self.lnwatcher = LNWalletWatcher(self, network)
self.lnwatcher.start_network(network)
self.network = network
+ self.swap_manager = SwapManager(self.wallet, network)
for chan in self.channels.values():
self.lnwatcher.add_channel(chan.funding_outpoint.to_str(), chan.get_funding_address())
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
@@ -6,10 +6,10 @@ from .ecc import ECPrivkey
from .bitcoin import address_to_script, script_to_p2wsh, redeem_script_to_address, opcodes, p2wsh_nested_script, push_script, is_segwit_address
from .transaction import TxOutpoint, PartialTxInput, PartialTxOutput, PartialTransaction, construct_witness
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
-from .transaction import Transaction
from .util import log_exceptions
from .bitcoin import dust_threshold
from typing import TYPE_CHECKING
+from .logging import Logger
if TYPE_CHECKING:
from .network import Network
@@ -73,182 +73,169 @@ def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amou
return tx
-@log_exceptions
-async def _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False):
- lnwatcher = lnworker.lnwatcher
- utxos = lnwatcher.get_addr_utxo(lockup_address)
- delta = lnwatcher.network.get_local_height() - locktime
- if is_refund and delta < 0:
- print('height not reached for refund', delta, locktime)
- return
- for txin in list(utxos.values()):
- fee = lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
- amount_sat = txin._trusted_value_sats - fee
- if amount_sat < dust_threshold():
- print('txo lower than dust threshold')
- continue
- tx = create_claim_tx(txin, redeem_script, preimage, privkey, address, amount_sat, locktime, is_refund)
- await lnwatcher.network.broadcast_transaction(tx)
-
-
-@log_exceptions
-async def claim_swap(key, wallet):
- lnworker = wallet.lnworker
- address = wallet.get_unused_address()
- swaps = wallet.db.get_dict('submarine_swaps')
- data = swaps[key]
- onchain_amount = data['onchainAmount']
- redeem_script = bytes.fromhex(data['redeemScript'])
- locktime = data['timeoutBlockHeight']
- lockup_address = data['lockupAddress']
- preimage = bytes.fromhex(data['preimage'])
- privkey = bytes.fromhex(data['privkey'])
- callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False)
- lnworker.lnwatcher.add_callback(lockup_address, callback)
- return True
-
-
-@log_exceptions
-async def refund_swap(key, wallet):
- lnworker = wallet.lnworker
- address = wallet.get_unused_address()
- swaps = wallet.db.get_dict('submarine_swaps')
- data = swaps[key]
- lockup_address = data['address']
- redeem_script = bytes.fromhex(data['redeemScript'])
- locktime = data['timeoutBlockHeight']
- preimage = bytes.fromhex(data['preimage'])
- privkey = bytes.fromhex(data['privkey'])
- callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=True)
- lnworker.lnwatcher.add_callback(lockup_address, callback)
- return True
-
-
-@log_exceptions
-async def normal_swap(amount_sat, wallet: 'Abstract_Wallet', network: 'Network', password):
- lnworker = wallet.lnworker
- privkey = os.urandom(32)
- pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
- key = await lnworker._add_request_coro(amount_sat, 'swap', expiry=3600*24)
- request = wallet.get_request(key)
- invoice = request['invoice']
- lnaddr = lnworker._check_invoice(invoice, amount_sat)
- payment_hash = lnaddr.paymenthash
- preimage = lnworker.get_preimage(payment_hash)
- address = wallet.get_unused_address()
- request_data = {
- "type": "submarine",
- "pairId": "BTC/BTC",
- "orderSide": "sell",
- "invoice": invoice,
- "refundPublicKey": pubkey.hex()
- }
- response = await network._send_http_on_proxy(
- 'post',
- API_URL + '/createswap',
- json=request_data,
- timeout=30)
- data = json.loads(response)
- response_id = data["id"]
- zeroconf = data["acceptZeroConf"]
- onchain_amount = data["expectedAmount"]
- locktime = data["timeoutBlockHeight"]
- lockup_address = data["address"]
- redeem_script = data["redeemScript"]
- # verify redeem_script is built with our pubkey and preimage
- redeem_script = bytes.fromhex(redeem_script)
- parsed_script = [x for x in script_GetOp(redeem_script)]
- assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP)
- assert script_to_p2wsh(redeem_script.hex()) == lockup_address
- assert hash_160(preimage) == parsed_script[1][1]
- assert pubkey == parsed_script[9][1]
- # verify that we will have enought time to get our tx confirmed
- assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little')
- assert locktime - network.get_local_height() == 140
- # save swap data in wallet in case we need a refund
- data['privkey'] = privkey.hex()
- data['preimage'] = preimage.hex()
- swaps = wallet.db.get_dict('submarine_swaps')
- swaps[response_id] = data
- callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=True)
- lnworker.lnwatcher.add_callback(lockup_address, callback)
- outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)]
- tx = wallet.create_transaction(outputs=outputs, rbf=False, password=password)
- await network.broadcast_transaction(tx)
- #
- attempt = await lnworker.await_payment(payment_hash)
- return {
- 'id':response_id,
- 'success':attempt.success,
- }
-
-
-@log_exceptions
-async def reverse_swap(amount_sat, wallet: 'Abstract_Wallet', network: 'Network'):
- privkey = os.urandom(32)
- pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
- preimage = os.urandom(32)
- preimage_hash = sha256(preimage)
- address = wallet.get_unused_address()
- request_data = {
- "type": "reversesubmarine",
- "pairId": "BTC/BTC",
- "orderSide": "buy",
- "invoiceAmount": amount_sat,
- "preimageHash": preimage_hash.hex(),
- "claimPublicKey": pubkey.hex()
- }
- response = await network._send_http_on_proxy(
- 'post',
- API_URL + '/createswap',
- json=request_data,
- timeout=30)
- data = json.loads(response)
- invoice = data['invoice']
- lockup_address = data['lockupAddress']
- redeem_script = data['redeemScript']
- locktime = data['timeoutBlockHeight']
- onchain_amount = data["onchainAmount"]
- response_id = data['id']
- # verify redeem_script is built with our pubkey and preimage
- redeem_script = bytes.fromhex(redeem_script)
- parsed_script = [x for x in script_GetOp(redeem_script)]
- assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP)
- assert script_to_p2wsh(redeem_script.hex()) == lockup_address
- assert hash_160(preimage) == parsed_script[5][1]
- assert pubkey == parsed_script[7][1]
- # verify that we will have enought time to get our tx confirmed
- assert locktime == int.from_bytes(parsed_script[10][1], byteorder='little')
- assert locktime - network.get_local_height() > 10
- # verify invoice preimage_hash
- lnworker = wallet.lnworker
- lnaddr = lnworker._check_invoice(invoice, amount_sat)
- assert lnaddr.paymenthash == preimage_hash
- # save swap data in wallet in case payment fails
- data['privkey'] = privkey.hex()
- data['preimage'] = preimage.hex()
- # save data to wallet file
- swaps = wallet.db.get_dict('submarine_swaps')
- swaps[response_id] = data
- # add callback to lnwatcher
- callback = lambda: _claim_swap(lnworker, lockup_address, redeem_script, preimage, privkey, address, locktime, is_refund=False)
- lnworker.lnwatcher.add_callback(lockup_address, callback)
- # initiate payment.
- success, log = await lnworker._pay(invoice, attempts=5)
- # discard data; this should be done by lnwatcher
- if success:
- swaps.pop(response_id)
- return {
- 'id':response_id,
- 'success':success,
- }
-
-
-@log_exceptions
-async def get_pairs(network):
- response = await network._send_http_on_proxy(
- 'get',
- API_URL + '/getpairs',
- timeout=30)
- data = json.loads(response)
- return data
+
+class SwapManager(Logger):
+
+ @log_exceptions
+ async def _claim_swap(self, lockup_address, redeem_script, preimage, privkey, locktime, is_refund=False):
+ utxos = self.lnwatcher.get_addr_utxo(lockup_address)
+ if not utxos:
+ return
+ delta = self.network.get_local_height() - locktime
+ if is_refund and delta < 0:
+ self.logger.info(f'height not reached for refund {lockup_address} {delta}, {locktime}')
+ return
+ for txin in list(utxos.values()):
+ fee = self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
+ amount_sat = txin._trusted_value_sats - fee
+ if amount_sat < dust_threshold():
+ self.logger.info('utxo value below dust threshold')
+ continue
+ address = self.wallet.get_unused_address()
+ tx = create_claim_tx(txin, redeem_script, preimage, privkey, address, amount_sat, locktime, is_refund)
+ await self.network.broadcast_transaction(tx)
+
+ def __init__(self, wallet: 'Abstract_Wallet', network:'Network'):
+ Logger.__init__(self)
+ self.network = network
+ self.wallet = wallet
+ self.lnworker = wallet.lnworker
+ self.lnwatcher = self.wallet.lnworker.lnwatcher
+ swaps = self.wallet.db.get_dict('submarine_swaps')
+ for key, data in swaps.items():
+ redeem_script = bytes.fromhex(data['redeemScript'])
+ locktime = data['timeoutBlockHeight']
+ preimage = bytes.fromhex(data['preimage'])
+ privkey = bytes.fromhex(data['privkey'])
+ if data.get('invoice'):
+ lockup_address = data['lockupAddress']
+ is_refund = False
+ else:
+ lockup_address = data['address']
+ is_refund = True
+ self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund)
+
+ def add_lnwatcher_callback(self, lockup_address, redeem_script, preimage, privkey, locktime, is_refund):
+ callback = lambda: self._claim_swap(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=is_refund)
+ self.lnwatcher.add_callback(lockup_address, callback)
+
+ @log_exceptions
+ async def normal_swap(self, amount_sat, password):
+ privkey = os.urandom(32)
+ pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
+ key = await self.lnworker._add_request_coro(amount_sat, 'swap', expiry=3600*24)
+ request = self.wallet.get_request(key)
+ invoice = request['invoice']
+ lnaddr = self.lnworker._check_invoice(invoice, amount_sat)
+ payment_hash = lnaddr.paymenthash
+ preimage = self.lnworker.get_preimage(payment_hash)
+ request_data = {
+ "type": "submarine",
+ "pairId": "BTC/BTC",
+ "orderSide": "sell",
+ "invoice": invoice,
+ "refundPublicKey": pubkey.hex()
+ }
+ response = await self.network._send_http_on_proxy(
+ 'post',
+ API_URL + '/createswap',
+ json=request_data,
+ timeout=30)
+ data = json.loads(response)
+ response_id = data["id"]
+ zeroconf = data["acceptZeroConf"]
+ onchain_amount = data["expectedAmount"]
+ locktime = data["timeoutBlockHeight"]
+ lockup_address = data["address"]
+ redeem_script = data["redeemScript"]
+ # verify redeem_script is built with our pubkey and preimage
+ redeem_script = bytes.fromhex(redeem_script)
+ parsed_script = [x for x in script_GetOp(redeem_script)]
+ assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_SWAP)
+ assert script_to_p2wsh(redeem_script.hex()) == lockup_address
+ assert hash_160(preimage) == parsed_script[1][1]
+ assert pubkey == parsed_script[9][1]
+ # verify that they are not locking up funds for more than a day
+ assert locktime == int.from_bytes(parsed_script[6][1], byteorder='little')
+ assert locktime - self.network.get_local_height() < 144
+ # save swap data in wallet in case we need a refund
+ data['privkey'] = privkey.hex()
+ data['preimage'] = preimage.hex()
+ swaps = self.wallet.db.get_dict('submarine_swaps')
+ swaps[response_id] = data
+ self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=True)
+ outputs = [PartialTxOutput.from_address_and_value(lockup_address, onchain_amount)]
+ tx = self.wallet.create_transaction(outputs=outputs, rbf=False, password=password)
+ await self.network.broadcast_transaction(tx)
+ #
+ attempt = await self.lnworker.await_payment(payment_hash)
+ return {
+ 'id':response_id,
+ 'success':attempt.success,
+ }
+
+ @log_exceptions
+ async def reverse_swap(self, amount_sat):
+ privkey = os.urandom(32)
+ pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True)
+ preimage = os.urandom(32)
+ preimage_hash = sha256(preimage)
+ request_data = {
+ "type": "reversesubmarine",
+ "pairId": "BTC/BTC",
+ "orderSide": "buy",
+ "invoiceAmount": amount_sat,
+ "preimageHash": preimage_hash.hex(),
+ "claimPublicKey": pubkey.hex()
+ }
+ response = await self.network._send_http_on_proxy(
+ 'post',
+ API_URL + '/createswap',
+ json=request_data,
+ timeout=30)
+ data = json.loads(response)
+ invoice = data['invoice']
+ lockup_address = data['lockupAddress']
+ redeem_script = data['redeemScript']
+ locktime = data['timeoutBlockHeight']
+ onchain_amount = data["onchainAmount"]
+ response_id = data['id']
+ # verify redeem_script is built with our pubkey and preimage
+ redeem_script = bytes.fromhex(redeem_script)
+ parsed_script = [x for x in script_GetOp(redeem_script)]
+ assert match_script_against_template(redeem_script, WITNESS_TEMPLATE_REVERSE_SWAP)
+ assert script_to_p2wsh(redeem_script.hex()) == lockup_address
+ assert hash_160(preimage) == parsed_script[5][1]
+ assert pubkey == parsed_script[7][1]
+ # verify that we will have enought time to get our tx confirmed
+ assert locktime == int.from_bytes(parsed_script[10][1], byteorder='little')
+ assert locktime - self.network.get_local_height() > 10
+ # verify invoice preimage_hash
+ lnaddr = self.lnworker._check_invoice(invoice, amount_sat)
+ assert lnaddr.paymenthash == preimage_hash
+ # save swap data in wallet in case payment fails
+ data['privkey'] = privkey.hex()
+ data['preimage'] = preimage.hex()
+ # save data to wallet file
+ swaps = self.wallet.db.get_dict('submarine_swaps')
+ swaps[response_id] = data
+ # add callback to lnwatcher
+ self.add_lnwatcher_callback(lockup_address, redeem_script, preimage, privkey, locktime, is_refund=False)
+ # initiate payment.
+ success, log = await self.lnworker._pay(invoice, attempts=5)
+ # discard data; this should be done by lnwatcher
+ if success:
+ swaps.pop(response_id)
+ return {
+ 'id':response_id,
+ 'success':success,
+ }
+
+ @log_exceptions
+ async def get_pairs(self):
+ response = await self.network._send_http_on_proxy(
+ 'get',
+ API_URL + '/getpairs',
+ timeout=30)
+ data = json.loads(response)
+ return data