electrum

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

commit 13678d9e1397613d09d961279c507f3c912ec5fd
parent 21e3bb79394524e75d9d8cab73984df4650f757e
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:
Mgui/kivy/main_window.py | 18+++++++++++++-----
Mgui/kivy/uix/dialogs/fx_dialog.py | 35++++++++++++-----------------------
Mgui/kivy/uix/dialogs/settings.py | 8++++----
Mgui/kivy/uix/screens.py | 5++---
Mgui/qt/history_list.py | 16+++++++++++++---
Mgui/qt/main_window.py | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mlib/daemon.py | 6+++++-
Alib/exchange_rate.py | 410+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/plugins.py | 3+++
Dplugins/exchange_rate/__init__.py | 5-----
Dplugins/exchange_rate/exchange_rate.py | 408-------------------------------------------------------------------------------
Dplugins/exchange_rate/kivy.py | 54------------------------------------------------------
Dplugins/exchange_rate/qt.py | 227-------------------------------------------------------------------------------
13 files changed, 621 insertions(+), 740 deletions(-)

diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py @@ -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()) @@ -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) @@ -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)) @@ -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) @@ -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 diff --git a/gui/kivy/uix/dialogs/fx_dialog.py b/gui/kivy/uix/dialogs/fx_dialog.py @@ -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()) diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py @@ -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' diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py @@ -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 @@ -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 diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py @@ -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 = [ @@ -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): @@ -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) @@ -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)): diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py @@ -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 @@ -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 @@ -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) @@ -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 @@ -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 @@ -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") @@ -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) @@ -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) @@ -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,'), @@ -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([]) @@ -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')) @@ -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: @@ -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')) diff --git a/lib/daemon.py b/lib/daemon.py @@ -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') @@ -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 diff --git a/lib/exchange_rate.py b/lib/exchange_rate.py @@ -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) diff --git a/lib/plugins.py b/lib/plugins.py @@ -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', []) diff --git a/plugins/exchange_rate/__init__.py b/plugins/exchange_rate/__init__.py @@ -1,5 +0,0 @@ -from electrum.i18n import _ - -fullname = _("Exchange rates") -description = _("Exchange rates and currency conversion tools.") -available_for = ['qt','kivy'] diff --git a/plugins/exchange_rate/exchange_rate.py b/plugins/exchange_rate/exchange_rate.py @@ -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) diff --git a/plugins/exchange_rate/kivy.py b/plugins/exchange_rate/kivy.py @@ -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 diff --git a/plugins/exchange_rate/qt.py b/plugins/exchange_rate/qt.py @@ -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)