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:
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)