diff --git a/electrum/plugins/swapserver/__init__.py b/electrum/plugins/swapserver/__init__.py new file mode 100644 index 000000000..867fb643e --- /dev/null +++ b/electrum/plugins/swapserver/__init__.py @@ -0,0 +1,6 @@ +from electrum.i18n import _ + +fullname = _('SwapServer') +description = '' + +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/swapserver/cmdline.py b/electrum/plugins/swapserver/cmdline.py new file mode 100644 index 000000000..7add7a386 --- /dev/null +++ b/electrum/plugins/swapserver/cmdline.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from .swapserver import SwapServerPlugin + +class Plugin(SwapServerPlugin): + pass + diff --git a/electrum/plugins/swapserver/qt.py b/electrum/plugins/swapserver/qt.py new file mode 100644 index 000000000..7add7a386 --- /dev/null +++ b/electrum/plugins/swapserver/qt.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +from .swapserver import SwapServerPlugin + +class Plugin(SwapServerPlugin): + pass + diff --git a/electrum/plugins/swapserver/server.py b/electrum/plugins/swapserver/server.py new file mode 100644 index 000000000..6a2f2ad70 --- /dev/null +++ b/electrum/plugins/swapserver/server.py @@ -0,0 +1,138 @@ +import os +import asyncio +import attr +import random +from collections import defaultdict + +from aiohttp import ClientResponse +from aiohttp import web, client_exceptions +from aiorpcx import timeout_after, TaskTimeout, ignore_after +from aiorpcx import NetAddress + + +from electrum.util import log_exceptions, ignore_exceptions +from electrum.logging import Logger +from electrum.util import EventListener, event_listener +from electrum.invoices import PR_PAID, PR_EXPIRED + + +class SwapServer(Logger, EventListener): + """ + public API: + - getpairs + - createswap + """ + + WWW_DIR = os.path.join(os.path.dirname(__file__), 'www') + + def __init__(self, config, wallet): + Logger.__init__(self) + self.config = config + self.wallet = wallet + self.addr = NetAddress.from_string(self.config.SWAPSERVER_ADDRESS) + self.register_callbacks() # eventlistener + + self.pending = defaultdict(asyncio.Event) + self.pending_msg = {} + + @ignore_exceptions + @log_exceptions + async def run(self): + self.root = '/root' + app = web.Application() + app.add_routes([web.get('/api/getpairs', self.get_pairs)]) + app.add_routes([web.post('/api/createswap', self.create_swap)]) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host=str(self.addr.host), port=self.addr.port, ssl_context=self.config.get_ssl_context()) + await site.start() + self.logger.info(f"now running and listening. addr={self.addr}") + + async def get_pairs(self, r): + sm = self.wallet.lnworker.swap_manager + sm.init_pairs() + pairs = { + "info": [], + "warnings": [], + "pairs": { + "BTC/BTC": { + "hash": "dfe692a026d6964601bfd79703611af333d1d5aa49ef5fedd288f5a620fced60", + "rate": 1, + "limits": { + "maximal": sm._max_amount, + "minimal": sm._min_amount, + "maximalZeroConf": { + "baseAsset": 0, + "quoteAsset": 0 + } + }, + "fees": { + "percentage": 0.5, + "minerFees": { + "baseAsset": { + "normal": sm.normal_fee, + "reverse": { + "claim": sm.claim_fee, + "lockup": sm.lockup_fee + } + }, + "quoteAsset": { + "normal": sm.normal_fee, + "reverse": { + "claim": sm.claim_fee, + "lockup": sm.lockup_fee + } + } + } + } + } + } + } + return web.json_response(pairs) + + async def create_swap(self, r): + sm = self.wallet.lnworker.swap_manager + sm.init_pairs() + request = await r.json() + req_type = request['type'] + assert request['pairId'] == 'BTC/BTC' + if req_type == 'reversesubmarine': + lightning_amount_sat=request['invoiceAmount'] + payment_hash=bytes.fromhex(request['preimageHash']) + their_pubkey=bytes.fromhex(request['claimPublicKey']) + assert len(payment_hash) == 32 + assert len(their_pubkey) == 33 + swap, payment_hash, invoice = sm.add_server_swap( + lightning_amount_sat=lightning_amount_sat, + payment_hash=payment_hash, + their_pubkey=their_pubkey + ) + response = { + 'id': payment_hash.hex(), + 'invoice': invoice, + 'minerFeeInvoice': None, + 'lockupAddress': swap.lockup_address, + 'redeemScript': swap.redeem_script.hex(), + 'timeoutBlockHeight': swap.locktime, + "onchainAmount": swap.onchain_amount, + } + elif req_type == 'submarine': + their_invoice=request['invoice'] + their_pubkey=bytes.fromhex(request['refundPublicKey']) + assert len(their_pubkey) == 33 + swap, payment_hash, invoice = sm.add_server_swap( + invoice=their_invoice, + their_pubkey=their_pubkey + ) + response = { + "id": payment_hash.hex(), + "acceptZeroConf": False, + "expectedAmount": swap.onchain_amount, + "timeoutBlockHeight": swap.locktime, + "address": swap.lockup_address, + "redeemScript": swap.redeem_script.hex() + } + else: + raise Exception('unsupported request type:' + req_type) + return web.json_response(response) diff --git a/electrum/plugins/swapserver/swapserver.py b/electrum/plugins/swapserver/swapserver.py new file mode 100644 index 000000000..d061c8bc1 --- /dev/null +++ b/electrum/plugins/swapserver/swapserver.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2023 The Electrum Developers +# +# Permission is hereby granted, free of charge, to any person +# obtaining a copy of this software and associated documentation files +# (the "Software"), to deal in the Software without restriction, +# including without limitation the rights to use, copy, modify, merge, +# publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import asyncio +import os +import random +from electrum.plugin import BasePlugin, hook +from electrum.util import log_exceptions, ignore_exceptions +from electrum import ecc + +from .server import SwapServer + + +class SwapServerPlugin(BasePlugin): + + def __init__(self, parent, config, name): + BasePlugin.__init__(self, parent, config, name) + self.config = config + self.server = None + + @hook + def daemon_wallet_loaded(self, daemon, wallet): + # we use the first wallet loaded + if self.server is not None: + return + if self.config.get('offline'): + return + + self.server = SwapServer(self.config, wallet) + sm = wallet.lnworker.swap_manager + jobs = [ + sm.pay_pending_invoices(), + self.server.run(), + ] + asyncio.run_coroutine_threadsafe(daemon._run(jobs=jobs), daemon.asyncio_loop) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index b0feab9e5..92ed68396 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -966,7 +966,7 @@ class SimpleConfig(Logger): # submarine swap server SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) SWAPSERVER_URL_TESTNET = ConfigVar('swapserver_url_testnet', default='https://swaps.electrum.org/testnet', type_=str) - SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='https://localhost/api', type_=str) + SWAPSERVER_URL_REGTEST = ConfigVar('swapserver_url_regtest', default='http://localhost:5455/api', type_=str) # connect to remote WT WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) @@ -981,6 +981,8 @@ class SimpleConfig(Logger): PAYSERVER_ROOT = ConfigVar('payserver_root', default='/r', type_=str) PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) + SWAPSERVER_ADDRESS = ConfigVar('swapserver_address', default='localhost:5455', type_=str) + PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index 4e7c7fe8e..32dda9eae 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -33,7 +33,10 @@ if TYPE_CHECKING: +CLAIM_FEE_SIZE = 136 +LOCKUP_FEE_SIZE = 153 # assuming 1 output, 2 outputs +MIN_LOCKTIME_DELTA = 60 WITNESS_TEMPLATE_SWAP = [ opcodes.OP_HASH160, @@ -102,14 +105,11 @@ class SwapData(StoredObject): is_redeemed = attr.ib(type=bool) _funding_prevout = None # type: Optional[TxOutpoint] # for RBF - __payment_hash = None + _payment_hash = None @property def payment_hash(self) -> bytes: - if self.__payment_hash is None: - self.__payment_hash = sha256(self.preimage) - return self.__payment_hash - + return self._payment_hash def create_claim_tx( *, @@ -139,6 +139,7 @@ class SwapManager(Logger): Logger.__init__(self) self.normal_fee = 0 self.lockup_fee = 0 + self.claim_fee = 0 # part of the boltz prococol, not used by Electrum self.percentage = 0 self._min_amount = None self._max_amount = None @@ -149,6 +150,7 @@ class SwapManager(Logger): self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData] self._swaps_by_lockup_address = {} # type: Dict[str, SwapData] for payment_hash, swap in self.swaps.items(): + swap._payment_hash = bytes.fromhex(payment_hash) self._add_or_reindex_swap(swap) self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash @@ -171,6 +173,30 @@ class SwapManager(Logger): continue self.add_lnwatcher_callback(swap) + async def pay_pending_invoices(self): + # for server + self.invoices_to_pay = set() + while True: + await asyncio.sleep(1) + for key in list(self.invoices_to_pay): + swap = self.swaps.get(key) + if not swap: + continue + invoice = self.wallet.get_invoice(key) + if not invoice: + continue + current_height = self.network.get_local_height() + delta = swap.locktime - current_height + if delta <= MIN_LOCKTIME_DELTA: + # fixme: should consider cltv of ln payment + self.logger.info(f'locktime too close {key}') + continue + success, log = await self.lnworker.pay_invoice(invoice.lightning_invoice, attempts=1) + if not success: + self.logger.info(f'failed to pay invoice {key}') + continue + self.invoices_to_pay.remove(key) + @log_exceptions async def _claim_swap(self, swap: SwapData) -> None: assert self.network @@ -187,9 +213,31 @@ class SwapManager(Logger): swap.funding_txid = txin.prevout.txid.hex() swap._funding_prevout = txin.prevout self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint + funding_conf = self.lnwatcher.adb.get_tx_height(txin.prevout.txid.hex()).conf spent_height = txin.spent_height + + if swap.is_reverse and swap.preimage is None: + if funding_conf <= 0: + continue + preimage = self.lnworker.get_preimage(swap.payment_hash) + if preimage is None: + self.invoices_to_pay.add(swap.payment_hash.hex()) + continue + swap.preimage = preimage + if spent_height is not None: swap.spending_txid = txin.spent_txid + if not swap.is_reverse and swap.preimage is None: + # we need to extract the preimage, add it to lnwatcher + # + tx = self.lnwatcher.adb.get_transaction(txin.spent_txid) + preimage = tx.inputs()[0].witness_elements()[1] + assert swap.payment_hash == sha256(preimage) + swap.preimage = preimage + self.logger.info(f'found preimage: {preimage.hex()}') + self.lnworker.preimages[swap.payment_hash.hex()] = preimage.hex() + # note: we must check the payment secret before we broadcast the funding tx + if spent_height > 0: if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: self.logger.info(f'stop watching swap {swap.lockup_address}') @@ -205,6 +253,10 @@ class SwapManager(Logger): if not swap.is_reverse and delta < 0: # too early for refund return + # + if swap.is_reverse and swap.preimage is None: + self.logger.info('preimage not available yet') + continue try: tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) except BelowDustLimit: @@ -215,11 +267,14 @@ class SwapManager(Logger): swap.spending_txid = tx.txid() def get_claim_fee(self): - return self._get_claim_fee(config=self.wallet.config) + return self.get_fee(CLAIM_FEE_SIZE) + + def get_fee(self, size): + return self._get_fee(size=size, config=self.wallet.config) @classmethod - def _get_claim_fee(cls, *, config: 'SimpleConfig'): - return config.estimate_fee(136, allow_fallback_to_static_rates=True) + def _get_fee(cls, *, size, config: 'SimpleConfig'): + return config.estimate_fee(size, allow_fallback_to_static_rates=True) def get_swap(self, payment_hash: bytes) -> Optional[SwapData]: # for history @@ -234,6 +289,73 @@ class SwapManager(Logger): callback = lambda: self._claim_swap(swap) self.lnwatcher.add_callback(swap.lockup_address, callback) + async def hold_invoice_callback(self, payment_hash): + key = payment_hash.hex() + if key in self.swaps: + swap = self.swaps[key] + if swap.funding_txid is None: + await self.start_normal_swap(swap, None, None) + + def add_server_swap(self, *, lightning_amount_sat=None, payment_hash=None, invoice=None, their_pubkey=None): + from .bitcoin import construct_script + from .crypto import ripemd + from .lnaddr import lndecode + from .invoices import Invoice + + locktime = self.network.get_local_height() + 140 + privkey = os.urandom(32) + our_pubkey = ECPrivkey(privkey).get_public_key_bytes(compressed=True) + is_reverse_for_server = (invoice is not None) + if is_reverse_for_server: + # client is doing a normal swap + lnaddr = lndecode(invoice) + payment_hash = lnaddr.paymenthash + lightning_amount_sat = int(lnaddr.get_amount_sat()) # should return int + onchain_amount_sat = self._get_send_amount(lightning_amount_sat, is_reverse=False) + redeem_script = construct_script( + WITNESS_TEMPLATE_SWAP, + {1:ripemd(payment_hash), 4:our_pubkey, 6:locktime, 9:their_pubkey} + ) + self.wallet.save_invoice(Invoice.from_bech32(invoice)) + else: + onchain_amount_sat = self._get_recv_amount(lightning_amount_sat, is_reverse=True) + lnaddr, invoice = self.lnworker.get_bolt11_invoice( + payment_hash=payment_hash, + amount_msat=lightning_amount_sat * 1000, + message='Submarine swap', + expiry=3600 * 24, + fallback_address=None, + channels=None, + ) + # add payment info to lnworker + self.lnworker.add_payment_info_for_hold_invoice(payment_hash, lightning_amount_sat) + self.lnworker.register_callback_for_hold_invoice(payment_hash, self.hold_invoice_callback, 60*60*24) + redeem_script = construct_script( + WITNESS_TEMPLATE_REVERSE_SWAP, + {1:32, 5:ripemd(payment_hash), 7:their_pubkey, 10:locktime, 13:our_pubkey} + ) + lockup_address = script_to_p2wsh(redeem_script) + receive_address = self.wallet.get_receiving_address() + swap = SwapData( + redeem_script = bytes.fromhex(redeem_script), + locktime = locktime, + privkey = privkey, + preimage = None, + prepay_hash = None, + lockup_address = lockup_address, + onchain_amount = onchain_amount_sat, + receive_address = receive_address, + lightning_amount = lightning_amount_sat, + is_reverse = is_reverse_for_server, + is_redeemed = False, + funding_txid = None, + spending_txid = None, + ) + swap._payment_hash = payment_hash + self._add_or_reindex_swap(swap) + self.add_lnwatcher_callback(swap) + return swap, payment_hash, invoice + async def normal_swap( self, *, @@ -304,18 +426,6 @@ class SwapManager(Logger): # verify that they are not locking up funds for more than a day if locktime - self.network.get_local_height() >= 144: raise Exception("fswap check failed: locktime too far in future") - # create funding tx - # note: rbf must not decrease payment - # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output - funding_output = PartialTxOutput.from_address_and_value(lockup_address, onchain_amount) - if tx is None: - tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password) - else: - dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), expected_onchain_amount_sat) - tx.outputs().remove(dummy_output) - tx.add_outputs([funding_output]) - tx.set_rbf(True) - self.wallet.sign_transaction(tx, password) # save swap data in wallet in case we need a refund receive_address = self.wallet.get_receiving_address() swap = SwapData( @@ -325,7 +435,7 @@ class SwapManager(Logger): preimage = preimage, prepay_hash = None, lockup_address = lockup_address, - onchain_amount = expected_onchain_amount_sat, + onchain_amount = onchain_amount, receive_address = receive_address, lightning_amount = lightning_amount_sat, is_reverse = False, @@ -333,10 +443,28 @@ class SwapManager(Logger): funding_txid = None, spending_txid = None, ) + swap._payment_hash = payment_hash self._add_or_reindex_swap(swap) self.add_lnwatcher_callback(swap) + return await self.start_normal_swap(swap, tx, password) + + @log_exceptions + async def start_normal_swap(self, swap, tx, password): + # create funding tx + # note: rbf must not decrease payment + # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output + funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) + if tx is None: + tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password) + else: + dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), swap.onchain_amount) + tx.outputs().remove(dummy_output) + tx.add_outputs([funding_output]) + tx.set_rbf(True) + self.wallet.sign_transaction(tx, password) await self.network.broadcast_transaction(tx) - return tx.txid() + swap.funding_txid = tx.txid() + return swap.funding_txid async def reverse_swap( self, @@ -401,7 +529,7 @@ class SwapManager(Logger): raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " f"{onchain_amount} < {expected_onchain_amount_sat}") # verify that we will have enough time to get our tx confirmed - if locktime - self.network.get_local_height() <= 60: + if locktime - self.network.get_local_height() <= MIN_LOCKTIME_DELTA: raise Exception("rswap check failed: locktime too close") # verify invoice preimage_hash lnaddr = self.lnworker._check_invoice(invoice) @@ -435,6 +563,7 @@ class SwapManager(Logger): funding_txid = None, spending_txid = None, ) + swap._payment_hash = preimage_hash self._add_or_reindex_swap(swap) # add callback to lnwatcher self.add_lnwatcher_callback(swap) @@ -459,6 +588,15 @@ class SwapManager(Logger): self._swaps_by_funding_outpoint[swap._funding_prevout] = swap self._swaps_by_lockup_address[swap.lockup_address] = swap + def init_pairs(self) -> None: + """ for server """ + self.percentage = 0.5 + self._min_amount = 20000 + self._max_amount = 10000000 + self.normal_fee = self.get_fee(CLAIM_FEE_SIZE) + self.lockup_fee = self.get_fee(LOCKUP_FEE_SIZE) + self.claim_fee = self.get_fee(CLAIM_FEE_SIZE) + async def get_pairs(self) -> None: """Might raise SwapServerError.""" from .network import Network @@ -479,6 +617,7 @@ class SwapManager(Logger): self.percentage = fees['percentage'] self.normal_fee = fees['minerFees']['baseAsset']['normal'] self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] + self.claim_fee = fees['minerFees']['baseAsset']['reverse']['claim'] limits = pairs['pairs']['BTC/BTC']['limits'] self._min_amount = limits['minimal'] self._max_amount = limits['maximal'] @@ -650,7 +789,7 @@ class SwapManager(Logger): ) -> PartialTransaction: # FIXME the mining fee should depend on swap.is_reverse. # the txs are not the same size... - amount_sat = txin.value_sats() - cls._get_claim_fee(config=config) + amount_sat = txin.value_sats() - cls._get_fee(size=CLAIM_FEE_SIZE, config=config) if amount_sat < dust_threshold(): raise BelowDustLimit() if swap.is_reverse: # successful reverse swap diff --git a/electrum/tests/regtest.py b/electrum/tests/regtest.py index 53eaa3e11..090425c60 100644 --- a/electrum/tests/regtest.py +++ b/electrum/tests/regtest.py @@ -47,6 +47,9 @@ class TestLightningAB(TestLightning): def test_collaborative_close(self): self.run_shell(['collaborative_close']) + def test_submarine_swap(self): + self.run_shell(['reverse_swap']) + def test_backup(self): self.run_shell(['backup']) diff --git a/electrum/tests/regtest/regtest.sh b/electrum/tests/regtest/regtest.sh index 38df96509..288127816 100755 --- a/electrum/tests/regtest/regtest.sh +++ b/electrum/tests/regtest/regtest.sh @@ -83,10 +83,11 @@ if [[ $1 == "init" ]]; then # alice is funded, bob is listening if [[ $2 == "bob" ]]; then $bob setconfig --offline lightning_listen localhost:9735 - else - echo "funding $2" - $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 + $bob setconfig --offline use_swapserver true + #else fi + echo "funding $2" + $bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1 fi @@ -170,6 +171,24 @@ if [[ $1 == "collaborative_close" ]]; then fi +if [[ $1 == "reverse_swap" ]]; then + wait_for_balance alice 1 + echo "alice opens channel" + bob_node=$($bob nodeid) + channel=$($alice open_channel $bob_node 0.15) + new_blocks 3 + wait_until_channel_open alice + echo "alice initiates swap" + dryrun=$($alice reverse_swap 0.02 dryrun) + echo $dryrun | jq + onchain_amount=$(echo $dryrun| jq -r ".onchain_amount") + $alice reverse_swap 0.02 $onchain_amount + new_blocks 1 + sleep 1 + new_blocks 1 +fi + + if [[ $1 == "extract_preimage" ]]; then # instead of settling bob will broadcast $bob enable_htlc_settle false