LBRY-Vault/electrum/gui/kivy/uix/dialogs/tx_dialog.py
2019-11-04 22:24:36 +01:00

292 lines
10 KiB
Python

from datetime import datetime
from typing import NamedTuple, Callable, TYPE_CHECKING
from kivy.app import App
from kivy.factory import Factory
from kivy.properties import ObjectProperty
from kivy.lang import Builder
from kivy.clock import Clock
from kivy.uix.label import Label
from kivy.uix.dropdown import DropDown
from kivy.uix.button import Button
from .question import Question
from electrum.gui.kivy.i18n import _
from electrum.util import InvalidPassword
from electrum.address_synchronizer import TX_HEIGHT_LOCAL
from electrum.wallet import CannotBumpFee
if TYPE_CHECKING:
from ...main_window import ElectrumWindow
from electrum.transaction import Transaction
Builder.load_string('''
<TxDialog>
id: popup
title: _('Transaction')
is_mine: True
can_sign: False
can_broadcast: False
can_rbf: False
fee_str: ''
feerate_str: ''
date_str: ''
date_label:''
amount_str: ''
tx_hash: ''
status_str: ''
description: ''
outputs_str: ''
BoxLayout:
orientation: 'vertical'
ScrollView:
scroll_type: ['bars', 'content']
bar_width: '25dp'
GridLayout:
height: self.minimum_height
size_hint_y: None
cols: 1
spacing: '10dp'
padding: '10dp'
GridLayout:
height: self.minimum_height
size_hint_y: None
cols: 1
spacing: '10dp'
BoxLabel:
text: _('Status')
value: root.status_str
BoxLabel:
text: _('Description') if root.description else ''
value: root.description
BoxLabel:
text: root.date_label
value: root.date_str
BoxLabel:
text: _('Amount sent') if root.is_mine else _('Amount received')
value: root.amount_str
BoxLabel:
text: _('Transaction fee') if root.fee_str else ''
value: root.fee_str
BoxLabel:
text: _('Transaction fee rate') if root.feerate_str else ''
value: root.feerate_str
TopLabel:
text: _('Transaction ID') + ':' if root.tx_hash else ''
TxHashLabel:
data: root.tx_hash
name: _('Transaction ID')
TopLabel:
text: _('Outputs') + ':'
OutputList:
id: output_list
Widget:
size_hint: 1, 0.1
BoxLayout:
size_hint: 1, None
height: '48dp'
Button:
id: action_button
size_hint: 0.5, None
height: '48dp'
text: ''
disabled: True
opacity: 0
on_release: root.on_action_button_clicked()
IconButton:
size_hint: 0.5, None
height: '48dp'
icon: 'atlas://electrum/gui/kivy/theming/light/qrcode'
on_release: root.show_qr()
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Label')
on_release: root.label_dialog()
Button:
size_hint: 0.5, None
height: '48dp'
text: _('Close')
on_release: root.dismiss()
''')
class ActionButtonOption(NamedTuple):
text: str
func: Callable
enabled: bool
class TxDialog(Factory.Popup):
def __init__(self, app, tx):
Factory.Popup.__init__(self)
self.app = app # type: ElectrumWindow
self.wallet = self.app.wallet
self.tx = tx # type: Transaction
self._action_button_fn = lambda btn: None
def on_open(self):
self.update()
def update(self):
format_amount = self.app.format_amount_and_units
tx_details = self.wallet.get_tx_info(self.tx)
tx_mined_status = tx_details.tx_mined_status
exp_n = tx_details.mempool_depth_bytes
amount, fee = tx_details.amount, tx_details.fee
self.status_str = tx_details.status
self.description = tx_details.label
self.can_broadcast = tx_details.can_broadcast
self.can_rbf = tx_details.can_bump
self.tx_hash = tx_details.txid or ''
if tx_mined_status.timestamp:
self.date_label = _('Date')
self.date_str = datetime.fromtimestamp(tx_mined_status.timestamp).isoformat(' ')[:-3]
elif exp_n:
self.date_label = _('Mempool depth')
self.date_str = _('{} from tip').format('%.2f MB'%(exp_n/1000000))
else:
self.date_label = ''
self.date_str = ''
if amount is None:
self.amount_str = _("Transaction unrelated to your wallet")
elif amount > 0:
self.is_mine = False
self.amount_str = format_amount(amount)
else:
self.is_mine = True
self.amount_str = format_amount(-amount)
if fee is not None:
self.fee_str = format_amount(fee)
fee_per_kb = fee / self.tx.estimated_size() * 1000
self.feerate_str = self.app.format_fee_rate(fee_per_kb)
else:
self.fee_str = _('unknown')
self.feerate_str = _('unknown')
self.can_sign = self.wallet.can_sign(self.tx)
self.ids.output_list.update(self.tx.outputs())
self.is_local_tx = tx_mined_status.height == TX_HEIGHT_LOCAL
self.update_action_button()
def update_action_button(self):
action_button = self.ids.action_button
options = (
ActionButtonOption(text=_('Sign'), func=lambda btn: self.do_sign(), enabled=self.can_sign),
ActionButtonOption(text=_('Broadcast'), func=lambda btn: self.do_broadcast(), enabled=self.can_broadcast),
ActionButtonOption(text=_('Bump fee'), func=lambda btn: self.do_rbf(), enabled=self.can_rbf),
ActionButtonOption(text=_('Remove'), func=lambda btn: self.remove_local_tx(), enabled=self.is_local_tx),
)
num_options = sum(map(lambda o: bool(o.enabled), options))
# if no options available, hide button
if num_options == 0:
action_button.disabled = True
action_button.opacity = 0
return
action_button.disabled = False
action_button.opacity = 1
if num_options == 1:
# only one option, button will correspond to that
for option in options:
if option.enabled:
action_button.text = option.text
self._action_button_fn = option.func
else:
# multiple options. button opens dropdown which has one sub-button for each
dropdown = DropDown()
action_button.text = _('Options')
self._action_button_fn = dropdown.open
for option in options:
if option.enabled:
btn = Button(text=option.text, size_hint_y=None, height='48dp')
btn.bind(on_release=option.func)
dropdown.add_widget(btn)
def on_action_button_clicked(self):
action_button = self.ids.action_button
self._action_button_fn(action_button)
def do_rbf(self):
from .bump_fee_dialog import BumpFeeDialog
is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx)
if fee is None:
self.app.show_error(_("Can't bump fee: unknown fee for original transaction."))
return
size = self.tx.estimated_size()
d = BumpFeeDialog(self.app, fee, size, self._do_rbf)
d.open()
def _do_rbf(self, new_fee_rate, is_final):
if new_fee_rate is None:
return
try:
new_tx = self.wallet.bump_fee(tx=self.tx,
new_fee_rate=new_fee_rate)
except CannotBumpFee as e:
self.app.show_error(str(e))
return
if is_final:
new_tx.set_rbf(False)
self.tx = new_tx
self.update()
self.do_sign()
def do_sign(self):
self.app.protected(_("Enter your PIN code in order to sign this transaction"), self._do_sign, ())
def _do_sign(self, password):
self.status_str = _('Signing') + '...'
Clock.schedule_once(lambda dt: self.__do_sign(password), 0.1)
def __do_sign(self, password):
try:
self.app.wallet.sign_transaction(self.tx, password)
except InvalidPassword:
self.app.show_error(_("Invalid PIN"))
self.update()
def do_broadcast(self):
self.app.broadcast(self.tx)
def show_qr(self):
from electrum.bitcoin import base_encode, bfh
raw_tx = self.tx.serialize()
text = bfh(raw_tx)
text = base_encode(text, base=43)
self.app.qr_dialog(_("Raw Transaction"), text, text_for_clipboard=raw_tx)
def remove_local_tx(self):
txid = self.tx.txid()
to_delete = {txid}
to_delete |= self.wallet.get_depending_transactions(txid)
question = _("Are you sure you want to remove this transaction?")
if len(to_delete) > 1:
question = (_("Are you sure you want to remove this transaction and {} child transactions?")
.format(len(to_delete) - 1))
def on_prompt(b):
if b:
for tx in to_delete:
self.wallet.remove_transaction(tx)
self.wallet.storage.write()
self.app._trigger_update_wallet() # FIXME private...
self.dismiss()
d = Question(question, on_prompt)
d.open()
def label_dialog(self):
from .label_dialog import LabelDialog
key = self.tx.txid()
text = self.app.wallet.get_label(key)
def callback(text):
self.app.wallet.set_label(key, text)
self.update()
self.app.history_screen.update()
d = LabelDialog(_('Enter Transaction Label'), text, callback)
d.open()