From e18892997290b81337f79ee8dfc1457d2ff000e6 Mon Sep 17 00:00:00 2001 From: Kirill Fomichev Date: Thu, 19 Nov 2015 18:40:54 +0300 Subject: [PATCH 01/90] Fix proof of work assert in verify_chunk --- lib/blockchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index 89fda7ed..a264543c 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -88,7 +88,7 @@ class Blockchain(util.PrintError): _hash = self.hash_header(header) assert previous_hash == header.get('prev_block_hash') assert bits == header.get('bits') - assert int('0x'+_hash,16) < target + assert int('0x'+_hash,16) <= target previous_header = header previous_hash = _hash From 334b84c3c3d3b6f7aa97c958e38b787669fc42e8 Mon Sep 17 00:00:00 2001 From: Kirill Fomichev Date: Thu, 19 Nov 2015 20:43:57 +0300 Subject: [PATCH 02/90] Add assertions to get_target --- lib/blockchain.py | 47 ++++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index a264543c..41e8f6f6 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -50,7 +50,11 @@ class Blockchain(util.PrintError): self.print_error("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash'))) return False - bits, target = self.get_target(height/2016, chain) + try: + bits, target = self.get_target(height/2016, chain) + except AssertionError as e: + self.print_error("target calculation error: %s" % (str(e))) + return False if bits != header.get('bits'): self.print_error("bits mismatch: %s vs %s" % (bits, header.get('bits'))) @@ -190,36 +194,33 @@ class Blockchain(util.PrintError): if h.get('block_height') == index*2016-1: last = h + # bits to target + bits = last.get('bits') + bitsN = (bits >> 24) & 0xff + assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]" + bitsBase = bits & 0xffffff + assert bitsN >= 0x8000 and bitsN <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" + target = bitsBase << (8*(bitsN-3)) + + # new target nActualTimespan = last.get('timestamp') - first.get('timestamp') nTargetTimespan = 14*24*60*60 nActualTimespan = max(nActualTimespan, nTargetTimespan/4) nActualTimespan = min(nActualTimespan, nTargetTimespan*4) + new_target = min(max_target, (target * nActualTimespan)/nTargetTimespan) - bits = last.get('bits') - # convert to bignum - MM = 256*256*256 - a = bits%MM - if a < 0x8000: - a *= 256 - target = (a) * pow(2, 8 * (bits/MM - 3)) - - # new target - new_target = min( max_target, (target * nActualTimespan)/nTargetTimespan ) - - # convert it to bits - c = ("%064X"%new_target)[2:] - i = 31 - while c[0:2]=="00": + # convert new target to bits + c = ("%064x" % new_target)[2:] + while c[:2] == '00' and len(c) > 6: c = c[2:] - i -= 1 - c = int('0x'+c[0:6],16) - if c >= 0x800000: - c /= 256 - i += 1 + bitsN, bitsBase = len(c)/2, int('0x'+c[:6], 16) + if bitsBase >= 0x800000: + bitsN += 1 + bitsBase >>= 8 - new_bits = c + MM * i - return new_bits, new_target + new_bits = bitsN << 24 | bitsBase + return new_bits, bitsBase << (8*(bitsN-3)) def connect_header(self, chain, header): '''Builds a header chain until it connects. Returns True if it has From 8e7c5a180fd9ad80a2d9af7bd79e7442f24a7d1f Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Dec 2015 11:33:30 +0100 Subject: [PATCH 03/90] kivy: handle absence of fiat exchange rate --- gui/kivy/uix/ui_screens/amount.kv | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/kivy/uix/ui_screens/amount.kv b/gui/kivy/uix/ui_screens/amount.kv index a910bc2e..56005039 100644 --- a/gui/kivy/uix/ui_screens/amount.kv +++ b/gui/kivy/uix/ui_screens/amount.kv @@ -18,7 +18,7 @@ Popup: id: a btc_text: (kb.amount + ' ' + app.base_unit) if kb.amount else '' fiat_text: (kb.fiat_amount + ' ' + app.fiat_unit) if kb.fiat_amount else '' - text: (self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if self.btc_text else '' + text: ((self.fiat_text + ' / ' + self.btc_text if kb.is_fiat else self.btc_text + ' / ' + self.fiat_text) if app.fiat_unit else self.btc_text) if self.btc_text else '' size_hint: 1, 1 font_size: '22dp' Widget: @@ -70,7 +70,7 @@ Popup: id: button_fiat size_hint: 1, None height: '48dp' - text: app.fiat_unit if kb.is_fiat else app.base_unit + text: (app.fiat_unit if kb.is_fiat else app.base_unit) if app.fiat_unit else '' on_release: app.toggle_fiat(kb) Button: From 1b0e29d385ccd62dbb55a47e768a32e5f0125358 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Dec 2015 11:33:58 +0100 Subject: [PATCH 04/90] kivy: add screens for invoices and requests, disable contacts --- gui/kivy/main.kv | 24 ++++++++++--- gui/kivy/main_window.py | 2 ++ gui/kivy/uix/screens.py | 42 ++++++++++++++++++++++- gui/kivy/uix/ui_screens/invoices.kv | 50 +++++++++++++++++++++++++++ gui/kivy/uix/ui_screens/requests.kv | 53 +++++++++++++++++++++++++++++ 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 gui/kivy/uix/ui_screens/invoices.kv create mode 100644 gui/kivy/uix/ui_screens/requests.kv diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index ea9d2521..dd8fccb2 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -292,9 +292,15 @@ ReceiveScreen: id: receive_screen tab: receive_tab - ContactsScreen: - id: contacts_screen - tab: contacts_tab + InvoicesScreen: + id: invoices_screen + tab: invoices_tab + RequestsScreen: + id: requests_screen + tab: requests_tab + #ContactsScreen: + # id: contacts_screen + # tab: contacts_tab CleanHeader: id: history_tab text: _('History') @@ -308,9 +314,17 @@ text: _('Receive') slide: 2 CleanHeader: - id: contacts_tab - text: _('Contacts') + id: invoices_tab + text: _('Invoices') slide: 3 + CleanHeader: + id: requests_tab + text: _('Requests') + slide: 4 + #CleanHeader: + # id: contacts_tab + # text: _('Contacts') + # slide: 4 on_release: diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 93b36a82..a1321881 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -9,6 +9,7 @@ import electrum from electrum import WalletStorage, Wallet from electrum.i18n import _, set_language from electrum.contacts import Contacts +from electrum.paymentrequest import InvoiceStore from electrum.util import profiler, InvalidPassword from electrum.plugins import run_hook from electrum.util import format_satoshis, format_satoshis_plain @@ -176,6 +177,7 @@ class ElectrumWindow(App): #self.config = self.gui_object.config self.contacts = Contacts(self.electrum_config) + self.invoices = InvoiceStore(self.electrum_config) self.bind(url=self.set_URI) # were we sent a url? diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index d806fca7..33a0712f 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -17,7 +17,7 @@ from kivy.lang import Builder from kivy.factory import Factory from electrum.i18n import _ -from electrum.util import profiler, parse_URI +from electrum.util import profiler, parse_URI, format_time from electrum import bitcoin from electrum.util import timestamp_to_datetime from electrum.plugins import run_hook @@ -311,6 +311,46 @@ class ContactsScreen(CScreen): contact_list.add_widget(ci) +class InvoicesScreen(CScreen): + kvname = 'invoices' + + def update(self): + invoices_list = self.screen.ids.invoices_container + invoices_list.clear_widgets() + for pr in self.app.invoices.sorted_list(): + ci = Factory.InvoiceItem() + ci.key = pr.get_id() + ci.requestor = pr.get_requestor() + ci.memo = pr.memo + ci.amount = self.app.format_amount(pr.get_amount()) + #ci.status = self.invoices.get_status(key) + exp = pr.get_expiration_date() + ci.date = format_time(exp) if exp else _('Never') + invoices_list.add_widget(ci) + +class RequestsScreen(CScreen): + kvname = 'requests' + + def update(self): + requests_list = self.screen.ids.requests_container + requests_list.clear_widgets() + for req in self.app.wallet.get_sorted_requests(self.app.electrum_config): + 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 = Factory.RequestItem() + ci.address = req['address'] + ci.memo = req.get('memo', '') + #ci.status = req.get('status') + ci.amount = self.app.format_amount(amount) if amount else '' + ci.date = format_time(timestamp) + requests_list.add_widget(ci) + + class CSpinner(Factory.Spinner): '''CustomDropDown that allows fading out the dropdown diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv new file mode 100644 index 00000000..3a0151c7 --- /dev/null +++ b/gui/kivy/uix/ui_screens/invoices.kv @@ -0,0 +1,50 @@ + + #color: .305, .309, .309, 1 + text_size: self.size + halign: 'left' + valign: 'middle' + + + requestor: '' + memo: '' + amount: '' + status: '' + date: '' + size_hint_y: None + height: '65dp' + padding: dp(12) + spacing: dp(5) + canvas.before: + Color: + rgba: 0.3, 0.3, 0.3, 1 + Rectangle: + size: self.size + pos: self.pos + InvoicesLabel: + text: root.requestor + InvoicesLabel: + text: root.memo + InvoicesLabel: + text: root.amount + +InvoicesScreen: + name: 'invoices' + on_activate: + if not self.action_view:\ + self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view + BoxLayout: + orientation: 'vertical' + spacing: '1dp' + ScrollView: + canvas.before: + Color: + rgba: .8901, .8901, .8901, 0 + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols: 1 + id: invoices_container + size_hint_y: None + height: self.minimum_height + spacing: '1dp' diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv new file mode 100644 index 00000000..96aebed3 --- /dev/null +++ b/gui/kivy/uix/ui_screens/requests.kv @@ -0,0 +1,53 @@ + + #color: .305, .309, .309, 1 + text_size: self.size + halign: 'left' + valign: 'middle' + + + address: '' + memo: '' + amount: '' + status: '' + date: '' + size_hint_y: None + height: '65dp' + padding: dp(12) + spacing: dp(5) + canvas.before: + Color: + rgba: 0.3, 0.3, 0.3, 1 + Rectangle: + size: self.size + pos: self.pos + InvoicesLabel: + text: root.address + font_size: '13dp' + InvoicesLabel: + text: root.memo + InvoicesLabel: + text: root.amount + #InvoicesLabel: + # text: root.status + +RequestsScreen: + name: 'requests' + on_activate: + if not self.action_view:\ + self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view + BoxLayout: + orientation: 'vertical' + spacing: '1dp' + ScrollView: + canvas.before: + Color: + rgba: .8901, .8901, .8901, 0 + Rectangle: + size: self.size + pos: self.pos + GridLayout: + cols: 1 + id: requests_container + size_hint_y: None + height: self.minimum_height + spacing: '1dp' From 4195001aed9a5a307cbd83b19f9e66af390643b6 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Thu, 10 Dec 2015 15:26:38 +0100 Subject: [PATCH 05/90] kivy: fix animate_to_center --- gui/kivy/uix/screens.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 33a0712f..0d42c342 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -380,20 +380,9 @@ class TabbedCarousel(Factory.TabbedPanel): scrlv = self._tab_strip.parent if not scrlv: return - idx = self.tab_list.index(value) - if idx == 0: - scroll_x = 1 - elif idx == len(self.tab_list) - 1: - scroll_x = 0 - else: - self_center_x = scrlv.center_x - vcenter_x = value.center_x - diff_x = (self_center_x - vcenter_x) - try: - scroll_x = scrlv.scroll_x - (diff_x / scrlv.width) - except ZeroDivisionError: - pass + n = len(self.tab_list) + scroll_x = 1. * (n - idx - 1) / (n - 1) mation = Factory.Animation(scroll_x=scroll_x, d=.25) mation.cancel_all(scrlv) mation.start(scrlv) From 81f070c83b1f4acbd4194505abce3265b60357ae Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Dec 2015 10:14:01 +0100 Subject: [PATCH 06/90] fix bug in get_target --- lib/blockchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index 41e8f6f6..90f992fe 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -199,7 +199,7 @@ class Blockchain(util.PrintError): bitsN = (bits >> 24) & 0xff assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]" bitsBase = bits & 0xffffff - assert bitsN >= 0x8000 and bitsN <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" + assert bitsBase >= 0x8000 and bitsBase <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" target = bitsBase << (8*(bitsN-3)) # new target From 2a1fbf01fe63dee9f950f7ba7b4d68d456799262 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Dec 2015 12:37:40 +0100 Subject: [PATCH 07/90] simplify blockchain.py --- lib/blockchain.py | 102 ++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 71 deletions(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index 90f992fe..4b71a20c 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -21,6 +21,7 @@ import os import util from bitcoin import * +MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 class Blockchain(util.PrintError): '''Manages blockchain headers and their verification''' @@ -39,70 +40,36 @@ class Blockchain(util.PrintError): self.set_local_height() self.print_error("%d blocks" % self.local_height) + def verify_headers(self, header, prev_header, bits, target): + prev_hash = self.hash_header(prev_header) + assert prev_hash == header.get('prev_block_hash'), "prev hash mismatch: %s vs %s"% (prev_hash, header.get('prev_block_hash')) + assert bits == header.get('bits'), "bits mismatch: %s vs %s" % (bits, header.get('bits')) + _hash = self.hash_header(header) + assert int('0x'+_hash, 16) <= target, "insufficient proof of work: %s vs target %s" % (int('0x'+_hash, 16), target) + def verify_chain(self, chain): first_header = chain[0] prev_header = self.read_header(first_header.get('block_height') -1) - for header in chain: height = header.get('block_height') - prev_hash = self.hash_header(prev_header) - if prev_hash != header.get('prev_block_hash'): - self.print_error("prev hash mismatch: %s vs %s" - % (prev_hash, header.get('prev_block_hash'))) - return False - try: - bits, target = self.get_target(height/2016, chain) - except AssertionError as e: - self.print_error("target calculation error: %s" % (str(e))) - return False - if bits != header.get('bits'): - self.print_error("bits mismatch: %s vs %s" - % (bits, header.get('bits'))) - return False - _hash = self.hash_header(header) - if int('0x'+_hash, 16) > target: - self.print_error("insufficient proof of work: %s vs target %s" - % (int('0x'+_hash, 16), target)) - return False - + bits, target = self.get_target(height/2016, chain) + self.verify_headers(header, prev_header, bits, target) prev_header = header - return True - - - def verify_chunk(self, index, hexdata): data = hexdata.decode('hex') - height = index*2016 num = len(data)/80 - - if index == 0: - previous_hash = ("0"*64) - else: - prev_header = self.read_header(index*2016-1) - if prev_header is None: raise - previous_hash = self.hash_header(prev_header) - + prev_header = None if index == 0 else self.read_header(index*2016-1) bits, target = self.get_target(index) - for i in range(num): - height = index*2016 + i raw_header = data[i*80:(i+1)*80] - header = self.header_from_string(raw_header) - _hash = self.hash_header(header) - assert previous_hash == header.get('prev_block_hash') - assert bits == header.get('bits') - assert int('0x'+_hash,16) <= target - - previous_header = header - previous_hash = _hash - + header = self.deserialize_header(raw_header) + self.verify_headers(header, prev_header, bits, target) + prev_header = header self.save_chunk(index, data) - self.print_error("validated chunk %d to height %d" % (index, height)) + self.print_error("validated chunk %d" % index) - - - def header_to_string(self, res): + def serialize_header(self, res): s = int_to_hex(res.get('version'),4) \ + rev_hex(res.get('prev_block_hash')) \ + rev_hex(res.get('merkle_root')) \ @@ -111,8 +78,7 @@ class Blockchain(util.PrintError): + int_to_hex(int(res.get('nonce')),4) return s - - def header_from_string(self, s): + def deserialize_header(self, s): hex_to_int = lambda s: int('0x' + s[::-1].encode('hex'), 16) h = {} h['version'] = hex_to_int(s[0:4]) @@ -124,7 +90,7 @@ class Blockchain(util.PrintError): return h def hash_header(self, header): - return rev_hex(Hash(self.header_to_string(header).decode('hex')).encode('hex')) + return "0"*64 if header is None else rev_hex(Hash(self.serialize_header(header).decode('hex')).encode('hex')) def path(self): return os.path.join(self.config.path, 'blockchain_headers') @@ -152,7 +118,7 @@ class Blockchain(util.PrintError): self.set_local_height() def save_header(self, header): - data = self.header_to_string(header).decode('hex') + data = self.serialize_header(header).decode('hex') assert len(data) == 80 height = header.get('block_height') filename = self.path() @@ -177,23 +143,19 @@ class Blockchain(util.PrintError): h = f.read(80) f.close() if len(h) == 80: - h = self.header_from_string(h) + h = self.deserialize_header(h) return h def get_target(self, index, chain=None): - if chain is None: - chain = [] # Do not use mutables as default values! - - max_target = 0x00000000FFFF0000000000000000000000000000000000000000000000000000 - if index == 0: return 0x1d00ffff, max_target - + if index == 0: + return 0x1d00ffff, MAX_TARGET first = self.read_header((index-1)*2016) last = self.read_header(index*2016-1) if last is None: for h in chain: if h.get('block_height') == index*2016-1: last = h - + assert last is not None # bits to target bits = last.get('bits') bitsN = (bits >> 24) & 0xff @@ -201,24 +163,20 @@ class Blockchain(util.PrintError): bitsBase = bits & 0xffffff assert bitsBase >= 0x8000 and bitsBase <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" target = bitsBase << (8*(bitsN-3)) - # new target nActualTimespan = last.get('timestamp') - first.get('timestamp') nTargetTimespan = 14*24*60*60 nActualTimespan = max(nActualTimespan, nTargetTimespan/4) nActualTimespan = min(nActualTimespan, nTargetTimespan*4) - new_target = min(max_target, (target * nActualTimespan)/nTargetTimespan) - + new_target = min(MAX_TARGET, (target * nActualTimespan)/nTargetTimespan) # convert new target to bits c = ("%064x" % new_target)[2:] while c[:2] == '00' and len(c) > 6: c = c[2:] - bitsN, bitsBase = len(c)/2, int('0x'+c[:6], 16) if bitsBase >= 0x800000: bitsN += 1 bitsBase >>= 8 - new_bits = bitsN << 24 | bitsBase return new_bits, bitsBase << (8*(bitsN-3)) @@ -242,18 +200,20 @@ class Blockchain(util.PrintError): # The chain is complete. Reverse to order by increasing height chain.reverse() - if self.verify_chain(chain): + try: + self.verify_chain(chain) self.print_error("connected at height:", previous_height) for header in chain: self.save_header(header) return True - - return False + except BaseException as e: + self.print_error(str(e)) + return False def connect_chunk(self, idx, chunk): try: self.verify_chunk(idx, chunk) return idx + 1 - except Exception: - self.print_error('verify_chunk failed') + except BaseException as e: + self.print_error('verify_chunk failed', str(e)) return idx - 1 From 0cef063ee231cc9459dff6829dc33ffc2ed499e2 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Dec 2015 15:21:21 +0100 Subject: [PATCH 08/90] add payment requests to kivy --- gui/kivy/main_window.py | 24 ++++++++++++++------- gui/kivy/uix/screens.py | 37 ++++++++++++++++++++++++--------- gui/kivy/uix/ui_screens/send.kv | 2 +- gui/qt/main_window.py | 28 ++++++++----------------- lib/util.py | 18 +++++++++++++++- 5 files changed, 71 insertions(+), 38 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index a1321881..3f8774a4 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -193,9 +193,24 @@ class ElectrumWindow(App): self._trigger_notify_transactions = \ Clock.create_trigger(self.notify_transactions, 5) + def on_pr(self, pr): + from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED + if pr.verify(self.contacts): + key = self.invoices.add(pr) + status = self.invoices.get_status(key) + #self.invoices_screen.update() + if status == PR_PAID: + self.show_message("invoice already paid") + self.send_screen.do_clear() + else: + self.send_screen.set_request(pr) + else: + self.show_message("invoice error:" + pr.error) + self.send_screen.do_clear() + def set_URI(self, url): try: - url = electrum.util.parse_URI(url) + url = electrum.util.parse_URI(url, self.on_pr) except: self.show_info("Invalid URI", url) return @@ -214,12 +229,7 @@ class ElectrumWindow(App): if resultCode == -1: # RESULT_OK: contents = intent.getStringExtra("SCAN_RESULT") if intent.getStringExtra("SCAN_RESULT_FORMAT") == 'QR_CODE': - try: - uri = electrum.util.parse_URI(contents) - except: - self.show_info("Invalid URI", url) - return - on_complete(uri) + on_complete(contents) activity.bind(on_activity_result=on_qr_result) PythonActivity.mActivity.startActivityForResult(intent, 0) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 0d42c342..586d2f02 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -181,6 +181,7 @@ class ScreenPassword(Factory.Screen): class SendScreen(CScreen): kvname = 'send' + payment_request = None def set_URI(self, uri): print "set uri", uri @@ -195,6 +196,16 @@ class SendScreen(CScreen): self.screen.amount = '' self.screen.message = '' self.screen.address = '' + self.payment_request = None + + def set_request(self, pr): + if pr.has_expired(): + self.app.show_error(_('Payment request has expired')) + return + self.payment_request = pr + self.screen.address = pr.get_requestor() + self.screen.amount = self.app.format_amount(pr.get_amount()) + self.screen.message = pr.get_memo() def do_paste(self): contents = unicode(self.app._clipboard.get()) @@ -206,18 +217,24 @@ class SendScreen(CScreen): self.set_URI(uri) def do_send(self): - address = str(self.screen.address) - 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 + if self.payment_request: + 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 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 = [('address', address, amount)] message = unicode(self.screen.message) fee = None - outputs = [('address', address, amount)] self.app.protected(self.send_tx, (outputs, fee, message)) def send_tx(self, *args): diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index 7a92fd17..1002d04b 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -78,7 +78,7 @@ SendScreen: id: qr text: _('QR Code') on_release: - app.scan_qr(on_complete=s.parent.set_URI) + app.scan_qr(on_complete=app.set_URI) Button: id: paste_button text: _('Paste') diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index bda09737..e57600f3 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -83,7 +83,6 @@ class StatusBarButton(QPushButton): from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED -from electrum.paymentrequest import PaymentRequest, get_payment_request pr_icons = { PR_UNPAID:":icons/unpaid.png", @@ -1346,37 +1345,28 @@ class ElectrumWindow(QMainWindow, PrintError): self.payment_request = None self.do_clear() + def on_pr(self, request): + self.payment_request = request + if self.payment_request.verify(self.contacts): + self.emit(SIGNAL('payment_request_ok')) + else: + self.emit(SIGNAL('payment_request_error')) + def pay_to_URI(self, URI): if not URI: return try: - out = util.parse_URI(unicode(URI)) - except Exception as e: + out = util.parse_URI(unicode(URI), self.on_pr) + except BaseException as e: QMessageBox.warning(self, _('Error'), _('Invalid bitcoin URI:') + '\n' + str(e), _('OK')) return self.tabs.setCurrentIndex(1) - r = out.get('r') sig = out.get('sig') name = out.get('name') if r or (name and sig): - def get_payment_request_thread(): - if name and sig: - from electrum import paymentrequest - pr = paymentrequest.serialize_request(out).SerializeToString() - self.payment_request = paymentrequest.PaymentRequest(pr) - else: - self.payment_request = get_payment_request(r) - if self.payment_request.verify(self.contacts): - self.emit(SIGNAL('payment_request_ok')) - else: - self.emit(SIGNAL('payment_request_error')) - t = threading.Thread(target=get_payment_request_thread) - t.setDaemon(True) - t.start() self.prepare_for_payment_request() return - address = out.get('address') amount = out.get('amount') label = out.get('label') diff --git a/lib/util.py b/lib/util.py index 375c21dc..061941ea 100644 --- a/lib/util.py +++ b/lib/util.py @@ -317,7 +317,7 @@ def block_explorer_URL(config, kind, item): #_ud = re.compile('%([0-9a-hA-H]{2})', re.MULTILINE) #urldecode = lambda x: _ud.sub(lambda m: chr(int(m.group(1), 16)), x) -def parse_URI(uri): +def parse_URI(uri, on_pr=None): import bitcoin from bitcoin import COIN @@ -364,6 +364,22 @@ def parse_URI(uri): if 'sig' in out: out['sig'] = bitcoin.base_decode(out['sig'], None, base=58).encode('hex') + r = out.get('r') + sig = out.get('sig') + name = out.get('name') + if r or (name and sig): + def get_payment_request_thread(): + import paymentrequest as pr + if name and sig: + s = pr.serialize_request(out).SerializeToString() + request = pr.PaymentRequest(s) + else: + request = pr.get_payment_request(r) + on_pr(request) + t = threading.Thread(target=get_payment_request_thread) + t.setDaemon(True) + t.start() + return out From c71b8d7328a21e69116da2e392ce3e20b1ba7ae9 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Dec 2015 15:32:48 +0100 Subject: [PATCH 09/90] kivy: reorganize tabs --- gui/kivy/main.kv | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index dd8fccb2..d17af667 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -281,20 +281,20 @@ TabbedCarousel: id: panel tab_height: '48dp' - #default_tab: send_tab + default_tab: history_tab strip_border: 0, 0, 0, 0 - HistoryScreen: - id: history_screen - tab: history_tab - SendScreen: - id: send_screen - tab: send_tab - ReceiveScreen: - id: receive_screen - tab: receive_tab InvoicesScreen: id: invoices_screen tab: invoices_tab + SendScreen: + id: send_screen + tab: send_tab + HistoryScreen: + id: history_screen + tab: history_tab + ReceiveScreen: + id: receive_screen + tab: receive_tab RequestsScreen: id: requests_screen tab: requests_tab @@ -302,20 +302,20 @@ # id: contacts_screen # tab: contacts_tab CleanHeader: - id: history_tab - text: _('History') + id: invoices_tab + text: _('Invoices') slide: 0 CleanHeader: id: send_tab text: _('Send') slide: 1 CleanHeader: - id: receive_tab - text: _('Receive') + id: history_tab + text: _('History') slide: 2 CleanHeader: - id: invoices_tab - text: _('Invoices') + id: receive_tab + text: _('Receive') slide: 3 CleanHeader: id: requests_tab From 680c9c888a4620527ef85eee6c0daac2f26c07dd Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 11 Dec 2015 15:48:56 +0100 Subject: [PATCH 10/90] kivy: add save_request button --- gui/kivy/uix/screens.py | 13 +++++++++++++ gui/kivy/uix/ui_screens/receive.kv | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 586d2f02..e1bfa03d 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -293,9 +293,22 @@ class ReceiveScreen(CScreen): uri = self.get_URI() self.app._clipboard.put(uri, 'text/plain') + def do_save(self): + addr = str(self.screen.address) + amount = str(self.screen.amount) + message = unicode(self.screen.message) + if not message and not amount: + self.app.show_error(_('No message or amount')) + return False + amount = self.app.get_amount(amount) + req = self.app.wallet.make_payment_request(addr, amount, message, None) + self.app.wallet.add_payment_request(req, self.app.electrum_config) + self.app.show_error(_('Request saved')) + def do_clear(self): self.screen.amount = '' self.screen.message = '' + self.update() diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index 5a09f0e2..3665cd75 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -93,5 +93,10 @@ ReceiveScreen: size_hint: 1, None height: '48dp' on_release: s.parent.do_clear() + Button: + text: _('Save') + size_hint: 1, None + height: '48dp' + on_release: s.parent.do_save() Widget: size_hint: 1, 0.3 From f3a7d3f2bff502e26e53cb63a1175da86d3bb08d Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 09:41:31 +0900 Subject: [PATCH 11/90] Show amount of inputs too. --- gui/qt/transaction_dialog.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index a69ecfac..4a657c8b 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -249,6 +249,9 @@ class TxDialog(QDialog): return chg if self.wallet.is_change(addr) else rec return ext + def format_amount(amt): + return self.parent.format_amount(amt, whitespaces = True) + i_text = QTextEdit() i_text.setFont(QFont(MONOSPACE_FONT)) i_text.setReadOnly(True) @@ -270,6 +273,7 @@ class TxDialog(QDialog): if addr is None: addr = _('unknown') cursor.insertText(addr, text_format(addr)) + cursor.insertText(format_amount(x['value']), ext) cursor.insertBlock() vbox.addWidget(i_text) @@ -283,7 +287,7 @@ class TxDialog(QDialog): cursor.insertText(addr, text_format(addr)) if v is not None: cursor.insertText('\t', ext) - cursor.insertText(self.parent.format_amount(v, whitespaces = True), ext) + cursor.insertText(format_amount(v), ext) cursor.insertBlock() vbox.addWidget(o_text) From ea49e8dc968cf41af98400208d3b7122492bec09 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 10:12:46 +0900 Subject: [PATCH 12/90] Remove unneeded buckets for Privacy coin chooser Commonize the code with the classic chooser and simplify. --- lib/coinchooser.py | 57 +++++++++++++++++++++------------------------- 1 file changed, 26 insertions(+), 31 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index 32a5ab44..eb847354 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -25,6 +25,15 @@ from util import NotEnoughFunds, PrintError, profiler Bucket = namedtuple('Bucket', ['desc', 'size', 'value', 'coins']) +def strip_unneeded(bkts, sufficient_funds): + '''Remove buckets that are unnecessary in achieving the spend amount''' + bkts = sorted(bkts, key = lambda bkt: bkt.value) + for i in range(len(bkts)): + if not sufficient_funds(bkts[i + 1:]): + return bkts[i:] + # Shouldn't get here + return bkts + class CoinChooserBase(PrintError): def keys(self, coins): @@ -72,12 +81,18 @@ class CoinChooserBase(PrintError): tx = Transaction.from_io([], outputs[:]) # Size of the transaction with no inputs and no change base_size = tx.estimated_size() - # Returns fee given input size - fee = lambda input_size: fee_estimator(base_size + input_size) + spent_amount = tx.output_value() + + def sufficient_funds(buckets): + '''Given a list of buckets, return True if it has enough + value to pay for the transaction''' + total_input = sum(bucket.value for bucket in buckets) + total_size = sum(bucket.size for bucket in buckets) + base_size + return total_input >= spent_amount + fee_estimator(total_size) # Collect the coins into buckets, choose a subset of the buckets buckets = self.bucketize_coins(coins) - buckets = self.choose_buckets(buckets, tx.output_value(), fee, + buckets = self.choose_buckets(buckets, sufficient_funds, self.penalty_func(tx)) tx.inputs = [coin for b in buckets for coin in b.coins] @@ -103,33 +118,20 @@ class CoinChooserClassic(CoinChooserBase): return [coin['prevout_hash'] + ':' + str(coin['prevout_n']) for coin in coins] - def choose_buckets(self, buckets, spent_amount, fee, penalty_func): + def choose_buckets(self, buckets, sufficient_funds, penalty_func): '''Spend the oldest buckets first.''' # Unconfirmed coins are young, not old adj_height = lambda height: 99999999 if height == 0 else height buckets.sort(key = lambda b: max(adj_height(coin['height']) for coin in b.coins)) - selected, value, size = [], 0, 0 + selected = [] for bucket in buckets: selected.append(bucket) - value += bucket.value - size += bucket.size - if value >= spent_amount + fee(size): - break + if sufficient_funds(selected): + return strip_unneeded(selected, sufficient_funds) else: raise NotEnoughFunds() - # Remove unneeded inputs starting with the smallest. - selected.sort(key = lambda b: b.value) - dropped = [] - for bucket in selected: - if value - bucket.value >= spent_amount + fee(size - bucket.size): - value -= bucket.value - size -= bucket.size - dropped.append(bucket) - - return [bucket for bucket in selected if bucket not in dropped] - class CoinChooserRandom(CoinChooserBase): def bucket_candidates(self, buckets, sufficient_funds): @@ -157,18 +159,11 @@ class CoinChooserRandom(CoinChooserBase): else: raise NotEnoughFunds() - return [[buckets[n] for n in candidate] for candidate in candidates] + candidates = [[buckets[n] for n in c] for c in candidates] + return [strip_unneeded(c, sufficient_funds) for c in candidates] - def choose_buckets(self, buckets, spent_amount, fee, penalty_func): - - def sufficient(buckets): - '''Given a set of buckets, return True if it has enough - value to pay for the transaction''' - total_input = sum(bucket.value for bucket in buckets) - total_size = sum(bucket.size for bucket in buckets) - return total_input >= spent_amount + fee(total_size) - - candidates = self.bucket_candidates(buckets, sufficient) + def choose_buckets(self, buckets, sufficient_funds, penalty_func): + candidates = self.bucket_candidates(buckets, sufficient_funds) penalties = [penalty_func(cand) for cand in candidates] winner = candidates[penalties.index(min(penalties))] self.print_error("Bucket sets:", len(buckets)) From 2763b0feea724c2b8a6a6532f9e22b26c7fa7b3e Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 11:53:17 +0900 Subject: [PATCH 13/90] Improved change handling for Privacy chooser Breaks up large change in such a way as to make it unclear what the real send might be. Fixes #1203 --- lib/coinchooser.py | 86 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 75 insertions(+), 11 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index eb847354..d6b5a646 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -17,7 +17,8 @@ # along with this program. If not, see . from collections import defaultdict, namedtuple -from random import shuffle +from random import choice, randint, shuffle +from math import floor, log10 from bitcoin import COIN from transaction import Transaction @@ -58,17 +59,23 @@ class CoinChooserBase(PrintError): return 0 return penalty - def add_change(self, tx, change_addrs, fee_estimator, dust_threshold): - # How much is left if we add 1 change output? - change_amount = tx.get_fee() - fee_estimator(1) + def change_amounts(self, tx, count, fee_estimator, dust_threshold): + # The amount left after adding 1 change output + return [tx.get_fee() - fee_estimator(1)] + def change_outputs(self, tx, change_addrs, fee_estimator, dust_threshold): + amounts = self.change_amounts(tx, len(change_addrs), fee_estimator, + dust_threshold) # If change is above dust threshold after accounting for the # size of the change output, add it to the transaction. - if change_amount > dust_threshold: - tx.outputs.append(('address', change_addrs[0], change_amount)) - self.print_error('change', change_amount) - elif change_amount: - self.print_error('not keeping dust', change_amount) + dust = sum(amount for amount in amounts if amount < dust_threshold) + amounts = [amount for amount in amounts if amount >= dust_threshold] + change = [('address', addr, amount) + for addr, amount in zip(change_addrs, amounts)] + self.print_error('change:', change) + if dust: + self.print_error('not keeping dust', dust) + return change def make_tx(self, coins, outputs, change_addrs, fee_estimator, dust_threshold): @@ -101,7 +108,8 @@ class CoinChooserBase(PrintError): # This takes a count of change outputs and returns a tx fee; # each pay-to-bitcoin-address output serializes as 34 bytes fee = lambda count: fee_estimator(tx_size + count * 34) - self.add_change(tx, change_addrs, fee, dust_threshold) + change = self.change_outputs(tx, change_addrs, fee, dust_threshold) + tx.outputs.extend(change) self.print_error("using %d inputs" % len(tx.inputs)) self.print_error("using buckets:", [bucket.desc for bucket in buckets]) @@ -179,7 +187,11 @@ class CoinChooserPrivacy(CoinChooserRandom): reduce blockchain UTXO bloat, and reduce future privacy loss that would come from reusing that address' remaining UTXOs. Second, it penalizes change that is quite different to the sent - amount. Third, it penalizes change that is too big.''' + amount. Third, it penalizes change that is too big. Fourth, it + breaks large change up into amounts comparable to the spent + amount. Finally, change is rounded to similar precision to + sent amounts. Extra change outputs and rounding might raise + the transaction fee slightly''' def keys(self, coins): return [coin['address'] for coin in coins] @@ -208,5 +220,57 @@ class CoinChooserPrivacy(CoinChooserRandom): return penalty + def change_amounts(self, tx, count, fee_estimator, dust_threshold): + + # Break change up if bigger than max_change + output_amounts = [o[2] for o in tx.outputs] + max_change = max(max(output_amounts) * 1.25, dust_threshold * 10) + + # Use N change outputs + for n in range(1, count + 1): + # How much is left if we add this many change outputs? + change_amount = tx.get_fee() - fee_estimator(n) + if change_amount // n < max_change: + break + + # Get a handle on the precision of the output amounts; round our + # change to look similar + def trailing_zeroes(val): + s = str(val) + return len(s) - len(s.rstrip('0')) + + zeroes = map(trailing_zeroes, output_amounts) + min_zeroes = min(zeroes) + max_zeroes = max(zeroes) + zeroes = range(max(0, min_zeroes - 1), min(max_zeroes + 1, 8) + 1) + + # Calculate change; randomize it a bit if using more than 1 output + remaining = change_amount + amounts = [] + while n > 1: + average = remaining // n + amount = randint(int(average * 0.7), int(average * 1.3)) + precision = min(choice(zeroes), int(floor(log10(amount)))) + amount = int(round(amount, -precision)) + amounts.append(amount) + remaining -= amount + n -= 1 + + # Last change output. Round down to maximum precision but lose + # no more than 100 satoshis to fees (2dp) + amount = remaining + N = min(2, zeroes[0]) + if N: + amount = int(round(amount, -N)) + if amount > remaining: + amount -= pow(10, N) + amounts.append(amount) + + assert sum(amounts) <= change_amount + assert min(amounts) >= 0 + + return amounts + + COIN_CHOOSERS = {'Classic': CoinChooserClassic, 'Privacy': CoinChooserPrivacy} From b8eea5f56eaf82618efc60c599da36312bd3fbcc Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 11:57:30 +0900 Subject: [PATCH 14/90] Update release notes for 2.6 --- RELEASE-NOTES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 6cd8fb8a..3d293c58 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -2,6 +2,9 @@ * separation between plugins and GUIs * the daemon supports jsonrpc commands * new command: 'notify
' + * improved coin selection to help preserve user privacy. This is an + experimental feature. Enable it by setting the Coin Selection + preference to Privacy. # Release 2.5.4 * increase MIN_RELAY_TX_FEE to avoid dust transactions From 34955bd0f5525c83edbd95fba5ab9a3d6c3ff537 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 17:52:19 +0900 Subject: [PATCH 15/90] Show value if available --- gui/qt/transaction_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 4a657c8b..ce601bf2 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -273,7 +273,8 @@ class TxDialog(QDialog): if addr is None: addr = _('unknown') cursor.insertText(addr, text_format(addr)) - cursor.insertText(format_amount(x['value']), ext) + if x.get('value'): + cursor.insertText(format_amount(x['value']), ext) cursor.insertBlock() vbox.addWidget(i_text) From 36aaad392dec6282c6519796057f6c7b047c25f6 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 18:11:07 +0900 Subject: [PATCH 16/90] Fix docstring display. --- gui/qt/main_window.py | 7 +++++-- lib/coinchooser.py | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index e57600f3..d3bd74bc 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2583,11 +2583,14 @@ class ElectrumWindow(QMainWindow, PrintError): nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) + def fmt_docs(key, klass): + lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")] + return '\n'.join([key, "", " ".join(lines)]) + choosers = sorted(COIN_CHOOSERS.keys()) chooser_name = self.wallet.coin_chooser_name(self.config) msg = _('Choose coin (UTXO) selection method. The following are available:\n\n') - msg += '\n\n'.join(key + ": " + klass.__doc__ - for key, klass in COIN_CHOOSERS.items()) + msg += '\n\n'.join(fmt_docs(*item) for item in COIN_CHOOSERS.items()) chooser_label = HelpLabel(_('Coin selection') + ':', msg) chooser_combo = QComboBox() chooser_combo.addItems(choosers) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index d6b5a646..162424f7 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -117,10 +117,9 @@ class CoinChooserBase(PrintError): return tx class CoinChooserClassic(CoinChooserBase): - ''' - The classic electrum algorithm. Chooses coins starting with - the oldest that are sufficient to cover the spent amount, and - then removes any unneeded starting with the smallest in value.''' + '''The classic electrum algorithm. Chooses coins starting with the + oldest that are sufficient to cover the spent amount, and then + removes any unneeded starting with the smallest in value.''' def keys(self, coins): return [coin['prevout_hash'] + ':' + str(coin['prevout_n']) @@ -179,19 +178,18 @@ class CoinChooserRandom(CoinChooserBase): return winner class CoinChooserPrivacy(CoinChooserRandom): - ''' - Attempts to better preserve user privacy. First, if any coin is + '''Attempts to better preserve user privacy. First, if any coin is spent from a user address, all coins are. Compared to spending from other addresses to make up an amount, this reduces information leakage about sender holdings. It also helps to - reduce blockchain UTXO bloat, and reduce future privacy loss - that would come from reusing that address' remaining UTXOs. - Second, it penalizes change that is quite different to the sent - amount. Third, it penalizes change that is too big. Fourth, it - breaks large change up into amounts comparable to the spent - amount. Finally, change is rounded to similar precision to - sent amounts. Extra change outputs and rounding might raise - the transaction fee slightly''' + reduce blockchain UTXO bloat, and reduce future privacy loss that + would come from reusing that address' remaining UTXOs. Second, it + penalizes change that is quite different to the sent amount. + Third, it penalizes change that is too big. Fourth, it breaks + large change up into amounts comparable to the spent amount. + Finally, change is rounded to similar precision to sent amounts. + Extra change outputs and rounding might raise the transaction fee + slightly.''' def keys(self, coins): return [coin['address'] for coin in coins] From 1524fa29af8f246726eb2d83894cdda4d72ec80d Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 18:20:49 +0900 Subject: [PATCH 17/90] Raise dialog width slightly. --- gui/qt/transaction_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index ce601bf2..d3cdfcde 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -54,7 +54,7 @@ class TxDialog(QDialog): self.desc = desc QDialog.__init__(self) - self.setMinimumWidth(600) + self.setMinimumWidth(660) self.setWindowTitle(_("Transaction")) vbox = QVBoxLayout() From e9d0dd578a27219db612813f00ead541e457681e Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 18:26:58 +0900 Subject: [PATCH 18/90] Put coin selection combo at bottom --- gui/qt/main_window.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index d3bd74bc..dd90f7f7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2583,24 +2583,6 @@ class ElectrumWindow(QMainWindow, PrintError): nz.valueChanged.connect(on_nz) gui_widgets.append((nz_label, nz)) - def fmt_docs(key, klass): - lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")] - return '\n'.join([key, "", " ".join(lines)]) - - choosers = sorted(COIN_CHOOSERS.keys()) - chooser_name = self.wallet.coin_chooser_name(self.config) - msg = _('Choose coin (UTXO) selection method. The following are available:\n\n') - msg += '\n\n'.join(fmt_docs(*item) for item in COIN_CHOOSERS.items()) - chooser_label = HelpLabel(_('Coin selection') + ':', msg) - chooser_combo = QComboBox() - chooser_combo.addItems(choosers) - chooser_combo.setCurrentIndex(choosers.index(chooser_name)) - def on_chooser(x): - chooser_name = choosers[chooser_combo.currentIndex()] - self.config.set_key('coin_chooser', chooser_name) - chooser_combo.currentIndexChanged.connect(on_chooser) - tx_widgets.append((chooser_label, chooser_combo)) - msg = _('Fee per kilobyte of transaction.') + '\n' \ + _('If you enable dynamic fees, this parameter will be used as upper bound.') fee_label = HelpLabel(_('Transaction fee per kb') + ':', msg) @@ -2777,6 +2759,24 @@ class ElectrumWindow(QMainWindow, PrintError): can_edit_fees_cb.setToolTip(_('This option lets you edit fees in the send tab.')) tx_widgets.append((can_edit_fees_cb, None)) + def fmt_docs(key, klass): + lines = [ln.lstrip(" ") for ln in klass.__doc__.split("\n")] + return '\n'.join([key, "", " ".join(lines)]) + + choosers = sorted(COIN_CHOOSERS.keys()) + chooser_name = self.wallet.coin_chooser_name(self.config) + msg = _('Choose coin (UTXO) selection method. The following are available:\n\n') + msg += '\n\n'.join(fmt_docs(*item) for item in COIN_CHOOSERS.items()) + chooser_label = HelpLabel(_('Coin selection') + ':', msg) + chooser_combo = QComboBox() + chooser_combo.addItems(choosers) + chooser_combo.setCurrentIndex(choosers.index(chooser_name)) + def on_chooser(x): + chooser_name = choosers[chooser_combo.currentIndex()] + self.config.set_key('coin_chooser', chooser_name) + chooser_combo.currentIndexChanged.connect(on_chooser) + tx_widgets.append((chooser_label, chooser_combo)) + tabs_info = [ (tx_widgets, _('Transactions')), (gui_widgets, _('Appearance')), From 1c528af433a6b7beb9eac12330b423407f7abdad Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 18:32:24 +0900 Subject: [PATCH 19/90] Description tweaks. Mention loss of priority, and rename Classic to Oldest First. --- lib/coinchooser.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index 162424f7..d2cfc0fb 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -116,7 +116,7 @@ class CoinChooserBase(PrintError): return tx -class CoinChooserClassic(CoinChooserBase): +class CoinChooserOldestFirst(CoinChooserBase): '''The classic electrum algorithm. Chooses coins starting with the oldest that are sufficient to cover the spent amount, and then removes any unneeded starting with the smallest in value.''' @@ -189,7 +189,8 @@ class CoinChooserPrivacy(CoinChooserRandom): large change up into amounts comparable to the spent amount. Finally, change is rounded to similar precision to sent amounts. Extra change outputs and rounding might raise the transaction fee - slightly.''' + slightly. Transaction priority might be less than if older coins + were chosen.''' def keys(self, coins): return [coin['address'] for coin in coins] @@ -270,5 +271,5 @@ class CoinChooserPrivacy(CoinChooserRandom): return amounts -COIN_CHOOSERS = {'Classic': CoinChooserClassic, +COIN_CHOOSERS = {'Oldest First': CoinChooserOldestFirst, 'Privacy': CoinChooserPrivacy} From 52fc73905233fbc424ed383209677e6786395fa2 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 18:39:03 +0900 Subject: [PATCH 20/90] Update default chooser name too. --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index 4bda5fab..c7352485 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -899,7 +899,7 @@ class Abstract_Wallet(PrintError): def coin_chooser_name(self, config): kind = config.get('coin_chooser') if not kind in COIN_CHOOSERS: - kind = 'Classic' + kind = 'OldestFirst' return kind def coin_chooser(self, config): From 06eb3142c44cdefb5baccb72ffd20b8a0235e2aa Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sat, 12 Dec 2015 19:01:14 +0900 Subject: [PATCH 21/90] Oldest First --- lib/wallet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/wallet.py b/lib/wallet.py index c7352485..f4b35b7d 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -899,7 +899,7 @@ class Abstract_Wallet(PrintError): def coin_chooser_name(self, config): kind = config.get('coin_chooser') if not kind in COIN_CHOOSERS: - kind = 'OldestFirst' + kind = 'Oldest First' return kind def coin_chooser(self, config): From d344ee0474426c7fe79e959dcce61c925cbeb8a5 Mon Sep 17 00:00:00 2001 From: Kirill Fomichev Date: Sat, 12 Dec 2015 15:43:07 +0300 Subject: [PATCH 22/90] Small blockchain changes --- lib/blockchain.py | 78 +++++++++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index 4b71a20c..d8cd42d6 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -28,7 +28,7 @@ class Blockchain(util.PrintError): def __init__(self, config, network): self.config = config self.network = network - self.headers_url = 'https://headers.electrum.org/blockchain_headers' + self.headers_url = "https://headers.electrum.org/blockchain_headers" self.local_height = 0 self.set_local_height() @@ -40,42 +40,42 @@ class Blockchain(util.PrintError): self.set_local_height() self.print_error("%d blocks" % self.local_height) - def verify_headers(self, header, prev_header, bits, target): + def verify_header(self, header, prev_header, bits, target): prev_hash = self.hash_header(prev_header) - assert prev_hash == header.get('prev_block_hash'), "prev hash mismatch: %s vs %s"% (prev_hash, header.get('prev_block_hash')) + assert prev_hash == header.get('prev_block_hash'), "prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')) assert bits == header.get('bits'), "bits mismatch: %s vs %s" % (bits, header.get('bits')) _hash = self.hash_header(header) - assert int('0x'+_hash, 16) <= target, "insufficient proof of work: %s vs target %s" % (int('0x'+_hash, 16), target) + assert int('0x' + _hash, 16) <= target, "insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target) def verify_chain(self, chain): first_header = chain[0] - prev_header = self.read_header(first_header.get('block_height') -1) + prev_header = self.read_header(first_header.get('block_height') - 1) for header in chain: height = header.get('block_height') - bits, target = self.get_target(height/2016, chain) - self.verify_headers(header, prev_header, bits, target) + bits, target = self.get_target(height / 2016, chain) + self.verify_header(header, prev_header, bits, target) prev_header = header def verify_chunk(self, index, hexdata): data = hexdata.decode('hex') - num = len(data)/80 - prev_header = None if index == 0 else self.read_header(index*2016-1) + num = len(data) / 80 + prev_header = None + if index != 0: + prev_header = self.read_header(index*2016 - 1) bits, target = self.get_target(index) for i in range(num): - raw_header = data[i*80:(i+1)*80] + raw_header = data[i*80:(i+1) * 80] header = self.deserialize_header(raw_header) - self.verify_headers(header, prev_header, bits, target) + self.verify_header(header, prev_header, bits, target) prev_header = header - self.save_chunk(index, data) - self.print_error("validated chunk %d" % index) def serialize_header(self, res): - s = int_to_hex(res.get('version'),4) \ + s = int_to_hex(res.get('version'), 4) \ + rev_hex(res.get('prev_block_hash')) \ + rev_hex(res.get('merkle_root')) \ - + int_to_hex(int(res.get('timestamp')),4) \ - + int_to_hex(int(res.get('bits')),4) \ - + int_to_hex(int(res.get('nonce')),4) + + int_to_hex(int(res.get('timestamp')), 4) \ + + int_to_hex(int(res.get('bits')), 4) \ + + int_to_hex(int(res.get('nonce')), 4) return s def deserialize_header(self, s): @@ -90,7 +90,9 @@ class Blockchain(util.PrintError): return h def hash_header(self, header): - return "0"*64 if header is None else rev_hex(Hash(self.serialize_header(header).decode('hex')).encode('hex')) + if header is None: + return '0' * 64 + return hash_encode(Hash(self.serialize_header(header).decode('hex'))) def path(self): return os.path.join(self.config.path, 'blockchain_headers') @@ -102,17 +104,17 @@ class Blockchain(util.PrintError): try: import urllib, socket socket.setdefaulttimeout(30) - self.print_error("downloading ", self.headers_url ) + self.print_error("downloading ", self.headers_url) urllib.urlretrieve(self.headers_url, filename) self.print_error("done.") except Exception: - self.print_error( "download failed. creating file", filename ) - open(filename,'wb+').close() + self.print_error("download failed. creating file", filename) + open(filename, 'wb+').close() def save_chunk(self, index, chunk): filename = self.path() - f = open(filename,'rb+') - f.seek(index*2016*80) + f = open(filename, 'rb+') + f.seek(index * 2016 * 80) h = f.write(chunk) f.close() self.set_local_height() @@ -122,8 +124,8 @@ class Blockchain(util.PrintError): assert len(data) == 80 height = header.get('block_height') filename = self.path() - f = open(filename,'rb+') - f.seek(height*80) + f = open(filename, 'rb+') + f.seek(height * 80) h = f.write(data) f.close() self.set_local_height() @@ -138,8 +140,8 @@ class Blockchain(util.PrintError): def read_header(self, block_height): name = self.path() if os.path.exists(name): - f = open(name,'rb') - f.seek(block_height*80) + f = open(name, 'rb') + f.seek(block_height * 80) h = f.read(80) f.close() if len(h) == 80: @@ -149,11 +151,11 @@ class Blockchain(util.PrintError): def get_target(self, index, chain=None): if index == 0: return 0x1d00ffff, MAX_TARGET - first = self.read_header((index-1)*2016) - last = self.read_header(index*2016-1) + first = self.read_header((index-1) * 2016) + last = self.read_header(index*2016 - 1) if last is None: for h in chain: - if h.get('block_height') == index*2016-1: + if h.get('block_height') == index*2016 - 1: last = h assert last is not None # bits to target @@ -162,23 +164,23 @@ class Blockchain(util.PrintError): assert bitsN >= 0x03 and bitsN <= 0x1d, "First part of bits should be in [0x03, 0x1d]" bitsBase = bits & 0xffffff assert bitsBase >= 0x8000 and bitsBase <= 0x7fffff, "Second part of bits should be in [0x8000, 0x7fffff]" - target = bitsBase << (8*(bitsN-3)) + target = bitsBase << (8 * (bitsN-3)) # new target nActualTimespan = last.get('timestamp') - first.get('timestamp') - nTargetTimespan = 14*24*60*60 - nActualTimespan = max(nActualTimespan, nTargetTimespan/4) - nActualTimespan = min(nActualTimespan, nTargetTimespan*4) - new_target = min(MAX_TARGET, (target * nActualTimespan)/nTargetTimespan) + nTargetTimespan = 14 * 24 * 60 * 60 + nActualTimespan = max(nActualTimespan, nTargetTimespan / 4) + nActualTimespan = min(nActualTimespan, nTargetTimespan * 4) + new_target = min(MAX_TARGET, (target*nActualTimespan) / nTargetTimespan) # convert new target to bits c = ("%064x" % new_target)[2:] while c[:2] == '00' and len(c) > 6: c = c[2:] - bitsN, bitsBase = len(c)/2, int('0x'+c[:6], 16) + bitsN, bitsBase = len(c) / 2, int('0x' + c[:6], 16) if bitsBase >= 0x800000: bitsN += 1 bitsBase >>= 8 new_bits = bitsN << 24 | bitsBase - return new_bits, bitsBase << (8*(bitsN-3)) + return new_bits, bitsBase << (8 * (bitsN-3)) def connect_header(self, chain, header): '''Builds a header chain until it connects. Returns True if it has @@ -213,6 +215,8 @@ class Blockchain(util.PrintError): def connect_chunk(self, idx, chunk): try: self.verify_chunk(idx, chunk) + self.print_error("validated chunk %d" % index) + self.save_chunk(index, data) return idx + 1 except BaseException as e: self.print_error('verify_chunk failed', str(e)) From 6bd37723d3ddf89e9e34236ee07324c02ef1df14 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 12 Dec 2015 16:54:32 +0100 Subject: [PATCH 23/90] kivy: add context menus --- gui/kivy/main.kv | 36 ++++++++++++++++----- gui/kivy/main_window.py | 17 ++-------- gui/kivy/uix/context_menu.py | 43 +++++++++++++++++++++++++ gui/kivy/uix/dialogs/__init__.py | 1 + gui/kivy/uix/screens.py | 49 ++++++++++++++++++++++++----- gui/kivy/uix/ui_screens/history.kv | 34 ++------------------ gui/kivy/uix/ui_screens/invoices.kv | 15 ++------- gui/kivy/uix/ui_screens/requests.kv | 22 +++++-------- 8 files changed, 129 insertions(+), 88 deletions(-) create mode 100644 gui/kivy/uix/context_menu.py diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index d17af667..907cadcb 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -187,6 +187,34 @@ size: self.size pos: self.pos + + canvas.before: + Color: + rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 + Rectangle + size: self.size + pos: self.x, self.y + dp(5) + padding: '2dp', '2dp' + spacing: '2dp' + height: self.minimum_height + + + + size_hint: 1, None + height: '65dp' + group: 'requests' + padding: dp(12) + spacing: dp(5) + screen: None + on_release: + self.screen.show_menu(args[0]) if self.state == 'down' else self.screen.hide_menu() + canvas.before: + Color: + rgba: (0.192, .498, 0.745, 1) if self.state == 'down' else (0.3, 0.3, 0.3, 1) + Rectangle: + size: self.size + pos: self.pos + icon: 'atlas://gui/kivy/theming/light/globe' values: [] #app.wallet.addresses() if app.wallet else [] @@ -388,14 +416,6 @@ BoxLayout: font_size: '22dp' minimum_width: '1dp' - ActionButton: - id: context_button - text: app.context - width: 0 - on_text: - self.width = 20 if self.text else 0 - on_release: app.context_action() - ActionOverflow: id: ao ActionOvrButton: diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 3f8774a4..739e7986 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -78,8 +78,6 @@ class ElectrumWindow(App): keys = sorted(base_units.keys()) self.base_unit = keys[ (keys.index(self.base_unit) + 1) % len(keys)] - context = StringProperty('') - context_action = lambda x: None status = StringProperty('') fiat_unit = StringProperty('') @@ -749,22 +747,11 @@ class ElectrumWindow(App): pos = (win.center[0], win.center[1] - (info_bubble.height/2)) info_bubble.show(pos, duration, width, modal=modal, exit=exit) - def tx_dialog(self, tx_hash): + def tx_dialog(self, obj): popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv') - popup.tx_hash = tx_hash + popup.tx_hash = obj.tx_hash popup.open() - def tx_selected(self, txid, state): - if state == 'down': - self.context = 'tx' - self.context_action = lambda: self.tx_dialog(txid) - else: - self.reset_context() - - def reset_context(self): - self.context = '' - self.context_action = lambda: None - def amount_dialog(self, screen, show_max): popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv') but_max = popup.ids.but_max diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py new file mode 100644 index 00000000..8091a6c4 --- /dev/null +++ b/gui/kivy/uix/context_menu.py @@ -0,0 +1,43 @@ +#!python +#!/usr/bin/env python +from kivy.app import App +from kivy.uix.bubble import Bubble +from kivy.animation import Animation +from kivy.uix.floatlayout import FloatLayout +from kivy.lang import Builder +from kivy.factory import Factory + +Builder.load_string(''' + + background_color: .2, .9, 1, 1 + height: '40dp' + size_hint: 1, None + + + size_hint: 1, None + height: '32dp' + #size: 120, 250 + pos: (0, 0) + show_arrow: False + padding: 0 + orientation: 'horizontal' + BoxLayout: + size_hint: 1, 1 + height: '40dp' + orientation: 'horizontal' + id: buttons +''') + + +class MenuItem(Factory.Button): + pass + +class ContextMenu(Bubble): + def __init__(self, obj, action_list): + Bubble.__init__(self) + self.obj = obj + for k, v in action_list: + l = MenuItem() + l.text = k + l.on_release = lambda: v(obj) + self.ids.buttons.add_widget(l) diff --git a/gui/kivy/uix/dialogs/__init__.py b/gui/kivy/uix/dialogs/__init__.py index 682181e5..cd63d04d 100644 --- a/gui/kivy/uix/dialogs/__init__.py +++ b/gui/kivy/uix/dialogs/__init__.py @@ -144,6 +144,7 @@ class InfoBubble(Factory.Bubble): m.add_widget(self) else: Window.add_widget(self) + # wait for the bubble to adjust it's size according to text then animate Clock.schedule_once(lambda dt: self._show(pos, duration)) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index e1bfa03d..311cc821 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -22,12 +22,17 @@ from electrum import bitcoin from electrum.util import timestamp_to_datetime from electrum.plugins import run_hook +from context_menu import ContextMenu + + 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): @@ -65,8 +70,19 @@ class CScreen(Factory.Screen): self.dispatch('on_deactivate') def on_deactivate(self): - pass - #Clock.schedule_once(lambda dt: self._change_action_view()) + self.hide_menu() + + def hide_menu(self): + if self.context_menu: + self.screen.remove_widget(self.context_menu) + self.context_menu = None + + def show_menu(self, obj): + if self.context_menu is None: + self.context_menu = ContextMenu(obj, self.menu_actions) + self.screen.remove_widget(self.context_menu) + self.screen.add_widget(self.context_menu) + class HistoryScreen(CScreen): @@ -77,6 +93,7 @@ class HistoryScreen(CScreen): def __init__(self, **kwargs): self.ra_dialog = None super(HistoryScreen, self).__init__(**kwargs) + self.menu_actions = [(_('Details'), self.app.tx_dialog)] def get_history_rate(self, btc_balance, timestamp): date = timestamp_to_datetime(timestamp) @@ -120,14 +137,12 @@ class HistoryScreen(CScreen): if self.app.wallet is None: return - history_card = self.screen.ids.recent_activity_card + history_card = self.screen.ids.history_container history = self.parse_history(reversed( self.app.wallet.get_history(self.app.current_account))) # repopulate History Card - last_widget = history_card.ids.content.children[-1] - history_card.ids.content.clear_widgets() - history_add = history_card.ids.content.add_widget - history_add(last_widget) + history_card.clear_widgets() + history_add = history_card.add_widget RecentActivityItem = Factory.RecentActivityItem count = 0 for item in history: @@ -141,6 +156,7 @@ class HistoryScreen(CScreen): ri.quote_text = quote_text ri.confirmations = conf ri.tx_hash = tx + ri.screen = self history_add(ri) if count == 8 and not see_all: break @@ -345,6 +361,7 @@ class InvoicesScreen(CScreen): kvname = 'invoices' def update(self): + self.menu_actions = [(_('Pay'), self.do_pay), (_('Delete'), self.do_delete)] invoices_list = self.screen.ids.invoices_container invoices_list.clear_widgets() for pr in self.app.invoices.sorted_list(): @@ -356,12 +373,22 @@ class InvoicesScreen(CScreen): #ci.status = self.invoices.get_status(key) exp = pr.get_expiration_date() ci.date = format_time(exp) if exp else _('Never') + ci.screen = self invoices_list.add_widget(ci) + def do_pay(self, x): + pass + + def do_delete(self, x): + pass + class RequestsScreen(CScreen): kvname = 'requests' def update(self): + + self.menu_actions = [(_('View'), self.do_view), (_('Delete'), self.do_delete)] + requests_list = self.screen.ids.requests_container requests_list.clear_widgets() for req in self.app.wallet.get_sorted_requests(self.app.electrum_config): @@ -378,9 +405,17 @@ class RequestsScreen(CScreen): #ci.status = req.get('status') ci.amount = self.app.format_amount(amount) if amount else '' ci.date = format_time(timestamp) + ci.screen = self requests_list.add_widget(ci) + def do_view(self, o): + print o + + def do_delete(self, o): + print o + + class CSpinner(Factory.Spinner): '''CustomDropDown that allows fading out the dropdown diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index 5f298d2b..1894a21f 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -38,20 +38,6 @@ size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) - - canvas.before: - Color: - rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 - Rectangle - size: self.size - pos: self.x, self.y + dp(5) - cols: 1 - padding: '2dp', '2dp' - spacing: '2dp' - size_hint: 1, None - height: self.minimum_height - group: 'history' - icon: 'atlas://gui/kivy/theming/light/important' address: 'no address set' @@ -62,8 +48,7 @@ date: '0/0/0' quote_text: '.' spacing: '9dp' - on_release: - app.tx_selected(root.tx_hash, self.state) + cols: 1 BoxLayout: size_hint: 1, None spacing: '8dp' @@ -100,16 +85,6 @@ u'[/color]'.format(amount_color=root.amount_color,\ amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ unit=app.base_unit) - CardSeparator - - - GridLayout: - id: content - spacing: '7dp' - cols: 1 - size_hint: 1, None - height: self.minimum_height - CardSeparator HistoryScreen: @@ -119,12 +94,9 @@ HistoryScreen: id: content do_scroll_x: False GridLayout - id: grid - cols: 1 #if root.width < root.height else 2 + id: history_container + cols: 1 size_hint: 1, None height: self.minimum_height padding: '12dp' spacing: '12dp' - CardRecentActivity: - id: recent_activity_card - diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv index 3a0151c7..4af2ca23 100644 --- a/gui/kivy/uix/ui_screens/invoices.kv +++ b/gui/kivy/uix/ui_screens/invoices.kv @@ -4,22 +4,12 @@ halign: 'left' valign: 'middle' - + requestor: '' memo: '' amount: '' status: '' date: '' - size_hint_y: None - height: '65dp' - padding: dp(12) - spacing: dp(5) - canvas.before: - Color: - rgba: 0.3, 0.3, 0.3, 1 - Rectangle: - size: self.size - pos: self.pos InvoicesLabel: text: root.requestor InvoicesLabel: @@ -45,6 +35,7 @@ InvoicesScreen: GridLayout: cols: 1 id: invoices_container - size_hint_y: None + size_hint: 1, None height: self.minimum_height spacing: '1dp' + padding: '12dp' diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv index 96aebed3..b789ba6c 100644 --- a/gui/kivy/uix/ui_screens/requests.kv +++ b/gui/kivy/uix/ui_screens/requests.kv @@ -1,35 +1,26 @@ - + #color: .305, .309, .309, 1 text_size: self.size halign: 'left' valign: 'middle' - + address: '' memo: '' amount: '' status: '' date: '' - size_hint_y: None - height: '65dp' - padding: dp(12) - spacing: dp(5) - canvas.before: - Color: - rgba: 0.3, 0.3, 0.3, 1 - Rectangle: - size: self.size - pos: self.pos - InvoicesLabel: + RequestLabel: text: root.address font_size: '13dp' - InvoicesLabel: + RequestLabel: text: root.memo - InvoicesLabel: + RequestLabel: text: root.amount #InvoicesLabel: # text: root.status + RequestsScreen: name: 'requests' on_activate: @@ -51,3 +42,4 @@ RequestsScreen: size_hint_y: None height: self.minimum_height spacing: '1dp' + padding: '12dp' From eef62112a83345d268cb7e7fd955e73c26429c5e Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sat, 12 Dec 2015 23:23:58 +0100 Subject: [PATCH 24/90] kivy: invoices and requests handlers --- gui/kivy/main_window.py | 99 ++++++++++------------------- gui/kivy/uix/context_menu.py | 2 +- gui/kivy/uix/screens.py | 39 +++++++----- gui/kivy/uix/ui_screens/history.kv | 2 +- gui/kivy/uix/ui_screens/invoices.kv | 5 +- gui/kivy/uix/ui_screens/receive.kv | 2 +- gui/kivy/uix/ui_screens/requests.kv | 5 +- 7 files changed, 61 insertions(+), 93 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 739e7986..8796a865 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -13,6 +13,7 @@ from electrum.paymentrequest import InvoiceStore from electrum.util import profiler, InvalidPassword from electrum.plugins import run_hook from electrum.util import format_satoshis, format_satoshis_plain +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED from kivy.app import App from kivy.core.window import Window @@ -164,6 +165,9 @@ class ElectrumWindow(App): self.nfcscanner = None self.tabs = None + self.receive_address = None + self.current_invoice = None + super(ElectrumWindow, self).__init__(**kwargs) title = _('Electrum App') @@ -191,19 +195,31 @@ class ElectrumWindow(App): self._trigger_notify_transactions = \ Clock.create_trigger(self.notify_transactions, 5) + def get_receive_address(self): + return self.receive_address if self.receive_address else self.wallet.get_unused_address(None) + + def do_pay(self, obj): + pr = self.invoices.get(obj.key) + self.on_pr(pr) + def on_pr(self, pr): - from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED if pr.verify(self.contacts): key = self.invoices.add(pr) status = self.invoices.get_status(key) #self.invoices_screen.update() if status == PR_PAID: - self.show_message("invoice already paid") + self.show_error("invoice already paid") self.send_screen.do_clear() else: - self.send_screen.set_request(pr) + if pr.has_expired(): + self.show_error(_('Payment request has expired')) + else: + self.current_pr = pr + self.update_screen('send') + send_tab = self.tabs.ids.send_tab + self.tabs.ids.panel.switch_to(send_tab) else: - self.show_message("invoice error:" + pr.error) + self.show_error("invoice error:" + pr.error) self.send_screen.do_clear() def set_URI(self, url): @@ -214,6 +230,17 @@ class ElectrumWindow(App): return self.send_screen.set_URI(url) + def update_screen(self, name): + s = getattr(self, name + '_screen', None) + if s: + s.update() + + def show_request(self, addr): + self.receive_address = addr + self.update_screen('receive') + receive_tab = self.tabs.ids.receive_tab + self.tabs.ids.panel.switch_to(receive_tab) + req = self.wallet.receive_requests.get(addr) def scan_qr(self, on_complete): from jnius import autoclass @@ -412,6 +439,7 @@ class ElectrumWindow(App): self.network.register_callback(self.on_network, interests) self.wallet = None + self.tabs = self.root.ids['tabs'] def on_network(self, event, *args): if event == 'updated': @@ -603,64 +631,10 @@ class ElectrumWindow(App): else: self.show_error(_('Invalid Address')) - def send_payment(self, address, amount=0, label='', message=''): - tabs = self.tabs - screen_send = tabs.ids.screen_send - - if label and self.wallet.labels.get(address) != label: - #if self.question('Give label "%s" to address %s ?'%(label,address)): - if address not in self.wallet.addressbook and not self.wallet. is_mine(address): - self.wallet.addressbook.append(address) - self.wallet.set_label(address, label) - - # switch_to the send screen - tabs.ids.panel.switch_to(tabs.ids.tab_send) - - label = self.wallet.labels.get(address) - m_addr = label + ' <'+ address +'>' if label else address - - # populate - def set_address(*l): - content = screen_send.ids - content.payto_e.text = m_addr - content.message_e.text = message - if amount: - content.amount_e.text = amount - - # wait for screen to load - Clock.schedule_once(set_address, .5) def set_send(self, address, amount, label, message): self.send_payment(address, amount=amount, label=label, message=message) - def prepare_for_payment_request(self): - tabs = self.tabs - screen_send = tabs.ids.screen_send - - # switch_to the send screen - tabs.ids.panel.switch_to(tabs.ids.tab_send) - - content = screen_send.ids - if content: - self.set_frozen(content, False) - screen_send.screen_label.text = _("please wait...") - return True - - def payment_request_ok(self): - tabs = self.tabs - screen_send = tabs.ids.screen_send - - # switch_to the send screen - tabs.ids.panel.switch_to(tabs.ids.tab_send) - - self.set_frozen(content, True) - - screen_send.ids.payto_e.text = self.gui_object.payment_request.domain - screen_send.ids.amount_e.text = self.format_amount(self.gui_object.payment_request.get_amount()) - screen_send.ids.message_e.text = self.gui_object.payment_request.memo - - # wait for screen to load - Clock.schedule_once(set_address, .5) def set_frozen(self, entry, frozen): if frozen: @@ -670,15 +644,6 @@ class ElectrumWindow(App): entry.disabled = False Factory.Animation(opacity=1).start(content) - def payment_request_error(self): - tabs = self.tabs - screen_send = tabs.ids.screen_send - - # switch_to the send screen - tabs.ids.panel.switch_to(tabs.ids.tab_send) - - self.do_clear() - self.show_info(self.gui_object.payment_request.error) def show_error(self, error, width='200dp', pos=None, arrow_pos=None, exit=False, icon='atlas://gui/kivy/theming/light/error', duration=0, diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py index 8091a6c4..2b80bb65 100644 --- a/gui/kivy/uix/context_menu.py +++ b/gui/kivy/uix/context_menu.py @@ -39,5 +39,5 @@ class ContextMenu(Bubble): for k, v in action_list: l = MenuItem() l.text = k - l.on_release = lambda: v(obj) + l.on_release = lambda f=v: f(obj) self.ids.buttons.add_widget(l) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 311cc821..9d05adbb 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -208,6 +208,10 @@ class SendScreen(CScreen): amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point())) self.screen.amount = amount_str + ' ' + self.app.base_unit + def update(self): + if self.app.current_invoice: + self.set_request(self.app.current_invoice) + def do_clear(self): self.screen.amount = '' self.screen.message = '' @@ -215,9 +219,6 @@ class SendScreen(CScreen): self.payment_request = None def set_request(self, pr): - if pr.has_expired(): - self.app.show_error(_('Payment request has expired')) - return self.payment_request = pr self.screen.address = pr.get_requestor() self.screen.amount = self.app.format_amount(pr.get_amount()) @@ -283,7 +284,13 @@ class ReceiveScreen(CScreen): kvname = 'receive' def update(self): - self.screen.address = self.app.wallet.get_unused_address(None) + addr = self.app.get_receive_address() + self.screen.address = addr + req = self.app.wallet.receive_requests.get(addr) + if req: + self.screen.message = req.get('memo') + self.screen.amount = self.app.format_amount(req.get('amount')) + ' ' + self.app.base_unit + def amount_callback(self, popup): amount_label = self.screen.ids.get('amount') @@ -320,8 +327,10 @@ class ReceiveScreen(CScreen): req = self.app.wallet.make_payment_request(addr, amount, message, None) self.app.wallet.add_payment_request(req, self.app.electrum_config) self.app.show_error(_('Request saved')) + self.app.update_screen('requests') def do_clear(self): + self.app.receive_address = None self.screen.amount = '' self.screen.message = '' self.update() @@ -376,18 +385,19 @@ class InvoicesScreen(CScreen): ci.screen = self invoices_list.add_widget(ci) - def do_pay(self, x): - pass + def do_pay(self, obj): + self.app.do_pay(obj) - def do_delete(self, x): - pass + def do_delete(self, obj): + self.app.invoices.remove(obj.key) + self.app.update_screen('invoices') class RequestsScreen(CScreen): kvname = 'requests' def update(self): - self.menu_actions = [(_('View'), self.do_view), (_('Delete'), self.do_delete)] + self.menu_actions = [(_('Show'), self.do_show), (_('Delete'), self.do_delete)] requests_list = self.screen.ids.requests_container requests_list.clear_widgets() @@ -408,13 +418,12 @@ class RequestsScreen(CScreen): ci.screen = self requests_list.add_widget(ci) + def do_show(self, obj): + self.app.show_request(obj.address) - def do_view(self, o): - print o - - def do_delete(self, o): - print o - + def do_delete(self, obj): + self.app.wallet.remove_payment_request(obj.address, self.app.electrum_config) + self.update() class CSpinner(Factory.Spinner): diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index 1894a21f..c856e371 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -99,4 +99,4 @@ HistoryScreen: size_hint: 1, None height: self.minimum_height padding: '12dp' - spacing: '12dp' + spacing: '2dp' diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv index 4af2ca23..4e1ae26e 100644 --- a/gui/kivy/uix/ui_screens/invoices.kv +++ b/gui/kivy/uix/ui_screens/invoices.kv @@ -19,9 +19,6 @@ InvoicesScreen: name: 'invoices' - on_activate: - if not self.action_view:\ - self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view BoxLayout: orientation: 'vertical' spacing: '1dp' @@ -37,5 +34,5 @@ InvoicesScreen: id: invoices_container size_hint: 1, None height: self.minimum_height - spacing: '1dp' + spacing: '2dp' padding: '12dp' diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index 3665cd75..e4ae62fc 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -89,7 +89,7 @@ ReceiveScreen: height: '48dp' on_release: s.parent.do_copy() Button: - text: _('Clear') + text: _('New') size_hint: 1, None height: '48dp' on_release: s.parent.do_clear() diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv index b789ba6c..b9a478c3 100644 --- a/gui/kivy/uix/ui_screens/requests.kv +++ b/gui/kivy/uix/ui_screens/requests.kv @@ -23,9 +23,6 @@ RequestsScreen: name: 'requests' - on_activate: - if not self.action_view:\ - self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view BoxLayout: orientation: 'vertical' spacing: '1dp' @@ -41,5 +38,5 @@ RequestsScreen: id: requests_container size_hint_y: None height: self.minimum_height - spacing: '1dp' + spacing: '2dp' padding: '12dp' From f30149ad49f220da4639d897586345e7def531e4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 06:33:06 +0100 Subject: [PATCH 25/90] fix #1579 --- lib/blockchain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/blockchain.py b/lib/blockchain.py index d8cd42d6..2f6a090e 100644 --- a/lib/blockchain.py +++ b/lib/blockchain.py @@ -56,8 +56,7 @@ class Blockchain(util.PrintError): self.verify_header(header, prev_header, bits, target) prev_header = header - def verify_chunk(self, index, hexdata): - data = hexdata.decode('hex') + def verify_chunk(self, index, data): num = len(data) / 80 prev_header = None if index != 0: @@ -212,11 +211,12 @@ class Blockchain(util.PrintError): self.print_error(str(e)) return False - def connect_chunk(self, idx, chunk): + def connect_chunk(self, idx, hexdata): try: - self.verify_chunk(idx, chunk) - self.print_error("validated chunk %d" % index) - self.save_chunk(index, data) + data = hexdata.decode('hex') + self.verify_chunk(idx, data) + self.print_error("validated chunk %d" % idx) + self.save_chunk(idx, data) return idx + 1 except BaseException as e: self.print_error('verify_chunk failed', str(e)) From cd0ab62cae17f80dc8ceb4c60d420b512d680649 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 06:41:22 +0100 Subject: [PATCH 26/90] kivy: various small fixes --- gui/kivy/main.kv | 12 ------------ gui/kivy/main_window.py | 10 ++++++++-- gui/kivy/uix/screens.py | 12 +++++------- gui/kivy/uix/ui_screens/send.kv | 10 +++++----- 4 files changed, 18 insertions(+), 26 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index 907cadcb..4caeaaa2 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -187,18 +187,6 @@ size: self.size pos: self.pos - - canvas.before: - Color: - rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 - Rectangle - size: self.size - pos: self.x, self.y + dp(5) - padding: '2dp', '2dp' - spacing: '2dp' - height: self.minimum_height - - size_hint: 1, None height: '65dp' diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 8796a865..ade3c3ee 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -205,8 +205,8 @@ class ElectrumWindow(App): def on_pr(self, pr): if pr.verify(self.contacts): key = self.invoices.add(pr) + self.invoices_screen.update() status = self.invoices.get_status(key) - #self.invoices_screen.update() if status == PR_PAID: self.show_error("invoice already paid") self.send_screen.do_clear() @@ -214,7 +214,7 @@ class ElectrumWindow(App): if pr.has_expired(): self.show_error(_('Payment request has expired')) else: - self.current_pr = pr + self.current_invoice = pr self.update_screen('send') send_tab = self.tabs.ids.send_tab self.tabs.ids.panel.switch_to(send_tab) @@ -521,6 +521,9 @@ class ElectrumWindow(App): def format_amount(self, x, is_diff=False, whitespaces=False): return format_satoshis(x, is_diff, 0, self.decimal_point(), whitespaces) + def format_amount_and_units(self, x): + return format_satoshis_plain(x, self.decimal_point()) + ' ' + self.base_unit + @profiler def update_wallet(self, *dt): self._trigger_update_status() @@ -717,6 +720,9 @@ class ElectrumWindow(App): popup.tx_hash = obj.tx_hash popup.open() + def address_dialog(self, screen): + pass + def amount_dialog(self, screen, show_max): popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv') but_max = popup.ids.but_max diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 9d05adbb..1743b50a 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -205,8 +205,7 @@ class SendScreen(CScreen): self.screen.message = uri.get('message', '') amount = uri.get('amount') if amount: - amount_str = str( Decimal(amount) / pow(10, self.app.decimal_point())) - self.screen.amount = amount_str + ' ' + self.app.base_unit + self.screen.amount = self.format_amount_and_units(amount) def update(self): if self.app.current_invoice: @@ -221,7 +220,7 @@ class SendScreen(CScreen): def set_request(self, pr): self.payment_request = pr self.screen.address = pr.get_requestor() - self.screen.amount = self.app.format_amount(pr.get_amount()) + self.screen.amount = self.app.format_amount_and_units(pr.get_amount()) self.screen.message = pr.get_memo() def do_paste(self): @@ -289,8 +288,7 @@ class ReceiveScreen(CScreen): req = self.app.wallet.receive_requests.get(addr) if req: self.screen.message = req.get('memo') - self.screen.amount = self.app.format_amount(req.get('amount')) + ' ' + self.app.base_unit - + self.screen.amount = self.app.format_amount_and_units(req.get('amount')) def amount_callback(self, popup): amount_label = self.screen.ids.get('amount') @@ -378,7 +376,7 @@ class InvoicesScreen(CScreen): ci.key = pr.get_id() ci.requestor = pr.get_requestor() ci.memo = pr.memo - ci.amount = self.app.format_amount(pr.get_amount()) + ci.amount = self.app.format_amount_and_units(pr.get_amount()) #ci.status = self.invoices.get_status(key) exp = pr.get_expiration_date() ci.date = format_time(exp) if exp else _('Never') @@ -413,7 +411,7 @@ class RequestsScreen(CScreen): ci.address = req['address'] ci.memo = req.get('memo', '') #ci.status = req.get('status') - ci.amount = self.app.format_amount(amount) if amount else '' + ci.amount = self.app.format_amount_and_units(amount) if amount else '' ci.date = format_time(timestamp) ci.screen = self requests_list.add_widget(ci) diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index 1002d04b..33f34a36 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -32,10 +32,10 @@ SendScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - TextInputBlue: + AmountButton: id: payto_e - text: s.address - hint_text: "Recipient" + text: s.address if s.address else _('Recipient') + on_release: app.address_dialog(s) CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color @@ -49,7 +49,7 @@ SendScreen: pos_hint: {'center_y': .5} AmountButton: id: amount_e - text: s.amount if s.amount else 'Amount' + text: s.amount if s.amount else _('Amount') on_release: app.amount_dialog(s, True) CardSeparator: @@ -68,7 +68,7 @@ SendScreen: pos_hint: {'center_y': .5} TextInputBlue: id: message_e - hint_text: 'Description' + hint_text: _('Description') text: s.message on_text_validate: s.message = self.text BoxLayout: From b1704ce9113880216467832341be9ea961549890 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Sun, 13 Dec 2015 18:13:24 +0900 Subject: [PATCH 27/90] Fix multiple trustedcoin bugs. --- plugins/trustedcoin/qt.py | 35 +++++++++++++++++++++++++++--- plugins/trustedcoin/trustedcoin.py | 13 ----------- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py index b1dd00e8..955618d7 100644 --- a/plugins/trustedcoin/qt.py +++ b/plugins/trustedcoin/qt.py @@ -1,4 +1,23 @@ +#!/usr/bin/env python +# +# Electrum - Lightweight Bitcoin Client +# Copyright (C) 2015 Thomas Voegtlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + from functools import partial +from threading import Thread from PyQt4.QtGui import * from PyQt4.QtCore import * @@ -10,7 +29,19 @@ from electrum_gui.qt.main_window import StatusBarButton from electrum.i18n import _ from electrum.plugins import hook -from trustedcoin import TrustedCoinPlugin +from trustedcoin import TrustedCoinPlugin, Wallet_2fa + +def need_server(wallet, tx): + from electrum.account import BIP32_Account + # Detect if the server is needed + long_id, short_id = wallet.get_user_id() + xpub3 = wallet.master_public_keys['x3/'] + for x in tx.inputs_to_sign(): + if x[0:2] == 'ff': + xpub, sequence = BIP32_Account.parse_xpubkey(x) + if xpub == xpub3: + return True + return False class Plugin(TrustedCoinPlugin): @@ -240,5 +271,3 @@ class Plugin(TrustedCoinPlugin): except: QMessageBox.information(window, _('Message'), _('Incorrect password'), _('OK')) pw.setText('') - - diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py index 25bcf4dc..7bd88760 100644 --- a/plugins/trustedcoin/trustedcoin.py +++ b/plugins/trustedcoin/trustedcoin.py @@ -16,7 +16,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from threading import Thread import socket import os import re @@ -270,18 +269,6 @@ def make_billing_address(wallet, num): address = public_key_to_bc_address( cK ) return address -def need_server(wallet, tx): - from electrum.account import BIP32_Account - # Detect if the server is needed - long_id, short_id = wallet.get_user_id() - xpub3 = wallet.master_public_keys['x3/'] - for x in tx.inputs_to_sign(): - if x[0:2] == 'ff': - xpub, sequence = BIP32_Account.parse_xpubkey(x) - if xpub == xpub3: - return True - return False - class TrustedCoinPlugin(BasePlugin): From 9caf174d7f1b3e3c1bcc0fcacc2b00b8fbbe21d8 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 10:42:48 +0100 Subject: [PATCH 28/90] kivy: add status icons, improve requests layout --- gui/kivy/uix/context_menu.py | 8 ++--- gui/kivy/uix/screens.py | 34 ++++++++++++++------- gui/kivy/uix/ui_screens/history.kv | 46 ++++++++--------------------- gui/kivy/uix/ui_screens/invoices.kv | 33 +++++++++++++++++---- gui/kivy/uix/ui_screens/requests.kv | 36 ++++++++++++++++------ 5 files changed, 94 insertions(+), 63 deletions(-) diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py index 2b80bb65..d12a326d 100644 --- a/gui/kivy/uix/context_menu.py +++ b/gui/kivy/uix/context_menu.py @@ -10,20 +10,20 @@ from kivy.factory import Factory Builder.load_string(''' background_color: .2, .9, 1, 1 - height: '40dp' + height: '48dp' size_hint: 1, None size_hint: 1, None - height: '32dp' - #size: 120, 250 + height: '48dp' pos: (0, 0) show_arrow: False + arrow_pos: 'top_mid' padding: 0 orientation: 'horizontal' BoxLayout: size_hint: 1, 1 - height: '40dp' + height: '48dp' orientation: 'horizontal' id: buttons ''') diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 1743b50a..90189189 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -24,6 +24,8 @@ from electrum.plugins import run_hook from context_menu import ContextMenu +from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED + class CScreen(Factory.Screen): @@ -74,14 +76,14 @@ class CScreen(Factory.Screen): def hide_menu(self): if self.context_menu: - self.screen.remove_widget(self.context_menu) + self.remove_widget(self.context_menu) self.context_menu = None def show_menu(self, obj): if self.context_menu is None: self.context_menu = ContextMenu(obj, self.menu_actions) - self.screen.remove_widget(self.context_menu) - self.screen.add_widget(self.context_menu) + self.remove_widget(self.context_menu) + self.add_widget(self.context_menu) @@ -143,15 +145,14 @@ class HistoryScreen(CScreen): # repopulate History Card history_card.clear_widgets() history_add = history_card.add_widget - RecentActivityItem = Factory.RecentActivityItem count = 0 for item in history: count += 1 - conf, icon, date_time, address, value, tx, quote_text = item - ri = RecentActivityItem() + conf, icon, date_time, message, value, tx, quote_text = item + ri = Factory.HistoryItem() ri.icon = icon ri.date = date_time - ri.address = address + ri.message = message ri.value = value ri.quote_text = quote_text ri.confirmations = conf @@ -377,7 +378,14 @@ class InvoicesScreen(CScreen): ci.requestor = pr.get_requestor() ci.memo = pr.memo ci.amount = self.app.format_amount_and_units(pr.get_amount()) - #ci.status = self.invoices.get_status(key) + status = self.app.invoices.get_status(ci.key) + if status == PR_PAID: + icon = "atlas://gui/kivy/theming/light/confirmed" + elif status == PR_EXPIRED: + icon = "atlas://gui/kivy/theming/light/important" + else: + icon = "atlas://gui/kivy/theming/light/important" + exp = pr.get_expiration_date() ci.date = format_time(exp) if exp else _('Never') ci.screen = self @@ -406,11 +414,17 @@ class RequestsScreen(CScreen): expiration = req.get('exp', None) status = req.get('status') signature = req.get('sig') - ci = Factory.RequestItem() ci.address = req['address'] ci.memo = req.get('memo', '') - #ci.status = req.get('status') + status = req.get('status') + if status == PR_PAID: + icon = "atlas://gui/kivy/theming/light/confirmed" + elif status == PR_EXPIRED: + icon = "atlas://gui/kivy/theming/light/important" + else: + icon = "atlas://gui/kivy/theming/light/important" + ci.amount = self.app.format_amount_and_units(amount) if amount else '' ci.date = format_time(timestamp) ci.screen = self diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index c856e371..4ea76001 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -5,23 +5,9 @@ #:set mbtc_symbol unichr(187) - - cols: 1 - padding: '12dp' , '22dp', '12dp' , '12dp' - spacing: '12dp' - size_hint: 1, None - height: max(100, self.minimum_height) - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - border: 18, 18, 18, 18 - source: 'atlas://gui/kivy/theming/light/card' - size: self.size - pos: self.pos - color: 0.45, 0.45, 0.45, 1 + color: 0.95, 0.95, 0.95, 1 size_hint: 1, None text: '' text_size: self.width, None @@ -29,21 +15,13 @@ halign: 'left' valign: 'top' - - background_normal: 'atlas://gui/kivy/theming/light/card_btn' - bold: True - font_size: '10sp' - color: 0.699, 0.699, 0.699, 1 - size_hint: None, None - size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) - - + icon: 'atlas://gui/kivy/theming/light/important' - address: 'no address set' + message: '' value: 0 amount: app.format_amount(self.value, True) if self.value is not None else '--' - amount_color: '#DB3627' if self.value < 0 else '#2EA442' + amount_color: '#FF6657' if self.value < 0 else '#2EA442' confirmations: 0 date: '0/0/0' quote_text: '.' @@ -62,26 +40,26 @@ BoxLayout: orientation: 'vertical' Widget - CardLabel: - shorten: True - text: root.address - markup: False - text_size: self.size CardLabel: color: .699, .699, .699, 1 text: root.date - font_size: '12sp' + font_size: '14sp' + CardLabel: + shorten: True + text: root.message + markup: False + text_size: self.size Widget CardLabel: halign: 'right' - font_size: '13sp' + font_size: '15sp' size_hint: None, 1 width: '110sp' markup: True font_name: font_light text: u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ - u'[color=#B2B3B3][size=12sp]{qt}[/size]'\ + u'[color=#B2B3B3][size=13sp]{qt}[/size]'\ u'[/color]'.format(amount_color=root.amount_color,\ amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ unit=app.base_unit) diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv index 4e1ae26e..74eeadde 100644 --- a/gui/kivy/uix/ui_screens/invoices.kv +++ b/gui/kivy/uix/ui_screens/invoices.kv @@ -1,8 +1,8 @@ #color: .305, .309, .309, 1 - text_size: self.size + text_size: self.width, None halign: 'left' - valign: 'middle' + valign: 'top' requestor: '' @@ -10,11 +10,32 @@ amount: '' status: '' date: '' + icon: 'atlas://gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + InvoicesLabel: + text: root.requestor + shorten: True + InvoicesLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget InvoicesLabel: - text: root.requestor - InvoicesLabel: - text: root.memo - InvoicesLabel: + halign: 'right' + font_size: '15sp' + size_hint: None, 1 + width: '80sp' text: root.amount InvoicesScreen: diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv index b9a478c3..af69225c 100644 --- a/gui/kivy/uix/ui_screens/requests.kv +++ b/gui/kivy/uix/ui_screens/requests.kv @@ -1,8 +1,8 @@ #color: .305, .309, .309, 1 - text_size: self.size + text_size: self.width, None halign: 'left' - valign: 'middle' + valign: 'top' address: '' @@ -10,15 +10,33 @@ amount: '' status: '' date: '' + icon: 'atlas://gui/kivy/theming/light/important' + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True + BoxLayout: + spacing: '8dp' + height: '32dp' + orientation: 'vertical' + Widget + RequestLabel: + text: root.address + shorten: True + RequestLabel: + text: root.memo + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + Widget RequestLabel: - text: root.address - font_size: '13dp' - RequestLabel: - text: root.memo - RequestLabel: + halign: 'right' + font_size: '15sp' + size_hint: None, 1 + width: '80sp' text: root.amount - #InvoicesLabel: - # text: root.status RequestsScreen: From a190d1dbe67bc9868f79da620a375ad40d5dcb04 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 15:26:08 +0100 Subject: [PATCH 29/90] kivy: fixes --- gui/kivy/main_window.py | 6 ++- gui/kivy/uix/screens.py | 35 ++++++++------- gui/kivy/uix/ui_screens/history.kv | 68 +++++++++++++---------------- gui/kivy/uix/ui_screens/invoices.kv | 2 +- gui/kivy/uix/ui_screens/receive.kv | 11 +++-- gui/kivy/uix/ui_screens/requests.kv | 2 +- 6 files changed, 61 insertions(+), 63 deletions(-) diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index ade3c3ee..00f8851d 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -240,7 +240,6 @@ class ElectrumWindow(App): self.update_screen('receive') receive_tab = self.tabs.ids.receive_tab self.tabs.ids.panel.switch_to(receive_tab) - req = self.wallet.receive_requests.get(addr) def scan_qr(self, on_complete): from jnius import autoclass @@ -715,11 +714,14 @@ class ElectrumWindow(App): pos = (win.center[0], win.center[1] - (info_bubble.height/2)) info_bubble.show(pos, duration, width, modal=modal, exit=exit) - def tx_dialog(self, obj): + def tx_details_dialog(self, obj): popup = Builder.load_file('gui/kivy/uix/ui_screens/transaction.kv') popup.tx_hash = obj.tx_hash popup.open() + def tx_label_dialog(self, obj): + pass + def address_dialog(self, screen): pass diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 90189189..956e857e 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -95,7 +95,7 @@ class HistoryScreen(CScreen): def __init__(self, **kwargs): self.ra_dialog = None super(HistoryScreen, self).__init__(**kwargs) - self.menu_actions = [(_('Details'), self.app.tx_dialog)] + self.menu_actions = [ (_('Label'), self.app.tx_label_dialog), (_('Details'), self.app.tx_details_dialog)] def get_history_rate(self, btc_balance, timestamp): date = timestamp_to_datetime(timestamp) @@ -288,8 +288,8 @@ class ReceiveScreen(CScreen): self.screen.address = addr req = self.app.wallet.receive_requests.get(addr) if req: - self.screen.message = req.get('memo') self.screen.amount = self.app.format_amount_and_units(req.get('amount')) + self.screen.message = unicode(req.get('memo', '')) def amount_callback(self, popup): amount_label = self.screen.ids.get('amount') @@ -318,24 +318,27 @@ class ReceiveScreen(CScreen): def do_save(self): addr = str(self.screen.address) amount = str(self.screen.amount) - message = unicode(self.screen.message) + message = str(self.screen.message) #.ids.message_input.text) if not message and not amount: self.app.show_error(_('No message or amount')) - return False - amount = self.app.get_amount(amount) + return + if amount: + amount = self.app.get_amount(amount) + else: + amount = 0 + print "saving", amount, message req = self.app.wallet.make_payment_request(addr, amount, message, None) self.app.wallet.add_payment_request(req, self.app.electrum_config) self.app.show_error(_('Request saved')) self.app.update_screen('requests') - def do_clear(self): + def do_new(self): self.app.receive_address = None self.screen.amount = '' self.screen.message = '' self.update() - class ContactsScreen(CScreen): kvname = 'contacts' @@ -380,12 +383,11 @@ class InvoicesScreen(CScreen): ci.amount = self.app.format_amount_and_units(pr.get_amount()) status = self.app.invoices.get_status(ci.key) if status == PR_PAID: - icon = "atlas://gui/kivy/theming/light/confirmed" + ci.icon = "atlas://gui/kivy/theming/light/confirmed" elif status == PR_EXPIRED: - icon = "atlas://gui/kivy/theming/light/important" + ci.icon = "atlas://gui/kivy/theming/light/important" else: - icon = "atlas://gui/kivy/theming/light/important" - + ci.icon = "atlas://gui/kivy/theming/light/important" exp = pr.get_expiration_date() ci.date = format_time(exp) if exp else _('Never') ci.screen = self @@ -416,15 +418,16 @@ class RequestsScreen(CScreen): signature = req.get('sig') ci = Factory.RequestItem() ci.address = req['address'] - ci.memo = req.get('memo', '') + label, is_default = self.app.wallet.get_label(address) + if label: + ci.memo = label status = req.get('status') if status == PR_PAID: - icon = "atlas://gui/kivy/theming/light/confirmed" + ci.icon = "atlas://gui/kivy/theming/light/confirmed" elif status == PR_EXPIRED: - icon = "atlas://gui/kivy/theming/light/important" + ci.icon = "atlas://gui/kivy/theming/light/important" else: - icon = "atlas://gui/kivy/theming/light/important" - + ci.icon = "atlas://gui/kivy/theming/light/important" ci.amount = self.app.format_amount_and_units(amount) if amount else '' ci.date = format_time(timestamp) ci.screen = self diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index 4ea76001..2a403a77 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -23,46 +23,40 @@ amount: app.format_amount(self.value, True) if self.value is not None else '--' amount_color: '#FF6657' if self.value < 0 else '#2EA442' confirmations: 0 - date: '0/0/0' - quote_text: '.' + date: '' + quote_text: '' spacing: '9dp' - cols: 1 + Image: + id: icon + source: root.icon + size_hint: None, 1 + width: self.height *.54 + mipmap: True BoxLayout: - size_hint: 1, None - spacing: '8dp' - height: '32dp' - Image: - id: icon - source: root.icon - size_hint: None, 1 - width: self.height *.54 - mipmap: True - BoxLayout: - orientation: 'vertical' - Widget - CardLabel: - color: .699, .699, .699, 1 - text: root.date - font_size: '14sp' - CardLabel: - shorten: True - text: root.message - markup: False - text_size: self.size - Widget + orientation: 'vertical' + Widget CardLabel: - halign: 'right' - font_size: '15sp' - size_hint: None, 1 - width: '110sp' - markup: True - font_name: font_light - text: - u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ - u'[color=#B2B3B3][size=13sp]{qt}[/size]'\ - u'[/color]'.format(amount_color=root.amount_color,\ - amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ - unit=app.base_unit) + text: root.date + font_size: '14sp' + CardLabel: + color: .699, .699, .699, 1 + font_size: '13sp' + shorten: True + text: root.message if root.message else ' ' + Widget + CardLabel: + halign: 'right' + font_size: '15sp' + size_hint: None, 1 + width: '110sp' + markup: True + font_name: font_light + text: + u'[color={amount_color}]{sign}{amount} {unit}[/color]\n'\ + u'[color=#B2B3B3][size=13sp]{qt}[/size]'\ + u'[/color]'.format(amount_color=root.amount_color,\ + amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ + unit=app.base_unit) HistoryScreen: diff --git a/gui/kivy/uix/ui_screens/invoices.kv b/gui/kivy/uix/ui_screens/invoices.kv index 74eeadde..7c01dbef 100644 --- a/gui/kivy/uix/ui_screens/invoices.kv +++ b/gui/kivy/uix/ui_screens/invoices.kv @@ -35,7 +35,7 @@ halign: 'right' font_size: '15sp' size_hint: None, 1 - width: '80sp' + width: '110sp' text: root.amount InvoicesScreen: diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index e4ae62fc..653f2f05 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -79,7 +79,6 @@ ReceiveScreen: hint_text: 'Description' text: s.message on_text_validate: s.message = self.text - BoxLayout: size_hint: 1, None height: '48dp' @@ -88,15 +87,15 @@ ReceiveScreen: size_hint: 1, None height: '48dp' on_release: s.parent.do_copy() - Button: - text: _('New') - size_hint: 1, None - height: '48dp' - on_release: s.parent.do_clear() Button: text: _('Save') size_hint: 1, None height: '48dp' on_release: s.parent.do_save() + Button: + text: _('New') + size_hint: 1, None + height: '48dp' + on_release: s.parent.do_new() Widget: size_hint: 1, 0.3 diff --git a/gui/kivy/uix/ui_screens/requests.kv b/gui/kivy/uix/ui_screens/requests.kv index af69225c..e407c58f 100644 --- a/gui/kivy/uix/ui_screens/requests.kv +++ b/gui/kivy/uix/ui_screens/requests.kv @@ -35,7 +35,7 @@ halign: 'right' font_size: '15sp' size_hint: None, 1 - width: '80sp' + width: '110sp' text: root.amount From f7ffdfc2b1ef53d67dd844a6622cc871d4e70b39 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 16:21:50 +0100 Subject: [PATCH 30/90] kivy: fix qrcodewidget (only update from gui thread) --- gui/kivy/uix/qrcodewidget.py | 33 +++++------------------------- gui/kivy/uix/screens.py | 2 +- gui/kivy/uix/ui_screens/history.kv | 2 +- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/gui/kivy/uix/qrcodewidget.py b/gui/kivy/uix/qrcodewidget.py index a4c8a2b2..9283afec 100644 --- a/gui/kivy/uix/qrcodewidget.py +++ b/gui/kivy/uix/qrcodewidget.py @@ -42,14 +42,9 @@ Builder.load_string(''' class QRCodeWidget(FloatLayout): data = StringProperty(None, allow_none=True) - background_color = ListProperty((1, 1, 1, 1)) - foreground_color = ListProperty((0, 0, 0, 0)) - - #loading_image = StringProperty('gui/kivy/theming/loading.gif') - def __init__(self, **kwargs): super(QRCodeWidget, self).__init__(**kwargs) self.data = None @@ -57,21 +52,11 @@ class QRCodeWidget(FloatLayout): self._qrtexture = None def on_data(self, instance, value): - print "on data", value if not (self.canvas or value): return - img = self.ids.get('qrimage', None) - - if not img: - # if texture hasn't yet been created delay the texture updation - Clock.schedule_once(lambda dt: self.on_data(instance, value)) - return - - #Thread(target=partial(self.update_qr, )).start() self.update_qr() def set_data(self, data): - print "set data", data if self.data == data: return MinSize = 210 if len(data) < 128 else 500 @@ -98,7 +83,7 @@ class QRCodeWidget(FloatLayout): # currently unused, do we need this? self._texture_size = size - def _create_texture(self, k, dt): + def _create_texture(self, k): self._qrtexture = texture = Texture.create(size=(k,k), colorfmt='rgb') # don't interpolate texture texture.min_filter = 'nearest' @@ -107,32 +92,24 @@ class QRCodeWidget(FloatLayout): def update_texture(self): if not self.qr: return - matrix = self.qr.get_matrix() k = len(matrix) - # create the texture in main UI thread otherwise - # this will lead to memory corruption - Clock.schedule_once(partial(self._create_texture, k), -1) + # create the texture + self._create_texture(k) buff = [] bext = buff.extend cr, cg, cb, ca = self.background_color[:] cr, cg, cb = cr*255, cg*255, cb*255 - for r in range(k): for c in range(k): bext([0, 0, 0] if matrix[r][c] else [cr, cg, cb]) - # then blit the buffer buff = ''.join(map(chr, buff)) - # update texture in UI thread. - Clock.schedule_once(lambda dt: self._upd_texture(buff), .1) + # update texture + self._upd_texture(buff) def _upd_texture(self, buff): texture = self._qrtexture - if not texture: - # if texture hasn't yet been created delay the texture updation - Clock.schedule_once(lambda dt: self._upd_texture(buff), .1) - return texture.blit_buffer(buff, colorfmt='rgb', bufferfmt='ubyte') img =self.ids.qrimage img.anim_delay = -1 diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 956e857e..9f11b557 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -308,7 +308,7 @@ class ReceiveScreen(CScreen): @profiler def update_qr(self): uri = self.get_URI() - qr = self.screen.ids.get('qr') + qr = self.screen.ids.qr qr.set_data(uri) def do_copy(self): diff --git a/gui/kivy/uix/ui_screens/history.kv b/gui/kivy/uix/ui_screens/history.kv index 2a403a77..f832eac1 100644 --- a/gui/kivy/uix/ui_screens/history.kv +++ b/gui/kivy/uix/ui_screens/history.kv @@ -42,7 +42,7 @@ color: .699, .699, .699, 1 font_size: '13sp' shorten: True - text: root.message if root.message else ' ' + text: root.message Widget CardLabel: halign: 'right' From 9b44635e3cc29f18c9bb42751ee63a023b0408cc Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 17:36:23 +0100 Subject: [PATCH 31/90] kivy: make tabs one third of screen width and fix tabs scrolling --- gui/kivy/main.kv | 2 ++ gui/kivy/uix/screens.py | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index 4caeaaa2..3bcc56f5 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -244,6 +244,7 @@ : on_parent: if self.parent: self.parent.bar_width = 0 + if self.parent: self.parent.scroll_x = 0.5 @@ -297,6 +298,7 @@ TabbedCarousel: id: panel tab_height: '48dp' + tab_width: panel.width/3 default_tab: history_tab strip_border: 0, 0, 0, 0 InvoicesScreen: diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 9f11b557..89707589 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -471,14 +471,18 @@ class TabbedCarousel(Factory.TabbedPanel): return idx = self.tab_list.index(value) n = len(self.tab_list) - scroll_x = 1. * (n - idx - 1) / (n - 1) + 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): - if value.text == 'default_tab': - return self.animate_tab_to_center(value) def on_index(self, instance, value): From 98d43846417ee6986cd96b5699b03a55ca5eb916 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Sun, 13 Dec 2015 17:49:51 +0100 Subject: [PATCH 32/90] kivy: fix set amount --- gui/kivy/uix/screens.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 89707589..50504a7b 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -201,12 +201,11 @@ class SendScreen(CScreen): payment_request = None def set_URI(self, uri): - print "set uri", uri self.screen.address = uri.get('address', '') self.screen.message = uri.get('message', '') amount = uri.get('amount') if amount: - self.screen.amount = self.format_amount_and_units(amount) + self.screen.amount = self.app.format_amount_and_units(amount) def update(self): if self.app.current_invoice: @@ -221,7 +220,9 @@ class SendScreen(CScreen): def set_request(self, pr): self.payment_request = pr self.screen.address = pr.get_requestor() - self.screen.amount = self.app.format_amount_and_units(pr.get_amount()) + amount = pr.get_amount() + if amount: + self.screen.amount = self.app.format_amount_and_units(amount) self.screen.message = pr.get_memo() def do_paste(self): @@ -288,8 +289,10 @@ class ReceiveScreen(CScreen): self.screen.address = addr req = self.app.wallet.receive_requests.get(addr) if req: - self.screen.amount = self.app.format_amount_and_units(req.get('amount')) self.screen.message = unicode(req.get('memo', '')) + amount = req.get('amount') + if amount: + self.screen.amount = self.app.format_amount_and_units(amount) def amount_callback(self, popup): amount_label = self.screen.ids.get('amount') From cead9cd7c6d7f9ca4270da1fa7950fe45faa8949 Mon Sep 17 00:00:00 2001 From: Neil Booth Date: Mon, 14 Dec 2015 07:45:01 +0900 Subject: [PATCH 33/90] Ensure zeroes is always non-empty. --- lib/coinchooser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/coinchooser.py b/lib/coinchooser.py index d2cfc0fb..ccd2dbd6 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -241,7 +241,7 @@ class CoinChooserPrivacy(CoinChooserRandom): zeroes = map(trailing_zeroes, output_amounts) min_zeroes = min(zeroes) max_zeroes = max(zeroes) - zeroes = range(max(0, min_zeroes - 1), min(max_zeroes + 1, 8) + 1) + zeroes = range(max(0, min_zeroes - 1), (max_zeroes + 1) + 1) # Calculate change; randomize it a bit if using more than 1 output remaining = change_amount From 8977493a6278256e42d2b32b9e47959c92f7c1ea Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 12:08:11 +0100 Subject: [PATCH 34/90] kivy: label dialogs --- gui/kivy/main.kv | 4 +-- gui/kivy/main_window.py | 13 +++++-- gui/kivy/uix/context_menu.py | 5 ++- gui/kivy/uix/dialogs/label_dialog.py | 54 ++++++++++++++++++++++++++++ gui/kivy/uix/screens.py | 12 ++++++- gui/kivy/uix/ui_screens/receive.kv | 12 +++---- gui/kivy/uix/ui_screens/send.kv | 14 ++++---- 7 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 gui/kivy/uix/dialogs/label_dialog.py diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index 3bcc56f5..d5053808 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -208,12 +208,12 @@ values: [] #app.wallet.addresses() if app.wallet else [] text: _("Select Your address") -: +: background_color: .238, .585, .878, 0 halign: 'left' text_size: (self.width-10, None) size_hint: 0.5, None - default_text: 'Amount' + default_text: '' text: self.default_text padding: '5dp', '5db' height: '40dp' diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 00f8851d..e2528e9c 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -33,6 +33,7 @@ Factory.register('InstallWizard', Factory.register('InfoBubble', module='electrum_gui.kivy.uix.dialogs') Factory.register('ELTextInput', module='electrum_gui.kivy.uix.screens') + #from kivy.core.window import Window #Window.softinput_mode = 'below_target' @@ -719,12 +720,18 @@ class ElectrumWindow(App): popup.tx_hash = obj.tx_hash popup.open() - def tx_label_dialog(self, obj): - pass - def address_dialog(self, screen): pass + def description_dialog(self, screen): + from uix.dialogs.label_dialog import LabelDialog + text = screen.message + def callback(text): + screen.message = text + d = LabelDialog(_('Enter description'), text, callback) + d.open() + + def amount_dialog(self, screen, show_max): popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv') but_max = popup.ids.but_max diff --git a/gui/kivy/uix/context_menu.py b/gui/kivy/uix/context_menu.py index d12a326d..2d968cc5 100644 --- a/gui/kivy/uix/context_menu.py +++ b/gui/kivy/uix/context_menu.py @@ -39,5 +39,8 @@ class ContextMenu(Bubble): for k, v in action_list: l = MenuItem() l.text = k - l.on_release = lambda f=v: f(obj) + def func(f=v): + f(obj) + if self.parent: self.parent.hide_menu() + l.on_release = func self.ids.buttons.add_widget(l) diff --git a/gui/kivy/uix/dialogs/label_dialog.py b/gui/kivy/uix/dialogs/label_dialog.py new file mode 100644 index 00000000..f04b7509 --- /dev/null +++ b/gui/kivy/uix/dialogs/label_dialog.py @@ -0,0 +1,54 @@ +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder + +Builder.load_string(''' + + id: popup + title: '' + size_hint: 0.8, 0.3 + BoxLayout: + orientation: 'vertical' + Widget: + size_hint: 1, 0.2 + TextInput: + id:input + padding: '5dp' + size_hint: 1, None + height: '27dp' + pos_hint: {'center_y':.5} + text:'' + multiline: False + background_normal: 'atlas://gui/kivy/theming/light/tab_btn' + background_active: 'atlas://gui/kivy/theming/light/textinput_active' + hint_text_color: self.foreground_color + foreground_color: 1, 1, 1, 1 + font_size: '16dp' + focus: True + Widget: + size_hint: 1, 0.2 + BoxLayout: + orientation: 'horizontal' + size_hint: 1, 0.5 + Button: + text: 'Cancel' + size_hint: 0.5, None + height: '48dp' + on_release: popup.dismiss() + Button: + text: 'OK' + size_hint: 0.5, None + height: '48dp' + on_release: + root.callback(input.text) + popup.dismiss() +''') + +class LabelDialog(Factory.Popup): + + def __init__(self, title, text, callback): + Factory.Popup.__init__(self) + self.ids.input.text = text + self.callback = callback + self.title = title diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 50504a7b..3b609fc0 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -95,7 +95,17 @@ class HistoryScreen(CScreen): def __init__(self, **kwargs): self.ra_dialog = None super(HistoryScreen, self).__init__(**kwargs) - self.menu_actions = [ (_('Label'), self.app.tx_label_dialog), (_('Details'), self.app.tx_details_dialog)] + self.menu_actions = [ (_('Label'), self.label_dialog), (_('Details'), self.app.tx_details_dialog)] + + def label_dialog(self, obj): + from dialogs.label_dialog import LabelDialog + key = obj.tx_hash + text = self.app.wallet.get_label(key)[0] + def callback(text): + self.app.wallet.set_label(key, text) + self.update() + d = LabelDialog(_('Enter Transaction Label'), text, callback) + d.open() def get_history_rate(self, btc_balance, timestamp): date = timestamp_to_datetime(timestamp) diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index 653f2f05..5adef2de 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -56,8 +56,9 @@ ReceiveScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - AmountButton: + BlueButton: id: amount_label + default_text: 'Amount' text: s.amount if s.amount else 'Amount' on_release: app.amount_dialog(s, False) CardSeparator: @@ -74,11 +75,10 @@ ReceiveScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - TextInputBlue: - id: message_input - hint_text: 'Description' - text: s.message - on_text_validate: s.message = self.text + BlueButton: + id: description + text: s.message if s.message else _('Description') + on_release: app.description_dialog(s) BoxLayout: size_hint: 1, None height: '48dp' diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index 33f34a36..d6a380a7 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -32,7 +32,7 @@ SendScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - AmountButton: + BlueButton: id: payto_e text: s.address if s.address else _('Recipient') on_release: app.address_dialog(s) @@ -47,8 +47,9 @@ SendScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - AmountButton: + BlueButton: id: amount_e + default_text: _('Amount') text: s.amount if s.amount else _('Amount') on_release: app.amount_dialog(s, True) @@ -66,11 +67,10 @@ SendScreen: size_hint: None, None size: '22dp', '22dp' pos_hint: {'center_y': .5} - TextInputBlue: - id: message_e - hint_text: _('Description') - text: s.message - on_text_validate: s.message = self.text + BlueButton: + id: description + text: s.message if s.message else _('Description') + on_release: app.description_dialog(s) BoxLayout: size_hint: 1, None height: '48dp' From c55a253f6d8b659bacaac90ab4ea4ccb9fe7ab15 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 13:37:19 +0100 Subject: [PATCH 35/90] kivy: improve layout --- gui/kivy/uix/dialogs/label_dialog.py | 1 + gui/kivy/uix/ui_screens/receive.kv | 1 + gui/kivy/uix/ui_screens/send.kv | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/kivy/uix/dialogs/label_dialog.py b/gui/kivy/uix/dialogs/label_dialog.py index f04b7509..cf7aa2de 100644 --- a/gui/kivy/uix/dialogs/label_dialog.py +++ b/gui/kivy/uix/dialogs/label_dialog.py @@ -8,6 +8,7 @@ Builder.load_string(''' id: popup title: '' size_hint: 0.8, 0.3 + pos_hint: {'top':0.9} BoxLayout: orientation: 'vertical' Widget: diff --git a/gui/kivy/uix/ui_screens/receive.kv b/gui/kivy/uix/ui_screens/receive.kv index 5adef2de..d5cce9f5 100644 --- a/gui/kivy/uix/ui_screens/receive.kv +++ b/gui/kivy/uix/ui_screens/receive.kv @@ -51,6 +51,7 @@ ReceiveScreen: BoxLayout: size_hint: 1, None height: blue_bottom.item_height + spacing: '5dp' Image: source: 'atlas://gui/kivy/theming/light/globe' size_hint: None, None diff --git a/gui/kivy/uix/ui_screens/send.kv b/gui/kivy/uix/ui_screens/send.kv index d6a380a7..76a47081 100644 --- a/gui/kivy/uix/ui_screens/send.kv +++ b/gui/kivy/uix/ui_screens/send.kv @@ -23,6 +23,7 @@ SendScreen: id: blue_bottom size_hint: 1, None height: self.minimum_height + spacing: '5dp' BoxLayout: size_hint: 1, None height: blue_bottom.item_height @@ -42,6 +43,7 @@ SendScreen: BoxLayout: size_hint: 1, None height: blue_bottom.item_height + spacing: '5dp' Image: source: 'atlas://gui/kivy/theming/light/globe' size_hint: None, None @@ -52,13 +54,11 @@ SendScreen: default_text: _('Amount') text: s.amount if s.amount else _('Amount') on_release: app.amount_dialog(s, True) - CardSeparator: opacity: message_selection.opacity color: blue_bottom.foreground_color BoxLayout: id: message_selection - opacity: 1 size_hint: 1, None height: blue_bottom.item_height spacing: '5dp' From 5e5f3202b10a65061b433c6bde55da5ee941c497 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 14:27:39 +0100 Subject: [PATCH 36/90] kivy: move amount dialog --- gui/kivy/main_window.py | 23 +- .../amount.kv => dialogs/amount_dialog.py} | 31 +- gui/kivy/uix/ui_screens/mainscreen.kv | 1774 ----------------- 3 files changed, 31 insertions(+), 1797 deletions(-) rename gui/kivy/uix/{ui_screens/amount.kv => dialogs/amount_dialog.py} (81%) delete mode 100644 gui/kivy/uix/ui_screens/mainscreen.kv diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index e2528e9c..2d32876b 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -733,26 +733,17 @@ class ElectrumWindow(App): def amount_dialog(self, screen, show_max): - popup = Builder.load_file('gui/kivy/uix/ui_screens/amount.kv') - but_max = popup.ids.but_max - if not show_max: - but_max.disabled = True - but_max.opacity = 0 - else: - but_max.disabled = False - but_max.opacity = 1 - + from uix.dialogs.amount_dialog import AmountDialog amount = screen.amount if amount: - a, u = str(amount).split() + amount, u = str(amount).split() assert u == self.base_unit - popup.ids.kb.amount = a + else: + amount = None - def cb(): - o = popup.ids.a.btc_text - screen.amount = o - - popup.on_dismiss = cb + def cb(amount): + screen.amount = amount + popup = AmountDialog(show_max, amount, cb) popup.open() def protected(self, f, args): diff --git a/gui/kivy/uix/ui_screens/amount.kv b/gui/kivy/uix/dialogs/amount_dialog.py similarity index 81% rename from gui/kivy/uix/ui_screens/amount.kv rename to gui/kivy/uix/dialogs/amount_dialog.py index 56005039..79f33915 100644 --- a/gui/kivy/uix/ui_screens/amount.kv +++ b/gui/kivy/uix/dialogs/amount_dialog.py @@ -1,13 +1,15 @@ -#:import Decimal decimal.Decimal +from kivy.app import App +from kivy.factory import Factory +from kivy.properties import ObjectProperty +from kivy.lang import Builder -Popup: +Builder.load_string(''' + id: popup title: _('Amount') - AnchorLayout: anchor_x: 'center' - BoxLayout: orientation: 'vertical' size_hint: 0.8, 1 @@ -60,6 +62,8 @@ Popup: text: '<' Button: id: but_max + opacity: 1 if root.show_max else 0 + disabled: not root.show_max size_hint: 1, None height: '48dp' text: 'Max' @@ -80,10 +84,8 @@ Popup: on_release: kb.amount = '' kb.fiat_amount = '' - Widget: size_hint: 1, None - BoxLayout: size_hint: 1, None height: '48dp' @@ -94,4 +96,19 @@ Popup: size_hint: 1, None height: '48dp' text: _('OK') - on_release: popup.dismiss() + on_release: + root.callback(a.btc_text) + popup.dismiss() +''') + +from kivy.properties import BooleanProperty + +class AmountDialog(Factory.Popup): + show_max = BooleanProperty(False) + def __init__(self, show_max, amount, cb): + Factory.Popup.__init__(self) + self.show_max = show_max + self.callback = cb + if amount: + self.ids.kb.amount = amount + diff --git a/gui/kivy/uix/ui_screens/mainscreen.kv b/gui/kivy/uix/ui_screens/mainscreen.kv deleted file mode 100644 index e7e45e95..00000000 --- a/gui/kivy/uix/ui_screens/mainscreen.kv +++ /dev/null @@ -1,1774 +0,0 @@ -#:import _ electrum.i18n._ -#:import Cache kivy.cache.Cache -#:import Factory kivy.factory.Factory -#:import Decimal decimal.Decimal -#:set font_light 'data/fonts/Roboto-Condensed.ttf' -#:set btc_symbol unichr(171) -#:set mbtc_symbol unichr(187) - - - app_icon: 'atlas://gui/kivy/theming/light/' + ('wallets' if app.ui_mode[0] != 't' else 'tab_btn') - with_previous: False - size_hint: None, 1 - mipmap: True - on_release: app.root.children[0].toggle_drawer() - - - source: 'atlas://gui/kivy/theming/light/closebutton' - opacity: 1 if self.state == 'normal' else .75 - size_hint: None, None - size: '27dp', '27dp' - -####################### -# Screen Contacts -####################### -: - source: 'atlas://gui/kivy/theming/light/contact_avatar' - size_hint_x: None - width: self.height - canvas: - Color: - rgba: 1, 1, 1, 1 - Ellipse: - source: root.source - size: self.width + dp(6), self.height + dp(6) - pos: self.x - dp(3), self.y - dp(3) - Ellipse: - source: 'atlas://gui/kivy/theming/light/contact_overlay' - size: self.width + dp(11), self.height + dp(11) - pos: self.x - dp(5.5), self.y - dp(5.5) - - - color: .305, .309, .309, 1 - text_size: self.size - halign: 'left' - valign: 'middle' - - - canvas.before: - Color: - rgba: .890, .890, .890, 1 - Rectangle: - size: self.size - pos: self.x, self.y + dp(9) - size_hint: None, None - size: '1dp', '22dp' - pos_hint_y: .5 - - - background_normal: self.background_down - background_down: 'atlas://gui/kivy/theming/light/tab_btn' - size_hint_y: None - height: '22dp' - - - source: 'atlas://gui/kivy/theming/light/bit_logo' - size_hint_x: None - width: '32dp' - - - address: '' - label: '' - tx_amt: 0 - size_hint_y: None - height: '65dp' - padding: dp(12) - spacing: dp(5) - canvas.before: - Color: - rgba: 1, 1, 1, 1 - Rectangle: - size: self.size - pos: self.pos - ContactImage: - id: contact_image - Widget: - size_hint_x: None - width: '9dp' - ContactLabel: - id: contact_label - text: root.label - ContactSeperator: - ContactBitLogo: - - - name: 'contacts' - on_activate: - if not self.action_view:\ - self.action_view = app.root.main_screen.ids.tabs.ids.screen_dashboard.action_view - BoxLayout: - orientation: 'vertical' - spacing: '1dp' - ContactTextInput: - ScrollView: - canvas.before: - Color: - rgba: .8901, .8901, .8901, 1 - Rectangle: - size: self.size - pos: self.pos - GridLayout: - cols: 1 - id: contact_container - size_hint_y: None - height: self.minimum_height - spacing: '1dp' - - - foreground_color: (.466, .466, .466, 1) - color_active: (0.235, .588, .89, 1) - WalletActionPrevious: - id: action_previous - width: but_star.width - ActionButton: - id: action_logo - important: True - size_hint: 1, 1 - markup: True - mipmap: True - bold: True - markup: True - color: 1, 1, 1, 1 - text: - "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ - .format(app.base_unit, app.status) - font_size: '22dp' - minimum_width: '1dp' - Butt_star: - id: but_star - on_release: - if self.state == 'down':\ - app.show_info_bubble(\ - text='[b]Expert mode on[/b]\n you can now select your address',\ - icon='atlas://gui/kivy/theming/light/star_big_inactive',\ - duration=1, arrow_pos='', width='250dp') - - - - padding: '5dp' - size_hint: 1, None - height: '27dp' - pos_hint: {'center_y':.5} - multiline: False - hint_text_color: self.foreground_color - foreground_color: .843, .914, .972, 1 - background_color: 1, 1, 1, 1 - background_normal: 'atlas://gui/kivy/theming/light/tab_btn' - background_active: 'atlas://gui/kivy/theming/light/textinput_active' - - - - return_obj: None - min_fee: app.format_amount(app.wallet.fee) - title: - '[size=9dp] \n[/size]Transaction Fee[size=9dp]\n'\ - '[color=#ADAEAE]Minimum is BTC {}[/color][/size]'.format(self.min_fee) - title_size: '24sp' - on_activate: - ti_fee.focus = True - if self.return_obj:\ - ti_fee.text = "BTC " + self.return_obj.amt - on_deactivate: ti_fee.focus = False - on_release: - if self.return_obj and ti_fee.text:\ - txt = ti_fee.text;\ - spc = txt.rfind(' ') + 1;\ - txt = '' if spc == 0 else txt[spc:];\ - num = 0 if not txt else float(txt);\ - self.return_obj.amt = max(self.min_fee, txt) - root.dismiss() - ELTextInput - id: ti_fee - size_hint: 1, None - height: '34dp' - multiline: False - on_text_validate: root.dispatch('on_release', self) - pos_hint: {'center_y': .7} - text: "BTC " + root.min_fee - input_type: 'number' - - - - mode: 'address' - name: 'send' - action_view: Factory.SendActionView() - on_deactivate: - self.ids.amount_e.focus = False - self.ids.payto_e.focus = False - self.ids.message_e.focus = False - BoxLayout - padding: '12dp', '12dp', '12dp', '12dp' - spacing: '12dp' - orientation: 'vertical' - mode: 'address' - SendReceiveToggle: - SendToggle: - id: toggle_address - text: 'ADDRESS' - group: 'send_type' - state: 'down' if root.mode == 'address' else 'normal' - source: 'atlas://gui/kivy/theming/light/globe' - background_down: 'atlas://gui/kivy/theming/light/btn_send_address' - on_release: - if root.mode == 'address': root.mode = 'fc' - root.mode = 'address' - SendToggle: - id: toggle_nfc - text: 'NFC' - group: 'send_type' - state: 'down' if root.mode == 'nfc' else 'normal' - source: 'atlas://gui/kivy/theming/light/nfc' - background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' - on_release: - if root.mode == 'nfc': root.mode = 'str' - root.mode = 'nfc' - GridLayout: - id: grid - cols: 1 - size_hint: 1, None - height: self.minimum_height - SendReceiveCardTop - id: card_address - BoxLayout - size_hint: 1, None - height: '42dp' - rows: 1 - Label - id: lbl_symbl - bold: True - color: amount_e.foreground_color - text_size: self.size - valign: 'bottom' - halign: 'left' - font_size: '22sp' - text: - u'[font={fnt}]{smbl}[/font]'.\ - format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) - size_hint_x: .25 - ELTextInput: - id: amount_e - input_type: 'number' - multiline: False - bold: True - font_size: '50sp' - foreground_color: .308, .308, .308, 1 - background_normal: 'atlas://gui/kivy/theming/light/tab_btn' - pos_hint: {'top': 1.5} - size_hint: .7, None - height: '67dp' - hint_text: 'Amount' - text: '0.0' - on_text_validate: payto_e.focus = True - CardSeparator - BoxLayout: - size_hint: 1, None - height: '42dp' - spacing: '5dp' - Label: - id: fee_e - color: .761, .761, .761, 1 - font_size: '12dp' - amt: app.format_amount(app.wallet.fee_per_kb(app.gui_object.config)) if app.wallet else 0 - text: - u'[b]{sign}{symbl}{amt}[/b] of fee'.\ - format(symbl=lbl_symbl.text,\ - sign='+' if self.amt > 0 else '-', amt=self.amt) - size_hint_x: None - width: self.texture_size[0] - halign: 'left' - valign: 'middle' - IconButton: - color: 0.694, 0.694, 0.694, 1 - source: 'atlas://gui/kivy/theming/light/gear' - pos_hint: {'center_y': .5} - size_hint: None, None - size: '22dp', '22dp' - on_release: - dlg = Cache.get('electrum_widgets', 'TransactionFeeDialog') - - if not dlg:\ - Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ - dlg = Factory.TransactionFeeDialog();\ - Cache.append('electrum_widgets', 'TransactionDialog', dlg) - - dlg.return_obj = fee_e - dlg.open() - Label: - font_size: '12dp' - color: fee_e.color - text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' - text_size: self.size - halign: 'right' - valign: 'middle' - SendReceiveBlueBottom: - id: blue_bottom - size_hint: 1, None - height: self.minimum_height - BoxLayout - size_hint: 1, None - height: blue_bottom.item_height - spacing: '5dp' - Image: - source: 'atlas://gui/kivy/theming/light/contact' - size_hint: None, None - size: '22dp', '22dp' - pos_hint: {'center_y': .5} - TextInputSendBlue: - id: payto_e - hint_text: "Enter Contact or adress" - on_text_validate: - Factory.Animation(opacity=1,\ - height=blue_bottom.item_height)\ - .start(message_selection) - message_e.focus = True - Widget: - size_hint: None, None - width: dp(2) - height: qr.height - pos_hint: {'center_y':.5} - canvas.after: - Rectangle: - size: self.size - pos: self.pos - IconButton: - id: qr - source: 'atlas://gui/kivy/theming/light/qrcode' - pos_hint: {'center_y': .5} - size_hint: None, None - size: '22dp', '22dp' - on_release: app.scan_qr(on_complete=root.set_qr_data) - CardSeparator - opacity: message_selection.opacity - color: blue_bottom.foreground_color - BoxLayout: - id: message_selection - opacity: 1 if app.expert_mode else 0 - size_hint: 1, None - height: blue_bottom.item_height if app.expert_mode else 0 - spacing: '5dp' - Image: - source: 'atlas://gui/kivy/theming/light/pen' - size_hint: None, None - size: '22dp', '22dp' - pos_hint: {'center_y': .5} - TextInputSendBlue: - id: message_e - hint_text: 'Enter description here' - on_text_validate: - anim = Factory.Animation(opacity=1, height=blue_bottom.item_height) - anim.start(wallet_selection) - #anim.start(address_selection) - CardSeparator - opacity: wallet_selection.opacity - color: blue_bottom.foreground_color - WalletSelector: - id: wallet_selection - foreground_color: blue_bottom.foreground_color - opacity: 1 if app.expert_mode else 0 - size_hint: 1, None - height: blue_bottom.item_height if app.expert_mode else 0 - CardSeparator - opacity: address_selection.opacity - color: blue_bottom.foreground_color - AddressSelector: - id: address_selection - foreground_color: blue_bottom.foreground_color - opacity: 1 if app.expert_mode else 0 - size_hint: 1, None - height: blue_bottom.item_height if app.expert_mode else 0 - CreateAccountButtonGreen: - background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) - text: _('Send') - size_hint_y: None - height: '38dp' - disabled: False - on_release: app.do_send() - Widget - - - - source: '' - group: 'transfer_type' - markup: False - bold: True - border: 4, 4, 4, 4 - background_normal: self.background_down - color: - (.140, .140, .140, 1) if self.state == 'down' else (.796, .796, .796, 1) - canvas.after: - Color: - rgba: 1, 1, 1, 1 - Image: - source: root.source - color: root.color - size: '30dp', '30dp' - center_x: root.center_x - ((root.texture_size[0]/2)+(self.width/1.5)) - center_y: root.center_y - - - foreground_color: 1, 1, 1, 1 - spacing: '9dp' - text: '' - values: ('', ) - icon: '' - Image: - source: root.icon - size_hint: None, None - size: '22dp', '22dp' - pos_hint: {'center_y': .5} - OppositeSpinner: - color: root.foreground_color - background_normal: 'atlas://gui/kivy/theming/light/action_group_light' - markup: False - shorten: True - font_size: '16dp' - size_hint: 1, .7 - pos_hint: {'center_y': .5} - text: root.text - text_size: self.size - halign: 'left' - valign: 'middle' - on_text: - root.text = args[1] - values: root.values - - - icon: 'atlas://gui/kivy/theming/light/globe' - values: app.wallet.addresses() if app.wallet else [] - text: _("Select Your address") - - - icon: 'atlas://gui/kivy/theming/light/wallet' - values: ('default Wallet',) - text: _('Select your wallet') - - - padding: '5dp', '5dp' - size_hint: 1, None - height: '45dp' - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - border: 12, 12, 12, 12 - source: 'atlas://gui/kivy/theming/light/card' - size: self.width + dp(3), self.height - pos: self.x - dp(1.5), self.y - - - canvas.before: - BorderImage: - border: 9, 9, 9, 9 - source: 'atlas://gui/kivy/theming/light/card_top' - size: self.size - pos:self.pos - padding: '12dp', '22dp', '12dp', 0 - cols: 1 - size_hint: 1, None - height: '120dp' - spacing: '4dp' - - - canvas.before: - Color: - rgba: .238, .585, .878, 1 - BorderImage: - border: 9, 9, 9, 9 - source: 'atlas://gui/kivy/theming/light/card_bottom' - size: self.size - pos: self.pos - Color: - rgba: 1, 1, 1, 1 - - item_height: dp(42) - foreground_color: .843, .914, .972, 1 - cols: 1 - padding: '12dp', 0 - - - - mode: 'qr' - name: 'receive' - on_mode: if args[1] == 'nfc': from electrum_gui.kivy.nfc_scanner import NFCScanner - action_view: Factory.ReceiveActionView() - on_activate: - self.ids.toggle_qr.state = 'down' - first_address = app.wallet.addresses()[0] - qr.data = app.encode_uri(first_address, - amount=amount_e.text, - label=app.wallet.labels.get(first_address, first_address), - message='') if app.wallet and app.wallet.addresses() else '' - on_deactivate: - self.ids.amount_e.focus = False - BoxLayout - padding: '12dp', '12dp', '12dp', '12dp' - spacing: '12dp' - mode: 'qr' - orientation: 'vertical' - SendReceiveToggle - SendToggle: - id: toggle_qr - text: 'QR' - state: 'down' if root.mode == 'qr' else 'normal' - source: 'atlas://gui/kivy/theming/light/qrcode' - background_down: 'atlas://gui/kivy/theming/light/btn_send_address' - on_release: - if root.mode == 'qr': root.mode = 'nr' - root.mode = 'qr' - SendToggle: - id: toggle_nfc - text: 'NFC' - state: 'down' if root.mode == 'nfc' else 'normal' - source: 'atlas://gui/kivy/theming/light/nfc' - background_down: 'atlas://gui/kivy/theming/light/btn_send_nfc' - on_release: - if root.mode == 'nfc': root.mode = 'nr' - root.mode = 'nfc' - GridLayout: - id: grid - cols: 1 - #size_hint: 1, None - #height: self.minimum_height - SendReceiveCardTop - height: '110dp' - BoxLayout: - size_hint: 1, None - height: '42dp' - rows: 1 - Label: - color: amount_e.foreground_color - bold: True - text_size: self.size - valign: 'bottom' - font_size: '22sp' - text: - u'[font={fnt}]{smbl}[/font]'.\ - format(smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light) - size_hint_x: .25 - ELTextInput: - id: amount_e - input_type: 'number' - multiline: False - bold: True - font_size: '50sp' - foreground_color: .308, .308, .308, 1 - background_normal: 'atlas://gui/kivy/theming/light/tab_btn' - pos_hint: {'top': 1.5} - size_hint: .7, None - height: '67dp' - hint_text: 'Amount' - text: '0.0' - CardSeparator - BoxLayout: - size_hint: 1, None - height: '32dp' - spacing: '5dp' - Label: - color: lbl_quote.color - font_size: '12dp' - text: 'Ask to scan the QR below' - text_size: self.size - halign: 'left' - valign: 'middle' - Label: - id: lbl_quote - font_size: '12dp' - size_hint: .5, 1 - color: .761, .761, .761, 1 - text: u'= {}'.format(app.create_quote_text(Decimal(float(amount_e.text)), mode='symbol')) if amount_e.text else u'0' - text_size: self.size - halign: 'right' - valign: 'middle' - SendReceiveBlueBottom - id: blue_bottom - padding: '12dp', 0, '12dp', '12dp' - WalletSelector: - id: wallet_selection - foreground_color: blue_bottom.foreground_color - size_hint: 1, None - height: blue_bottom.item_height - CardSeparator - opacity: wallet_selection.opacity - color: blue_bottom.foreground_color - AddressSelector: - id: address_selection - foreground_color: blue_bottom.foreground_color - opacity: 1 if app.expert_mode else 0 - size_hint: 1, None - height: blue_bottom.item_height if app.expert_mode else 0 - on_text: - if not args[1].startswith('Select'):\ - qr.data = app.encode_uri(args[1],\ - amount=amount_e.text,\ - label=app.wallet.labels.get(args[1], args[1]),\ - message='') - CardSeparator - opacity: address_selection.opacity - color: blue_bottom.foreground_color - Widget: - size_hint_y: None - height: dp(10) - FloatLayout - id: bl - QRCodeWidget: - id: qr - size_hint: None, 1 - width: min(self.height, bl.width) - pos_hint: {'center': (.5, .5)} - on_touch_down: - if self.collide_point(*args[1].pos):\ - app.show_info_bubble(icon=self.ids.qrimage.texture, text='texture') - CreateAccountButtonGreen: - background_color: (1, 1, 1, 1) if self.disabled else ((.258, .80, .388, 1) if self.state == 'normal' else (.203, .490, .741, 1)) - text: _('Goto next step') if app.wallet and app.wallet.seed else _('Create unsigned transaction') - size_hint_y: None - height: '38dp' - disabled: True if wallet_selection.opacity == 0 else False - on_release: - message = 'sending {} {} to {}'.format(\ - app.base_unit, amount_e.text, payto_e.text) - app.gui.main_gui.do_send(self, message=message) - - - WalletActionPrevious: - id: action_previous - width: '32dp' - ActionButton: - id: action_logo - important: True - size_hint: 1, 1 - markup: True - mipmap: True - bold: True - markup: True - color: 1, 1, 1, 1 - text: - "[color=#777777][sub] [sup][size=9dp]{}[/size][/sup][/sub]{}[/color]"\ - .format(app.base_unit, app.status) - font_size: '22dp' - minimum_width: '1dp' - Butt_star: - id: but_star - on_release: - if self.state == 'down':\ - app.show_info_bubble(\ - text='[b]Expert mode on[/b]\n you can now select your address',\ - icon='atlas://gui/kivy/theming/light/star_big_inactive',\ - duration=1, arrow_pos='', width='250dp') - -############################################### -## Wallet Management -############################################### - - - canvas.before: - Color: - rgba: .145, .145, .145, 1 - Rectangle: - size: root.size - pos: root.pos - VGridLayout: - Wallets: - id: wallets_section - Plugins: - id: plugins_section - Commands: - id: commands_section - - - - - - - Header - - - Header - - - Header - - - padding: 0, 0, 0, 0 - - - carousel: carousel - do_default_tab: False - Carousel: - scroll_timeout: 190 - anim_type: 'out_quart' - min_move: .05 - anim_move_duration: .1 - anim_cancel_duration: .54 - scroll_distance: '10dp' - on_index: root.on_index(*args) - id: carousel - - - content: None - state: 'deactivated' - on_enter: self.state = 'loading' - on_activate: - self.state = 'activated' - on_leave: self.state = 'unloading' - on_deactivate: self.state = 'deactivated' - color: 1, 1, 1, 1 - Image: - source: 'atlas://gui/kivy/theming/light/logo_atom_dull' - size_hint: (None, None) - pos_hint: {'center_x': .5, 'y': .02} - allow_stretch: True - size: ('32dp', '32dp') - #Label: - #id: screen_label - #size: root.size - #font_size: '45sp' - #color: root.color - #text: '' if root.state == 'activated' or root.content else 'Loading...' - - - background_color: .929, .929, .929 ,1 - effects: [self.scrollflex] - - - #auto_width: False - size_hint: None, None - size: self.container.minimum_size if self.container else (0, 0) - on_container: if args[1]: self.container.padding = '4dp', '4dp', '4dp', '4dp' - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - pos:self.pos - border: 20, 20, 20, 20 - source: 'atlas://gui/kivy/theming/light/dropdown_background' - size: self.size - - - font_size: '14sp' - border: 4, 4, 4, 4 - color: 0.439, 0.439, 0.439, .8 - background_normal: 'atlas://gui/kivy/theming/light/action_button_group' - background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' - size_hint_y: None - height: '48dp' - text_size: self.size[0] - dp(20), self.size[1] - halign: 'left' - valign: 'middle' - shorten: True - on_press: - ddn = self.parent.parent - Factory.Animation(opacity=0, d=.25).start(ddn) - - - - dropdown_cls: Factory.OppositeDropDown - option_cls: Factory.LightOptions - border: 20, 20, 9, 9 - background_normal: 'atlas://gui/kivy/theming/light/action_group_dark' - background_down: self.background_normal - values: ('Copy to clipboard', 'Send Payment') - size_hint: None, 1 - width: '12dp' - on_release: - ddn = self._dropdown - ddn.opacity = 0 - Factory.Animation(opacity=1, d=.25).start(ddn) - - - title: _('[size=7dp] \n[/size] Add a Contact[size=7dp]\n[/size]') - title_size: '24sp' - border: 7, 7, 7, 7 - size_hint: None, None - size: '320dp', '235dp' - pos_hint: {'center_y': .53} - separator_color: .89, .89, .89, 1 - separator_height: '1.2dp' - title_color: .437, .437, .437, 1 - background: 'atlas://gui/kivy/theming/light/dialog' - padding: 0, 0 - on_parent: - self.content.padding = 0, 0, 0, 0 - self.content.parent.parent.padding = 2, 12, 2, 2 - self.content.parent.parent.spacing = 0, -2 - BoxLayout: - id: bl - spacing: '1.2dp' - canvas: - Color: - rgba: .901, .901, .901, 1 - Rectangle: - size: self.size - pos: self.pos - ContactButton: - source: 'atlas://gui/kivy/theming/light/qrcode' - text: _('QR Scan') - on_release: root.load_qr_scanner() - ContactButton: - id: but_contact - source: 'atlas://gui/kivy/theming/light/manualentry' - text: 'Manual Enrty' - on_release: - root.new_contact() - FloatLayout: - size_hint: None, None - size: 0, 0 - CloseButton: - id: but_close - top: root.top - dp(5) - right: root.right - dp(5) - on_release: root.dismiss() - - - - border: 12, 12, 12, 12 - background_normal: 'atlas://gui/kivy/theming/light/dialog' - background_down: '' - background_color: (.94, .94, .94, 1) if self.state == 'normal' else (.191, .496, .742, 1) - - - - source: '' - color: 0, 0, 0, 0 - item_color: (.211, .211, .211, 1) if root.state == 'normal' else (1, 1, 1, 1) - Image: - id: img - allow_stretch: True - color: root.item_color - mipmap: True - source: root.source - center: (root.center, self.size)[0] - size: root.width/2., root.height/2. - Label: - text: root.text - color: root.item_color - size: img.width, self.texture_size[1] - center: root.center_x, img.y - (self.height/2) - - - - border: 0, 0, 0, 0 - text: repr(self) - color: 0, 0, 0, 0 - background_normal: 'atlas://gui/kivy/theming/light/carousel_selected' - background_down: 'atlas://gui/kivy/theming/light/carousel_deselected' - on_state: - o = .5 if args[1] == 'down' else 1 - anim=Factory.Animation(opacity=o, d=.2).start(self) - - - tab_pos: 'bottom_mid' - tab_height: '32dp' - tab_width: self.tab_height - background_image: 'atlas://data/images/defaulttheme/action_item' - strip_border: 0, 0, 0, 0 - -<-CarouselDialog> - header_color: '#707070ff' - text_color: 0.701, 0.701, 0.701, 1 - title_size: '13sp' - title: '' - separator_color: 0.89, 0.89, 0.89, 1 - background: 'atlas://gui/kivy/theming/light/tab_btn' - carousel_content: carousel_content - canvas.before: - Color: - rgba: 0, 0, 0, .9 - Rectangle: - size: Window.size - pos: 0, 0 - Color: - rgba: 1, 1, 1, 1 - BorderImage: - border: 12, 12, 12, 12 - source: 'atlas://gui/kivy/theming/light/dialog' - size: root.width, root.height - self.carousel_content.tab_height if self.carousel_content else 0 - pos: root.x, self.y + self.carousel_content.tab_height if self.carousel_content else 10 - BoxLayout: - orientation: 'vertical' - GridLayout: - cols: 1 - size_hint: 1, None - height: self.minimum_height - padding: 0, '7sp' - Label: - font_size: root.title_size - text: u'[color={}]{}[/color]'.format(root.header_color, root.title) - text_size: self.width, None - halign: 'left' - size_hint: 1, None - height: self.texture_size[1] - CardSeparator: - color: root.separator_color - height: root.separator_height - FloatLayout: - size_hint: None, None - size: 0, 0 - CloseButton: - id: but_close - top: root.top - dp(10) - right: root.right - dp(10) - on_release: root.dismiss() - CarouselIndicator: - id: carousel_content - - - wallet_name: 'Default Wallet' - on_parent: if args[1]: self.children[0].padding = '15dp', '6dp', '15dp', 0 - title_color: .437, .437, .437 - title_size: '23sp' - separator_height: '1dp' - size_hint: None, None - width: min(Window.width - dp(27), dp(360)) - height: min(Window.height - dp(70), self.width * 1.5) - title: '[size=5sp] \n[/size]{}[size=9sp]\n'.format(self.wallet_name) - - - name: 'addresses' - BoxLayout: - size_hint: 1, .95 - pos_hint: {'top':1} - orientation: 'vertical' - padding: 0, '2.2dp' - Label: - markup: False - color: 0, 0, 0, .3 - text: 'Your bitcoin address:' - font_size: '15sp' - text_size: self.width, None - halign: 'left' - size_hint: 1, None - height: self.texture_size[1] - shorten: True - BoxLayout: - orientation: 'vertical' - spacing: dp(5) - BoxLayout: - size_hint: 1, None - spacing: dp(5) - height: '27dp' - OppositeSpinner: - id: btn_address - markup: False - color: 0.439, 0.439, 0.439, 1 - font_size: '14sp' - text_size: self.width - dp(22), self.height - shorten: True - halign: 'left' - valign: 'middle' - size_hint: .8, .8 - pos_hint: {'center_y': .5} - on_text: - if args[1]: qr.data = app.encode_uri(root.labels[args[1]]) - FloatLayout - id: fl_paste - size_hint: None, 1 - width: '37dp' - Button: - right: fl_paste.right - y: fl_paste.y - size_hint: None, None - height: '37dp' - width: self.height - border: 2, 2, 2, 2 - background_color: .705, .705, .705, 1.5 if self.state == 'normal' else 2 - background_normal: 'atlas://gui/kivy/theming/light/paste_icon' - background_down: self.background_normal - on_release: - app.copy(root.labels[btn_address.text]) - app.show_info_bubble(\ - text='Copied', width='50dp', arrow_pos='', duration=3,\ - pos=(Window.width/2, root.parent.top + dp(18))) - QRCodeWidget: - id: qr - show_border: False - - - color: .698, .701, .701, 1 - font_size: '14sp' - text_size: self.width, None - halign: 'left' - - - size_hint_x: None - width: '60dp' - text_size: self.width, None - font_size: '18sp' - halign: 'left' - color: .698, .701, .701, 1 - - - border: 7, 7, 7, 7 - size_hint: None, None - size: '320dp', '200dp' - pos_hint: {'center_y': .53} - separator_color: .89, .89, .89, 1 - separator_height: '1.1dp' - title_color: .437, .437, .437, 1 - background: 'atlas://gui/kivy/theming/light/dialog' - on_parent: self.content.parent.parent.padding = '12dp', 0, '12dp', '12dp' - RelativeLayout: - orientation: 'vertical' - padding: 0, '6dp', 0, 0 - RelativeLayout: - id: container - BoxLayout - size_hint_y: None - height: '48dp' - pos_hint: {'y':0} - DialogButton: - id: btn_cancel - text: _('Cancel') - markup: False - color: .439, .439, .439, 1 - background_color: - (.235, .588, .882, 1) if self.state[0] == 'd'\ - else (.890, .890, .890, 1) - on_release: - root.dismiss() - Widget: - size_hint_x: None - width: '24dp' - DialogButton: - id: btn_ok - text: _('Ok') - background_color: - (.235, .588, .882, 1) if self.state[0] == 'n'\ - else (.890, .890, .890, 1) - on_release: - root.dispatch('on_release', self) - FloatLayout: - size_hint: None, None - size: 0, 0 - CloseButton: - id: but_close - top: root.height - dp(18) - right: root.width - dp(18) - on_release: root.dismiss() - - - - title: '[size=9dp] \n[/size]Currency Selection[size=7dp]\n[/size]' - title_size: '24sp' - on_activate: - spinner_exchanges.text = app.exchanger.use_exchange - spinner_currencies.text = app.exchanger.currency - on_release: - app.exchanger.currency = spinner_currencies.text - app.exchanger.use_exchange = spinner_exchanges.text - root.dismiss() - BoxLayout - size_hint_y: None - height: '24dp' - spacing: '24dp' - pos_hint: {'top': .95, 'x': 0} - CurrencyLabel: - text: _('Currency') - size_hint_x: .4 - CurrencyLabel: - id: lbl_source - text: _('Exchange Source') - BoxLayout: - spacing: '24dp' - size_hint_y: None - height: '32dp' - pos_hint: {'x': 0, 'top': .8} - CurrencySpinner: - id: spinner_currencies - text: app.exchanger.currency - values: app.currencies - Widget: - CurrencySpinner: - id: spinner_exchanges - text: app.exchanger.use_exchange - pos: lbl_source.x, spinner_currencies.y - width: '140dp' - height: spinner_currencies.height - values: app.exchanger.exchanges - - - on_parent: - if args[1]:\ - self.children[0].padding = '15dp', '6dp', '15dp', 0 - is_mine: True - amount: '0.00' - amount_color: '#000000ff' - quote_text: '0' - address: u'' - address_known: unicode(self.address or self.address[1:]) in app.contacts.keys() + self.labels.keys() - confirmations: 0 - date: '0/0/0' - fee: _('unknown') - labels: {} - status: 'unknown' - separator_height: '1dp' - time: _('00:00') - tx_hash: None - title: - u'[size=9] \n[/size]'\ - u'[size={sz}sp][color={clr}]'.format(sz=25.7 if self.width > dp(300) else 22, clr=root.header_color) + \ - _(u'You ') + (_(u'sent') if self.is_mine else _(u'received')) + u'[/color]'\ - '[color={clr}] [font={fnt}]{smbl}[/font]{amt}[/color][/size]\n'.format(\ - clr=root.amount_color, smbl=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol, fnt=font_light, amt=root.amount[:9]) + \ - _(u'About ') + root.quote_text + _(u' at transaction time') + \ - u'[size={}dp] \n[/size]'.format(5 if self.width > dp(300) else 1) - size_hint: None, None - width: min(Window.width - dp(27), dp(320)) - height: max(grid.height + dp(120), dp(320)) - font_size: '13sp' - CScreen: - tab: screen_details - GridLayout - id: grid - padding: 0, 0, 0, '12dp' - spacing: '18dp' - cols: 1 - size_hint: 1, None - height: self.minimum_height - pos_hint: {'top':1} - GridLayout: - cols: 1 - size_hint_y: None - height: '39sp' - spacing: '5dp' - CardLabel: - color: root.text_color - font_size: root.font_size - text: _('To') if root.is_mine else _('From') - BoxLayout: - id: bl_address - spacing: '4dp' - size_hint_y: None - height: '32dp' - padding: 0, 0, 0, '9dp' - OppositeSpinner: - id: btn_address - markup: False - color: 0.45, 0.45, 0.45, 1 - font_name: font_light - font_size: '15sp' - text: root.address - shorten: True - size_hint: None, 1 - text_size: self.width - dp(30), self.height - halign: 'left' - width: min(self._label.get_extents(self.text)[0] + dp(32), bl_address.width) - on_release: - self._dropdown.auto_width = False - self._dropdown.width = max(self.width, dp(140)) - on_text: - if args[1] != root.address[1:]:\ - root.dropdown_selected(args[1]);\ - self.text = root.address - GridLayout: - cols: 2 - spacing: '18dp' - size_hint: 1, None - height: self.minimum_height - CardLabel: - color: root.text_color - font_size: root.font_size - text: - _('Date') + '[color={}][size=4dp]\n\n[/size]'\ - '[size=18dp]{}[/size][/color]'.format(\ - root.header_color, root.date) - CardLabel: - color: root.text_color - font_size: root.font_size - text: - _('Status') +\ - '[size=4dp]\n\n[/size]'\ - '[color={}][size=18dp]{}[/size][/color]'.format(\ - '#009900' if root.status == 'Validated' else root.header_color,\ - root.status) - CardLabel: - color: root.text_color - font_size: root.font_size - text: - _('Time') +\ - '[size=4dp]\n\n[/size]'\ - '[color={}][size=18dp]{}[/size][/color]'.format(\ - root.header_color, root.time) - CardLabel: - color: root.text_color - font_size: root.font_size - text: - _('Confirmations') +\ - '[size=4dp]\n\n[/size]'\ - '[color={}][size=18dp]{}'\ - '[/size][/color]'.format(root.header_color, root.confirmations) - GridLayout: - id: fl - size_hint: 1, None - height: self.minimum_height - cols: 1 - BoxLayout: - size_hint: 1, None - height: '48sp' if self.opacity else 0 - opacity: 1 if root.is_mine else 0 - CardLabel - size_hint: 1, 1 - text_size: self.size - valign: 'top' - font_size: root.font_size - color: root.text_color - markup: True - text: - _('Transaction Fees') +\ - '[size=4dp]\n\n[/size]'\ - '[color={}][size=18dp]{}'\ - '[/size][/color]'.format(root.header_color, root.fee) - CardLabel: - size_hint: 1, 1 - text_size: self.size - valign: 'top' - font_size: root.font_size - color: root.text_color - text: _('Wallet') - markup: True - DialogButton: - id: btn_dialog - size_hint: 1, None - height: '48sp' if self.opacity else 0 - opacity: 0 if root.is_mine or root.address_known else 1 - background_color: - (.191, .496, .742, 1) if self.state == 'normal'\ - else (.933, .933, .933, 1) - color: - (1, 1, 1, 1) if self.state == 'normal'\ - else (.218, .218, .218, 1) - disabled: False if self.opacity else True - text: _('Add address to contacts') - on_release: - root.dismiss() - app.save_new_contact(root.address[1:], '') - CScreen: - tab: transaction_id - color: .5, .5, .5, .5 - on_activate: root.activate_screen_transactionid(self) - CScreen: - id: list_inputs - color: .5, .5, .5, .5 - data: '' - tab: inputs - on_enter: root.activate_screen_inputs(self) - CScreen: - id: list_outputs - color: .5, .5, .5, .5 - data: '' - tab: outputs - on_enter: root.activate_screen_outputs(self) - - # define the tab headers here - CarouselHeader: - id: screen_details - slide: 0 - CarouselHeader: - id: transaction_id - slide: 1 - CarouselHeader: - id: inputs - slide: 2 - CarouselHeader: - id: outputs - slide: 3 - - - pos_hint:{'top': 1} - size_hint_y: .9 - headers: [_('Address'), _('Amount')] - widths:[root.width*.63, root.width*.36] - - - pos_hint:{'top': 1} - size_hint_y: .9 - headers: [_('Address'), _('Previous output')] - widths:[root.width*.63, root.width*.36] - - - cols: 1 - padding: '5dp' - spacing: '2dp' - text_color: 1, 1, 1 - tx_hash: None - carousel_content: None - CardLabel: - color: root.text_color - font_size: '13dp' - text: _('Transaction ID :') - ELTextInput: - readonly: True - text: root.tx_hash if root.tx_hash else '' - size_hint_y: None - height: '30dp' - BoxLayout: - padding: 0, '9pt', 0, 0 - orientation: 'vertical' - spacing: '5dp' - DialogButton: - background_color: - (.191, .496, .742, 1) if self.state == 'normal'\ - else (.933, .933, .933, 1) - color: - (1, 1, 1, 1) if self.state == 'normal'\ - else (.218, .218, .218, 1) - text: 'Inputs >>' - size_hint_y: None - height: '38dp' - on_release: - root.carousel_content.carousel.load_next() - DialogButton: - text: 'Outputs >>>' - background_color: - (.191, .496, .742, 1) if self.state == 'normal'\ - else (.933, .933, .933, 1) - color: - (1, 1, 1, 1) if self.state == 'normal'\ - else (.218, .218, .218, 1) - size_hint_y: None - height: '38dp' - on_release: - carousel = root.carousel_content.carousel - carousel.load_slide(carousel.slides[3]) - Widget - - -################################ -## Cards (under Dashboard) -################################ - - - cols: 1 - padding: '12dp' , '22dp', '12dp' , '12dp' - spacing: '12dp' - size_hint: 1, None - height: max(100, self.minimum_height) - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - border: 18, 18, 18, 18 - source: 'atlas://gui/kivy/theming/light/card' - size: self.size - pos: self.pos - - - color: 0.45, 0.45, 0.45, 1 - size_hint: 1, None - text: '' - text_size: self.width, None - height: self.texture_size[1] - halign: 'left' - valign: 'top' - - - background_normal: 'atlas://gui/kivy/theming/light/card_btn' - bold: True - font_size: '10sp' - color: 0.699, 0.699, 0.699, 1 - size_hint: None, None - size: self.texture_size[0] + dp(32), self.texture_size[1] + dp(7) - - - size_hint: 1, None - height: dp(1) - color: .909, .909, .909, 1 - canvas: - Color: - rgba: root.color if root.color else (0, 0, 0, 0) - Rectangle: - size: self.size - pos: self.pos - - - canvas.before: - Color: - rgba: 0.192, .498, 0.745, 1 if self.state == 'down' else 0 - Rectangle - size: self.size - pos: self.x, self.y + dp(5) - cols: 1 - padding: '2dp', '2dp' - spacing: '2dp' - size_hint: 1, None - height: self.minimum_height - - - icon: 'atlas://gui/kivy/theming/light/important' - address:'no address set' - amount: '+0.00' - balance: 'xyz'# balance_after - amount_color: '#DB3627' if float(self.amount) < 0 else '#2EA442' - confirmations: 0 - date: '0/0/0' - quote_text: '.' - - spacing: '9dp' - on_release: - dash = app.root.main_screen.ids.tabs.ids.screen_dashboard - dash.show_tx_details(root) - BoxLayout: - size_hint: 1, None - spacing: '8dp' - height: '32dp' - Image: - id: icon - source: root.icon - size_hint: None, 1 - width: self.height *.54 - mipmap: True - BoxLayout: - orientation: 'vertical' - Widget - CardLabel: - shorten: True - text: root.address - markup: False - text_size: self.size - CardLabel: - color: .699, .699, .699, 1 - text: root.date - font_size: '12sp' - Widget - CardLabel: - halign: 'right' - font_size: '13sp' - size_hint: None, 1 - width: '90sp' - markup: True - font_name: font_light - text: - u'[color={amount_color}]{sign}{symbol}{amount}[/color]\n'\ - u'[color=#B2B3B3][size=12sp]{qt}[/size]'\ - u'[/color]'.format(amount_color=root.amount_color,\ - amount=root.amount[1:], qt=root.quote_text, sign=root.amount[0],\ - symbol=btc_symbol if app.base_unit == 'BTC' else mbtc_symbol) - CardSeparator - - - BoxLayout: - size_hint: 1, None - height: lbl.height - CardLabel: - id: lbl - text: _('RECENT ACTIVITY') - CardButton: - id: btn_see_all - disabled: True if not self.opacity else False - text: _('SEE ALL') - font_size: '12sp' - on_release: app.update_history_tab(see_all=True) - GridLayout: - id: content - spacing: '7dp' - cols: 1 - size_hint: 1, None - height: self.minimum_height - CardSeparator - - - CardLabel: - text: _('PAYMENT REQUEST') - CardSeparator: - - - padding: '12dp' , '12dp' - status: app.status - quote_text: '' - unconfirmed: '' - cols: 2 - FloatLayout - anchor_x: 'left' - size_hint: 1, None - height: '82dp' - IconButton: - mipmap: True - pos_hint: {'x': 0, 'center_y': .45} - color: .90, .90, .90, 1 - source: 'atlas://gui/kivy/theming/light/qrcode' - size_hint: None, .85 - width: self.height - on_release: - dlg = Cache.get('electrum_widgets', 'WalletAddressesDialog') - - if not dlg:\ - Factory.register('WalletAddressesDialog', module='electrum_gui.kivy.uix.dialogs.carousel_dialog');\ - dlg = Factory.WalletAddressesDialog();\ - Cache.append('electrum_widgets', 'WalletAddressesDialog', dlg) - - dlg.open() - CardLabel: - id: top_label - halign: 'right' - valign: 'top' - bold: True - pos_hint: {'top': 1, 'right': 1} - font_name: font_light - balance_in_numbers: bool(ord(root.status[0]) not in range(ord('A'), ord('z'))) - font_size: '50sp' if self.balance_in_numbers else '30sp' - text_size: self.width, root.height/2 - text: - u'[color=#4E4F4F]{}{}[/color]'\ - .format('' if not self.balance_in_numbers else\ - (btc_symbol if app.base_unit == 'BTC' else mbtc_symbol), root.status) - BoxLayout - pos_hint: {'y': 0, 'right': 1} - spacing: '5dp' - CardLabel - halign: 'right' - markup: True - font_size: '22dp' - font_name: font_light - text: u'[color=#c3c3c3]{}[/color]'.format(root.quote_text) - IconButton - color: .698, .698, .698, 1 - source: 'atlas://gui/kivy/theming/light/gear' - size_hint_y: None - height: '28dp' - opacity: .5 if self.state == 'down' else 1 - on_release: - dlg = Cache.get('electrum_widgets', 'CurrencySelectionDialog') - - if not dlg:\ - Factory.register('SelectionDialog', module='electrum_gui.kivy.uix.dialogs');\ - dlg = Factory.CurrencySelectionDialog();\ - Cache.append('electrum_widgets', 'CurrencySelectionDialog', dlg) - - dlg.open() - - - auto_width: False - on_size: if not self.auto_width: self.width = dp(190) - on_container: self.container.padding = '4dp', '4dp', '4dp', '4dp' - on_parent: - if args[1]:\ - self.opacity = 0;\ - anim = Factory.Animation(opacity=1, d=.25);\ - anim.start(self) - canvas.before: - Color: - rgba: 1, 1, 1, 1 - BorderImage: - pos:self.pos - border: 20, 20, 20, 20 - source: 'atlas://gui/kivy/theming/light/overflow_background' - size: self.size - - - color: 0.235, 0.239, 0.239, 1 - -: - border: 4, 0, 0, 0 - background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' - - - text_size: dp(172), None - last: False - halign: 'left' - valign: 'middle' - overflow: None - background_normal: - 'atlas://gui/kivy/theming/light/' +\ - ('action_button_group'\ - if (self.inside_group and not self.last) else 'tab_btn') - on_press: - ddn = self.overflow._dropdown - Factory.Animation.cancel_all(ddn) - anim = Factory.Animation(opacity=0, d=.25) - anim.bind(on_complete=ddn.dismiss) - anim.start(ddn) - - - WalletActionPrevious: - id: action_previous - size_hint_x: None - width: action_preferences.width + action_contact.width - ActionButton: - id: action_logo - important: True - pos_hint: {'center_y': .5} - size_hint: 1, .93 - bold: True - icon: 'atlas://gui/kivy/theming/light/logo' - background_down: self.background_normal - minimum_width: '1dp' - ActionButton: - id: action_contact - important: True - width: '25dp' - icon: 'atlas://gui/kivy/theming/light/add_contact' - text: 'Add Contact' - on_release: - Factory.register('NewContactDialog', module='electrum_gui.kivy.uix.dialogs.new_contact') - dlg = Factory.NewContactDialog().open() - - ActionOverflow: - id: action_preferences - background_down: 'atlas://gui/kivy/theming/light/overflow_btn_dn' - border: 0, 0, 0, 0 - overflow_image: 'atlas://gui/kivy/theming/light/settings' - width: '40dp' - size_hint_x: None - canvas.after: - Color: - rgba: 1, 1, 1, 1 - OverflowButton: - text: _('Settings') - overflow: action_preferences - OverflowButton: - text: _('Help') - last: True - overflow: action_preferences - - - name: 'dashboard' - action_view: Factory.DashboardActionView() - content: content - ScrollView: - id: content - do_scroll_x: False - GridLayout - id: grid - cols: 1 #if root.width < root.height else 2 - size_hint: 1, None - height: self.minimum_height - padding: '12dp' - spacing: '12dp' - GridLayout: - cols: 1 - size_hint: 1, None - height: self.minimum_height - spacing: '12dp' - orientation: 'vertical' - CardStatusInfo: - id: status_card - CardPaymentRequest: - id: payment_card - CardRecentActivity: - id: recent_activity_card - - - - border: 0, 0, 4, 0 - markup: False - color: (.188, 0.505, 0.854, 1) if self.state == 'down' else (0.636, 0.636, 0.636, 1) - text_size: self.size - halign: 'center' - valign: 'middle' - bold: True - font_size: '12.5sp' - background_normal: 'atlas://gui/kivy/theming/light/tab_btn' - background_disabled_normal: 'atlas://gui/kivy/theming/light/tab_btn_disabled' - background_down: 'atlas://gui/kivy/theming/light/tab_btn_pressed' - canvas.before: - Color: - rgba: 1, 1, 1, .7 - Rectangle: - size: self.size - pos: self.x + 1, self.y - 1 - texture: self.texture - - - TabbedCarousel: - id: panel - tab_height: '48dp' - background_image: 'atlas://gui/kivy/theming/light/tab' - strip_image: 'atlas://gui/kivy/theming/light/tab_strip' - strip_border: dp(4), 0, dp(2), 0 - ScreenDashboard: - id: screen_dashboard - tab: tab_dashboard - ScreenSend: - id: screen_send - tab: tab_send - ScreenReceive: - id: screen_receive - tab: tab_receive - ScreenContacts: - id: screen_contacts - tab: tab_contacts - CleanHeader: - id: tab_dashboard - text: _('DASHBOARD') - slide: 0 - CleanHeader: - id: tab_send - text: _('SEND') - slide: 1 - CleanHeader: - id: tab_receive - text: _('RECEIVE') - slide: 2 - CleanHeader: - id: tab_contacts - text: _('CONTACTS') - slide: 3 - - - name: 'main_screen' - canvas.before: - Color: - rgba: 0.917, 0.917, 0.917, 1 - Rectangle: - size: self.size - pos: self.pos - BoxLayout: - orientation: 'vertical' - ActionBar: - id: action_bar - size_hint: 1, None - height: '48dp' - border: 4, 4, 4, 4 - background_image: 'atlas://gui/kivy/theming/light/action_bar' - ScreenManager: - id: manager - ScreenTabs: - id: tabs - name: "tabs" - #ScreenPassword: - # id: password - # name: 'password - - - #overlay_widget: overlay_widget - RelativeLayout: - id: hidden_widget - size_hint: None, None - width: - (root.width * .877) if app.ui_mode[0] == 'p'\ - else root.width * .35 if app.orientation[0] == 'l'\ - else root.width * .10 - height: root.height - canvas.before: - Color: - rgba: .176, .176, .176, 1 - Rectangle: - size: self.size - pos: self.pos - Color - rgba: 1, 1, 1, 1 - BorderImage - border: 0, 32, 0, 0 - source: 'atlas://gui/kivy/theming/light/shadow_right' - size: root.overlay_widget.x if root.overlay_widget else self.width, self.height - RelativeLayout: - id: overlay_widget - size_hint: None, None - x: hidden_widget.width if app.ui_mode[0] == 't' else 0 - size_hint: None, None - width: (root.width - self.x) if app.ui_mode[0] == 't' else root.width - height: root.height - -####################################################### -## This is our child of the root widget of the app -####################################################### -Drawer - size_hint: None, None - size: Window.size - WalletManagement - id: wallet_management - ScreenManager: - # Screen manager for screens meant to change everything including - # ActionBar, currently we only have the main screen here. - id: manager - MainScreen \ No newline at end of file From b5b367940412177cd531ae6a22faa1454eb382a0 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 15:02:57 +0100 Subject: [PATCH 37/90] kivy: recreate context menu everytime an item is selected --- gui/kivy/uix/screens.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index 3b609fc0..a247fda5 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -75,18 +75,16 @@ class CScreen(Factory.Screen): self.hide_menu() def hide_menu(self): - if self.context_menu: + if self.context_menu is not None: self.remove_widget(self.context_menu) self.context_menu = None def show_menu(self, obj): - if self.context_menu is None: - self.context_menu = ContextMenu(obj, self.menu_actions) - self.remove_widget(self.context_menu) + self.hide_menu() + self.context_menu = ContextMenu(obj, self.menu_actions) self.add_widget(self.context_menu) - class HistoryScreen(CScreen): tab = ObjectProperty(None) From 0b1561f4474038e9ca6c29272d78794bba594c57 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 21:32:57 +0100 Subject: [PATCH 38/90] kivy: fix qr code --- gui/kivy/uix/qrcodewidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/kivy/uix/qrcodewidget.py b/gui/kivy/uix/qrcodewidget.py index 9283afec..03e0866d 100644 --- a/gui/kivy/uix/qrcodewidget.py +++ b/gui/kivy/uix/qrcodewidget.py @@ -102,7 +102,7 @@ class QRCodeWidget(FloatLayout): cr, cg, cb = cr*255, cg*255, cb*255 for r in range(k): for c in range(k): - bext([0, 0, 0] if matrix[r][c] else [cr, cg, cb]) + bext([0, 0, 0] if matrix[k-1-r][c] else [cr, cg, cb]) # then blit the buffer buff = ''.join(map(chr, buff)) # update texture From 3568c325eae1aef10d1d2f7d2b1da401951b39b4 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Mon, 14 Dec 2015 21:34:25 +0100 Subject: [PATCH 39/90] kivy: use Clock to improve button responsiveness --- gui/kivy/main.kv | 12 ++++++++++-- gui/kivy/main_window.py | 5 +---- gui/kivy/uix/context_menu.py | 10 ++++++++-- gui/kivy/uix/screens.py | 3 +++ gui/kivy/uix/ui_screens/send.kv | 2 +- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv index d5053808..d66031c5 100644 --- a/gui/kivy/main.kv +++ b/gui/kivy/main.kv @@ -3,6 +3,8 @@ #:import _ electrum.i18n._ # Custom Global Widgets +