commit 5fa09970b65ed0480a7040621c73afab3cabcd77
parent 3874f7ec77bdd49f3dac02078e7b8a468efb5f88
Author: ThomasV <thomasv@electrum.org>
Date: Thu, 28 May 2020 13:11:32 +0200
swaps: move fee logic to swap_manager, fix command line
Diffstat:
3 files changed, 119 insertions(+), 91 deletions(-)
diff --git a/electrum/commands.py b/electrum/commands.py
@@ -79,6 +79,8 @@ def satoshis(amount):
# satoshi conversion must not be performed by the parser
return int(COIN*Decimal(amount)) if amount not in ['!', None] else amount
+def format_satoshis(x):
+ return str(Decimal(x)/COIN) if x is not None else None
def json_normalize(x):
# note: The return value of commands, when going through the JSON-RPC interface,
@@ -1102,12 +1104,56 @@ class Commands:
return await self.network.local_watchtower.sweepstore.get_ctn(channel_point, None)
@command('wnp')
- async def submarine_swap(self, amount, password=None, wallet: Abstract_Wallet = None):
- return await wallet.lnworker.swap_manager.normal_swap(satoshis(amount), password)
+ async def normal_swap(self, onchain_amount, lightning_amount, password=None, wallet: Abstract_Wallet = None):
+ """
+ Normal submarine swap: send on-chain BTC, receive on Lightning
+ Note that your funds will be locked for 24h if you do not have enough incoming capacity.
+ """
+ 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, onchain_amount_sat, password)
+ return {
+ 'txid': txid,
+ 'lightning_amount': format_satoshis(lightning_amount_sat),
+ 'onchain_amount': format_satoshis(onchain_amount_sat),
+ }
@command('wn')
- async def reverse_swap(self, amount, wallet: Abstract_Wallet = None):
- return await wallet.lnworker.swap_manager.reverse_swap(satoshis(amount))
+ async def reverse_swap(self, lightning_amount, onchain_amount, wallet: Abstract_Wallet = None):
+ """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)
+ success = 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)
+ success = None
+ else:
+ lightning_amount_sat = satoshis(lightning_amount)
+ onchain_amount_sat = satoshis(onchain_amount)
+ success = await wallet.lnworker.swap_manager.reverse_swap(lightning_amount_sat, onchain_amount_sat)
+ return {
+ 'success': success,
+ 'lightning_amount': format_satoshis(lightning_amount_sat),
+ 'onchain_amount': format_satoshis(onchain_amount_sat),
+ }
def eval_bool(x: str) -> bool:
@@ -1135,6 +1181,8 @@ param_descriptions = {
'requested_amount': 'Requested amount (in BTC).',
'outputs': 'list of ["address", amount]',
'redeem_script': 'redeem script (hexadecimal)',
+ 'lightning_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
+ 'onchain_amount': "Amount sent or received in a submarine swap. Set it to 'dryrun' to receive a value",
}
command_options = {
diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py
@@ -29,12 +29,6 @@ class SwapDialog(WindowModalDialog):
self.config = window.config
self.swap_manager = self.window.wallet.lnworker.swap_manager
self.network = window.network
- self.normal_fee = 0
- self.lockup_fee = 0
- self.claim_fee = self.swap_manager.get_tx_fee()
- self.percentage = 0
- self.min_amount = 0
- self.max_amount = 0
vbox = QVBoxLayout(self)
vbox.addWidget(WWLabel('Swap lightning funds for on-chain funds if you need to increase your receiving capacity. This service is powered by the Boltz backend.'))
self.send_amount_e = BTCAmountEdit(self.window.get_decimal_point)
@@ -82,8 +76,6 @@ class SwapDialog(WindowModalDialog):
self.config.set_key('fee_level', pos, False)
else:
self.config.set_key('fee_per_kb', fee_rate, False)
- # read claim_fee from config
- self.claim_fee = self.swap_manager.get_tx_fee()
if self.send_follows:
self.on_recv_edited()
else:
@@ -102,7 +94,7 @@ class SwapDialog(WindowModalDialog):
self.send_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
amount = self.send_amount_e.get_amount()
self.recv_amount_e.follows = True
- self.recv_amount_e.setAmount(self.get_recv_amount(amount))
+ self.recv_amount_e.setAmount(self.swap_manager.get_recv_amount(amount, self.is_reverse))
self.recv_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
self.recv_amount_e.follows = False
self.send_follows = False
@@ -113,72 +105,26 @@ class SwapDialog(WindowModalDialog):
self.recv_amount_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
amount = self.recv_amount_e.get_amount()
self.send_amount_e.follows = True
- self.send_amount_e.setAmount(self.get_send_amount(amount))
+ self.send_amount_e.setAmount(self.swap_manager.get_send_amount(amount, self.is_reverse))
self.send_amount_e.setStyleSheet(ColorScheme.BLUE.as_stylesheet())
self.send_amount_e.follows = False
self.send_follows = True
- def on_pairs(self, pairs):
- fees = pairs['pairs']['BTC/BTC']['fees']
- 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']
- self.update()
-
def update(self):
+ sm = self.swap_manager
self.send_button.setIcon(read_QIcon("lightning.png" if self.is_reverse else "bitcoin.png"))
self.recv_button.setIcon(read_QIcon("lightning.png" if not self.is_reverse else "bitcoin.png"))
- fee = self.lockup_fee + self.claim_fee if self.is_reverse else self.normal_fee
+ fee = sm.lockup_fee + sm.get_claim_fee() if self.is_reverse else sm.normal_fee
self.fee_label.setText(self.window.format_amount(fee) + ' ' + self.window.base_unit())
- self.percentage_label.setText('%.2f'%self.percentage + '%')
-
- def set_minimum(self):
- self.send_amount_e.setAmount(self.min_amount)
-
- def set_maximum(self):
- self.send_amount_e.setAmount(self.max_amount)
-
- def get_recv_amount(self, send_amount):
- if send_amount is None:
- return
- if send_amount < self.min_amount or send_amount > self.max_amount:
- return
- x = send_amount
- if self.is_reverse:
- x = int(x * (100 - self.percentage) / 100)
- x -= self.lockup_fee
- x -= self.claim_fee
- else:
- x -= self.normal_fee
- x = int(x * (100 - self.percentage) / 100)
- if x < 0:
- return
- return x
-
- def get_send_amount(self, recv_amount):
- if not recv_amount:
- return
- x = recv_amount
- if self.is_reverse:
- x += self.lockup_fee
- x += self.claim_fee
- x = int(x * 100 / (100 - self.percentage)) + 1
- else:
- x = int(x * 100 / (100 - self.percentage)) + 1
- x += self.normal_fee
- return x
+ self.percentage_label.setText('%.2f'%sm.percentage + '%')
def run(self):
- self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), self.on_pairs)
+ self.window.run_coroutine_from_thread(self.swap_manager.get_pairs(), lambda x: self.update())
if not self.exec_():
return
if self.is_reverse:
lightning_amount = self.send_amount_e.get_amount()
- onchain_amount = self.recv_amount_e.get_amount() + self.claim_fee
+ onchain_amount = self.recv_amount_e.get_amount() + self.swap_manager.get_claim_fee()
coro = self.swap_manager.reverse_swap(lightning_amount, onchain_amount)
self.window.run_coroutine_from_thread(coro)
else:
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
@@ -96,6 +96,23 @@ def create_claim_tx(txin, witness_script, preimage, privkey:bytes, address, amou
class SwapManager(Logger):
+ def __init__(self, wallet: 'Abstract_Wallet', network:'Network'):
+ Logger.__init__(self)
+ self.normal_fee = 0
+ self.lockup_fee = 0
+ self.percentage = 0
+ self.min_amount = 0
+ self.max_amount = 0
+ self.network = network
+ self.wallet = wallet
+ self.lnworker = wallet.lnworker
+ self.lnwatcher = self.wallet.lnworker.lnwatcher
+ self.swaps = self.wallet.db.get_dict('submarine_swaps')
+ for swap in self.swaps.values():
+ if swap.is_redeemed:
+ continue
+ self.add_lnwatcher_callback(swap)
+
@log_exceptions
async def _claim_swap(self, swap):
if not self.lnwatcher.is_up_to_date():
@@ -117,7 +134,7 @@ class SwapManager(Logger):
self.lnwatcher.remove_callback(swap.lockup_address)
swap.is_redeemed = True
continue
- amount_sat = txin._trusted_value_sats - self.get_tx_fee()
+ amount_sat = txin._trusted_value_sats - self.get_claim_fee()
if amount_sat < dust_threshold():
self.logger.info('utxo value below dust threshold')
continue
@@ -128,21 +145,9 @@ class SwapManager(Logger):
# save txid
swap.spending_txid = tx.txid()
- def get_tx_fee(self):
+ def get_claim_fee(self):
return self.lnwatcher.config.estimate_fee(136, allow_fallback_to_static_rates=True)
- 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
- self.swaps = self.wallet.db.get_dict('submarine_swaps')
- for swap in self.swaps.values():
- if swap.is_redeemed:
- continue
- self.add_lnwatcher_callback(swap)
-
def get_swap(self, payment_hash):
return self.swaps.get(payment_hash.hex())
@@ -211,12 +216,7 @@ class SwapManager(Logger):
self.swaps[payment_hash.hex()] = swap
self.add_lnwatcher_callback(swap)
await self.network.broadcast_transaction(tx)
- #
- attempt = await self.lnworker.await_payment(payment_hash)
- return {
- 'id':response_id,
- 'success':attempt.success,
- }
+ return tx.txid()
@log_exceptions
async def reverse_swap(self, amount_sat, expected_amount):
@@ -278,10 +278,7 @@ class SwapManager(Logger):
self.add_lnwatcher_callback(swap)
# initiate payment.
success, log = await self.lnworker._pay(invoice, attempts=10)
- return {
- 'id':response_id,
- 'success':success,
- }
+ return success
@log_exceptions
async def get_pairs(self):
@@ -289,5 +286,42 @@ class SwapManager(Logger):
'get',
API_URL + '/getpairs',
timeout=30)
- data = json.loads(response)
- return data
+ pairs = json.loads(response)
+ fees = pairs['pairs']['BTC/BTC']['fees']
+ self.percentage = fees['percentage']
+ self.normal_fee = fees['minerFees']['baseAsset']['normal']
+ self.lockup_fee = fees['minerFees']['baseAsset']['reverse']['lockup']
+ limits = pairs['pairs']['BTC/BTC']['limits']
+ self.min_amount = limits['minimal']
+ self.max_amount = limits['maximal']
+
+ def get_recv_amount(self, send_amount, is_reverse):
+ if send_amount is None:
+ return
+ if send_amount < self.min_amount or send_amount > self.max_amount:
+ return
+ x = send_amount
+ if is_reverse:
+ x = int(x * (100 - self.percentage) / 100)
+ x -= self.lockup_fee
+ x -= self.get_claim_fee()
+ else:
+ x -= self.normal_fee
+ x = int(x * (100 - self.percentage) / 100)
+ if x < 0:
+ return
+ return x
+
+ def get_send_amount(self, recv_amount, is_reverse):
+ if not recv_amount:
+ return
+ x = recv_amount
+ if is_reverse:
+ x += self.lockup_fee
+ x += self.get_claim_fee()
+ x = int(x * 100 / (100 - self.percentage)) + 1
+ else:
+ x = int(x * 100 / (100 - self.percentage)) + 1
+ x += self.normal_fee
+ return x
+