from weakref import ref from decimal import Decimal import re import datetime import traceback, sys from kivy.app import App from kivy.cache import Cache from kivy.clock import Clock from kivy.compat import string_types from kivy.properties import (ObjectProperty, DictProperty, NumericProperty, ListProperty, StringProperty) from kivy.uix.label import Label from kivy.lang import Builder from kivy.factory import Factory from kivy.utils import platform from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds from electrum import bitcoin from electrum.util import timestamp_to_datetime from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from context_menu import ContextMenu from electrum_gui.kivy.i18n import _ class EmptyLabel(Factory.Label): pass class CScreen(Factory.Screen): __events__ = ('on_activate', 'on_deactivate', 'on_enter', 'on_leave') action_view = ObjectProperty(None) loaded = False kvname = None context_menu = None menu_actions = [] app = App.get_running_app() def _change_action_view(self): app = App.get_running_app() action_bar = app.root.manager.current_screen.ids.action_bar _action_view = self.action_view if (not _action_view) or _action_view.parent: return action_bar.clear_widgets() action_bar.add_widget(_action_view) def on_enter(self): # FIXME: use a proper event don't use animation time of screen Clock.schedule_once(lambda dt: self.dispatch('on_activate'), .25) pass def update(self): pass @profiler def load_screen(self): self.screen = Builder.load_file('gui/kivy/uix/ui_screens/' + self.kvname + '.kv') self.add_widget(self.screen) self.loaded = True self.update() setattr(self.app, self.kvname + '_screen', self) def on_activate(self): if self.kvname and not self.loaded: self.load_screen() #Clock.schedule_once(lambda dt: self._change_action_view()) def on_leave(self): self.dispatch('on_deactivate') def on_deactivate(self): self.hide_menu() def hide_menu(self): if self.context_menu is not None: self.remove_widget(self.context_menu) self.context_menu = None def show_menu(self, obj): self.hide_menu() self.context_menu = ContextMenu(obj, self.menu_actions) self.add_widget(self.context_menu) TX_ICONS = [ "close", "close", "close", "unconfirmed", "close", "clock1", "clock2", "clock3", "clock4", "clock5", "confirmed", ] class HistoryScreen(CScreen): tab = ObjectProperty(None) kvname = 'history' cards = {} def __init__(self, **kwargs): self.ra_dialog = None super(HistoryScreen, self).__init__(**kwargs) self.menu_actions = [ ('Label', self.label_dialog), ('Details', self.show_tx)] def show_tx(self, obj): tx_hash = obj.tx_hash tx = self.app.wallet.transactions.get(tx_hash) if not tx: return self.app.tx_dialog(tx) def label_dialog(self, obj): from dialogs.label_dialog import LabelDialog key = obj.tx_hash text = self.app.wallet.get_label(key) def callback(text): self.app.wallet.set_label(key, text) self.update() d = LabelDialog(_('Enter Transaction Label'), text, callback) d.open() def get_card(self, tx_hash, height, conf, timestamp, value, balance): status, status_str = self.app.wallet.get_tx_status(tx_hash, height, conf, timestamp) icon = "atlas://gui/kivy/theming/light/" + TX_ICONS[status] label = self.app.wallet.get_label(tx_hash) if tx_hash else _('Pruned transaction outputs') date = timestamp_to_datetime(timestamp) ri = self.cards.get(tx_hash) if ri is None: ri = Factory.HistoryItem() ri.screen = self ri.tx_hash = tx_hash self.cards[tx_hash] = ri ri.icon = icon ri.date = status_str ri.message = label ri.value = value or 0 ri.amount = self.app.format_amount(value, True) if value is not None else '--' ri.confirmations = conf if self.app.fiat_unit and date: rate = self.app.fx.history_rate(date) if rate: s = self.app.fx.value_str(value, rate) ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit return ri def update(self, see_all=False): if self.app.wallet is None: return history = reversed(self.app.wallet.get_history()) history_card = self.screen.ids.history_container history_card.clear_widgets() count = 0 for item in history: ri = self.get_card(*item) count += 1 history_card.add_widget(ri) if count == 0: msg = _('This screen shows your list of transactions. It is currently empty.') history_card.add_widget(EmptyLabel(text=msg)) class SendScreen(CScreen): kvname = 'send' payment_request = None def set_URI(self, text): import electrum try: uri = electrum.util.parse_URI(text, self.app.on_pr) except: self.app.show_info(_("Not a Bitcoin URI")) return amount = uri.get('amount') self.screen.address = uri.get('address', '') self.screen.message = uri.get('message', '') self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.payment_request = None self.screen.is_pr = False def update(self): pass def do_clear(self): self.screen.amount = '' self.screen.message = '' self.screen.address = '' self.payment_request = None self.screen.is_pr = False def set_request(self, pr): self.screen.address = pr.get_requestor() amount = pr.get_amount() self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' self.screen.message = pr.get_memo() if pr.is_pr(): self.screen.is_pr = True self.payment_request = pr else: self.screen.is_pr = False self.payment_request = None def do_save(self): if not self.screen.address: return if self.screen.is_pr: # it sould be already saved return # save address as invoice from electrum.paymentrequest import make_unsigned_request, PaymentRequest req = {'address':self.screen.address, 'memo':self.screen.message} amount = self.app.get_amount(self.screen.amount) if self.screen.amount else 0 req['amount'] = amount pr = make_unsigned_request(req).SerializeToString() pr = PaymentRequest(pr) self.app.wallet.invoices.add(pr) self.app.update_tab('invoices') self.app.show_info(_("Invoice saved")) if pr.is_pr(): self.screen.is_pr = True self.payment_request = pr else: self.screen.is_pr = False self.payment_request = None def do_paste(self): contents = unicode(self.app._clipboard.paste()) if not contents: self.app.show_info(_("Clipboard is empty")) return self.set_URI(contents) def do_send(self): if self.screen.is_pr: if self.payment_request.has_expired(): self.app.show_error(_('Payment request has expired')) return outputs = self.payment_request.get_outputs() else: address = str(self.screen.address) if not address: self.app.show_error(_('Recipient not specified.') + ' ' + _('Please scan a Bitcoin address or a payment request')) return if not bitcoin.is_address(address): self.app.show_error(_('Invalid Bitcoin Address') + ':\n' + address) return try: amount = self.app.get_amount(self.screen.amount) except: self.app.show_error(_('Invalid amount') + ':\n' + self.screen.amount) return outputs = [(bitcoin.TYPE_ADDRESS, address, amount)] message = unicode(self.screen.message) amount = sum(map(lambda x:x[2], outputs)) if self.app.electrum_config.get('use_rbf'): from dialogs.question import Question d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b)) d.open() else: self._do_send(amount, message, outputs, False) def _do_send(self, amount, message, outputs, rbf): # make unsigned transaction coins = self.app.wallet.get_spendable_coins() config = self.app.electrum_config try: tx = self.app.wallet.make_unsigned_transaction(coins, outputs, config, None) except NotEnoughFunds: self.app.show_error(_("Not enough funds")) return except Exception as e: traceback.print_exc(file=sys.stdout) self.app.show_error(str(e)) return if rbf: tx.set_sequence(0) fee = tx.get_fee() msg = [ _("Amount to be sent") + ": " + self.app.format_amount_and_units(amount), _("Mining fee") + ": " + self.app.format_amount_and_units(fee), ] if fee >= config.get('confirm_fee', 100000): msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high.")) msg.append(_("Enter your PIN code to proceed")) self.app.protected('\n'.join(msg), self.send_tx, (tx, message)) def send_tx(self, tx, message, password): def on_success(tx): if tx.is_complete(): self.app.broadcast(tx, self.payment_request) self.app.wallet.set_label(tx.hash(), message) else: self.app.tx_dialog(tx) def on_failure(error): self.app.show_error(error) if self.app.wallet.can_sign(tx): self.app.show_info("Signing...") self.app.sign_tx(tx, password, on_success, on_failure) else: self.app.tx_dialog(tx) class ReceiveScreen(CScreen): kvname = 'receive' def update(self): if not self.screen.address: self.get_new_address() else: status = self.app.wallet.get_request_status(self.screen.address) self.screen.status = _('Payment received') if status == PR_PAID else '' def clear(self): self.screen.address = '' self.screen.amount = '' self.screen.message = '' def get_new_address(self): if not self.app.wallet: return False self.clear() addr = self.app.wallet.get_unused_address() if addr is None: addr = self.app.wallet.get_receiving_address() or '' b = False else: b = True self.screen.address = addr return b def on_address(self, addr): req = self.app.wallet.get_payment_request(addr, self.app.electrum_config) self.screen.status = '' if req: self.screen.message = unicode(req.get('memo', '')) amount = req.get('amount') self.screen.amount = self.app.format_amount_and_units(amount) if amount else '' status = req.get('status', PR_UNKNOWN) self.screen.status = _('Payment received') if status == PR_PAID else '' Clock.schedule_once(lambda dt: self.update_qr()) def get_URI(self): from electrum.util import create_URI amount = self.screen.amount if amount: a, u = self.screen.amount.split() assert u == self.app.base_unit amount = Decimal(a) * pow(10, self.app.decimal_point()) return create_URI(self.screen.address, amount, self.screen.message) @profiler def update_qr(self): uri = self.get_URI() qr = self.screen.ids.qr qr.set_data(uri) def do_share(self): uri = self.get_URI() self.app.do_share(uri, _("Share Bitcoin Request")) def do_copy(self): uri = self.get_URI() self.app._clipboard.copy(uri) self.app.show_info(_('Request copied to clipboard')) def save_request(self): addr = str(self.screen.address) amount = str(self.screen.amount) message = unicode(self.screen.message) amount = self.app.get_amount(amount) if amount else 0 req = self.app.wallet.make_payment_request(addr, amount, message, None) self.app.wallet.add_payment_request(req, self.app.electrum_config) self.app.update_tab('requests') def on_amount_or_message(self): self.save_request() Clock.schedule_once(lambda dt: self.update_qr()) def do_new(self): addr = self.get_new_address() if not addr: self.app.show_info(_('Please use the existing requests first.')) else: self.save_request() self.app.show_info(_('New request added to your list.')) invoice_text = { PR_UNPAID:_('Pending'), PR_UNKNOWN:_('Unknown'), PR_PAID:_('Paid'), PR_EXPIRED:_('Expired') } request_text = { PR_UNPAID: _('Pending'), PR_UNKNOWN: _('Unknown'), PR_PAID: _('Received'), PR_EXPIRED: _('Expired') } pr_icon = { PR_UNPAID: 'atlas://gui/kivy/theming/light/important', PR_UNKNOWN: 'atlas://gui/kivy/theming/light/important', PR_PAID: 'atlas://gui/kivy/theming/light/confirmed', PR_EXPIRED: 'atlas://gui/kivy/theming/light/close' } class InvoicesScreen(CScreen): kvname = 'invoices' cards = {} def get_card(self, pr): key = pr.get_id() ci = self.cards.get(key) if ci is None: ci = Factory.InvoiceItem() ci.key = key ci.screen = self self.cards[key] = ci ci.requestor = pr.get_requestor() ci.memo = pr.get_memo() amount = pr.get_amount() if amount: ci.amount = self.app.format_amount_and_units(amount) status = self.app.wallet.invoices.get_status(ci.key) ci.status = invoice_text[status] ci.icon = pr_icon[status] else: ci.amount = _('No Amount') ci.status = '' exp = pr.get_expiration_date() ci.date = format_time(exp) if exp else _('Never') return ci def update(self): self.menu_actions = [('Pay', self.do_pay), ('Details', self.do_view), ('Delete', self.do_delete)] invoices_list = self.screen.ids.invoices_container invoices_list.clear_widgets() _list = self.app.wallet.invoices.sorted_list() for pr in _list: ci = self.get_card(pr) invoices_list.add_widget(ci) if not _list: msg = _('This screen shows the list of payment requests that have been sent to you. You may also use it to store contact addresses.') invoices_list.add_widget(EmptyLabel(text=msg)) def do_pay(self, obj): pr = self.app.wallet.invoices.get(obj.key) self.app.on_pr(pr) def do_view(self, obj): pr = self.app.wallet.invoices.get(obj.key) pr.verify(self.app.wallet.contacts) self.app.show_pr_details(pr.get_dict(), obj.status, True) def do_delete(self, obj): from dialogs.question import Question def cb(result): if result: self.app.wallet.invoices.remove(obj.key) self.app.update_tab('invoices') d = Question(_('Delete invoice?'), cb) d.open() class RequestsScreen(CScreen): kvname = 'requests' cards = {} def get_card(self, req): address = req['address'] timestamp = req.get('time', 0) amount = req.get('amount') expiration = req.get('exp', None) status = req.get('status') signature = req.get('sig') ci = self.cards.get(address) if ci is None: ci = Factory.RequestItem() ci.screen = self ci.address = address self.cards[address] = ci ci.memo = self.app.wallet.get_label(address) if amount: status = req.get('status') ci.status = request_text[status] else: received = self.app.wallet.get_addr_received(address) ci.status = self.app.format_amount_and_units(amount) ci.icon = pr_icon[status] ci.amount = self.app.format_amount_and_units(amount) if amount else _('No Amount') ci.date = format_time(timestamp) return ci def update(self): self.menu_actions = [('Show', self.do_show), ('Details', self.do_view), ('Delete', self.do_delete)] requests_list = self.screen.ids.requests_container requests_list.clear_widgets() _list = self.app.wallet.get_sorted_requests(self.app.electrum_config) if self.app.wallet else [] for req in _list: ci = self.get_card(req) requests_list.add_widget(ci) if not _list: msg = _('This screen shows the list of payment requests you made.') requests_list.add_widget(EmptyLabel(text=msg)) def do_show(self, obj): self.app.show_request(obj.address) def do_view(self, obj): req = self.app.wallet.get_payment_request(obj.address, self.app.electrum_config) status = req.get('status') amount = req.get('amount') address = req['address'] if amount: status = req.get('status') status = request_text[status] else: received_amount = self.app.wallet.get_addr_received(address) status = self.app.format_amount_and_units(received_amount) self.app.show_pr_details(req, status, False) def do_delete(self, obj): from dialogs.question import Question def cb(result): if result: self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config) self.update() d = Question(_('Delete request?'), cb) d.open() class TabbedCarousel(Factory.TabbedPanel): '''Custom TabbedPanel using a carousel used in the Main Screen ''' carousel = ObjectProperty(None) def animate_tab_to_center(self, value): scrlv = self._tab_strip.parent if not scrlv: return idx = self.tab_list.index(value) n = len(self.tab_list) if idx in [0, 1]: scroll_x = 1 elif idx in [n-1, n-2]: scroll_x = 0 else: scroll_x = 1. * (n - idx - 1) / (n - 1) mation = Factory.Animation(scroll_x=scroll_x, d=.25) mation.cancel_all(scrlv) mation.start(scrlv) def on_current_tab(self, instance, value): self.animate_tab_to_center(value) def on_index(self, instance, value): current_slide = instance.current_slide if not hasattr(current_slide, 'tab'): return tab = current_slide.tab ct = self.current_tab try: if ct.text != tab.text: carousel = self.carousel carousel.slides[ct.slide].dispatch('on_leave') self.switch_to(tab) carousel.slides[tab.slide].dispatch('on_enter') except AttributeError: current_slide.dispatch('on_enter') def switch_to(self, header): # we have to replace the functionality of the original switch_to if not header: return if not hasattr(header, 'slide'): header.content = self.carousel super(TabbedCarousel, self).switch_to(header) try: tab = self.tab_list[-1] except IndexError: return self._current_tab = tab tab.state = 'down' return carousel = self.carousel self.current_tab.state = "normal" header.state = 'down' self._current_tab = header # set the carousel to load the appropriate slide # saved in the screen attribute of the tab head slide = carousel.slides[header.slide] if carousel.current_slide != slide: carousel.current_slide.dispatch('on_leave') carousel.load_slide(slide) slide.dispatch('on_enter') def add_widget(self, widget, index=0): if isinstance(widget, Factory.CScreen): self.carousel.add_widget(widget) return super(TabbedCarousel, self).add_widget(widget, index=index)