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) BITCOIN_BIP21_URI_SCHEME, LIGHTNING_URI_SCHEME)
from .uix.dialogs.lightning_open_channel import LightningOpenChannelDialog 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: if TYPE_CHECKING:
from . import ElectrumGui from . import ElectrumGui
@ -718,6 +718,10 @@ class ElectrumWindow(App, Logger):
d = LightningOpenChannelDialog(self) d = LightningOpenChannelDialog(self)
d.open() d.open()
def swap_dialog(self):
d = SwapDialog(self, self.electrum_config)
d.open()
def open_channel_dialog_with_warning(self, b): def open_channel_dialog_with_warning(self, b):
if b: if b:
d = LightningOpenChannelDialog(self) d = LightningOpenChannelDialog(self)

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

@ -1,11 +1,11 @@
import asyncio import asyncio
import binascii from typing import TYPE_CHECKING, Optional, Tuple
from typing import TYPE_CHECKING
from kivy.lang import Builder from kivy.lang import Builder
from kivy.factory import Factory from kivy.factory import Factory
from kivy.uix.popup import Popup from kivy.uix.popup import Popup
from kivy.clock import Clock from kivy.clock import Clock
from .fee_dialog import FeeDialog
from electrum.util import bh2u from electrum.util import bh2u
from electrum.logging import Logger 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.lnchannel import AbstractChannel, Channel
from electrum.gui.kivy.i18n import _ from electrum.gui.kivy.i18n import _
from .question import Question 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: if TYPE_CHECKING:
from ...main_window import ElectrumWindow from ...main_window import ElectrumWindow
from electrum import SimpleConfig
Builder.load_string(r''' 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> <LightningChannelItem@CardItem>
details: {} details: {}
active: False active: False
@ -95,14 +218,20 @@ Builder.load_string(r'''
Button: Button:
size_hint: 0.3, None size_hint: 0.3, None
height: '48dp' height: '48dp'
text: _('Show Gossip') text: _('Open')
on_release: popup.app.popup_dialog('lightning') disabled: not root.has_lightning
on_release: popup.app.popup_dialog('lightning_open_channel_dialog')
Button: Button:
size_hint: 0.3, None size_hint: 0.3, None
height: '48dp' height: '48dp'
text: _('New...') text: _('Swap')
disabled: not root.has_lightning 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>: <ChannelDetailsPopup@Popup>:
@ -332,6 +461,7 @@ class ChannelBackupPopup(Popup, Logger):
self.app.wallet.lnbackups.remove_channel_backup(self.chan.channel_id) self.app.wallet.lnbackups.remove_channel_backup(self.chan.channel_id)
self.dismiss() self.dismiss()
class ChannelDetailsPopup(Popup, Logger): class ChannelDetailsPopup(Popup, Logger):
def __init__(self, chan: Channel, app: 'ElectrumWindow', **kwargs): def __init__(self, chan: Channel, app: 'ElectrumWindow', **kwargs):
@ -486,3 +616,189 @@ class LightningChannelsDialog(Factory.Popup):
return return
self.can_send = self.app.format_amount_and_units(lnworker.num_sats_can_send()) 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()) 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