Browse Source

swaps: add swaps to android

patch-4
bitromortac 4 years ago
committed by ThomasV
parent
commit
fe78ed2a8e
  1. 6
      electrum/gui/kivy/main_window.py
  2. 328
      electrum/gui/kivy/uix/dialogs/lightning_channels.py

6
electrum/gui/kivy/main_window.py

@ -84,7 +84,7 @@ from electrum.util import (NoDynamicFeeEstimates, NotEnoughFunds,
BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME)
from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog
from .uix.dialogs.lightning_channels import LightningChannelsDialog
from .uix.dialogs.lightning_channels import LightningChannelsDialog, SwapDialog
if TYPE_CHECKING:
from . import ElectrumGui
@ -718,6 +718,10 @@ class ElectrumWindow(App, Logger):
d = LightningOpenChannelDialog(self)
d.open()
def swap_dialog(self):
d = SwapDialog(self, self.electrum_config)
d.open()
def open_channel_dialog_with_warning(self, b):
if b:
d = LightningOpenChannelDialog(self)

328
electrum/gui/kivy/uix/dialogs/lightning_channels.py

@ -1,11 +1,11 @@
import asyncio
import binascii
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional, Tuple
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.uix.popup import Popup
from kivy.clock import Clock
from .fee_dialog import FeeDialog
from electrum.util import bh2u
from electrum.logging import Logger
@ -13,12 +13,135 @@ from electrum.lnutil import LOCAL, REMOTE, format_short_channel_id
from electrum.lnchannel import AbstractChannel, Channel
from electrum.gui.kivy.i18n import _
from .question import Question
from electrum.transaction import PartialTxOutput, PartialTransaction
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, format_fee_satoshis
from electrum.lnutil import ln_dummy_address
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum import SimpleConfig
Builder.load_string(r'''
<SwapDialog@Popup>
id: popup
title: _('Lightning Swap')
size_hint: 0.8, 0.8
pos_hint: {'top':0.9}
method: 0
BoxLayout:
orientation: 'vertical'
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Swap Settings')
background_color: (0,0,0,0)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('You Send') + ':'
size_hint: 0.4, 1
Label:
id: send_amount_label
size_hint: 0.6, 1
text: _('0')
background_color: (0,0,0,0)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('You Receive') + ':'
size_hint: 0.4, 1
Label:
id: receive_amount_label
text: _('0')
background_color: (0,0,0,0)
size_hint: 0.6, 1
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Server Fee') + ':'
size_hint: 0.4, 1
Label:
id: server_fee_label
text: _('0')
background_color: (0,0,0,0)
size_hint: 0.6, 1
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Mining Fee') + ':'
size_hint: 0.4, 1
Label:
id: mining_fee_label
text: _('0')
background_color: (0,0,0,0)
size_hint: 0.6, 1
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
id: swap_action_label
text: _('Adds receiving capacity')
background_color: (0,0,0,0)
font_size: '14dp'
Slider:
id: swap_slider
range: 0, 4
step: 1
on_value: root.swap_slider_moved(self.value)
Widget:
size_hint: 1, 0.5
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Onchain Fees')
background_color: (0,0,0,0)
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Label:
text: _('Fee rate:')
Button:
id: fee_rate
text: '? sat/B'
background_color: (0,0,0,0)
bold: True
on_release:
root.on_fee_button()
Widget:
size_hint: 1, 0.5
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
TopLabel:
id: fee_estimate
text: ''
font_size: '14dp'
Widget:
size_hint: 1, 0.5
BoxLayout:
orientation: 'horizontal'
size_hint: 1, 0.5
Button:
text: 'Cancel'
size_hint: 0.5, None
height: '48dp'
on_release: root.dismiss()
Button:
id: ok_button
text: 'OK'
size_hint: 0.5, None
height: '48dp'
on_release:
root.on_ok()
root.dismiss()
<LightningChannelItem@CardItem>
details: {}
active: False
@ -95,14 +218,20 @@ Builder.load_string(r'''
Button:
size_hint: 0.3, None
height: '48dp'
text: _('Show Gossip')
on_release: popup.app.popup_dialog('lightning')
text: _('Open')
disabled: not root.has_lightning
on_release: popup.app.popup_dialog('lightning_open_channel_dialog')
Button:
size_hint: 0.3, None
height: '48dp'
text: _('New...')
text: _('Swap')
disabled: not root.has_lightning
on_release: popup.app.popup_dialog('lightning_open_channel_dialog')
on_release: popup.app.popup_dialog('swap_dialog')
Button:
size_hint: 0.3, None
height: '48dp'
text: _('Gossip')
on_release: popup.app.popup_dialog('lightning')
<ChannelDetailsPopup@Popup>:
@ -332,6 +461,7 @@ class ChannelBackupPopup(Popup, Logger):
self.app.wallet.lnbackups.remove_channel_backup(self.chan.channel_id)
self.dismiss()
class ChannelDetailsPopup(Popup, Logger):
def __init__(self, chan: Channel, app: 'ElectrumWindow', **kwargs):
@ -486,3 +616,189 @@ class LightningChannelsDialog(Factory.Popup):
return
self.can_send = self.app.format_amount_and_units(lnworker.num_sats_can_send())
self.can_receive = self.app.format_amount_and_units(lnworker.num_sats_can_receive())
# Swaps should be done in due time which is why we recommend a certain fee.
RECOMMEND_BLOCKS_SWAP = 25
class SwapDialog(Factory.Popup):
def __init__(self, app: 'ElectrumWindow', config: 'SimpleConfig'):
super(SwapDialog, self).__init__()
self.app = app
self.config = config
self.fmt_amt = self.app.format_amount_and_units
self.lnworker = self.app.wallet.lnworker
# swap related
self.swap_manager = self.lnworker.swap_manager
self.send_amount: Optional[int] = None
self.receive_amount: Optional[int] = None
self.tx = None # only for forward swap
# init swaps and sliders
asyncio.run(self.swap_manager.get_pairs())
self.update_and_init()
def update_and_init(self):
self.update_fee_text()
self.update_swap_slider()
self.swap_slider_moved(0)
def on_fee_button(self):
fee_dialog = FeeDialog(self, self.config, self.after_fee_changed)
fee_dialog.open()
def after_fee_changed(self):
self.update_fee_text()
self.update_swap_slider()
self.swap_slider_moved(self.ids.swap_slider.value)
def update_fee_text(self):
fee_per_kb = self.config.fee_per_kb()
# eta is -1 when block inclusion cannot be estimated for low fees
eta = self.config.fee_to_eta(fee_per_kb)
fee_per_b = format_fee_satoshis(fee_per_kb / 1000)
suggest_fee = self.config.eta_target_to_fee(RECOMMEND_BLOCKS_SWAP)
suggest_fee_per_b = format_fee_satoshis(suggest_fee / 1000)
s = 's' if eta > 1 else ''
if eta > RECOMMEND_BLOCKS_SWAP or eta == -1:
msg = f'Warning: Your fee rate of {fee_per_b} sat/B may be too ' \
f'low for the swap to succeed before its timeout. ' \
f'The recommended fee rate is at least {suggest_fee_per_b} ' \
f'sat/B.'
else:
msg = f'Info: Your swap is estimated to be processed in {eta} ' \
f'block{s} with an onchain fee rate of {fee_per_b} sat/B.'
self.ids.fee_rate.text = f'{fee_per_b} sat/B'
self.ids.fee_estimate.text = msg
def update_tx(self, onchain_amount: int):
"""Updates the transaction associated with a forward swap."""
if onchain_amount is None:
self.tx = None
self.ids.ok_button.disabled = True
return
outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)]
coins = self.app.wallet.get_spendable_coins(None)
try:
self.tx = self.app.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
self.tx = None
self.ids.ok_button.disabled = True
def update_swap_slider(self):
"""Sets the minimal and maximal amount that can be swapped for the swap
slider."""
# tx is updated again afterwards with send_amount in case of normal swap
# this is just to estimate the maximal spendable onchain amount for HTLC
self.update_tx('!')
try:
max_onchain_spend = self.tx.output_value_for_address(ln_dummy_address())
except AttributeError: # happens if there are no utxos
max_onchain_spend = 0
reverse = int(min(self.lnworker.num_sats_can_send(),
self.swap_manager.get_max_amount()))
forward = int(min(self.lnworker.num_sats_can_receive(),
# maximally supported swap amount by provider
self.swap_manager.get_max_amount(),
max_onchain_spend))
# we expect range to adjust the value of the swap slider to be in the
# correct range, i.e., to correct an overflow when reducing the limits
self.ids.swap_slider.range = (-reverse, forward)
def swap_slider_moved(self, position: float):
position = int(position)
# pay_amount and receive_amounts are always with fees already included
# so they reflect the net balance change after the swap
if position < 0: # reverse swap
self.ids.swap_action_label.text = "Adds Lightning receiving capacity."
self.is_reverse = True
pay_amount = abs(position)
self.send_amount = pay_amount
self.ids.send_amount_label.text = \
f"{self.fmt_amt(pay_amount)} (offchain)" if pay_amount else ""
receive_amount = self.swap_manager.get_recv_amount(
send_amount=pay_amount, is_reverse=True)
self.receive_amount = receive_amount
self.ids.receive_amount_label.text = \
f"{self.fmt_amt(receive_amount)} (onchain)" if receive_amount else ""
# fee breakdown
self.ids.server_fee_label.text = \
f"{self.swap_manager.percentage:0.1f}% + {self.fmt_amt(self.swap_manager.lockup_fee)}"
self.ids.mining_fee_label.text = \
f"{self.fmt_amt(self.swap_manager.get_claim_fee())}"
else: # forward (normal) swap
self.ids.swap_action_label.text = f"Adds Lightning sending capacity."
self.is_reverse = False
self.send_amount = position
self.update_tx(self.send_amount)
# add lockup fees, but the swap amount is position
pay_amount = position + self.tx.get_fee() if self.tx else 0
self.ids.send_amount_label.text = \
f"{self.fmt_amt(pay_amount)} (onchain)" if self.fmt_amt(pay_amount) else ""
receive_amount = self.swap_manager.get_recv_amount(
send_amount=position, is_reverse=False)
self.receive_amount = receive_amount
self.ids.receive_amount_label.text = \
f"{self.fmt_amt(receive_amount)} (offchain)" if receive_amount else ""
# fee breakdown
self.ids.server_fee_label.text = \
f"{self.swap_manager.percentage:0.1f}% + {self.fmt_amt(self.swap_manager.normal_fee)}"
self.ids.mining_fee_label.text = \
f"{self.fmt_amt(self.tx.get_fee())}" if self.tx else ""
if pay_amount and receive_amount:
self.ids.ok_button.disabled = False
else:
# add more nuanced error reporting?
self.ids.swap_action_label.text = "Swap below minimal swap size, change the slider."
self.ids.ok_button.disabled = True
def do_normal_swap(self, lightning_amount, onchain_amount, password):
tx = self.tx
assert tx
if lightning_amount is None or onchain_amount is None:
return
loop = self.app.network.asyncio_loop
coro = self.swap_manager.normal_swap(
lightning_amount, onchain_amount, password, tx=tx)
asyncio.run_coroutine_threadsafe(coro, loop)
def do_reverse_swap(self, lightning_amount, onchain_amount, password):
if lightning_amount is None or onchain_amount is None:
return
loop = self.app.network.asyncio_loop
coro = self.swap_manager.reverse_swap(
lightning_amount, onchain_amount + self.swap_manager.get_claim_fee())
asyncio.run_coroutine_threadsafe(coro, loop)
def on_ok(self):
if not self.app.network:
self.window.show_error(_("You are offline."))
return
if self.is_reverse:
lightning_amount = self.send_amount
onchain_amount = self.receive_amount
self.app.protected(
'Do you want to do a reverse submarine swap?',
self.do_reverse_swap, (lightning_amount, onchain_amount))
else:
lightning_amount = self.receive_amount
onchain_amount = self.send_amount
self.app.protected(
'Do you want to do a submarine swap? '
'You will need to wait for the swap transaction to confirm.',
self.do_normal_swap, (lightning_amount, onchain_amount))

Loading…
Cancel
Save