electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 1179a4cf9e11a41ada5f8dfc006a92c71da3aa8d
parent 1bbb211671e46702ec913f5bddffbb0665ff6bbc
Author: qua-non <akshayaurora@gmail.com>
Date:   Tue, 11 Mar 2014 00:18:12 +0530

manage exchange plugins, make sure ui doesn't stall while saving seed
and numerous other small fixes.

Diffstat:
Mgui/kivy/dialog.py | 4+++-
Mgui/kivy/drawer.py | 15+++++++++++----
Mgui/kivy/installwizard.py | 28+++++++++++++++++-----------
Mgui/kivy/main.kv | 3++-
Mgui/kivy/main_window.py | 53+++++++++++++++++++++++++++++++++--------------------
Mgui/kivy/plugins/exchange_rate.py | 325+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mlib/util.py | 7+++++++
7 files changed, 316 insertions(+), 119 deletions(-)

diff --git a/gui/kivy/dialog.py b/gui/kivy/dialog.py @@ -192,7 +192,7 @@ class InfoBubble(Bubble): anim.start(self) - def hide(self, *dt): + def hide(self, now=False): ''' Auto fade out the Bubble ''' def on_stop(*l): @@ -205,6 +205,8 @@ class InfoBubble(Bubble): App.get_running_app().stop() import sys sys.exit() + if now: + return on_stop() anim = Animation(opacity=0, d=.25) anim.bind(on_complete=on_stop) diff --git a/gui/kivy/drawer.py b/gui/kivy/drawer.py @@ -82,11 +82,13 @@ class Drawer(StencilView): if app.ui_mode[0] == 't': return super(Drawer, self).on_touch_down(touch) + state = self.state touch.ud['send_touch_down'] = False + start = 0 if state[0] == 'c' else self._hidden_widget.right drag_area = ((self.width * self.drag_area) if self.state[0] == 'c' else - self._hidden_widget.width) - if touch.x > drag_area: + self.width) + if touch.x not in range(int(start), int(drag_area)): return super(Drawer, self).on_touch_down(touch) self._touch = touch Clock.schedule_once(self._change_touch_mode, @@ -106,8 +108,13 @@ class Drawer(StencilView): if not touch.ud.get('in_drag_area', None): return super(Drawer, self).on_touch_move(touch) - self._overlay_widget.x = min(self._hidden_widget.width, - max(self._overlay_widget.x + touch.dx*2, 0)) + ov = self._overlay_widget + ov.x=min(self._hidden_widget.width, + max(ov.x + touch.dx*2, 0)) + #_anim = Animation(x=x, duration=1/60) + #_anim.cancel_all(ov) + #_anim.start(ov) + if abs(touch.x - touch.ox) < self.scroll_distance: return touch.ud['send_touch_down'] = False diff --git a/gui/kivy/installwizard.py b/gui/kivy/installwizard.py @@ -45,10 +45,16 @@ class InstallWizard(Widget): ''' def target(): + # run your threaded function - task() + try: + task() + except Exception as err: + Clock.schedule_once(lambda dt: app.show_error(str(err))) + # on completion hide message - Clock.schedule_once(lambda dt: app.info_bubble.hide()) + Clock.schedule_once(lambda dt: app.info_bubble.hide(now=True), -1) + # call completion routine if on_complete: Clock.schedule_once(lambda dt: on_complete()) @@ -138,14 +144,14 @@ class InstallWizard(Widget): brainwallet = seed - msg2 = _("[color=#414141][b]"+\ + msg2 = _("[color=#414141]"+\ "[b]PLEASE WRITE DOWN YOUR SEED PASS[/b][/color]"+\ "[size=9]\n\n[/size]" +\ "[color=#929292]If you ever forget your pincode, your seed" +\ " phrase will be the [color=#EB984E]"+\ "[b]only way to recover[/b][/color] your wallet. Your " +\ " [color=#EB984E][b]Bitcoins[/b][/color] will otherwise be" +\ - " [color=#EB984E]lost forever![/color]") + " [color=#EB984E][b]lost forever![/b][/color]") if wallet.imported_keys: msg2 += "[b][color=#ff0000ff]" + _("WARNING") + "[/color]:[/b] " +\ @@ -234,13 +240,13 @@ class InstallWizard(Widget): return app.show_error(_('Passwords do not match'), duration=.5) if mode == 'restore': - try: - wallet.save_seed(new_password) - except Exception as err: - app.show_error(str(err)) - return - _dlg.close() - self.load_network(wallet, mode='restore') + def on_complete(*l): + _dlg.close() + self.load_network(wallet, mode='restore') + + self.waiting_dialog(lambda: wallet.save_seed(new_password), + msg=_("saving seed"), + on_complete=on_complete) return if not instance: # create diff --git a/gui/kivy/main.kv b/gui/kivy/main.kv @@ -2,6 +2,7 @@ #:import _ electrum.i18n._ #:import partial functools.partial + # Custom Global Widgets <VGridLayout@GridLayout>: @@ -27,7 +28,7 @@ <Label> markup: True - font_name: 'data/fonts/Roboto.ttf' + font_name: 'Roboto' font_size: '16sp' <ListItemButton> diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -93,7 +93,7 @@ class ElectrumWindow(App): 'While trying to save value to config') base_unit = AliasProperty(_get_bu, _set_bu, bind=('decimal_point',)) - '''BTC or UBTC or ... + '''BTC or UBTC or mBTC... :attr:`base_unit` is a `AliasProperty` defaults to the unit set in electrum config. @@ -148,13 +148,13 @@ class ElectrumWindow(App): self.network = network = kwargs.get('network') self.electrum_config = config = kwargs.get('config') - # create triggers so as to minimize updation a max of 5 times a sec + # create triggers so as to minimize updation a max of 2 times a sec self._trigger_update_status =\ - Clock.create_trigger(self.update_status, .2) + Clock.create_trigger(self.update_status, .5) self._trigger_update_console =\ - Clock.create_trigger(self.update_console, .2) + Clock.create_trigger(self.update_console, .5) self._trigger_notify_transactions = \ - Clock.create_trigger(self.notify_transactions, .2) + Clock.create_trigger(self.notify_transactions, .5) def build(self): from kivy.lang import Builder @@ -174,6 +174,15 @@ class ElectrumWindow(App): Window.bind(size=self.on_size, on_keyboard=self.on_keyboard) Window.bind(on_key_down=self.on_key_down) + + # register fonts + from kivy.core.text import Label + Label.register('Roboto', + 'data/fonts/Roboto.ttf', + 'data/fonts/Roboto.ttf', + 'data/fonts/Roboto-Bold.ttf', + 'data/fonts/Roboto-Bold.ttf') + if platform == 'android': # Window.bind(keyboard_height=self.on_keyboard_height) @@ -261,17 +270,20 @@ class ElectrumWindow(App): self.load_wallet(wallet) - # check and remove this load_wallet calls update_wallet no - # need for this here - #Clock.schedule_once(update_wallet) - + #TODO: URI handling #self.windows.append(w) #if url: w.set_url(url) - #w.app = self.app - #w.connect_slots(s) - #w.update_wallet() - #self.app.exec_() + # TODO:remove properties are used instead + #Clock.schedule_interval(self.timer_actions, .5) + + + #TODO: remove not needed properties allow on_property events + #def timer_actions(self): + # if self.need_update.is_set(): + # self.update_wallet() + # self.need_update.clear() + # run_hook('timer_actions') def init_ui(self): ''' Initialize The Ux part of electrum. This function performs the basic @@ -326,12 +338,12 @@ class ElectrumWindow(App): from electrum_gui.kivy.plugins.exchange_rate import Exchanger self.exchanger = Exchanger(self) self.exchanger.start() - quote_currency = self.electrum_config.get("currency", 'EUR') + quote_currency = self.exchanger.currency quote_balance = self.exchanger.exchange(btc_balance, quote_currency) - if mode == 'symbol': - if quote_currency: - quote_currency = self.exchanger.symbols[quote_currency] + if quote_currency and mode == 'symbol': + quote_currency = self.exchanger.symbols.get(quote_currency, + quote_currency) if quote_balance is None: quote_text = "" @@ -340,8 +352,9 @@ class ElectrumWindow(App): return quote_text def set_currencies(self, quote_currencies): - self._trigger_update_status - #self.currencies = sorted(quote_currencies.keys()) + #TODO remove this and just directly update a observable property + self._trigger_update_status() + self.currencies = sorted(quote_currencies.keys()) def update_console(self, *dt): if self.console: @@ -399,7 +412,7 @@ class ElectrumWindow(App): #if quote: # text += " (%s)"%quote - self.notify(_("Balance: ") + text) + #self.notify(_("Balance: ") + text) #icon = QIcon(":icons/status_connected.png") else: text = _("Not connected") diff --git a/gui/kivy/plugins/exchange_rate.py b/gui/kivy/plugins/exchange_rate.py @@ -2,109 +2,240 @@ '''Module exchange_rate: -This module is responsible for getting the conversion rates between different -currencies. +This module is responsible for getting the conversion rates from different +bitcoin exchanges. ''' from kivy.network.urlrequest import UrlRequest -#kivy.event import EventDispatcher +from kivy.event import EventDispatcher +from kivy.properties import (OptionProperty, StringProperty, AliasProperty, + ListProperty) from kivy.clock import Clock import decimal import json -class Exchanger(object): - ''' +EXCHANGES = ["BitcoinAverage", + "BitcoinVenezuela", + "BitPay", + "Blockchain", + "BTCChina", + "CaVirtEx", + "Coinbase", + "CoinDesk", + "LocalBitcoins", + "Winkdex"] + + +class Exchanger(EventDispatcher): + ''' Provide exchanges rate between crypto and different national + currencies. See Module Documentation for details. ''' symbols = {'ALL': 'Lek', 'AED': 'د.إ', 'AFN':'؋', 'ARS': '$', 'AMD': '֏', 'AWG': 'ƒ', 'ANG': 'ƒ', 'AOA': 'Kz', 'BDT': '৳', 'BHD': 'BD', - 'BIF': 'FBu', 'BTC': 'BTC', 'BTN': 'Nu', - 'AUD': '$', 'AZN': 'ман', 'BSD': '$', 'BBD': '$', 'BYR': 'p', + 'BIF': 'FBu', 'BTC': 'BTC', 'BTN': 'Nu', 'CDF': 'FC', 'CHF': 'CHF', + 'CLF': 'UF', 'CLP':'$', 'CVE': '$', 'DJF':'Fdj', 'DZD': 'دج', + 'AUD': '$', 'AZN': 'ман', 'BSD': '$', 'BBD': '$', 'BYR': 'p', 'CRC': '₡', 'BZD': 'BZ$', 'BMD': '$', 'BOB': '$b', 'BAM': 'KM', 'BWP': 'P', 'BGN': 'лв', 'BRL': 'R$', 'BND': '$', 'KHR': '៛', 'CAD': '$', - 'KYD': '$', 'USD': '$', 'CLP': '$', 'CNY': '¥', 'COP': '$', 'CRC': '₡', + 'ERN': 'Nfk', 'ETB': 'Br', 'KYD': '$', 'USD': '$', 'CLP': '$', 'HRK': 'kn', 'CUP':'₱', 'CZK': 'Kč', 'DKK': 'kr', 'DOP': 'RD$', 'XCD': '$', 'EGP': '£', 'SVC': '$' , 'EEK': 'kr', 'EUR': '€', 'FKP': '£', 'FJD': '$', 'GHC': '¢', 'GIP': '£', 'GTQ': 'Q', 'GBP': '£', 'GYD': '$', 'HNL': 'L', 'HKD': '$', 'HUF': 'Ft', 'ISK': 'kr', - 'INR': '₹', 'IDR': 'Rp', 'IRR': '﷼', 'IMP': '£', 'ILS': '₪', + 'INR': '₹', 'IDR': 'Rp', 'IRR': '﷼', 'IMP': '£', 'ILS': '₪', 'COP': '$', 'JMD': 'J$', 'JPY': '¥', 'JEP': '£', 'KZT': 'лв', 'KPW': '₩', - 'KRW': '₩', 'KGS': 'лв', 'LAK': '₭', 'LVL': 'Ls'} + 'KRW': '₩', 'KGS': 'лв', 'LAK': '₭', 'LVL': 'Ls', 'CNY': '¥'} + + _use_exchange = OptionProperty('Blockchain', options=EXCHANGES) + '''This is the exchange to be used for getting the currency exchange rates + ''' + + _currency = StringProperty('EUR') + '''internal use only + ''' + + def _set_currency(self, value): + exchanger = self.exchanger + if self.use_exchange == 'CoinDesk': + self._update_cd_currency(self.currency) + return + try: + self._currency = value + self.electrum_cinfig.set_key('currency', value, True) + except AttributeError: + self._currency = 'EUR' + + def _get_currency(self): + try: + self._currency = self.electrum_config.get('currency', 'EUR') + except AttributeError: + pass + finally: + return self._currency + + currency = AliasProperty(_get_currency, _set_currency, bind=('_currency',)) + + currencies = ListProperty(['EUR', 'GBP', 'USD']) + '''List of currencies supported by the current exchanger plugin. + + :attr:`currencies` is a `ListProperty` default to ['Eur', 'GBP'. 'USD']. + ''' + + def _get_useex(self): + if not self.parent: + return self._use_exchange + + self._use_exchange = self.parent.electrum_config.get('use_exchange', + 'Blockchain') + return self._use_exchange + + def _set_useex(self, value): + if not self.parent: + return self._use_exchange + self.parent.electrum_config.set_key('use_exchange', value, True) + self._use_exchange = value + + use_exchange = AliasProperty(_get_useex, _set_useex, + bind=('_use_exchange', )) def __init__(self, parent): + super(Exchanger, self).__init__() self.parent = parent self.quote_currencies = None - self.exchanges = ('BlockChain', 'Coinbase', 'CoinDesk') - try: - self.use_exchange = parent.electrum_config.get('use_exchange', - 'BlockChain') - except AttributeError: - self.use_exchange = 'BlockChain' - self.currencies = self.symbols.keys() + self.exchanges = EXCHANGES def exchange(self, btc_amount, quote_currency): if self.quote_currencies is None: return None + quote_currencies = self.quote_currencies.copy() if quote_currency not in quote_currencies: return None - if self.use_exchange == "CoinDesk": - try: - connection = httplib.HTTPSConnection('api.coindesk.com') - connection.request("GET", "/v1/bpi/currentprice/" + str(quote_currency) + ".json") - except Exception: - return - resp = connection.getresponse() - if resp.reason == httplib.responses[httplib.NOT_FOUND]: - return - try: - resp_rate = json.loads(resp.read()) - except Exception: - return - return btc_amount * decimal.Decimal(str(resp_rate["bpi"][str(quote_currency)]["rate_float"])) + return btc_amount * decimal.Decimal(quote_currencies[quote_currency]) - def check_rates(self, dt): - if self.use_exchange == 'BlockChain': - self.check_blockchain() - elif self.use_exchange == 'CoinDesk': - self.check_coindesk() - elif self.use_exchange == 'Coinbase': - self.check_coinbase() + def update_rate(self, dt): + ''' This is called from :method:`start` every X seconds; to update the + rates for currencies for the currently selected exchange. + ''' + update_rates = { + "BitcoinAverage": self.update_ba, + "BitcoinVenezuela": self.update_bv, + "BitPay": self.update_bp, + "Blockchain": self.update_bc, + "BTCChina": self.update_CNY, + "CaVirtEx": self.update_cv, + "CoinDesk": self.update_cd, + "Coinbase": self.update_cb, + "LocalBitcoins": self.update_lb, + "Winkdex": self.update_wd, + } + try: + update_rates[self.use_exchange]() + except KeyError: + return - def check_coindesk(self): + def update_wd(self): - def _lookup_rate(response, quote_id): - return decimal.Decimal(str(response[str(quote_id)]["15m"])) + def on_success(request, response): + response = json.loads(response) + quote_currencies = {'USD': 0.0} + lenprices = len(response["prices"]) + usdprice = response['prices'][lenprices-1]['y'] + + try: + quote_currencies["USD"] = decimal.Decimal(usdprice) + except KeyError: + pass + + self.quote_currencies = quote_currencies + self.parent.set_currencies(quote_currencies) + + req = UrlRequest( + url='https://winkdex.com/static/data/0_600_288.json', + on_success=on_success, + timeout=5) + + def update_cd_currency(self, currency): + + def on_success(request, response): + response = json.loads(response) + quote_currencies = self.quote_currencies + quote_currencies[currency] =\ + str(response['bpi'][str(currency)]['rate_float']) + self.parent.set_currencies(quote_currencies) + + req = UrlRequest( + url='https://api.coindesk.com/v1/bpi/currentprice/'\ + + str(currency) + '.json',on_success=on_success, timeout=5) + + def update_cd(self): def on_success(request, response): quote_currencies = {} + response = json.loads(response) + + for cur in response: + quote_currencies[str(cur["currency"])] = 0.0 + + self.quote_currencies = quote_currencies + self.update_cd_currency(self.currency) + + req = UrlRequest( + url='https://api.coindesk.com/v1/bpi/supported-currencies.json', + on_success=on_success, + timeout=5) + + def update_cv(self): + def on_success(request, response): + response = json.loads(response) + quote_currencies = {"CAD": 0.0} + cadprice = response["last"] try: - for r in response: - quote_currencies[r] = _lookup_rate(response, r) + quote_currencies["CAD"] = decimal.Decimal(cadprice) self.quote_currencies = quote_currencies except KeyError: pass self.parent.set_currencies(quote_currencies) - def on_failure(*args): - pass + req = UrlRequest(url='https://www.cavirtex.com/api/CAD/ticker.json', + on_success=on_success, + timeout=5) - def on_error(*args): - pass + def update_CNY(self): - def on_redirect(*args): - pass + def on_success(request, response): + quote_currencies = {"CNY": 0.0} + cnyprice = response["ticker"]["last"] + try: + quote_currencies["CNY"] = decimal.Decimal(cnyprice) + self.quote_currencies = quote_currencies + except KeyError: + pass + self.parent.set_currencies(quote_currencies) - req = UrlRequest( - url='https://api.coindesk.com/v1/bpi/supported-currencies.json', + req = UrlRequest(url='https://data.btcchina.com/data/ticker', + on_success=on_success, + timeout=5) + + def update_bp(self): + + def on_success(request, response): + quote_currencies = {} + try: + for r in response: + quote_currencies[str(r['code'])] = decimal.Decimal(r['rate']) + self.quote_currencies = quote_currencies + except KeyError: + pass + self.parent.set_currencies(quote_currencies) + + req = UrlRequest(url='https://bitpay.com/api/rates', on_success=on_success, - on_failure=on_failure, - on_error=on_error, - on_redirect=on_redirect, timeout=5) - def check_coinbase(self): + def update_cb(self): def _lookup_rate(response, quote_id): return decimal.Decimal(str(response[str(quote_id)])) @@ -121,24 +252,12 @@ class Exchanger(object): pass self.parent.set_currencies(quote_currencies) - def on_failure(*args): - pass - - def on_error(*args): - pass - - def on_redirect(*args): - pass - req = UrlRequest( url='https://coinbase.com/api/v1/currencies/exchange_rates', on_success=on_success, - on_failure=on_failure, - on_error=on_error, - on_redirect=on_redirect, timeout=5) - def check_blockchain(self): + def update_bc(self): def _lookup_rate(response, quote_id): return decimal.Decimal(str(response[str(quote_id)]["15m"])) @@ -153,27 +272,69 @@ class Exchanger(object): pass self.parent.set_currencies(quote_currencies) - def on_failure(*args): - pass + req = UrlRequest(url='https://blockchain.info/ticker', + on_success=on_success, + timeout=5) - def on_error(*args): - pass + def update_lb(self): + def _lookup_rate(response, quote_id): + return decimal.Decimal(response[str(quote_id)]["rates"]["last"]) - def on_redirect(*args): - pass + def on_success(request, response): + quote_currencies = {} + try: + for r in response: + quote_currencies[r] = _lookup_rate(response, r) + self.quote_currencies = quote_currencies + except KeyError: + pass + self.parent.set_currencies(quote_currencies) - req = UrlRequest(url='https://blockchain.info/ticker', + req = UrlRequest( + url='https://localbitcoins.com/bitcoinaverage/ticker-all-currencies/', + on_success=on_success, + timeout=5) + + + def update_ba(self): + + def on_success(request, response): + quote_currencies = {} + try: + for r in response: + quote_currencies[r] = decimal.Decimal(response[r][u'last']) + self.quote_currencies = quote_currencies + except TypeError: + pass + self.parent.set_currencies(quote_currencies) + + req = UrlRequest(url='https://api.bitcoinaverage.com/ticker/global/all', + on_success=on_success, + timeout=5) + + def update_bv(self): + + def on_success(request, response): + quote_currencies = {} + try: + for r in response["BTC"]: + quote_currencies[r] = decimal.Decimal(response['BTC'][r]) + self.quote_currencies = quote_currencies + except KeyError: + pass + self.parent.set_currencies(quote_currencies) + + req = UrlRequest(url='https://api.bitcoinvenezuela.com/', on_success=on_success, - on_failure=on_failure, - on_error=on_error, - on_redirect=on_redirect, timeout=5) def start(self): - # check every 5 seconds - self.check_rates(0) - Clock.schedule_interval(self.check_rates, 5) + # check rates every few seconds + self.update_rate(0) + # check every few seconds + Clock.unschedule(self.update_rate) + Clock.schedule_interval(self.update_rate, 20) def stop(self): - Clock.unschedule(self.check_rates) + Clock.unschedule(self.update_rate) diff --git a/lib/util.py b/lib/util.py @@ -142,6 +142,13 @@ def user_dir(): elif "LOCALAPPDATA" in os.environ: return os.path.join(os.environ["LOCALAPPDATA"], "Electrum") elif 'ANDROID_DATA' in os.environ: + try: + import jnius + env = jnius.autoclass('android.os.Environment') + _dir = env.getExternalStorageDirectory().getPath() + return _dir + '/electrum/' + except ImportError: + pass return "/sdcard/electrum/" else: #raise Exception("No home directory found in environment variables.")