Browse Source

submarine swap server plugin:

- hold invoices
 - uses the same web API as the Boltz backend
master
ThomasV 3 years ago
parent
commit
098c65d732
  1. 6
      electrum/plugins/swapserver/__init__.py
  2. 31
      electrum/plugins/swapserver/cmdline.py
  3. 31
      electrum/plugins/swapserver/qt.py
  4. 138
      electrum/plugins/swapserver/server.py
  5. 58
      electrum/plugins/swapserver/swapserver.py
  6. 4
      electrum/simple_config.py
  7. 187
      electrum/submarine_swaps.py
  8. 3
      electrum/tests/regtest.py
  9. 25
      electrum/tests/regtest/regtest.sh

6
electrum/plugins/swapserver/__init__.py

@ -0,0 +1,6 @@
from electrum.i18n import _
fullname = _('SwapServer')
description = ''
available_for = ['qt', 'cmdline']

31
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

31
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

138
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)

58
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)

4
electrum/simple_config.py

@ -966,7 +966,7 @@ class SimpleConfig(Logger):
# submarine swap server # submarine swap server
SWAPSERVER_URL_MAINNET = ConfigVar('swapserver_url_mainnet', default='https://swaps.electrum.org/api', type_=str) 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_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 # connect to remote WT
WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool) WATCHTOWER_CLIENT_ENABLED = ConfigVar('use_watchtower', default=False, type_=bool)
WATCHTOWER_CLIENT_URL = ConfigVar('watchtower_url', default=None, type_=str) 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_ROOT = ConfigVar('payserver_root', default='/r', type_=str)
PAYSERVER_ALLOW_CREATE_INVOICE = ConfigVar('payserver_allow_create_invoice', default=False, type_=bool) 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) PLUGIN_TRUSTEDCOIN_NUM_PREPAY = ConfigVar('trustedcoin_prepay', default=20, type_=int)

187
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 = [ WITNESS_TEMPLATE_SWAP = [
opcodes.OP_HASH160, opcodes.OP_HASH160,
@ -102,14 +105,11 @@ class SwapData(StoredObject):
is_redeemed = attr.ib(type=bool) is_redeemed = attr.ib(type=bool)
_funding_prevout = None # type: Optional[TxOutpoint] # for RBF _funding_prevout = None # type: Optional[TxOutpoint] # for RBF
__payment_hash = None _payment_hash = None
@property @property
def payment_hash(self) -> bytes: def payment_hash(self) -> bytes:
if self.__payment_hash is None: return self._payment_hash
self.__payment_hash = sha256(self.preimage)
return self.__payment_hash
def create_claim_tx( def create_claim_tx(
*, *,
@ -139,6 +139,7 @@ class SwapManager(Logger):
Logger.__init__(self) Logger.__init__(self)
self.normal_fee = 0 self.normal_fee = 0
self.lockup_fee = 0 self.lockup_fee = 0
self.claim_fee = 0 # part of the boltz prococol, not used by Electrum
self.percentage = 0 self.percentage = 0
self._min_amount = None self._min_amount = None
self._max_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_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData]
self._swaps_by_lockup_address = {} # type: Dict[str, SwapData] self._swaps_by_lockup_address = {} # type: Dict[str, SwapData]
for payment_hash, swap in self.swaps.items(): for payment_hash, swap in self.swaps.items():
swap._payment_hash = bytes.fromhex(payment_hash)
self._add_or_reindex_swap(swap) self._add_or_reindex_swap(swap)
self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash self.prepayments = {} # type: Dict[bytes, bytes] # fee_rhash -> rhash
@ -171,6 +173,30 @@ class SwapManager(Logger):
continue continue
self.add_lnwatcher_callback(swap) 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 @log_exceptions
async def _claim_swap(self, swap: SwapData) -> None: async def _claim_swap(self, swap: SwapData) -> None:
assert self.network assert self.network
@ -187,9 +213,31 @@ class SwapManager(Logger):
swap.funding_txid = txin.prevout.txid.hex() swap.funding_txid = txin.prevout.txid.hex()
swap._funding_prevout = txin.prevout swap._funding_prevout = txin.prevout
self._add_or_reindex_swap(swap) # to update _swaps_by_funding_outpoint 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 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: if spent_height is not None:
swap.spending_txid = txin.spent_txid 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 spent_height > 0:
if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY: if current_height - spent_height > REDEEM_AFTER_DOUBLE_SPENT_DELAY:
self.logger.info(f'stop watching swap {swap.lockup_address}') 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: if not swap.is_reverse and delta < 0:
# too early for refund # too early for refund
return return
#
if swap.is_reverse and swap.preimage is None:
self.logger.info('preimage not available yet')
continue
try: try:
tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config) tx = self._create_and_sign_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
except BelowDustLimit: except BelowDustLimit:
@ -215,11 +267,14 @@ class SwapManager(Logger):
swap.spending_txid = tx.txid() swap.spending_txid = tx.txid()
def get_claim_fee(self): 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 @classmethod
def _get_claim_fee(cls, *, config: 'SimpleConfig'): def _get_fee(cls, *, size, config: 'SimpleConfig'):
return config.estimate_fee(136, allow_fallback_to_static_rates=True) return config.estimate_fee(size, allow_fallback_to_static_rates=True)
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]: def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
# for history # for history
@ -234,6 +289,73 @@ class SwapManager(Logger):
callback = lambda: self._claim_swap(swap) callback = lambda: self._claim_swap(swap)
self.lnwatcher.add_callback(swap.lockup_address, callback) 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( async def normal_swap(
self, self,
*, *,
@ -304,18 +426,6 @@ class SwapManager(Logger):
# verify that they are not locking up funds for more than a day # verify that they are not locking up funds for more than a day
if locktime - self.network.get_local_height() >= 144: if locktime - self.network.get_local_height() >= 144:
raise Exception("fswap check failed: locktime too far in future") 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 # save swap data in wallet in case we need a refund
receive_address = self.wallet.get_receiving_address() receive_address = self.wallet.get_receiving_address()
swap = SwapData( swap = SwapData(
@ -325,7 +435,7 @@ class SwapManager(Logger):
preimage = preimage, preimage = preimage,
prepay_hash = None, prepay_hash = None,
lockup_address = lockup_address, lockup_address = lockup_address,
onchain_amount = expected_onchain_amount_sat, onchain_amount = onchain_amount,
receive_address = receive_address, receive_address = receive_address,
lightning_amount = lightning_amount_sat, lightning_amount = lightning_amount_sat,
is_reverse = False, is_reverse = False,
@ -333,10 +443,28 @@ class SwapManager(Logger):
funding_txid = None, funding_txid = None,
spending_txid = None, spending_txid = None,
) )
swap._payment_hash = payment_hash
self._add_or_reindex_swap(swap) self._add_or_reindex_swap(swap)
self.add_lnwatcher_callback(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) await self.network.broadcast_transaction(tx)
return tx.txid() swap.funding_txid = tx.txid()
return swap.funding_txid
async def reverse_swap( async def reverse_swap(
self, self,
@ -401,7 +529,7 @@ class SwapManager(Logger):
raise Exception(f"rswap check failed: onchain_amount is less than what we expected: " raise Exception(f"rswap check failed: onchain_amount is less than what we expected: "
f"{onchain_amount} < {expected_onchain_amount_sat}") f"{onchain_amount} < {expected_onchain_amount_sat}")
# verify that we will have enough time to get our tx confirmed # 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") raise Exception("rswap check failed: locktime too close")
# verify invoice preimage_hash # verify invoice preimage_hash
lnaddr = self.lnworker._check_invoice(invoice) lnaddr = self.lnworker._check_invoice(invoice)
@ -435,6 +563,7 @@ class SwapManager(Logger):
funding_txid = None, funding_txid = None,
spending_txid = None, spending_txid = None,
) )
swap._payment_hash = preimage_hash
self._add_or_reindex_swap(swap) self._add_or_reindex_swap(swap)
# add callback to lnwatcher # add callback to lnwatcher
self.add_lnwatcher_callback(swap) self.add_lnwatcher_callback(swap)
@ -459,6 +588,15 @@ class SwapManager(Logger):
self._swaps_by_funding_outpoint[swap._funding_prevout] = swap self._swaps_by_funding_outpoint[swap._funding_prevout] = swap
self._swaps_by_lockup_address[swap.lockup_address] = 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: async def get_pairs(self) -> None:
"""Might raise SwapServerError.""" """Might raise SwapServerError."""
from .network import Network from .network import Network
@ -479,6 +617,7 @@ class SwapManager(Logger):
self.percentage = fees['percentage'] self.percentage = fees['percentage']
self.normal_fee = fees['minerFees']['baseAsset']['normal'] self.normal_fee = fees['minerFees']['baseAsset']['normal']
self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup'] self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup']
self.claim_fee = fees['minerFees']['baseAsset']['reverse']['claim']
limits = pairs['pairs']['BTC/BTC']['limits'] limits = pairs['pairs']['BTC/BTC']['limits']
self._min_amount = limits['minimal'] self._min_amount = limits['minimal']
self._max_amount = limits['maximal'] self._max_amount = limits['maximal']
@ -650,7 +789,7 @@ class SwapManager(Logger):
) -> PartialTransaction: ) -> PartialTransaction:
# FIXME the mining fee should depend on swap.is_reverse. # FIXME the mining fee should depend on swap.is_reverse.
# the txs are not the same size... # 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(): if amount_sat < dust_threshold():
raise BelowDustLimit() raise BelowDustLimit()
if swap.is_reverse: # successful reverse swap if swap.is_reverse: # successful reverse swap

3
electrum/tests/regtest.py

@ -47,6 +47,9 @@ class TestLightningAB(TestLightning):
def test_collaborative_close(self): def test_collaborative_close(self):
self.run_shell(['collaborative_close']) self.run_shell(['collaborative_close'])
def test_submarine_swap(self):
self.run_shell(['reverse_swap'])
def test_backup(self): def test_backup(self):
self.run_shell(['backup']) self.run_shell(['backup'])

25
electrum/tests/regtest/regtest.sh

@ -83,10 +83,11 @@ if [[ $1 == "init" ]]; then
# alice is funded, bob is listening # alice is funded, bob is listening
if [[ $2 == "bob" ]]; then if [[ $2 == "bob" ]]; then
$bob setconfig --offline lightning_listen localhost:9735 $bob setconfig --offline lightning_listen localhost:9735
else $bob setconfig --offline use_swapserver true
echo "funding $2" #else
$bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1
fi fi
echo "funding $2"
$bitcoin_cli sendtoaddress $($agent getunusedaddress -o) 1
fi fi
@ -170,6 +171,24 @@ if [[ $1 == "collaborative_close" ]]; then
fi 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 if [[ $1 == "extract_preimage" ]]; then
# instead of settling bob will broadcast # instead of settling bob will broadcast
$bob enable_htlc_settle false $bob enable_htlc_settle false

Loading…
Cancel
Save