URI: 
       tMerge exchange_rate plugin with main code * fixes #2037 (tab indexes) - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 13678d9e1397613d09d961279c507f3c912ec5fd
   DIR parent 21e3bb79394524e75d9d8cab73984df4650f757e
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Tue,  3 Jan 2017 09:02:26 +0100
       
       Merge exchange_rate plugin with main code
       * fixes #2037 (tab indexes)
       
       Diffstat:
         M gui/kivy/main_window.py             |      18 +++++++++++++-----
         M gui/kivy/uix/dialogs/fx_dialog.py   |      35 +++++++++++--------------------
         M gui/kivy/uix/dialogs/settings.py    |       8 ++++----
         M gui/kivy/uix/screens.py             |       5 ++---
         M gui/qt/history_list.py              |      16 +++++++++++++---
         M gui/qt/main_window.py               |     166 +++++++++++++++++++++++++++++--
         M lib/daemon.py                       |       6 +++++-
         A lib/exchange_rate.py                |     410 ++++++++++++++++++++++++++++++
         M lib/plugins.py                      |       3 +++
         D plugins/exchange_rate/__init__.py   |       5 -----
         D plugins/exchange_rate/exchange_rat… |     408 -------------------------------
         D plugins/exchange_rate/kivy.py       |      54 -------------------------------
         D plugins/exchange_rate/qt.py         |     227 -------------------------------
       
       13 files changed, 621 insertions(+), 740 deletions(-)
       ---
   DIR diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
       t@@ -92,11 +92,12 @@ class ElectrumWindow(App):
                _.switch_lang(language)
        
            def on_quotes(self, d):
       -        #Logger.info("on_quotes")
       -        pass
       +        Logger.info("on_quotes")
       +        if self.history_screen:
       +            Clock.schedule_once(lambda dt: self.history_screen.update())
        
            def on_history(self, d):
       -        #Logger.info("on_history")
       +        Logger.info("on_history")
                if self.history_screen:
                    Clock.schedule_once(lambda dt: self.history_screen.update())
        
       t@@ -124,7 +125,7 @@ class ElectrumWindow(App):
            def btc_to_fiat(self, amount_str):
                if not amount_str:
                    return ''
       -        rate = run_hook('exchange_rate')
       +        rate = self.fx.exchange_rate()
                if not rate:
                    return ''
                fiat_amount = self.get_amount(amount_str + ' ' + self.base_unit) * rate / pow(10, 8)
       t@@ -133,7 +134,7 @@ class ElectrumWindow(App):
            def fiat_to_btc(self, fiat_amount):
                if not fiat_amount:
                    return ''
       -        rate = run_hook('exchange_rate')
       +        rate = self.fx.exchange_rate()
                if not rate:
                    return ''
                satoshis = int(pow(10,8) * Decimal(fiat_amount) / Decimal(rate))
       t@@ -198,6 +199,7 @@ class ElectrumWindow(App):
        
                self.gui_object = kwargs.get('gui_object', None)
                self.daemon = self.gui_object.daemon
       +        self.fx = self.daemon.fx
        
                self.contacts = Contacts(self.electrum_config)
                self.invoices = InvoiceStore(self.electrum_config)
       t@@ -386,6 +388,12 @@ class ElectrumWindow(App):
                self.load_wallet_by_name(self.electrum_config.get_wallet_path())
                # init plugins
                run_hook('init_kivy', self)
       +
       +        # fiat currency
       +        self.fiat_unit = self.fx.ccy if self.fx.is_enabled() else ''
       +        self.network.register_callback(self.on_quotes, ['on_quotes'])
       +        self.network.register_callback(self.on_history, ['on_history'])
       +
                # default tab
                self.switch_to('history')
                # bind intent for bitcoin: URI scheme
   DIR diff --git a/gui/kivy/uix/dialogs/fx_dialog.py b/gui/kivy/uix/dialogs/fx_dialog.py
       t@@ -86,46 +86,35 @@ class FxDialog(Factory.Popup):
                self.app = app
                self.config = config
                self.callback = callback
       -        self.plugins = plugins
       -        p = self.plugins.get('exchange_rate')
       -        self.ids.enabled.active = bool(p)
       +        self.fx = self.app.fx
       +        self.ids.enabled.active = self.fx.is_enabled()
        
            def on_active(self, b):
       -        if b:
       -            p = self.plugins.get('exchange_rate')
       -            if p is None:
       -                p = self.plugins.enable('exchange_rate')
       -                p.init_kivy(self.app)
       -        else:
       -            self.plugins.disable('exchange_rate')
       +        self.fx.set_enabled(b)
                Clock.schedule_once(lambda dt: self.add_currencies())
        
            def add_exchanges(self):
       -        p = self.plugins.get('exchange_rate')
       -        exchanges = sorted(p.exchanges_by_ccy.get(p.get_currency())) if p else []
       -        mx = p.exchange.name() if p else ''
       +        exchanges = sorted(self.fx.exchanges_by_ccy.get(self.fx.get_currency())) if self.fx.is_enabled() else []
       +        mx = self.fx.exchange.name() if self.fx.is_enabled() else ''
                ex = self.ids.exchanges
                ex.values = exchanges
       -        ex.text = (mx if mx in exchanges else exchanges[0]) if p else ''
       +        ex.text = (mx if mx in exchanges else exchanges[0]) if self.fx.is_enabled() else ''
        
            def on_exchange(self, text):
                if not text:
                    return
       -        p = self.plugins.get('exchange_rate')
       -        if p and text != p.exchange.name():
       -            p.set_exchange(text)
       +        if self.fx.is_enabled() and text != self.fx.exchange.name():
       +            self.fx.set_exchange(text)
        
            def add_currencies(self):
       -        p = self.plugins.get('exchange_rate')
       -        currencies = sorted(p.exchanges_by_ccy.keys()) if p else []
       -        my_ccy = p.get_currency() if p else ''
       +        currencies = sorted(self.fx.exchanges_by_ccy.keys()) if self.fx else []
       +        my_ccy = self.fx.get_currency() if self.fx.is_enabled() else ''
                self.ids.ccy.values = currencies
                self.ids.ccy.text = my_ccy
        
            def on_currency(self, ccy):
                if ccy:
       -            p = self.plugins.get('exchange_rate')
       -            if p and ccy != p.get_currency():
       -                p.set_currency(ccy)
       +            if self.fx.is_enabled() and ccy != self.fx.get_currency():
       +                self.fx.set_currency(ccy)
                    self.app.fiat_unit = ccy
                Clock.schedule_once(lambda dt: self.add_exchanges())
   DIR diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py
       t@@ -242,10 +242,10 @@ class SettingsDialog(Factory.Popup):
                self._rbf_dialog.open()
        
            def fx_status(self):
       -        p = self.plugins.get('exchange_rate')
       -        if p:
       -            source = p.exchange.name()
       -            ccy = p.get_currency()
       +        fx = self.app.fx
       +        if fx.is_enabled():
       +            source = fx.exchange.name()
       +            ccy = fx.get_currency()
                    return '%s [%s]' %(ccy, source)
                else:
                    return 'Disabled'
   DIR diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py
       t@@ -20,7 +20,6 @@ from kivy.utils import platform
        from electrum.util import profiler, parse_URI, format_time, InvalidPassword, NotEnoughFunds
        from electrum import bitcoin
        from electrum.util import timestamp_to_datetime
       -from electrum.plugins import run_hook
        from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
        
        from context_menu import ContextMenu
       t@@ -148,9 +147,9 @@ class HistoryScreen(CScreen):
                ri.value_known = value is not None
                ri.confirmations = conf
                if self.app.fiat_unit and date:
       -            rate = run_hook('history_rate', date)
       +            rate = self.app.fx.history_rate(date)
                    if rate:
       -                s = run_hook('value_str', value, rate)
       +                s = self.app.fx.value_str(value, rate)
                        ri.quote_text = '' if s is None else s + ' ' + self.app.fiat_unit
                return ri
        
   DIR diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py
       t@@ -30,6 +30,7 @@ from util import *
        from electrum.i18n import _
        from electrum.util import block_explorer_URL, format_satoshis, format_time
        from electrum.plugins import run_hook
       +from electrum.util import timestamp_to_datetime
        
        
        TX_ICONS = [
       t@@ -55,8 +56,10 @@ class HistoryList(MyTreeWidget):
                self.setColumnHidden(1, True)
        
            def refresh_headers(self):
       +        ccy = self.parent.fx.ccy
                headers = ['', '', _('Date'), _('Description') , _('Amount'), _('Balance')]
       -        run_hook('history_tab_headers', headers)
       +        if self.parent.fx.show_history():
       +            headers.extend(['%s '%ccy + _('Amount'), '%s '%ccy + _('Balance')])
                self.update_headers(headers)
        
            def get_domain(self):
       t@@ -69,7 +72,10 @@ class HistoryList(MyTreeWidget):
                item = self.currentItem()
                current_tx = item.data(0, Qt.UserRole).toString() if item else None
                self.clear()
       -        run_hook('history_tab_update_begin')
       +
       +        fx = self.parent.fx
       +        fx.history_used_spot = False
       +
                for h_item in h:
                    tx_hash, height, conf, timestamp, value, balance = h_item
                    status, status_str = self.wallet.get_tx_status(tx_hash, height, conf, timestamp)
       t@@ -78,7 +84,11 @@ class HistoryList(MyTreeWidget):
                    balance_str = self.parent.format_amount(balance, whitespaces=True)
                    label = self.wallet.get_label(tx_hash)
                    entry = ['', tx_hash, status_str, label, v_str, balance_str]
       -            run_hook('history_tab_update', h_item, entry)
       +            if fx.show_history():
       +                date = timestamp_to_datetime(time.time() if conf <= 0 else timestamp)
       +                for amount in [value, balance]:
       +                    text = fx.historical_value_str(amount, date)
       +                    entry.append(text)
                    item = QTreeWidgetItem(entry)
                    item.setIcon(0, icon)
                    for i in range(len(entry)):
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -54,7 +54,7 @@ from electrum import util, bitcoin, commands, coinchooser
        from electrum import SimpleConfig, paymentrequest
        from electrum.wallet import Wallet, Multisig_Wallet
        
       -from amountedit import BTCAmountEdit, MyLineEdit, BTCkBEdit
       +from amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, BTCkBEdit
        from network_dialog import NetworkDialog
        from qrcodewidget import QRCodeWidget, QRDialog
        from qrtextedit import ShowQRTextEdit
       t@@ -98,6 +98,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                self.gui_object = gui_object
                self.config = config = gui_object.config
                self.network = gui_object.daemon.network
       +        self.fx = gui_object.daemon.fx
                self.invoices = gui_object.invoices
                self.contacts = gui_object.contacts
                self.tray = gui_object.tray
       t@@ -166,10 +167,36 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    # set initial message
                    self.console.showMessage(self.network.banner)
        
       +            self.network.register_callback(self.on_quotes, ['on_quotes'])
       +            self.network.register_callback(self.on_history, ['on_history'])
       +            self.connect(self, SIGNAL('new_fx_quotes'), self.on_fx_quotes)
       +            self.connect(self, SIGNAL('new_fx_history'), self.on_fx_history)
       +
                self.load_wallet(wallet)
                self.connect_slots(gui_object.timer)
                self.fetch_alias()
        
       +    def on_history(self, b):
       +        self.emit(SIGNAL('new_fx_history'))
       +
       +    def on_fx_history(self):
       +        self.history_list.refresh_headers()
       +        self.history_list.update()
       +
       +    def on_quotes(self, b):
       +        self.emit(SIGNAL('new_fx_quotes'))
       +
       +    def on_fx_quotes(self):
       +        self.update_status()
       +        # Refresh edits with the new rate
       +        edit = self.fiat_send_e if self.fiat_send_e.is_last_edited else self.amount_e
       +        edit.textEdited.emit(edit.text())
       +        edit = self.fiat_receive_e if self.fiat_receive_e.is_last_edited else self.receive_amount_e
       +        edit.textEdited.emit(edit.text())
       +        # History tab needs updating if it used spot
       +        if self.fx.history_used_spot:
       +            self.history_list.update()
       +
            def toggle_addresses_tab(self):
                show_addr = not self.config.get('show_addresses_tab', False)
                self.config.set_key('show_addresses_tab', show_addr)
       t@@ -528,7 +555,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
        
            def format_amount_and_units(self, amount):
                text = self.format_amount(amount) + ' '+ self.base_unit()
       -        x = run_hook('format_amount_and_units', amount)
       +        x = self.fx.format_amount_and_units(amount)
                if text and x:
                    text += ' (%s)'%x
                return text
       t@@ -546,6 +573,43 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                    return 'BTC'
                raise Exception('Unknown base unit')
        
       +    def connect_fields(self, window, btc_e, fiat_e, fee_e):
       +
       +        def edit_changed(edit):
       +            if edit.follows:
       +                return
       +            edit.setStyleSheet(BLACK_FG)
       +            fiat_e.is_last_edited = (edit == fiat_e)
       +            amount = edit.get_amount()
       +            rate = self.fx.exchange_rate()
       +            if rate is None or amount is None:
       +                if edit is fiat_e:
       +                    btc_e.setText("")
       +                    if fee_e:
       +                        fee_e.setText("")
       +                else:
       +                    fiat_e.setText("")
       +            else:
       +                if edit is fiat_e:
       +                    btc_e.follows = True
       +                    btc_e.setAmount(int(amount / Decimal(rate) * COIN))
       +                    btc_e.setStyleSheet(BLUE_FG)
       +                    btc_e.follows = False
       +                    if fee_e:
       +                        window.update_fee()
       +                else:
       +                    fiat_e.follows = True
       +                    fiat_e.setText(self.fx.ccy_amount_str(
       +                        amount * Decimal(rate) / COIN, False))
       +                    fiat_e.setStyleSheet(BLUE_FG)
       +                    fiat_e.follows = False
       +
       +        btc_e.follows = False
       +        fiat_e.follows = False
       +        fiat_e.textChanged.connect(partial(edit_changed, fiat_e))
       +        btc_e.textChanged.connect(partial(edit_changed, btc_e))
       +        fiat_e.is_last_edited = False
       +
            def update_status(self):
                if not self.wallet:
                    return
       t@@ -573,10 +637,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                            text +=  " [%s unconfirmed]"%(self.format_amount(u, True).strip())
                        if x:
                            text +=  " [%s unmatured]"%(self.format_amount(x, True).strip())
       -                # append fiat balance and price from exchange rate plugin
       -                rate = run_hook('get_fiat_status_text', c + u + x)
       -                if rate:
       -                    text += rate
       +
       +                # append fiat balance and price
       +                if self.fx.is_enabled():
       +                    text += self.fx.get_fiat_status_text(c + u + x) or ''
                        icon = QIcon(":icons/status_connected.png")
                else:
                    text = _("Not connected")
       t@@ -641,6 +705,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                grid.addWidget(self.receive_amount_e, 2, 1)
                self.receive_amount_e.textChanged.connect(self.update_receive_qr)
        
       +        self.fiat_receive_e = AmountEdit(self.fx.get_currency)
       +        grid.addWidget(self.fiat_receive_e, 2, 2, Qt.AlignLeft)
       +        self.connect_fields(self, self.receive_amount_e, self.fiat_receive_e, None)
       +
                self.expires_combo = QComboBox()
                self.expires_combo.addItems(map(lambda x:x[0], expiration_values))
                self.expires_combo.setCurrentIndex(1)
       t@@ -893,6 +961,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                grid.addWidget(amount_label, 4, 0)
                grid.addWidget(self.amount_e, 4, 1)
        
       +        self.fiat_send_e = AmountEdit(self.fx.get_currency)
       +        grid.addWidget(self.fiat_send_e, 4, 2, Qt.AlignLeft)
       +        self.amount_e.frozen.connect(
       +            lambda: self.fiat_send_e.setFrozen(self.amount_e.isReadOnly()))
       +
                self.max_button = EnterButton(_("Max"), self.spend_max)
                hbox = QHBoxLayout()
                hbox.addWidget(self.max_button)
       t@@ -927,6 +1000,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                # This is so that when the user blanks the fee and moves on,
                # we go back to auto-calculate mode and put a fee back.
                self.fee_e.editingFinished.connect(self.update_fee)
       +        self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
        
                self.rbf_checkbox = QCheckBox(_('Replaceable'))
                msg = [_('If you check this box, your transaction will be marked as non-final,'),
       t@@ -1380,7 +1454,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                self.not_enough_funds = False
                self.payment_request = None
                self.payto_e.is_pr = False
       -        for e in [self.payto_e, self.message_e, self.amount_e, self.fee_e]:
       +        for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e, self.fee_e]:
                    e.setText('')
                    e.setFrozen(False)
                self.set_pay_from([])
       t@@ -2241,6 +2315,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                title, msg = _('Import private keys'), _("Enter private keys")
                self._do_import(title, msg, lambda x: self.wallet.import_key(x, password))
        
       +    def update_fiat(self):
       +        b = self.fx.is_enabled()
       +        self.fiat_send_e.setVisible(b)
       +        self.fiat_receive_e.setVisible(b)
       +        self.history_list.refresh_headers()
       +        self.history_list.update()
       +        self.update_status()
       +
            def settings_dialog(self):
                self.need_restart = False
                d = WindowModalDialog(self, _('Preferences'))
       t@@ -2490,10 +2572,73 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
                chooser_combo.currentIndexChanged.connect(on_chooser)
                tx_widgets.append((chooser_label, chooser_combo))
        
       +        # Fiat Currency
       +        hist_checkbox = QCheckBox()
       +        ccy_combo = QComboBox()
       +        ex_combo = QComboBox()
       +
       +        def update_currencies():
       +            currencies = sorted(self.fx.exchanges_by_ccy.keys())
       +            ccy_combo.clear()
       +            ccy_combo.addItems([_('None')] + currencies)
       +            if self.fx.is_enabled():
       +                ccy_combo.setCurrentIndex(ccy_combo.findText(self.fx.get_currency()))
       +
       +        def update_history_cb():
       +            hist_checkbox.setChecked(self.fx.get_history_config())
       +            hist_checkbox.setEnabled(self.fx.is_enabled())
       +
       +        def update_exchanges():
       +            b = self.fx.is_enabled()
       +            ex_combo.setEnabled(b)
       +            if b:
       +                h = self.fx.get_history_config()
       +                c = self.fx.get_currency()
       +                exchanges = self.fx.get_exchanges_by_ccy(c, h)
       +            else:
       +                exchanges = self.fx.exchanges.keys()
       +            ex_combo.clear()
       +            ex_combo.addItems(sorted(exchanges))
       +            ex_combo.setCurrentIndex(ex_combo.findText(self.fx.config_exchange()))
       +
       +        def on_currency(hh):
       +            b = bool(ccy_combo.currentIndex())
       +            ccy = str(ccy_combo.currentText()) if b else None
       +            self.fx.set_enabled(b)
       +            if b and ccy != self.fx.ccy:
       +                self.fx.set_currency(ccy)
       +            update_history_cb()
       +            update_exchanges()
       +            self.update_fiat()
       +
       +        def on_exchange(idx):
       +            exchange = str(ex_combo.currentText())
       +            if self.fx.is_enabled() and exchange != self.fx.exchange.name():
       +                self.fx.set_exchange(exchange)
       +
       +        def on_history(checked):
       +            self.fx.set_history_config(checked)
       +            self.history_list.refresh_headers()
       +            if self.fx.is_enabled() and checked:
       +                self.fx.get_historical_rates()
       +
       +        update_currencies()
       +        update_history_cb()
       +        update_exchanges()
       +        ccy_combo.currentIndexChanged.connect(on_currency)
       +        hist_checkbox.stateChanged.connect(on_history)
       +        ex_combo.currentIndexChanged.connect(on_exchange)
       +
       +        fiat_widgets = []
       +        fiat_widgets.append((QLabel(_('Fiat currency')), ccy_combo))
       +        fiat_widgets.append((QLabel(_('Show history rates')), hist_checkbox))
       +        fiat_widgets.append((QLabel(_('Source')), ex_combo))
       +
                tabs_info = [
                    (fee_widgets, _('Fees')),
                    (tx_widgets, _('Transactions')),
                    (gui_widgets, _('Appearance')),
       +            (fiat_widgets, _('Fiat')),
                    (id_widgets, _('Identity')),
                ]
                for widgets, name in tabs_info:
       t@@ -2517,12 +2662,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
        
                # run the dialog
                d.exec_()
       +
       +        if self.fx:
       +            self.fx.timeout = 0
       +
                self.disconnect(self, SIGNAL('alias_received'), set_alias_color)
        
                run_hook('close_settings_dialog')
                if self.need_restart:
                    self.show_warning(_('Please restart Electrum to activate the new GUI settings'), title=_('Success'))
        
       +
       +
       +
            def run_network_dialog(self):
                if not self.network:
                    self.show_warning(_('You are using Electrum in offline mode; restart Electrum if you want to get connected'), title=_('Offline'))
   DIR diff --git a/lib/daemon.py b/lib/daemon.py
       t@@ -39,6 +39,7 @@ from wallet import WalletStorage, Wallet
        from commands import known_commands, Commands
        from simple_config import SimpleConfig
        from plugins import run_hook
       +from exchange_rate import FxThread
        
        def get_lockfile(config):
            return os.path.join(config.path, 'daemon')
       t@@ -100,14 +101,17 @@ class RequestHandler(SimpleJSONRPCRequestHandler):
        class Daemon(DaemonThread):
        
            def __init__(self, config, fd):
       -
                DaemonThread.__init__(self)
                self.config = config
                if config.get('offline'):
                    self.network = None
       +            self.fx = None
                else:
                    self.network = Network(config)
                    self.network.start()
       +            self.fx = FxThread(config, self.network)
       +            self.network.add_jobs([self.fx])
       +
                self.gui = None
                self.wallets = {}
                # Setup JSONRPC server
   DIR diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py
       t@@ -0,0 +1,410 @@
       +from datetime import datetime
       +import inspect
       +import requests
       +import sys
       +from threading import Thread
       +import time
       +import traceback
       +import csv
       +from decimal import Decimal
       +
       +from bitcoin import COIN
       +from i18n import _
       +from util import PrintError, ThreadJob
       +from util import format_satoshis
       +
       +
       +# See https://en.wikipedia.org/wiki/ISO_4217
       +CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
       +                  'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
       +                  'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
       +                  'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
       +                  'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
       +                  'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
       +
       +class ExchangeBase(PrintError):
       +
       +    def __init__(self, on_quotes, on_history):
       +        self.history = {}
       +        self.quotes = {}
       +        self.on_quotes = on_quotes
       +        self.on_history = on_history
       +
       +    def protocol(self):
       +        return "https"
       +
       +    def get_json(self, site, get_string):
       +        url = "".join([self.protocol(), '://', site, get_string])
       +        response = requests.request('GET', url,
       +                                    headers={'User-Agent' : 'Electrum'})
       +        return response.json()
       +
       +    def get_csv(self, site, get_string):
       +        url = "".join([self.protocol(), '://', site, get_string])
       +        response = requests.request('GET', url,
       +                                    headers={'User-Agent' : 'Electrum'})
       +        reader = csv.DictReader(response.content.split('\n'))
       +        return list(reader)
       +
       +    def name(self):
       +        return self.__class__.__name__
       +
       +    def update_safe(self, ccy):
       +        try:
       +            self.print_error("getting fx quotes for", ccy)
       +            self.quotes = self.get_rates(ccy)
       +            self.print_error("received fx quotes")
       +        except BaseException as e:
       +            self.print_error("failed fx quotes:", e)
       +        self.on_quotes()
       +
       +    def update(self, ccy):
       +        t = Thread(target=self.update_safe, args=(ccy,))
       +        t.setDaemon(True)
       +        t.start()
       +
       +    def get_historical_rates_safe(self, ccy):
       +        try:
       +            self.print_error("requesting fx history for", ccy)
       +            self.history[ccy] = self.historical_rates(ccy)
       +            self.print_error("received fx history for", ccy)
       +            self.on_history()
       +        except BaseException as e:
       +            self.print_error("failed fx history:", e)
       +
       +    def get_historical_rates(self, ccy):
       +        result = self.history.get(ccy)
       +        if not result and ccy in self.history_ccys():
       +            t = Thread(target=self.get_historical_rates_safe, args=(ccy,))
       +            t.setDaemon(True)
       +            t.start()
       +        return result
       +
       +    def history_ccys(self):
       +        return []
       +
       +    def historical_rate(self, ccy, d_t):
       +        return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
       +
       +
       +class BitcoinAverage(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('api.bitcoinaverage.com', '/ticker/global/all')
       +        return dict([(r, Decimal(json[r]['last']))
       +                     for r in json if r != 'timestamp'])
       +
       +    def history_ccys(self):
       +        return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS',
       +                'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
       +                'ZAR']
       +
       +    def historical_rates(self, ccy):
       +        history = self.get_csv('api.bitcoinaverage.com',
       +                               "/history/%s/per_day_all_time_history.csv" % ccy)
       +        return dict([(h['DateTime'][:10], h['Average'])
       +                     for h in history])
       +
       +class BitcoinVenezuela(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('api.bitcoinvenezuela.com', '/')
       +        rates = [(r, json['BTC'][r]) for r in json['BTC']
       +                 if json['BTC'][r] is not None]  # Giving NULL for LTC
       +        return dict(rates)
       +
       +    def protocol(self):
       +        return "http"
       +
       +    def history_ccys(self):
       +        return ['ARS', 'EUR', 'USD', 'VEF']
       +
       +    def historical_rates(self, ccy):
       +        return self.get_json('api.bitcoinvenezuela.com',
       +                             "/historical/index.php?coin=BTC")[ccy +'_BTC']
       +
       +class BTCParalelo(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('btcparalelo.com', '/api/price')
       +        return {'VEF': Decimal(json['price'])}
       +
       +    def protocol(self):
       +        return "http"
       +
       +class Bitso(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('api.bitso.com', '/v2/ticker')
       +        return {'MXN': Decimal(json['last'])}
       +
       +    def protocol(self):
       +        return "http"
       +
       +class Bitcurex(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('pln.bitcurex.com', '/data/ticker.json')
       +        pln_price = json['last']
       +        return {'PLN': Decimal(pln_price)}
       +
       +class Bitmarket(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
       +        return {'PLN': Decimal(json['last'])}
       +
       +class BitPay(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('bitpay.com', '/api/rates')
       +        return dict([(r['code'], Decimal(r['rate'])) for r in json])
       +
       +class BitStamp(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('www.bitstamp.net', '/api/ticker/')
       +        return {'USD': Decimal(json['last'])}
       +
       +class BlockchainInfo(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('blockchain.info', '/ticker')
       +        return dict([(r, Decimal(json[r]['15m'])) for r in json])
       +
       +    def name(self):
       +        return "Blockchain"
       +
       +class BTCChina(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('data.btcchina.com', '/data/ticker')
       +        return {'CNY': Decimal(json['ticker']['last'])}
       +
       +class Coinbase(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('coinbase.com',
       +                             '/api/v1/currencies/exchange_rates')
       +        return dict([(r[7:].upper(), Decimal(json[r]))
       +                     for r in json if r.startswith('btc_to_')])
       +
       +class CoinDesk(ExchangeBase):
       +    def get_rates(self, ccy):
       +        dicts = self.get_json('api.coindesk.com',
       +                              '/v1/bpi/supported-currencies.json')
       +        json = self.get_json('api.coindesk.com',
       +                             '/v1/bpi/currentprice/%s.json' % ccy)
       +        ccys = [d['currency'] for d in dicts]
       +        result = dict.fromkeys(ccys)
       +        result[ccy] = Decimal(json['bpi'][ccy]['rate_float'])
       +        return result
       +
       +    def history_starts(self):
       +        return { 'USD': '2012-11-30' }
       +
       +    def history_ccys(self):
       +        return self.history_starts().keys()
       +
       +    def historical_rates(self, ccy):
       +        start = self.history_starts()[ccy]
       +        end = datetime.today().strftime('%Y-%m-%d')
       +        # Note ?currency and ?index don't work as documented.  Sigh.
       +        query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
       +                 % (start, end))
       +        json = self.get_json('api.coindesk.com', query)
       +        return json['bpi']
       +
       +class Coinsecure(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
       +        return {'INR': Decimal(json['lastprice'] / 100.0 )}
       +
       +class Unocoin(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('www.unocoin.com', 'trade?buy')
       +        return {'INR': Decimal(json)}
       +
       +class itBit(ExchangeBase):
       +    def get_rates(self, ccy):
       +        ccys = ['USD', 'EUR', 'SGD']
       +        json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
       +        result = dict.fromkeys(ccys)
       +        if ccy in ccys:
       +            result[ccy] = Decimal(json['lastPrice'])
       +        return result
       +
       +class Kraken(ExchangeBase):
       +    def get_rates(self, ccy):
       +        ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
       +        pairs = ['XBT%s' % c for c in ccys]
       +        json = self.get_json('api.kraken.com',
       +                             '/0/public/Ticker?pair=%s' % ','.join(pairs))
       +        return dict((k[-3:], Decimal(float(v['c'][0])))
       +                     for k, v in json['result'].items())
       +
       +class LocalBitcoins(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('localbitcoins.com',
       +                             '/bitcoinaverage/ticker-all-currencies/')
       +        return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
       +
       +class Winkdex(ExchangeBase):
       +    def get_rates(self, ccy):
       +        json = self.get_json('winkdex.com', '/api/v0/price')
       +        return {'USD': Decimal(json['price'] / 100.0)}
       +
       +    def history_ccys(self):
       +        return ['USD']
       +
       +    def historical_rates(self, ccy):
       +        json = self.get_json('winkdex.com',
       +                             "/api/v0/series?start_time=1342915200")
       +        history = json['series'][0]['results']
       +        return dict([(h['timestamp'][:10], h['price'] / 100.0)
       +                     for h in history])
       +
       +class MercadoBitcoin(ExchangeBase):
       +    def get_rates(self,ccy):
       +        json = self.get_json('mercadobitcoin.net',
       +                                "/api/ticker/ticker_bitcoin")
       +        return {'BRL': Decimal(json['ticker']['last'])}
       +    
       +    def history_ccys(self):
       +        return ['BRL']
       +
       +class Bitcointoyou(ExchangeBase):
       +    def get_rates(self,ccy):
       +        json = self.get_json('bitcointoyou.com',
       +                                "/API/ticker.aspx")
       +        return {'BRL': Decimal(json['ticker']['last'])}
       +
       +    def history_ccys(self):
       +        return ['BRL']
       +
       +
       +def dictinvert(d):
       +    inv = {}
       +    for k, vlist in d.iteritems():
       +        for v in vlist:
       +            keys = inv.setdefault(v, [])
       +            keys.append(k)
       +    return inv
       +
       +def get_exchanges():
       +    is_exchange = lambda obj: (inspect.isclass(obj)
       +                               and issubclass(obj, ExchangeBase)
       +                               and obj != ExchangeBase)
       +    return dict(inspect.getmembers(sys.modules[__name__], is_exchange))
       +
       +def get_exchanges_by_ccy():
       +    "return only the exchanges that have history rates (which is hardcoded)"
       +    d = {}
       +    exchanges = get_exchanges()
       +    for name, klass in exchanges.items():
       +        exchange = klass(None, None)
       +        d[name] = exchange.history_ccys()
       +    return dictinvert(d)
       +
       +
       +
       +class FxThread(ThreadJob):
       +
       +    def __init__(self, config, network):
       +        self.config = config
       +        self.network = network
       +        self.ccy = self.get_currency()
       +        self.history_used_spot = False
       +        self.ccy_combo = None
       +        self.hist_checkbox = None
       +        self.exchanges = get_exchanges()
       +        self.exchanges_by_ccy = get_exchanges_by_ccy()
       +        self.set_exchange(self.config_exchange())
       +
       +    def get_exchanges_by_ccy(self, ccy, h):
       +        return self.exchanges_by_ccy.get(ccy)
       +
       +    def ccy_amount_str(self, amount, commas):
       +        prec = CCY_PRECISIONS.get(self.ccy, 2)
       +        fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
       +        return fmt_str.format(round(amount, prec))
       +
       +    def run(self):
       +        # This runs from the plugins thread which catches exceptions
       +        if self.is_enabled():
       +            if self.timeout ==0 and self.show_history():
       +                self.exchange.get_historical_rates(self.ccy)
       +            if self.timeout <= time.time():
       +                self.timeout = time.time() + 150
       +                self.exchange.update(self.ccy)
       +
       +    def is_enabled(self):
       +        return bool(self.config.get('use_exchange_rate'))
       +
       +    def set_enabled(self, b):
       +        return self.config.set_key('use_exchange_rate', bool(b))
       +
       +    def get_history_config(self):
       +        return bool(self.config.get('history_rates'))
       +
       +    def set_history_config(self, b):
       +        self.config.set_key('history_rates', bool(b))
       +
       +    def get_currency(self):
       +        '''Use when dynamic fetching is needed'''
       +        return self.config.get("currency", "EUR")
       +
       +    def config_exchange(self):
       +        return self.config.get('use_exchange', 'BitcoinAverage')
       +
       +    def show_history(self):
       +        return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys()
       +
       +    def set_currency(self, ccy):
       +        self.ccy = ccy
       +        self.config.set_key('currency', ccy, True)
       +        self.timeout = 0 # Because self.ccy changes
       +        self.on_quotes()
       +
       +    def set_exchange(self, name):
       +        class_ = self.exchanges.get(name) or self.exchanges.values()[0]
       +        name = class_.__name__
       +        self.print_error("using exchange", name)
       +        if self.config_exchange() != name:
       +            self.config.set_key('use_exchange', name, True)
       +
       +        self.exchange = class_(self.on_quotes, self.on_history)
       +        # A new exchange means new fx quotes, initially empty.  Force
       +        # a quote refresh
       +        self.timeout = 0
       +
       +    def on_quotes(self):
       +        self.network.trigger_callback('on_quotes')
       +
       +    def on_history(self):
       +        self.network.trigger_callback('on_history')
       +
       +    def exchange_rate(self):
       +        '''Returns None, or the exchange rate as a Decimal'''
       +        rate = self.exchange.quotes.get(self.ccy)
       +        if rate:
       +            return Decimal(rate)
       +
       +    def format_amount_and_units(self, btc_balance):
       +        rate = self.exchange_rate()
       +        return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
       +
       +    def get_fiat_status_text(self, btc_balance):
       +        rate = self.exchange_rate()
       +        return _("  (No FX rate available)") if rate is None else " 1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy)
       +
       +
       +    def value_str(self, satoshis, rate):
       +        if satoshis is None:  # Can happen with incomplete history
       +            return _("Unknown")
       +        if rate:
       +            value = Decimal(satoshis) / COIN * Decimal(rate)
       +            return "%s" % (self.ccy_amount_str(value, True))
       +        return _("No data")
       +
       +    def history_rate(self, d_t):
       +        rate = self.exchange.historical_rate(self.ccy, d_t)
       +        # Frequently there is no rate for today, until tomorrow :)
       +        # Use spot quotes in that case
       +        if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
       +            rate = self.exchange.quotes.get(self.ccy)
       +            self.history_used_spot = True
       +        return rate
       +
       +    def historical_value_str(self, satoshis, d_t):
       +        rate = self.history_rate(d_t)
       +        return self.value_str(satoshis, rate)
   DIR diff --git a/lib/plugins.py b/lib/plugins.py
       t@@ -63,6 +63,9 @@ class Plugins(DaemonThread):
        
            def load_plugins(self):
                for loader, name, ispkg in pkgutil.iter_modules([self.pkgpath]):
       +            # do not load deprecated plugins
       +            if name in ['plot', 'exchange_rate']:
       +                continue
                    m = loader.find_module(name).load_module(name)
                    d = m.__dict__
                    gui_good = self.gui_name in d.get('available_for', [])
   DIR diff --git a/plugins/exchange_rate/__init__.py b/plugins/exchange_rate/__init__.py
       t@@ -1,5 +0,0 @@
       -from electrum.i18n import _
       -
       -fullname = _("Exchange rates")
       -description = _("Exchange rates and currency conversion tools.")
       -available_for = ['qt','kivy']
   DIR diff --git a/plugins/exchange_rate/exchange_rate.py b/plugins/exchange_rate/exchange_rate.py
       t@@ -1,408 +0,0 @@
       -from datetime import datetime
       -import inspect
       -import requests
       -import sys
       -from threading import Thread
       -import time
       -import traceback
       -import csv
       -from decimal import Decimal
       -
       -from electrum.bitcoin import COIN
       -from electrum.plugins import BasePlugin, hook
       -from electrum.i18n import _
       -from electrum.util import PrintError, ThreadJob
       -from electrum.util import format_satoshis
       -
       -
       -# See https://en.wikipedia.org/wiki/ISO_4217
       -CCY_PRECISIONS = {'BHD': 3, 'BIF': 0, 'BYR': 0, 'CLF': 4, 'CLP': 0,
       -                  'CVE': 0, 'DJF': 0, 'GNF': 0, 'IQD': 3, 'ISK': 0,
       -                  'JOD': 3, 'JPY': 0, 'KMF': 0, 'KRW': 0, 'KWD': 3,
       -                  'LYD': 3, 'MGA': 1, 'MRO': 1, 'OMR': 3, 'PYG': 0,
       -                  'RWF': 0, 'TND': 3, 'UGX': 0, 'UYI': 0, 'VND': 0,
       -                  'VUV': 0, 'XAF': 0, 'XAU': 4, 'XOF': 0, 'XPF': 0}
       -
       -class ExchangeBase(PrintError):
       -
       -    def __init__(self, on_quotes, on_history):
       -        self.history = {}
       -        self.quotes = {}
       -        self.on_quotes = on_quotes
       -        self.on_history = on_history
       -
       -    def protocol(self):
       -        return "https"
       -
       -    def get_json(self, site, get_string):
       -        url = "".join([self.protocol(), '://', site, get_string])
       -        response = requests.request('GET', url,
       -                                    headers={'User-Agent' : 'Electrum'})
       -        return response.json()
       -
       -    def get_csv(self, site, get_string):
       -        url = "".join([self.protocol(), '://', site, get_string])
       -        response = requests.request('GET', url,
       -                                    headers={'User-Agent' : 'Electrum'})
       -        reader = csv.DictReader(response.content.split('\n'))
       -        return list(reader)
       -
       -    def name(self):
       -        return self.__class__.__name__
       -
       -    def update_safe(self, ccy):
       -        try:
       -            self.print_error("getting fx quotes for", ccy)
       -            self.quotes = self.get_rates(ccy)
       -            self.print_error("received fx quotes")
       -        except BaseException as e:
       -            self.print_error("failed fx quotes:", e)
       -        self.on_quotes()
       -
       -    def update(self, ccy):
       -        t = Thread(target=self.update_safe, args=(ccy,))
       -        t.setDaemon(True)
       -        t.start()
       -
       -    def get_historical_rates_safe(self, ccy):
       -        try:
       -            self.print_error("requesting fx history for", ccy)
       -            self.history[ccy] = self.historical_rates(ccy)
       -            self.print_error("received fx history for", ccy)
       -            self.on_history()
       -        except BaseException as e:
       -            self.print_error("failed fx history:", e)
       -
       -    def get_historical_rates(self, ccy):
       -        result = self.history.get(ccy)
       -        if not result and ccy in self.history_ccys():
       -            t = Thread(target=self.get_historical_rates_safe, args=(ccy,))
       -            t.setDaemon(True)
       -            t.start()
       -        return result
       -
       -    def history_ccys(self):
       -        return []
       -
       -    def historical_rate(self, ccy, d_t):
       -        return self.history.get(ccy, {}).get(d_t.strftime('%Y-%m-%d'))
       -
       -
       -class BitcoinAverage(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('api.bitcoinaverage.com', '/ticker/global/all')
       -        return dict([(r, Decimal(json[r]['last']))
       -                     for r in json if r != 'timestamp'])
       -
       -    def history_ccys(self):
       -        return ['AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'EUR', 'GBP', 'IDR', 'ILS',
       -                'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
       -                'ZAR']
       -
       -    def historical_rates(self, ccy):
       -        history = self.get_csv('api.bitcoinaverage.com',
       -                               "/history/%s/per_day_all_time_history.csv" % ccy)
       -        return dict([(h['datetime'][:10], h['average'])
       -                     for h in history])
       -
       -class BitcoinVenezuela(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('api.bitcoinvenezuela.com', '/')
       -        rates = [(r, json['BTC'][r]) for r in json['BTC']
       -                 if json['BTC'][r] is not None]  # Giving NULL for LTC
       -        return dict(rates)
       -
       -    def protocol(self):
       -        return "http"
       -
       -    def history_ccys(self):
       -        return ['ARS', 'EUR', 'USD', 'VEF']
       -
       -    def historical_rates(self, ccy):
       -        return self.get_json('api.bitcoinvenezuela.com',
       -                             "/historical/index.php?coin=BTC")[ccy +'_BTC']
       -
       -class BTCParalelo(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('btcparalelo.com', '/api/price')
       -        return {'VEF': Decimal(json['price'])}
       -
       -    def protocol(self):
       -        return "http"
       -
       -class Bitso(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('api.bitso.com', '/v2/ticker')
       -        return {'MXN': Decimal(json['last'])}
       -
       -    def protocol(self):
       -        return "http"
       -
       -class Bitcurex(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('pln.bitcurex.com', '/data/ticker.json')
       -        pln_price = json['last']
       -        return {'PLN': Decimal(pln_price)}
       -
       -class Bitmarket(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
       -        return {'PLN': Decimal(json['last'])}
       -
       -class BitPay(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('bitpay.com', '/api/rates')
       -        return dict([(r['code'], Decimal(r['rate'])) for r in json])
       -
       -class BitStamp(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('www.bitstamp.net', '/api/ticker/')
       -        return {'USD': Decimal(json['last'])}
       -
       -class BlockchainInfo(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('blockchain.info', '/ticker')
       -        return dict([(r, Decimal(json[r]['15m'])) for r in json])
       -
       -    def name(self):
       -        return "Blockchain"
       -
       -class BTCChina(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('data.btcchina.com', '/data/ticker')
       -        return {'CNY': Decimal(json['ticker']['last'])}
       -
       -class Coinbase(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('coinbase.com',
       -                             '/api/v1/currencies/exchange_rates')
       -        return dict([(r[7:].upper(), Decimal(json[r]))
       -                     for r in json if r.startswith('btc_to_')])
       -
       -class CoinDesk(ExchangeBase):
       -    def get_rates(self, ccy):
       -        dicts = self.get_json('api.coindesk.com',
       -                              '/v1/bpi/supported-currencies.json')
       -        json = self.get_json('api.coindesk.com',
       -                             '/v1/bpi/currentprice/%s.json' % ccy)
       -        ccys = [d['currency'] for d in dicts]
       -        result = dict.fromkeys(ccys)
       -        result[ccy] = Decimal(json['bpi'][ccy]['rate_float'])
       -        return result
       -
       -    def history_starts(self):
       -        return { 'USD': '2012-11-30' }
       -
       -    def history_ccys(self):
       -        return self.history_starts().keys()
       -
       -    def historical_rates(self, ccy):
       -        start = self.history_starts()[ccy]
       -        end = datetime.today().strftime('%Y-%m-%d')
       -        # Note ?currency and ?index don't work as documented.  Sigh.
       -        query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
       -                 % (start, end))
       -        json = self.get_json('api.coindesk.com', query)
       -        return json['bpi']
       -
       -class Coinsecure(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
       -        return {'INR': Decimal(json['lastprice'] / 100.0 )}
       -
       -class Unocoin(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('www.unocoin.com', 'trade?buy')
       -        return {'INR': Decimal(json)}
       -
       -class itBit(ExchangeBase):
       -    def get_rates(self, ccy):
       -        ccys = ['USD', 'EUR', 'SGD']
       -        json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
       -        result = dict.fromkeys(ccys)
       -        if ccy in ccys:
       -            result[ccy] = Decimal(json['lastPrice'])
       -        return result
       -
       -class Kraken(ExchangeBase):
       -    def get_rates(self, ccy):
       -        ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
       -        pairs = ['XBT%s' % c for c in ccys]
       -        json = self.get_json('api.kraken.com',
       -                             '/0/public/Ticker?pair=%s' % ','.join(pairs))
       -        return dict((k[-3:], Decimal(float(v['c'][0])))
       -                     for k, v in json['result'].items())
       -
       -class LocalBitcoins(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('localbitcoins.com',
       -                             '/bitcoinaverage/ticker-all-currencies/')
       -        return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
       -
       -class Winkdex(ExchangeBase):
       -    def get_rates(self, ccy):
       -        json = self.get_json('winkdex.com', '/api/v0/price')
       -        return {'USD': Decimal(json['price'] / 100.0)}
       -
       -    def history_ccys(self):
       -        return ['USD']
       -
       -    def historical_rates(self, ccy):
       -        json = self.get_json('winkdex.com',
       -                             "/api/v0/series?start_time=1342915200")
       -        history = json['series'][0]['results']
       -        return dict([(h['timestamp'][:10], h['price'] / 100.0)
       -                     for h in history])
       -
       -class MercadoBitcoin(ExchangeBase):
       -    def get_rates(self,ccy):
       -        json = self.get_json('mercadobitcoin.net',
       -                                "/api/ticker/ticker_bitcoin")
       -        return {'BRL': Decimal(json['ticker']['last'])}
       -    
       -    def history_ccys(self):
       -        return ['BRL']
       -
       -class Bitcointoyou(ExchangeBase):
       -    def get_rates(self,ccy):
       -        json = self.get_json('bitcointoyou.com',
       -                                "/API/ticker.aspx")
       -        return {'BRL': Decimal(json['ticker']['last'])}
       -
       -    def history_ccys(self):
       -        return ['BRL']
       -
       -
       -def dictinvert(d):
       -    inv = {}
       -    for k, vlist in d.iteritems():
       -        for v in vlist:
       -            keys = inv.setdefault(v, [])
       -            keys.append(k)
       -    return inv
       -
       -def get_exchanges():
       -    is_exchange = lambda obj: (inspect.isclass(obj)
       -                               and issubclass(obj, ExchangeBase)
       -                               and obj != ExchangeBase)
       -    return dict(inspect.getmembers(sys.modules[__name__], is_exchange))
       -
       -def get_exchanges_by_ccy():
       -    "return only the exchanges that have history rates (which is hardcoded)"
       -    d = {}
       -    exchanges = get_exchanges()
       -    for name, klass in exchanges.items():
       -        exchange = klass(None, None)
       -        d[name] = exchange.history_ccys()
       -    return dictinvert(d)
       -
       -
       -
       -class FxPlugin(BasePlugin, ThreadJob):
       -
       -    def __init__(self, parent, config, name):
       -        BasePlugin.__init__(self, parent, config, name)
       -        self.ccy = self.get_currency()
       -        self.history_used_spot = False
       -        self.ccy_combo = None
       -        self.hist_checkbox = None
       -        self.exchanges = get_exchanges()
       -        self.exchanges_by_ccy = get_exchanges_by_ccy()
       -        self.set_exchange(self.config_exchange())
       -
       -    def ccy_amount_str(self, amount, commas):
       -        prec = CCY_PRECISIONS.get(self.ccy, 2)
       -        fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
       -        return fmt_str.format(round(amount, prec))
       -
       -    def thread_jobs(self):
       -        return [self]
       -
       -    def run(self):
       -        # This runs from the plugins thread which catches exceptions
       -        if self.timeout <= time.time():
       -            self.timeout = time.time() + 150
       -            self.exchange.update(self.ccy)
       -
       -    def get_currency(self):
       -        '''Use when dynamic fetching is needed'''
       -        return self.config.get("currency", "EUR")
       -
       -    def config_exchange(self):
       -        return self.config.get('use_exchange', 'BitcoinAverage')
       -
       -    def show_history(self):
       -        return self.ccy in self.exchange.history_ccys()
       -
       -    def set_currency(self, ccy):
       -        self.ccy = ccy
       -        self.config.set_key('currency', ccy, True)
       -        self.get_historical_rates() # Because self.ccy changes
       -        self.on_quotes()
       -
       -    def set_exchange(self, name):
       -        class_ = self.exchanges.get(name) or self.exchanges.values()[0]
       -        name = class_.__name__
       -        self.print_error("using exchange", name)
       -        if self.config_exchange() != name:
       -            self.config.set_key('use_exchange', name, True)
       -
       -        self.exchange = class_(self.on_quotes, self.on_history)
       -        # A new exchange means new fx quotes, initially empty.  Force
       -        # a quote refresh
       -        self.timeout = 0
       -        self.get_historical_rates()
       -
       -    def on_quotes(self):
       -        pass
       -
       -    def on_history(self):
       -        pass
       -
       -    @hook
       -    def exchange_rate(self):
       -        '''Returns None, or the exchange rate as a Decimal'''
       -        rate = self.exchange.quotes.get(self.ccy)
       -        if rate:
       -            return Decimal(rate)
       -
       -    @hook
       -    def format_amount_and_units(self, btc_balance):
       -        rate = self.exchange_rate()
       -        return '' if rate is None else "%s %s" % (self.value_str(btc_balance, rate), self.ccy)
       -
       -    @hook
       -    def get_fiat_status_text(self, btc_balance):
       -        rate = self.exchange_rate()
       -        return _("  (No FX rate available)") if rate is None else "1 BTC~%s %s" % (self.value_str(COIN, rate), self.ccy)
       -
       -    def get_historical_rates(self):
       -        if self.show_history():
       -            self.exchange.get_historical_rates(self.ccy)
       -
       -    def requires_settings(self):
       -        return True
       -
       -    @hook
       -    def value_str(self, satoshis, rate):
       -        if satoshis is None:  # Can happen with incomplete history
       -            return _("Unknown")
       -        if rate:
       -            value = Decimal(satoshis) / COIN * Decimal(rate)
       -            return "%s" % (self.ccy_amount_str(value, True))
       -        return _("No data")
       -
       -    @hook
       -    def history_rate(self, d_t):
       -        rate = self.exchange.historical_rate(self.ccy, d_t)
       -        # Frequently there is no rate for today, until tomorrow :)
       -        # Use spot quotes in that case
       -        if rate is None and (datetime.today().date() - d_t.date()).days <= 2:
       -            rate = self.exchange.quotes.get(self.ccy)
       -            self.history_used_spot = True
       -        return rate
       -
       -    @hook
       -    def historical_value_str(self, satoshis, d_t):
       -        rate = self.history_rate(d_t)
       -        return self.value_str(satoshis, rate)
   DIR diff --git a/plugins/exchange_rate/kivy.py b/plugins/exchange_rate/kivy.py
       t@@ -1,54 +0,0 @@
       -from __future__ import absolute_import
       -
       -from .exchange_rate import FxPlugin
       -from electrum.plugins import hook
       -
       -
       -from kivy.event import EventDispatcher
       -
       -class MyEventDispatcher(EventDispatcher):
       -
       -    def __init__(self, **kwargs):
       -        self.register_event_type('on_quotes')
       -        self.register_event_type('on_history')
       -        super(MyEventDispatcher, self).__init__(**kwargs)
       -
       -    def on_quotes(self, *args):
       -        pass
       -
       -    def on_history(self, *args):
       -        pass
       -
       -
       -class Plugin(FxPlugin):
       -
       -    def __init__(self, parent, config, name):
       -        FxPlugin.__init__(self, parent, config, name)
       -        self.dispatcher = MyEventDispatcher()
       -
       -    def on_quotes(self):
       -        self.print_error("on_quotes", self.ccy)
       -        self.dispatcher.dispatch('on_quotes')
       -
       -    def on_history(self):
       -        self.print_error("on_history", self.ccy)
       -        self.dispatcher.dispatch('on_history')
       -
       -    def on_close(self):
       -        self.print_error("on close")
       -        self.window.fiat_unit = ''
       -        self.window.history_screen.update()
       -
       -    @hook
       -    def init_kivy(self, window):
       -        self.print_error("init_kivy")
       -        self.window = window
       -        self.dispatcher.bind(on_quotes=window.on_quotes)
       -        self.dispatcher.bind(on_history=window.on_history)
       -        self.window.fiat_unit = self.ccy
       -        self.dispatcher.dispatch('on_history')
       -
       -    @hook
       -    def load_wallet(self, wallet, window):
       -        self.window = window
       -        self.window.fiat_unit = self.ccy
   DIR diff --git a/plugins/exchange_rate/qt.py b/plugins/exchange_rate/qt.py
       t@@ -1,227 +0,0 @@
       -import time
       -from PyQt4.QtGui import *
       -from PyQt4.QtCore import *
       -from electrum_gui.qt.util import *
       -from electrum_gui.qt.amountedit import AmountEdit
       -
       -
       -from electrum.bitcoin import COIN
       -from electrum.i18n import _
       -from decimal import Decimal
       -from functools import partial
       -from electrum.plugins import hook
       -from exchange_rate import FxPlugin
       -from electrum.util import timestamp_to_datetime
       -
       -class Plugin(FxPlugin, QObject):
       -
       -    def __init__(self, parent, config, name):
       -        FxPlugin.__init__(self, parent, config, name)
       -        QObject.__init__(self)
       -
       -    def connect_fields(self, window, btc_e, fiat_e, fee_e):
       -
       -        def edit_changed(edit):
       -            if edit.follows:
       -                return
       -            edit.setStyleSheet(BLACK_FG)
       -            fiat_e.is_last_edited = (edit == fiat_e)
       -            amount = edit.get_amount()
       -            rate = self.exchange_rate()
       -            if rate is None or amount is None:
       -                if edit is fiat_e:
       -                    btc_e.setText("")
       -                    if fee_e:
       -                        fee_e.setText("")
       -                else:
       -                    fiat_e.setText("")
       -            else:
       -                if edit is fiat_e:
       -                    btc_e.follows = True
       -                    btc_e.setAmount(int(amount / Decimal(rate) * COIN))
       -                    btc_e.setStyleSheet(BLUE_FG)
       -                    btc_e.follows = False
       -                    if fee_e:
       -                        window.update_fee()
       -                else:
       -                    fiat_e.follows = True
       -                    fiat_e.setText(self.ccy_amount_str(
       -                        amount * Decimal(rate) / COIN, False))
       -                    fiat_e.setStyleSheet(BLUE_FG)
       -                    fiat_e.follows = False
       -
       -        btc_e.follows = False
       -        fiat_e.follows = False
       -        fiat_e.textChanged.connect(partial(edit_changed, fiat_e))
       -        btc_e.textChanged.connect(partial(edit_changed, btc_e))
       -        fiat_e.is_last_edited = False
       -
       -    @hook
       -    def init_qt(self, gui):
       -        for window in gui.windows:
       -            self.on_new_window(window)
       -
       -    @hook
       -    def do_clear(self, window):
       -        window.fiat_send_e.setText('')
       -
       -    def on_close(self):
       -        self.emit(SIGNAL('close_fx_plugin'))
       -
       -    def restore_window(self, window):
       -        window.update_status()
       -        window.history_list.refresh_headers()
       -        window.fiat_send_e.hide()
       -        window.fiat_receive_e.hide()
       -
       -    def on_quotes(self):
       -        self.emit(SIGNAL('new_fx_quotes'))
       -
       -    def on_history(self):
       -        self.emit(SIGNAL('new_fx_history'))
       -
       -    def on_fx_history(self, window):
       -        '''Called when historical fx quotes are updated'''
       -        window.history_list.update()
       -
       -    def on_fx_quotes(self, window):
       -        '''Called when fresh spot fx quotes come in'''
       -        window.update_status()
       -        self.populate_ccy_combo()
       -        # Refresh edits with the new rate
       -        edit = window.fiat_send_e if window.fiat_send_e.is_last_edited else window.amount_e
       -        edit.textEdited.emit(edit.text())
       -        edit = window.fiat_receive_e if window.fiat_receive_e.is_last_edited else window.receive_amount_e
       -        edit.textEdited.emit(edit.text())
       -        # History tab needs updating if it used spot
       -        if self.history_used_spot:
       -            self.on_fx_history(window)
       -
       -    def on_ccy_combo_change(self):
       -        '''Called when the chosen currency changes'''
       -        ccy = str(self.ccy_combo.currentText())
       -        if ccy and ccy != self.ccy:
       -            self.set_currency(ccy)
       -            self.hist_checkbox_update()
       -
       -    def hist_checkbox_update(self):
       -        if self.hist_checkbox:
       -            self.hist_checkbox.setEnabled(self.ccy in self.exchange.history_ccys())
       -            self.hist_checkbox.setChecked(self.config_history())
       -
       -    def populate_ccy_combo(self):
       -        # There should be at most one instance of the settings dialog
       -        combo = self.ccy_combo
       -        # NOTE: bool(combo) is False if it is empty.  Nuts.
       -        if combo is not None:
       -            combo.blockSignals(True)
       -            combo.clear()
       -            combo.addItems(sorted(self.exchange.quotes.keys()))
       -            combo.blockSignals(False)
       -            combo.setCurrentIndex(combo.findText(self.ccy))
       -
       -    @hook
       -    def on_new_window(self, window):
       -        # Additional send and receive edit boxes
       -        if not hasattr(window, 'fiat_send_e'):
       -            send_e = AmountEdit(self.get_currency)
       -            window.send_grid.addWidget(send_e, 4, 2, Qt.AlignLeft)
       -            window.amount_e.frozen.connect(
       -                lambda: send_e.setFrozen(window.amount_e.isReadOnly()))
       -            receive_e = AmountEdit(self.get_currency)
       -            window.receive_grid.addWidget(receive_e, 2, 2, Qt.AlignLeft)
       -            window.fiat_send_e = send_e
       -            window.fiat_receive_e = receive_e
       -            self.connect_fields(window, window.amount_e, send_e, window.fee_e)
       -            self.connect_fields(window, window.receive_amount_e, receive_e, None)
       -        else:
       -            window.fiat_send_e.show()
       -            window.fiat_receive_e.show()
       -        window.history_list.refresh_headers()
       -        window.update_status()
       -        window.connect(self, SIGNAL('new_fx_quotes'), lambda: self.on_fx_quotes(window))
       -        window.connect(self, SIGNAL('new_fx_history'), lambda: self.on_fx_history(window))
       -        window.connect(self, SIGNAL('close_fx_plugin'), lambda: self.restore_window(window))
       -        window.connect(self, SIGNAL('refresh_headers'), window.history_list.refresh_headers)
       -
       -    def settings_widget(self, window):
       -        return EnterButton(_('Settings'), partial(self.settings_dialog, window))
       -
       -    def settings_dialog(self, window):
       -        d = WindowModalDialog(window, _("Exchange Rate Settings"))
       -        layout = QGridLayout(d)
       -        layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
       -        layout.addWidget(QLabel(_('Currency: ')), 1, 0)
       -        layout.addWidget(QLabel(_('History Rates: ')), 2, 0)
       -
       -        # Currency list
       -        self.ccy_combo = QComboBox()
       -        self.ccy_combo.currentIndexChanged.connect(self.on_ccy_combo_change)
       -        self.populate_ccy_combo()
       -
       -        def on_change_ex(idx):
       -            exchange = str(combo_ex.currentText())
       -            if exchange != self.exchange.name():
       -                self.set_exchange(exchange)
       -                self.hist_checkbox_update()
       -
       -        def on_change_hist(checked):
       -            if checked:
       -                self.config.set_key('history_rates', 'checked')
       -                self.get_historical_rates()
       -            else:
       -                self.config.set_key('history_rates', 'unchecked')
       -            self.emit(SIGNAL('refresh_headers'))
       -
       -        def ok_clicked():
       -            self.timeout = 0
       -            self.ccy_combo = None
       -            d.accept()
       -
       -        combo_ex = QComboBox()
       -        combo_ex.addItems(sorted(self.exchanges.keys()))
       -        combo_ex.setCurrentIndex(combo_ex.findText(self.config_exchange()))
       -        combo_ex.currentIndexChanged.connect(on_change_ex)
       -
       -        self.hist_checkbox = QCheckBox()
       -        self.hist_checkbox.stateChanged.connect(on_change_hist)
       -        self.hist_checkbox_update()
       -
       -        ok_button = QPushButton(_("OK"))
       -        ok_button.clicked.connect(lambda: ok_clicked())
       -
       -        layout.addWidget(self.ccy_combo,1,1)
       -        layout.addWidget(combo_ex,0,1)
       -        layout.addWidget(self.hist_checkbox,2,1)
       -        layout.addWidget(ok_button,3,1)
       -
       -        return d.exec_()
       -
       -
       -    def config_history(self):
       -        return self.config.get('history_rates', 'unchecked') != 'unchecked'
       -
       -    def show_history(self):
       -        return self.config_history() and self.ccy in self.exchange.history_ccys()
       -
       -    @hook
       -    def history_tab_headers(self, headers):
       -        if self.show_history():
       -            headers.extend(['%s '%self.ccy + _('Amount'), '%s '%self.ccy + _('Balance')])
       -
       -    @hook
       -    def history_tab_update_begin(self):
       -        self.history_used_spot = False
       -
       -    @hook
       -    def history_tab_update(self, tx, entry):
       -        if not self.show_history():
       -            return
       -        tx_hash, height, conf, timestamp, value, balance = tx
       -        if conf <= 0:
       -            date = timestamp_to_datetime(time.time())
       -        else:
       -            date = timestamp_to_datetime(timestamp)
       -        for amount in [value, balance]:
       -            text = self.historical_value_str(amount, date)
       -            entry.append(text)