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