Exchange Rate History - Add APIs
[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             cur_exchange = self.config.get('use_exchange', "Blockchain")
375             try:
376                 tx_list = self.tx_list
377             except Exception:
378                 return
379
380             try:
381                 mintimestr = datetime.datetime.fromtimestamp(int(min(tx_list.items(), key=lambda x: x[1]['timestamp'])[1]['timestamp'])).strftime('%Y-%m-%d')
382             except Exception:
383                 return
384             maxtimestr = datetime.datetime.now().strftime('%Y-%m-%d')
385
386             if cur_exchange == "CoinDesk":
387                 try:
388                     resp_hist = self.exchanger.get_json('api.coindesk.com', "/v1/bpi/historical/close.json?start=" + mintimestr + "&end=" + maxtimestr)
389                 except Exception:
390                     return
391             elif cur_exchange == "Winkdex":
392                 try:
393                     resp_hist = self.exchanger.get_json('winkdex.com', "/static/data/0_86400_730.json")['prices']
394                 except Exception:
395                     return
396             elif cur_exchange == "BitcoinVenezuela":
397                 cur_currency = self.config.get('currency', "EUR")
398                 if cur_currency == "VEF":
399                     try:
400                         resp_hist = self.exchanger.get_json('api.bitcoinvenezuela.com', "/historical/index.php")['VEF_BTC']
401                     except Exception:
402                         return
403                 elif cur_currency == "ARS":
404                     try:
405                         resp_hist = self.exchanger.get_json('api.bitcoinvenezuela.com', "/historical/index.php")['ARS_BTC']
406                     except Exception:
407                         return
408                 else:
409                     return
410
411             self.gui.main_window.is_edit = True
412             self.gui.main_window.history_list.setColumnCount(6)
413             self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance'), _('Fiat Amount')] )
414             root = self.gui.main_window.history_list.invisibleRootItem()
415             childcount = root.childCount()
416             for i in range(childcount):
417                 item = root.child(i)
418                 try:
419                     tx_info = tx_list[str(item.data(0, Qt.UserRole).toPyObject())]
420                 except Exception:
421                     newtx = self.wallet.get_tx_history()
422                     v = newtx[[x[0] for x in newtx].index(str(item.data(0, Qt.UserRole).toPyObject()))][3]
423
424                     tx_info = {'timestamp':int(datetime.datetime.now().strftime("%s")), 'value': v }
425                     pass
426                 tx_time = int(tx_info['timestamp'])
427                 if cur_exchange == "CoinDesk":
428                     tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
429                     try:
430                         tx_USD_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(resp_hist['bpi'][tx_time_str]), "USD")
431                     except KeyError:
432                         tx_USD_val = "%.2f %s" % (self.btc_rate * Decimal(str(tx_info['value']))/100000000 , "USD")
433                 elif cur_exchange == "Winkdex":
434                     tx_time_str = int(tx_time) - (int(tx_time) % (60 * 60 * 24))
435                     try:
436                         tx_rate = resp_hist[[x['x'] for x in resp_hist].index(tx_time_str)]['y']
437                         tx_USD_val = "%.2f %s" % (Decimal(tx_info['value']) / 100000000 * Decimal(tx_rate), "USD")
438                     except ValueError:
439                         tx_USD_val = "%.2f %s" % (self.btc_rate * Decimal(tx_info['value'])/100000000 , "USD")
440                 elif cur_exchange == "BitcoinVenezuela":
441                     tx_time_str = datetime.datetime.fromtimestamp(tx_time).strftime('%Y-%m-%d')
442                     try:
443                         num = resp_hist[tx_time_str].replace(',','')
444                         tx_BTCVEN_val = "%.2f %s" % (Decimal(str(tx_info['value'])) / 100000000 * Decimal(num), cur_currency)
445                     except KeyError:
446                         tx_BTCVEN_val = _("No data")
447
448                 if cur_exchange == "CoinDesk" or cur_exchange == "Winkdex":
449                     item.setText(5, tx_USD_val)
450                 elif cur_exchange == "BitcoinVenezuela":
451                     item.setText(5, tx_BTCVEN_val)
452                 if Decimal(str(tx_info['value'])) < 0:
453                     item.setForeground(5, QBrush(QColor("#BC1E1E")))
454
455             for i, width in enumerate(self.gui.main_window.column_widths['history']):
456                 self.gui.main_window.history_list.setColumnWidth(i, width)
457             self.gui.main_window.history_list.setColumnWidth(4, 140)
458             self.gui.main_window.history_list.setColumnWidth(5, 120)
459             self.gui.main_window.is_edit = False
460
461
462     def settings_widget(self, window):
463         return EnterButton(_('Settings'), self.settings_dialog)
464
465     def settings_dialog(self):
466         d = QDialog()
467         d.setWindowTitle("Settings")
468         layout = QGridLayout(d)
469         layout.addWidget(QLabel(_('Exchange rate API: ')), 0, 0)
470         layout.addWidget(QLabel(_('Currency: ')), 1, 0)
471         layout.addWidget(QLabel(_('History Rates: ')), 2, 0)
472         combo = QComboBox()
473         combo_ex = QComboBox()
474         hist_checkbox = QCheckBox()
475         hist_checkbox.setEnabled(False)
476         if self.config.get('history_rates', 'unchecked') == 'unchecked':
477             hist_checkbox.setChecked(False)
478         else:
479             hist_checkbox.setChecked(True)
480         ok_button = QPushButton(_("OK"))
481
482         def on_change(x):
483             try:
484                 cur_request = str(self.currencies[x])
485             except Exception:
486                 return
487             if cur_request != self.config.get('currency', "EUR"):
488                 self.config.set_key('currency', cur_request, True)
489                 cur_exchange = self.config.get('use_exchange', "Blockchain")
490                 if cur_request == "USD" and (cur_exchange == "CoinDesk" or cur_exchange == "Winkdex"):
491                     hist_checkbox.setEnabled(True)
492                 elif cur_request == "VEF" and (cur_exchange == "BitcoinVenezuela"):
493                     hist_checkbox.setEnabled(True)
494                 elif cur_request == "ARS" and (cur_exchange == "BitcoinVenezuela"):
495                     hist_checkbox.setEnabled(True)
496                 else:
497                     hist_checkbox.setChecked(False)
498                     hist_checkbox.setEnabled(False)
499                 self.win.update_status()
500                 try:
501                     self.fiat_button
502                 except:
503                     pass
504                 else:
505                     self.fiat_button.setText(cur_request)
506
507         def disable_check():
508             hist_checkbox.setChecked(False)
509             hist_checkbox.setEnabled(False)
510
511         def on_change_ex(x):
512             cur_request = str(self.exchanges[x])
513             if cur_request != self.config.get('use_exchange', "Blockchain"):
514                 self.config.set_key('use_exchange', cur_request, True)
515                 self.currencies = []
516                 combo.clear()
517                 self.exchanger.query_rates.set()
518                 cur_currency = self.config.get('currency', "EUR")
519                 if cur_request == "CoinDesk" or cur_request == "Winkdex":
520                     if cur_currency == "USD":
521                         hist_checkbox.setEnabled(True)
522                     else:
523                         disable_check()
524                 elif cur_request == "BitcoinVenezuela":
525                     if cur_currency == "VEF" or cur_currency == "ARS":
526                         hist_checkbox.setEnabled(True)
527                     else:
528                         disable_check()
529                 else:
530                     disable_check()
531                 set_currencies(combo)
532                 self.win.update_status()
533
534         def on_change_hist(checked):
535             if checked:
536                 self.config.set_key('history_rates', 'checked')
537                 self.history_tab_update()
538             else:
539                 self.config.set_key('history_rates', 'unchecked')
540                 self.gui.main_window.history_list.setHeaderLabels( [ '', _('Date'), _('Description') , _('Amount'), _('Balance')] )
541                 self.gui.main_window.history_list.setColumnCount(5)
542                 for i,width in enumerate(self.gui.main_window.column_widths['history']):
543                     self.gui.main_window.history_list.setColumnWidth(i, width)
544
545         def set_hist_check(hist_checkbox):
546             cur_exchange = self.config.get('use_exchange', "Blockchain")
547             if cur_exchange == "CoinDesk" or cur_exchange == "Winkdex":
548                 hist_checkbox.setEnabled(True)
549             elif cur_exchange == "BitcoinVenezuela":
550                 hist_checkbox.setEnabled(True)
551             else:
552                 hist_checkbox.setEnabled(False)
553
554         def set_currencies(combo):
555             current_currency = self.config.get('currency', "EUR")
556             try:
557                 combo.clear()
558             except Exception:
559                 return
560             combo.addItems(self.currencies)
561             try:
562                 index = self.currencies.index(current_currency)
563             except Exception:
564                 index = 0
565             combo.setCurrentIndex(index)
566
567         def set_exchanges(combo_ex):
568             try:
569                 combo_ex.clear()
570             except Exception:
571                 return
572             combo_ex.addItems(self.exchanges)
573             try:
574                 index = self.exchanges.index(self.config.get('use_exchange', "Blockchain"))
575             except Exception:
576                 index = 0
577             combo_ex.setCurrentIndex(index)
578
579         def ok_clicked():
580             d.accept();
581
582         set_exchanges(combo_ex)
583         set_currencies(combo)
584         set_hist_check(hist_checkbox)
585         combo.currentIndexChanged.connect(on_change)
586         combo_ex.currentIndexChanged.connect(on_change_ex)
587         hist_checkbox.stateChanged.connect(on_change_hist)
588         combo.connect(self.win, SIGNAL('refresh_currencies_combo()'), lambda: set_currencies(combo))
589         combo_ex.connect(d, SIGNAL('refresh_exchanges_combo()'), lambda: set_exchanges(combo_ex))
590         ok_button.clicked.connect(lambda: ok_clicked())
591         layout.addWidget(combo,1,1)
592         layout.addWidget(combo_ex,0,1)
593         layout.addWidget(hist_checkbox,2,1)
594         layout.addWidget(ok_button,3,1)
595
596         if d.exec_():
597             return True
598         else:
599             return False
600
601     def fiat_unit(self):
602         quote_currency = self.config.get("currency", "???")
603         return quote_currency
604
605     def fiat_dialog(self):
606         if not self.config.get('use_exchange_rate'):
607           self.gui.main_window.show_message(_("To use this feature, first enable the exchange rate plugin."))
608           return
609
610         if not self.gui.main_window.network.is_connected():
611           self.gui.main_window.show_message(_("To use this feature, you must have a network connection."))
612           return
613
614         quote_currency = self.fiat_unit()
615
616         d = QDialog(self.gui.main_window)
617         d.setWindowTitle("Fiat")
618         vbox = QVBoxLayout(d)
619         text = "Amount to Send in " + quote_currency
620         vbox.addWidget(QLabel(_(text)+':'))
621
622         grid = QGridLayout()
623         fiat_e = AmountEdit(self.fiat_unit)
624         grid.addWidget(fiat_e, 1, 0)
625
626         r = {}
627         self.get_fiat_price_text(r)
628         quote = r.get(0)
629         if quote:
630           text = "1 BTC~%s"%quote
631           grid.addWidget(QLabel(_(text)), 4, 0, 3, 0)
632         else:
633             self.gui.main_window.show_message(_("Exchange rate not available.  Please check your network connection."))
634             return
635
636         vbox.addLayout(grid)
637         vbox.addLayout(ok_cancel_buttons(d))
638
639         if not d.exec_():
640             return
641
642         fiat = str(fiat_e.text())
643
644         if str(fiat) == "" or str(fiat) == ".":
645             fiat = "0"
646
647         quote = quote[:-4]
648         btcamount = Decimal(fiat) / Decimal(quote)
649         if str(self.gui.main_window.base_unit()) == "mBTC":
650             btcamount = btcamount * 1000
651         quote = "%.8f"%btcamount
652         self.gui.main_window.amount_e.setText( quote )
653
654     def exchange_rate_button(self, grid):
655         quote_currency = self.config.get("currency", "EUR")
656         self.fiat_button = EnterButton(_(quote_currency), self.fiat_dialog)
657         grid.addWidget(self.fiat_button, 4, 3, Qt.AlignHCenter)