Streamlined code - modified two procs (get_fiat_status_text and fiat_dialog) to call...
[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     def get_fiat_balance_text(self, btc_balance, r):
307         # return balance as: 1.23 USD
308         r[0] = self.create_fiat_balance_text(Decimal(btc_balance) / 100000000)
309
310     def get_fiat_price_text(self, r):
311         # return BTC price as: 123.45 USD
312         r[0] = self.create_fiat_balance_text(1)
313         quote = r[0]
314         if quote:
315             r[0] = "%s"%quote
316
317     def get_fiat_status_text(self, btc_balance, r2):
318         # return status as:   (1.23 USD)    1 BTC~123.45 USD
319         text = ""
320         r = {}
321         self.get_fiat_price_text(r)
322         quote = r.get(0)
323         if quote:
324             price_text = "1 BTC~%s"%quote
325             fiat_currency = quote[-3:]
326             btc_price = quote[:-4]
327             fiat_balance = Decimal(btc_price) * (Decimal(btc_balance)/100000000)
328             balance_text = "(%.2f %s)" % (fiat_balance,fiat_currency)
329             text = "  " + balance_text + "     " + price_text + " "
330         r2[0] = text
331
332     def create_fiat_balance_text(self, btc_balance):
333         quote_currency = self.config.get("currency", "EUR")
334         self.exchanger.use_exchange = self.config.get("use_exchange", "Blockchain")
335         cur_rate = self.exchanger.exchange(Decimal("1.0"), quote_currency)
336         if cur_rate is None:
337             quote_text = ""
338         else:
339             quote_balance = btc_balance * Decimal(cur_rate)
340             self.btc_rate = cur_rate
341             quote_text = "%.2f %s" % (quote_balance, quote_currency)
342         return quote_text
343
344     def load_wallet(self, wallet):
345         self.wallet = wallet
346         tx_list = {}
347         for item in self.wallet.get_tx_history(self.wallet.storage.get("current_account", None)):
348             tx_hash, conf, is_mine, value, fee, balance, timestamp = item
349             tx_list[tx_hash] = {'value': value, 'timestamp': timestamp, 'balance': balance}
350             
351         self.tx_list = tx_list
352         
353
354     def requires_settings(self):
355         return True
356
357
358     def toggle(self):
359         out = BasePlugin.toggle(self)
360         self.win.update_status()
361         if self.config.get('use_exchange_rate'):
362             try:
363                 self.fiat_button
364             except:
365                 self.gui.main_window.show_message(_("To see fiat amount when sending bitcoin, please restart Electrum to activate the new GUI settings."))
366         return out
367
368
369     def close(self):
370         self.exchanger.stop()
371
372     def history_tab_update(self):
373         if self.config.get('history_rates', 'unchecked') == "checked":
374             try:
375                 tx_list = self.tx_list
376             except Exception:
377                 return
378
379             try:
380                 mintimestr = datetime.datetime.fromtimestamp(int(min(tx_list.items(), key=lambda x: x[1]['timestamp'])[1]['timestamp'])).strftime('%Y-%m-%d')
381             except ValueError:
382                 return
383             maxtimestr = datetime.datetime.now().strftime('%Y-%m-%d')
384             try:
385                 resp_hist = self.exchanger.get_json('api.coindesk.com', "/v1/bpi/historical/close.json?start=" + mintimestr + "&end=" + maxtimestr)
386             except Exception:
387                 return
388
389             self.gui.main_window.is_edit = True
390             self.gui.main_window.history_list.setColumnCount(6)
391             self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance'), _('Fiat Amount')] )
392             root = self.gui.main_window.history_list.invisibleRootItem()
393             childcount = root.childCount()
394             for i in range(childcount):
395                 item = root.child(i)
396                 try:
397                     tx_info = tx_list[str(item.data(0, Qt.UserRole).toPyObject())]
398                 except Exception:
399                     newtx = self.wallet.get_tx_history()
400                     v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
401                    
402                     tx_info = {'timestamp':int(datetime.datetime.now().strftime("%s")), 'value': v }
403                     pass
404                 tx_time = int(tx_info['timestamp'])
405                 tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
406                 try:
407                     tx_USD_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(resp_hist['bpi'][tx_time_str]), "USD")
408                 except KeyError:
409                     tx_USD_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
410
411                 item.setText(5, tx_USD_val)
412                 if Decimal(str(tx_info['value'])) < 0:
413                     item.setForeground(5, QBrush(QColor("#BC1E1E")))
414
415             for i, width in enumerate(self.gui.main_window.column_widths['history']):
416                 self.gui.main_window.history_list.setColumnWidth(i, width)
417             self.gui.main_window.history_list.setColumnWidth(4, 140)
418             self.gui.main_window.history_list.setColumnWidth(5, 120)
419             self.gui.main_window.is_edit = False
420        
421
422     def settings_widget(self, window):
423         return EnterButton(_('Settings'), self.settings_dialog)
424
425     def settings_dialog(self):
426         d = QDialog()
427         d.setWindowTitle("Settings")
428         layout = QGridLayout(d)
429         layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
430         layout.addWidget(QLabel(_('Currency: ')), 1, 0)
431         layout.addWidget(QLabel(_('History Rates: ')), 2, 0)
432         combo = QComboBox()
433         combo_ex = QComboBox()
434         hist_checkbox = QCheckBox()
435         hist_checkbox.setEnabled(False)
436         if self.config.get('history_rates', 'unchecked') == 'unchecked':
437             hist_checkbox.setChecked(False)
438         else:
439             hist_checkbox.setChecked(True)
440         ok_button = QPushButton(_("OK"))
441
442         def on_change(x):
443             try:
444                 cur_request = str(self.currencies[x])
445             except Exception:
446                 return
447             if cur_request != self.config.get('currency', "EUR"):
448                 self.config.set_key('currency', cur_request, True)
449                 if cur_request == "USD" and self.config.get('use_exchange', "Blockchain") == "CoinDesk":
450                     hist_checkbox.setEnabled(True)
451                 else:
452                     hist_checkbox.setChecked(False)
453                     hist_checkbox.setEnabled(False)
454                 self.win.update_status()
455                 try:
456                     self.fiat_button
457                 except:
458                     pass
459                 else:
460                     self.fiat_button.setText(cur_request)
461
462         def disable_check():
463             hist_checkbox.setChecked(False)
464             hist_checkbox.setEnabled(False)
465
466         def on_change_ex(x):
467             cur_request = str(self.exchanges[x])
468             if cur_request != self.config.get('use_exchange', "Blockchain"):
469                 self.config.set_key('use_exchange', cur_request, True)
470                 self.currencies = []
471                 combo.clear()
472                 self.exchanger.query_rates.set()
473                 if cur_request == "CoinDesk":
474                     if self.config.get('currency', "EUR") == "USD":
475                         hist_checkbox.setEnabled(True)
476                     else:
477                         disable_check()
478                 else:
479                     disable_check()
480                 set_currencies(combo)
481                 self.win.update_status()
482
483         def on_change_hist(checked):
484             if checked:
485                 self.config.set_key('history_rates', 'checked')
486                 self.history_tab_update()
487             else:
488                 self.config.set_key('history_rates', 'unchecked')
489                 self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
490                 self.gui.main_window.history_list.setColumnCount(5)
491                 for i,width in enumerate(self.gui.main_window.column_widths['history']):
492                     self.gui.main_window.history_list.setColumnWidth(i, width)
493
494         def set_hist_check(hist_checkbox):
495             if self.config.get('use_exchange', "Blockchain") == "CoinDesk":
496                 hist_checkbox.setEnabled(True)
497             else:
498                 hist_checkbox.setEnabled(False) 
499         
500         def set_currencies(combo):
501             current_currency = self.config.get('currency', "EUR")
502             try:
503                 combo.clear()
504             except Exception:
505                 return
506             combo.addItems(self.currencies)
507             try:
508                 index = self.currencies.index(current_currency)
509             except Exception:
510                 index = 0
511             combo.setCurrentIndex(index)
512
513         def set_exchanges(combo_ex):
514             try:
515                 combo_ex.clear()
516             except Exception:
517                 return
518             combo_ex.addItems(self.exchanges)
519             try:
520                 index = self.exchanges.index(self.config.get('use_exchange', "Blockchain"))
521             except Exception:
522                 index = 0
523             combo_ex.setCurrentIndex(index)
524
525         def ok_clicked():
526             d.accept();
527
528         set_exchanges(combo_ex)
529         set_currencies(combo)
530         set_hist_check(hist_checkbox)
531         combo.currentIndexChanged.connect(on_change)
532         combo_ex.currentIndexChanged.connect(on_change_ex)
533         hist_checkbox.stateChanged.connect(on_change_hist)
534         combo.connect(self.win, SIGNAL('refresh_currencies_combo()'), lambda: set_currencies(combo))
535         combo_ex.connect(d, SIGNAL('refresh_exchanges_combo()'), lambda: set_exchanges(combo_ex))
536         ok_button.clicked.connect(lambda: ok_clicked())
537         layout.addWidget(combo,1,1)
538         layout.addWidget(combo_ex,0,1)
539         layout.addWidget(hist_checkbox,2,1)
540         layout.addWidget(ok_button,3,1)
541         
542         if d.exec_():
543             return True
544         else:
545             return False
546
547     def fiat_unit(self):
548         quote_currency = self.config.get("currency", "???")
549         return quote_currency
550
551     def fiat_dialog(self):
552         if not self.config.get('use_exchange_rate'):
553           self.gui.main_window.show_message(_("To use this feature, first enable the exchange rate plugin."))
554           return
555         
556         if not self.gui.main_window.network.is_connected():
557           self.gui.main_window.show_message(_("To use this feature, you must have a network connection."))
558           return
559
560         quote_currency = self.fiat_unit()
561
562         d = QDialog(self.gui.main_window)
563         d.setWindowTitle("Fiat")
564         vbox = QVBoxLayout(d)
565         text = "Amount to Send in " + quote_currency
566         vbox.addWidget(QLabel(_(text)+':'))
567
568         grid = QGridLayout()
569         fiat_e = AmountEdit(self.fiat_unit)
570         grid.addWidget(fiat_e, 1, 0)
571
572         r = {}
573         self.get_fiat_price_text(r)
574         quote = r.get(0)
575         if quote:
576           text = "1 BTC~%s"%quote
577           grid.addWidget(QLabel(_(text)), 4, 0, 3, 0)
578         else:
579             self.gui.main_window.show_message(_("Exchange rate not available.  Please check your network connection."))
580             return
581
582         vbox.addLayout(grid)
583         vbox.addLayout(ok_cancel_buttons(d))
584
585         if not d.exec_():
586             return
587
588         fiat = str(fiat_e.text())
589
590         if str(fiat) == "" or str(fiat) == ".":
591             fiat = "0"
592
593         quote = quote[:-4]
594         btcamount = Decimal(fiat) / Decimal(quote)
595         if str(self.gui.main_window.base_unit()) == "mBTC":
596             btcamount = btcamount * 1000
597         quote = "%.8f"%btcamount
598         self.gui.main_window.amount_e.setText( quote )
599
600     def exchange_rate_button(self, grid):
601         quote_currency = self.config.get("currency", "EUR")
602         self.fiat_button = EnterButton(_(quote_currency), self.fiat_dialog)
603         grid.addWidget(self.fiat_button, 4, 3, Qt.AlignHCenter)