non blocking exchange rate API calls
[electrum-nvc.git] / plugins / exchange_rate.py
1 from PyQt4.QtGui import *
2 from PyQt4.QtCore import *
3
4 import datetime
5 import decimal
6 import httplib
7 import json
8 import threading
9 import re
10 from decimal import Decimal
11 from electrum.plugins import BasePlugin
12 from electrum.i18n import _
13 from electrum_gui.qt.util import *
14
15
16 EXCHANGES = ["BitcoinAverage",
17              "BitPay",
18              "Blockchain",
19              "BTCChina",
20              "CaVirtEx",
21              "Coinbase",
22              "CoinDesk",
23              "LocalBitcoins",
24              "Winkdex"]
25              
26
27 class Exchanger(threading.Thread):
28
29     def __init__(self, parent):
30         threading.Thread.__init__(self)
31         self.daemon = True
32         self.parent = parent
33         self.quote_currencies = None
34         self.lock = threading.Lock()
35         self.query_rates = threading.Event()
36         self.use_exchange = self.parent.config.get('use_exchange', "Blockchain")
37         self.parent.exchanges = EXCHANGES
38         self.parent.currencies = ["EUR","GBP","USD"]
39         self.parent.win.emit(SIGNAL("refresh_exchanges_combo()"))
40         self.parent.win.emit(SIGNAL("refresh_currencies_combo()"))
41         self.is_running = False
42
43     def get_json(self, site, get_string):
44         try:
45             connection = httplib.HTTPSConnection(site)
46             connection.request("GET", get_string)
47         except Exception:
48             raise
49         resp = connection.getresponse()
50         if resp.reason == httplib.responses[httplib.NOT_FOUND]:
51             raise
52         try:
53             json_resp = json.loads(resp.read())
54         except Exception:
55             raise
56         return json_resp
57
58
59     def exchange(self, btc_amount, quote_currency):
60         with self.lock:
61             if self.quote_currencies is None:
62                 return None
63             quote_currencies = self.quote_currencies.copy()
64         if quote_currency not in quote_currencies:
65             return None
66         if self.use_exchange == "CoinDesk":
67             try:
68                 resp_rate = self.get_json('api.coindesk.com', "/v1/bpi/currentprice/" + str(quote_currency) + ".json")
69             except Exception:
70                 return
71             return btc_amount * decimal.Decimal(str(resp_rate["bpi"][str(quote_currency)]["rate_float"]))
72         return btc_amount * decimal.Decimal(quote_currencies[quote_currency])
73
74     def stop(self):
75         self.is_running = False
76
77     def update_rate(self):
78         self.use_exchange = self.parent.config.get('use_exchange', "Blockchain")
79         update_rates = {
80             "BitcoinAverage": self.update_ba,
81             "BitPay": self.update_bp,
82             "Blockchain": self.update_bc,
83             "BTCChina": self.update_CNY,
84             "CaVirtEx": self.update_cv,
85             "CoinDesk": self.update_cd,
86             "Coinbase": self.update_cb,
87             "LocalBitcoins": self.update_lb,
88             "Winkdex": self.update_wd,
89         }
90         try:
91             update_rates[self.use_exchange]()
92         except KeyError:
93             return
94
95     def run(self):
96         self.is_running = True
97         while self.is_running:
98             self.query_rates.clear()
99             self.update_rate()
100             self.query_rates.wait(150)
101
102
103     def update_cd(self):
104         try:
105             resp_currencies = self.get_json('api.coindesk.com', "/v1/bpi/supported-currencies.json")
106         except Exception:
107             return
108
109         quote_currencies = {}
110         for cur in resp_currencies:
111             quote_currencies[str(cur["currency"])] = 0.0
112         with self.lock:
113             self.quote_currencies = quote_currencies
114         self.parent.set_currencies(quote_currencies)
115     
116     def update_wd(self):
117         try:
118             winkresp = self.get_json('winkdex.com', "/static/data/0_600_288.json")
119             ####could need nonce value in GET, no Docs available
120         except Exception:
121             return
122         quote_currencies = {"USD": 0.0}
123         ####get y of highest x in "prices"
124         lenprices = len(winkresp["prices"])
125         usdprice = winkresp["prices"][lenprices-1]["y"]
126         try:
127             quote_currencies["USD"] = decimal.Decimal(usdprice)
128             with self.lock:
129                 self.quote_currencies = quote_currencies
130         except KeyError:
131             pass
132         self.parent.set_currencies(quote_currencies)
133             
134     def update_cv(self):
135         try:
136             jsonresp = self.get_json('www.cavirtex.com', "/api/CAD/ticker.json")
137         except Exception:
138             return
139         quote_currencies = {"CAD": 0.0}
140         cadprice = jsonresp["last"]
141         try:
142             quote_currencies["CAD"] = decimal.Decimal(cadprice)
143             with self.lock:
144                 self.quote_currencies = quote_currencies
145         except KeyError:
146             pass
147         self.parent.set_currencies(quote_currencies)
148
149     def update_CNY(self):
150         try:
151             jsonresp = self.get_json('data.btcchina.com', "/data/ticker")
152         except Exception:
153             return
154         quote_currencies = {"CNY": 0.0}
155         cnyprice = jsonresp["ticker"]["last"]
156         try:
157             quote_currencies["CNY"] = decimal.Decimal(cnyprice)
158             with self.lock:
159                 self.quote_currencies = quote_currencies
160         except KeyError:
161             pass
162         self.parent.set_currencies(quote_currencies)
163
164     def update_bp(self):
165         try:
166             jsonresp = self.get_json('bitpay.com', "/api/rates")
167         except Exception:
168             return
169         quote_currencies = {}
170         try:
171             for r in jsonresp:
172                 quote_currencies[str(r["code"])] = decimal.Decimal(r["rate"])
173             with self.lock:
174                 self.quote_currencies = quote_currencies
175         except KeyError:
176             pass
177         self.parent.set_currencies(quote_currencies)
178
179     def update_cb(self):
180         try:
181             jsonresp = self.get_json('coinbase.com', "/api/v1/currencies/exchange_rates")
182         except Exception:
183             return
184
185         quote_currencies = {}
186         try:
187             for r in jsonresp:
188                 if r[:7] == "btc_to_":
189                     quote_currencies[r[7:].upper()] = self._lookup_rate_cb(jsonresp, r)
190             with self.lock:
191                 self.quote_currencies = quote_currencies
192         except KeyError:
193             pass
194         self.parent.set_currencies(quote_currencies)
195
196
197     def update_bc(self):
198         try:
199             jsonresp = self.get_json('blockchain.info', "/ticker")
200         except Exception:
201             return
202         quote_currencies = {}
203         try:
204             for r in jsonresp:
205                 quote_currencies[r] = self._lookup_rate(jsonresp, r)
206             with self.lock:
207                 self.quote_currencies = quote_currencies
208         except KeyError:
209             pass
210         self.parent.set_currencies(quote_currencies)
211         # print "updating exchange rate", self.quote_currencies["USD"]
212
213     def update_lb(self):
214         try:
215             jsonresp = self.get_json('localbitcoins.com', "/bitcoinaverage/ticker-all-currencies/")
216         except Exception:
217             return
218         quote_currencies = {}
219         try:
220             for r in jsonresp:
221                 quote_currencies[r] = self._lookup_rate_lb(jsonresp, r)
222             with self.lock:
223                 self.quote_currencies = quote_currencies
224         except KeyError:
225             pass
226         self.parent.set_currencies(quote_currencies)
227                 
228
229     def update_ba(self):
230         try:
231             jsonresp = self.get_json('api.bitcoinaverage.com', "/ticker/global/all")
232         except Exception:
233             return
234         quote_currencies = {}
235         try:
236             for r in jsonresp:
237                 if not r == "timestamp":
238                     quote_currencies[r] = self._lookup_rate_ba(jsonresp, r)
239             with self.lock:
240                 self.quote_currencies = quote_currencies
241         except KeyError:
242             pass
243         self.parent.set_currencies(quote_currencies)
244
245
246     def get_currencies(self):
247         return [] if self.quote_currencies == None else sorted(self.quote_currencies.keys())
248
249     def _lookup_rate(self, response, quote_id):
250         return decimal.Decimal(str(response[str(quote_id)]["15m"]))
251     def _lookup_rate_cb(self, response, quote_id):
252         return decimal.Decimal(str(response[str(quote_id)]))
253     def _lookup_rate_ba(self, response, quote_id):
254         return decimal.Decimal(response[str(quote_id)]["last"])
255     def _lookup_rate_lb(self, response, quote_id):
256         return decimal.Decimal(response[str(quote_id)]["rates"]["last"])
257
258
259 class Plugin(BasePlugin):
260
261     def fullname(self):
262         return "Exchange rates"
263
264     def description(self):
265         return """exchange rates, retrieved from blockchain.info, CoinDesk, or Coinbase"""
266
267
268     def __init__(self,a,b):
269         BasePlugin.__init__(self,a,b)
270         self.currencies = [self.config.get('currency', "EUR")]
271         self.exchanges = [self.config.get('use_exchange', "Blockchain")]
272
273     def init(self):
274         self.win = self.gui.main_window
275         self.win.connect(self.win, SIGNAL("refresh_currencies()"), self.win.update_status)
276         # Do price discovery
277         self.exchanger = Exchanger(self)
278         self.exchanger.start()
279         self.gui.exchanger = self.exchanger #
280
281     def set_currencies(self, currency_options):
282         self.currencies = sorted(currency_options)
283         self.win.emit(SIGNAL("refresh_currencies()"))
284         self.win.emit(SIGNAL("refresh_currencies_combo()"))
285
286
287     def set_quote_text(self, btc_balance, r):
288         r[0] = self.create_quote_text(Decimal(btc_balance) / 100000000)
289
290     def create_quote_text(self, btc_balance):
291         quote_currency = self.config.get("currency", "EUR")
292         self.exchanger.use_exchange = self.config.get("use_exchange", "Blockchain")
293         quote_balance = self.exchanger.exchange(btc_balance, quote_currency)
294         if quote_balance is None:
295             quote_text = ""
296         else:
297             quote_text = "%.2f %s" % (quote_balance, quote_currency)
298         return quote_text
299
300     def load_wallet(self, wallet):
301         self.wallet = wallet
302         tx_list = {}
303         for item in self.wallet.get_tx_history(self.wallet.storage.get("current_account", None)):
304             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
305             tx_list[tx_hash] = {'value': value, 'timestamp': timestamp, 'balance': balance}
306             
307         self.tx_list = tx_list
308         
309
310     def requires_settings(self):
311         return True
312
313
314     def toggle(self):
315         out = BasePlugin.toggle(self)
316         self.win.update_status()
317         return out
318
319
320     def close(self):
321         self.exchanger.stop()
322
323     def history_tab_update(self):
324         if self.config.get('history_rates', 'unchecked') == "checked":
325             tx_list = self.tx_list
326             
327             mintimestr = datetime.datetime.fromtimestamp(int(min(tx_list.items(), key=lambda x: x[1]['timestamp'])[1]['timestamp'])).strftime('%Y-%m-%d')
328             maxtimestr = datetime.datetime.now().strftime('%Y-%m-%d')
329             try:
330                 resp_hist = self.exchanger.get_json('api.coindesk.com', "/v1/bpi/historical/close.json?start=" + mintimestr + "&end=" + maxtimestr)
331             except Exception:
332                 return
333
334             self.gui.main_window.is_edit = True
335             self.gui.main_window.history_list.setColumnCount(6)
336             self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance'), _('Fiat Amount')] )
337             root = self.gui.main_window.history_list.invisibleRootItem()
338             childcount = root.childCount()
339             for i in range(childcount):
340                 item = root.child(i)
341                 try:
342                     tx_info = tx_list[str(item.data(0, Qt.UserRole).toPyObject())]
343                 except Exception:
344                     newtx = self.wallet.get_tx_history()
345                     v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
346                    
347                     tx_info = {'timestamp':int(datetime.datetime.now().strftime("%s")), 'value': v }
348                     pass
349                 tx_time = int(tx_info['timestamp'])
350                 tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
351                 tx_USD_val = "%.2f %s" % (Decimal(tx_info['value']) / 100000000 * Decimal(resp_hist['bpi'][tx_time_str]), "USD")
352
353                 item.setText(5, tx_USD_val)
354                 if Decimal(tx_info['value']) < 0:
355                     item.setForeground(5, QBrush(QColor("#BC1E1E")))
356
357             for i, width in enumerate(self.gui.main_window.column_widths['history']):
358                 self.gui.main_window.history_list.setColumnWidth(i, width)
359             self.gui.main_window.history_list.setColumnWidth(4, 140)
360             self.gui.main_window.history_list.setColumnWidth(5, 120)
361             self.gui.main_window.is_edit = False
362        
363
364     def settings_widget(self, window):
365         return EnterButton(_('Settings'), self.settings_dialog)
366
367     def settings_dialog(self):
368         d = QDialog()
369         layout = QGridLayout(d)
370         layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
371         layout.addWidget(QLabel(_('Currency: ')), 1, 0)
372         layout.addWidget(QLabel(_('History Rates: ')), 2, 0)
373         combo = QComboBox()
374         combo_ex = QComboBox()
375         hist_checkbox = QCheckBox()
376         hist_checkbox.setEnabled(False)
377         if self.config.get('history_rates', 'unchecked') == 'unchecked':
378             hist_checkbox.setChecked(False)
379         else:
380             hist_checkbox.setChecked(True)
381         ok_button = QPushButton(_("OK"))
382
383         def on_change(x):
384             try:
385                 cur_request = str(self.currencies[x])
386             except Exception:
387                 return
388             if cur_request != self.config.get('currency', "EUR"):
389                 self.config.set_key('currency', cur_request, True)
390                 if cur_request == "USD" and self.config.get('use_exchange', "Blockchain") == "CoinDesk":
391                     hist_checkbox.setEnabled(True)
392                 else:
393                     hist_checkbox.setChecked(False)
394                     hist_checkbox.setEnabled(False)
395                 self.win.update_status()
396
397         def disable_check():
398             hist_checkbox.setChecked(False)
399             hist_checkbox.setEnabled(False)
400
401         def on_change_ex(x):
402             cur_request = str(self.exchanges[x])
403             if cur_request != self.config.get('use_exchange', "Blockchain"):
404                 self.config.set_key('use_exchange', cur_request, True)
405                 self.currencies = []
406                 combo.clear()
407                 self.exchanger.query_rates.set()
408                 if cur_request == "CoinDesk":
409                     if self.config.get('currency', "EUR") == "USD":
410                         hist_checkbox.setEnabled(True)
411                     else:
412                         disable_check()
413                 else:
414                     disable_check()
415                 set_currencies(combo)
416                 self.win.update_status()
417
418         def on_change_hist(checked):
419             if checked:
420                 self.config.set_key('history_rates', 'checked')
421                 self.history_tab_update()
422             else:
423                 self.config.set_key('history_rates', 'unchecked')
424                 self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
425                 self.gui.main_window.history_list.setColumnCount(5)
426                 for i,width in enumerate(self.gui.main_window.column_widths['history']):
427                     self.gui.main_window.history_list.setColumnWidth(i, width)
428
429         def set_hist_check(hist_checkbox):
430             if self.config.get('use_exchange', "Blockchain") == "CoinDesk":
431                 hist_checkbox.setEnabled(True)
432             else:
433                 hist_checkbox.setEnabled(False) 
434         
435         def set_currencies(combo):
436             current_currency = self.config.get('currency', "EUR")
437             try:
438                 combo.clear()
439             except Exception:
440                 return
441             combo.addItems(self.currencies)
442             try:
443                 index = self.currencies.index(current_currency)
444             except Exception:
445                 index = 0
446             combo.setCurrentIndex(index)
447
448         def set_exchanges(combo_ex):
449             try:
450                 combo_ex.clear()
451             except Exception:
452                 return
453             combo_ex.addItems(self.exchanges)
454             try:
455                 index = self.exchanges.index(self.config.get('use_exchange', "Blockchain"))
456             except Exception:
457                 index = 0
458             combo_ex.setCurrentIndex(index)
459
460         def ok_clicked():
461             d.accept();
462
463         set_exchanges(combo_ex)
464         set_currencies(combo)
465         set_hist_check(hist_checkbox)
466         combo.currentIndexChanged.connect(on_change)
467         combo_ex.currentIndexChanged.connect(on_change_ex)
468         hist_checkbox.stateChanged.connect(on_change_hist)
469         combo.connect(self.win, SIGNAL('refresh_currencies_combo()'), lambda: set_currencies(combo))
470         combo_ex.connect(d, SIGNAL('refresh_exchanges_combo()'), lambda: set_exchanges(combo_ex))
471         ok_button.clicked.connect(lambda: ok_clicked())
472         layout.addWidget(combo,1,1)
473         layout.addWidget(combo_ex,0,1)
474         layout.addWidget(hist_checkbox,2,1)
475         layout.addWidget(ok_button,3,1)
476         
477         if d.exec_():
478             return True
479         else:
480             return False
481
482
483