Add ability to specify fiat amount when sending bitcoin.
[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             self.gui.main_window.show_message("To see fiat amount when sending bitcoin, please restart Electrum to activate the new GUI settings.")
341         return out
342
343
344     def close(self):
345         self.exchanger.stop()
346
347     def history_tab_update(self):
348         if self.config.get('history_rates', 'unchecked') == "checked":
349             try:
350                 tx_list = self.tx_list
351             except Exception:
352                 return
353
354             try:
355                 mintimestr = datetime.datetime.fromtimestamp(int(min(tx_list.items(), key=lambda x: x[1]['timestamp'])[1]['timestamp'])).strftime('%Y-%m-%d')
356             except ValueError:
357                 return
358             maxtimestr = datetime.datetime.now().strftime('%Y-%m-%d')
359             try:
360                 resp_hist = self.exchanger.get_json('api.coindesk.com', "/v1/bpi/historical/close.json?start=" + mintimestr + "&end=" + maxtimestr)
361             except Exception:
362                 return
363
364             self.gui.main_window.is_edit = True
365             self.gui.main_window.history_list.setColumnCount(6)
366             self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance'), _('Fiat Amount')] )
367             root = self.gui.main_window.history_list.invisibleRootItem()
368             childcount = root.childCount()
369             for i in range(childcount):
370                 item = root.child(i)
371                 try:
372                     tx_info = tx_list[str(item.data(0, Qt.UserRole).toPyObject())]
373                 except Exception:
374                     newtx = self.wallet.get_tx_history()
375                     v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
376                    
377                     tx_info = {'timestamp':int(datetime.datetime.now().strftime("%s")), 'value': v }
378                     pass
379                 tx_time = int(tx_info['timestamp'])
380                 tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
381                 try:
382                     tx_USD_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(resp_hist['bpi'][tx_time_str]), "USD")
383                 except KeyError:
384                     tx_USD_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
385
386                 item.setText(5, tx_USD_val)
387                 if Decimal(str(tx_info['value'])) < 0:
388                     item.setForeground(5, QBrush(QColor("#BC1E1E")))
389
390             for i, width in enumerate(self.gui.main_window.column_widths['history']):
391                 self.gui.main_window.history_list.setColumnWidth(i, width)
392             self.gui.main_window.history_list.setColumnWidth(4, 140)
393             self.gui.main_window.history_list.setColumnWidth(5, 120)
394             self.gui.main_window.is_edit = False
395        
396
397     def settings_widget(self, window):
398         return EnterButton(_('Settings'), self.settings_dialog)
399
400     def settings_dialog(self):
401         d = QDialog()
402         d.setWindowTitle("Settings")
403         layout = QGridLayout(d)
404         layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
405         layout.addWidget(QLabel(_('Currency: ')), 1, 0)
406         layout.addWidget(QLabel(_('History Rates: ')), 2, 0)
407         combo = QComboBox()
408         combo_ex = QComboBox()
409         hist_checkbox = QCheckBox()
410         hist_checkbox.setEnabled(False)
411         if self.config.get('history_rates', 'unchecked') == 'unchecked':
412             hist_checkbox.setChecked(False)
413         else:
414             hist_checkbox.setChecked(True)
415         ok_button = QPushButton(_("OK"))
416
417         def on_change(x):
418             try:
419                 cur_request = str(self.currencies[x])
420             except Exception:
421                 return
422             if cur_request != self.config.get('currency', "EUR"):
423                 self.config.set_key('currency', cur_request, True)
424                 if cur_request == "USD" and self.config.get('use_exchange', "Blockchain") == "CoinDesk":
425                     hist_checkbox.setEnabled(True)
426                 else:
427                     hist_checkbox.setChecked(False)
428                     hist_checkbox.setEnabled(False)
429                 self.win.update_status()
430                 self.fiat_button.setText(cur_request)
431
432         def disable_check():
433             hist_checkbox.setChecked(False)
434             hist_checkbox.setEnabled(False)
435
436         def on_change_ex(x):
437             cur_request = str(self.exchanges[x])
438             if cur_request != self.config.get('use_exchange', "Blockchain"):
439                 self.config.set_key('use_exchange', cur_request, True)
440                 self.currencies = []
441                 combo.clear()
442                 self.exchanger.query_rates.set()
443                 if cur_request == "CoinDesk":
444                     if self.config.get('currency', "EUR") == "USD":
445                         hist_checkbox.setEnabled(True)
446                     else:
447                         disable_check()
448                 else:
449                     disable_check()
450                 set_currencies(combo)
451                 self.win.update_status()
452
453         def on_change_hist(checked):
454             if checked:
455                 self.config.set_key('history_rates', 'checked')
456                 self.history_tab_update()
457             else:
458                 self.config.set_key('history_rates', 'unchecked')
459                 self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
460                 self.gui.main_window.history_list.setColumnCount(5)
461                 for i,width in enumerate(self.gui.main_window.column_widths['history']):
462                     self.gui.main_window.history_list.setColumnWidth(i, width)
463
464         def set_hist_check(hist_checkbox):
465             if self.config.get('use_exchange', "Blockchain") == "CoinDesk":
466                 hist_checkbox.setEnabled(True)
467             else:
468                 hist_checkbox.setEnabled(False) 
469         
470         def set_currencies(combo):
471             current_currency = self.config.get('currency', "EUR")
472             try:
473                 combo.clear()
474             except Exception:
475                 return
476             combo.addItems(self.currencies)
477             try:
478                 index = self.currencies.index(current_currency)
479             except Exception:
480                 index = 0
481             combo.setCurrentIndex(index)
482
483         def set_exchanges(combo_ex):
484             try:
485                 combo_ex.clear()
486             except Exception:
487                 return
488             combo_ex.addItems(self.exchanges)
489             try:
490                 index = self.exchanges.index(self.config.get('use_exchange', "Blockchain"))
491             except Exception:
492                 index = 0
493             combo_ex.setCurrentIndex(index)
494
495         def ok_clicked():
496             d.accept();
497
498         set_exchanges(combo_ex)
499         set_currencies(combo)
500         set_hist_check(hist_checkbox)
501         combo.currentIndexChanged.connect(on_change)
502         combo_ex.currentIndexChanged.connect(on_change_ex)
503         hist_checkbox.stateChanged.connect(on_change_hist)
504         combo.connect(self.win, SIGNAL('refresh_currencies_combo()'), lambda: set_currencies(combo))
505         combo_ex.connect(d, SIGNAL('refresh_exchanges_combo()'), lambda: set_exchanges(combo_ex))
506         ok_button.clicked.connect(lambda: ok_clicked())
507         layout.addWidget(combo,1,1)
508         layout.addWidget(combo_ex,0,1)
509         layout.addWidget(hist_checkbox,2,1)
510         layout.addWidget(ok_button,3,1)
511         
512         if d.exec_():
513             return True
514         else:
515             return False
516
517
518         
519     def fiat_unit(self):
520         r = {}
521         self.set_quote_text(100000000, r)
522         quote = r.get(0)
523         if quote:
524           return quote[-3:]
525         else:
526           return "???"
527
528     def fiat_dialog(self):
529         if not self.config.get('use_exchange_rate'):
530           self.gui.main_window.show_message("To use this feature, first enable the exchange rate plugin.")
531           return
532
533         quote_currency = self.config.get("currency", "EUR")
534
535         d = QDialog(self.gui.main_window)
536         d.setWindowTitle("Fiat")
537         vbox = QVBoxLayout(d)
538         text = "Amount to Send in " + quote_currency
539         vbox.addWidget(QLabel(_(text)+':'))
540
541         grid = QGridLayout()
542         fiat_e = AmountEdit(self.fiat_unit)
543         grid.addWidget(fiat_e, 1, 0)
544
545         r = {}
546         self.set_quote_text(100000000, r)
547         quote = r.get(0)
548         if quote:
549           text = "  1 BTC=%s"%quote
550           grid.addWidget(QLabel(_(text)), 4, 0, 3, 0)
551
552         vbox.addLayout(grid)
553         vbox.addLayout(ok_cancel_buttons(d))
554
555         if not d.exec_():
556             return
557
558         fiat = self.gui.main_window.read_amount(str(fiat_e.text()))
559
560         if str(fiat) == "None" or str(fiat) == "0":
561             self.gui.main_window.amount_e.setText( "" )
562             return
563
564         r = {}
565         self.set_quote_text(100000000, r)
566         quote = r.get(0)
567         quote = quote[:-4]
568         quote = str(Decimal(fiat) / (Decimal(quote)*100000000))
569         if quote:
570             self.gui.main_window.amount_e.setText( quote )
571
572     def exchange_rate_button(self, grid):
573         quote_currency = self.config.get("currency", "EUR")
574         self.fiat_button = EnterButton(_(quote_currency), self.fiat_dialog)
575         grid.addWidget(self.fiat_button, 4, 3, Qt.AlignHCenter)