pylupdate4 kalam.pro
[bitcoin:spesmilo.git] / cashier.py
1 # -*- coding: utf-8 -*-
2 # Spesmilo -- Python Bitcoin user interface
3 # Copyright © 2011 Luke Dashjr
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation, version 3 only.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 import re
18 from PySide.QtCore import *
19 from PySide.QtGui import *
20 from PySide.QtWebKit import *
21 import send
22 from settings import humanAmount, format_number
23
24 class FocusLineEdit(QLineEdit):
25     def __init__(self, text):
26         super(FocusLineEdit, self).__init__(text)
27         self.setReadOnly(True)
28         self.setMaxLength(40)
29
30     def mousePressEvent(self, event):
31         if event.button() == Qt.LeftButton:
32             self.setCursorPosition(100)
33             self.selectAll()
34             event.accept()
35         else:
36             super(FocusLineEdit, self).mousePressEvent(event)
37
38     def focusOutEvent(self, event):
39         event.accept()
40
41     def sizeHint(self):
42         sizeh = super(FocusLineEdit, self).sizeHint()
43         sizeh.setWidth(self.fontMetrics().averageCharWidth() * self.maxLength())
44         return sizeh
45
46 class TransactionItem(QTableWidgetItem):
47     def __init__(self, text, align=Qt.AlignLeft):
48         super(TransactionItem, self).__init__(text)
49         self.setFlags(Qt.ItemIsEnabled)
50         self.setTextAlignment(align|Qt.AlignVCenter)
51
52 class TransactionsTable(QTableWidget):
53     # These are the proportions for the various columns
54     hedprops = (130, 150, 400, 100, 100)
55
56     def __init__(self):
57         super(TransactionsTable, self).__init__()
58
59         self.setColumnCount(5)
60         hedlabels = (self.tr('Status'),
61                      self.tr('Date'),
62                      self.tr('Transactions'),
63                      self.tr('Credits'),
64                      self.tr('Balance'))
65         self.setHorizontalHeaderLabels(hedlabels)
66         for i, sz in enumerate(self.hedprops):
67             self.horizontalHeader().resizeSection(i, sz)
68
69         self.setSelectionBehavior(self.SelectRows)
70         self.setSelectionMode(self.NoSelection)
71         self.setFocusPolicy(Qt.NoFocus)
72         self.setAlternatingRowColors(True)
73         self.verticalHeader().hide()
74         self.setShowGrid(False)
75         self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
76         self.horizontalHeader().setStretchLastSection(True)
77
78     # Resize columns while maintaining proportions
79     def resizeEvent(self, event):
80         self_width = event.size().width()
81         total_prop_width = sum(self.hedprops)
82         newszs = [sz * self_width / total_prop_width for sz in self.hedprops]
83         for i, sz in enumerate(newszs):
84             self.horizontalHeader().resizeSection(i, sz)
85
86     def update_confirmation(self, i, increment, adjustment = True):
87         status_item = self.item(i, 0)
88         status = status_item.text()
89         m = re.search('(?<=\()\d+(?=\))', status)
90         if not m:
91             return
92         A = m.start()
93         B = m.end()
94         confirms = int(status[A:B])
95         if adjustment:
96             if confirms < self.final_confirmation:
97                 return
98             confirms = confirms + increment
99         else:
100             if increment == confirms and A > 2:
101                 return
102             confirms = increment
103
104         row_disabled = False
105         if confirms >= self.final_confirmation:
106             status = self.tr('Confirmed (%s)')
107         elif confirms > 1:
108             status = self.tr('Processing... (%s)')
109         else:
110             status = self.tr('Validating... (%s)')
111             row_disabled = True
112         status %= (format_number(confirms),)
113
114         status_item.setText(status)
115
116         sf = self.disable_table_item if row_disabled else self.enable_table_item
117         for j in range(0, 5):
118             sf(self.item(i, j))
119
120     def add_transaction_entry(self, transaction):
121         self.insertRow(0)
122         confirms = 'N/A'
123         if 'confirmations' in transaction:
124             confirms = transaction['confirmations']
125         unixtime = transaction['time']
126         address = 'N/A'
127         if 'address' in transaction:
128             address = transaction['address']
129         credit =  transaction['amount']
130         balance = 'N/A'
131         category = transaction['category']
132
133         status_item = TransactionItem('(0)' if confirms != 'N/A' else 'N/A')
134         self.setItem(0, 0, status_item)
135
136         date_formatter = QDateTime()
137         date_formatter.setTime_t(unixtime)
138         # we need to do this in parts to have a month name translation
139         # datetime = date_formatter.toString('hh:mm d ')
140         # datetime += self.tr(date_formatter.toString('MMM '))
141         # datetime += date_formatter.toString('yy')
142         datetime = date_formatter.toString('hh:mm d MMM yy')
143         date_item = TransactionItem(datetime)
144         self.setItem(0, 1, date_item)
145
146         if category == 'send':
147             description = self.tr('Sent to %s')%address
148         elif category == 'receive':
149             description = self.tr('Received to %s')%address
150         elif category == 'generate':
151             description = self.tr('Generated')
152         elif category == 'move':
153             description = self.tr('Moved')
154         trans_item = TransactionItem(description)
155         self.setItem(0, 2, trans_item)
156
157         credits_item = TransactionItem(humanAmount(credit), Qt.AlignRight)
158         self.setItem(0, 3, credits_item)
159
160         balance_item = TransactionItem(humanAmount(balance), Qt.AlignRight)
161         self.setItem(0, 4, balance_item)
162
163         self.update_confirmation(0, confirms, adjustment=False)
164
165         return status_item
166
167     def update_confirmations(self, increment, adjustment = True):
168         if increment == 0 and adjustment:
169             return
170         for i in range(0, self.rowCount()):
171             self.update_confirmation(i, increment, adjustment)
172
173     def enable_table_item(self, item):
174         dummy = QTableWidgetItem()
175         brush = item.foreground()
176         brush.setColor(dummy.foreground().color())
177         item.setForeground(brush)
178         font = item.font()
179         font.setStyle(dummy.font().style())
180         item.setFont(font)
181
182     def disable_table_item(self, item):
183         brush = item.foreground()
184         brush.setColor(Qt.gray)
185         item.setForeground(brush)
186         font = item.font()
187         font.setStyle(font.StyleItalic)
188         item.setFont(font)
189
190 class Cashier(QDialog):
191     def __init__(self, core, clipboard, parent=None):
192         super(Cashier, self).__init__(parent)
193         self.core = core
194         self.clipboard = clipboard
195
196         self.create_actions()
197         main_layout = QVBoxLayout(self)
198
199         youraddy = QHBoxLayout()
200         # Balance + Send button
201         self.balance_label = QLabel()
202         self.refresh_balance()
203         sendbtn = QToolButton(self)
204         sendbtn.setDefaultAction(self.send_act)
205         # Address + New button + Copy button
206         uraddtext = QLabel(self.tr('Your address:'))
207         self.addy = FocusLineEdit(self.core.default_address())
208         newaddybtn = QToolButton(self)
209         newaddybtn.setDefaultAction(self.newaddy_act)
210         copyaddybtn = QToolButton(self)
211         copyaddybtn.setDefaultAction(self.copyaddy_act)
212         # Add them to the layout
213         youraddy.addWidget(uraddtext)
214         youraddy.addWidget(self.addy)
215         youraddy.addWidget(newaddybtn)
216         youraddy.addWidget(copyaddybtn)
217         youraddy.addStretch()
218         youraddy.addWidget(self.balance_label)
219         youraddy.addWidget(sendbtn)
220         main_layout.addLayout(youraddy)
221
222         self.transactions_table = TransactionsTable()
223         main_layout.addWidget(self.transactions_table)
224
225         #webview = QWebView()
226         #webview.load('http://bitcoinwatch.com/')
227         #webview.setFixedSize(880, 300)
228         #mf = webview.page().mainFrame()
229         #mf.setScrollBarPolicy(Qt.Horizontal,
230         #                      Qt.ScrollBarAlwaysOff)
231         #mf.setScrollBarPolicy(Qt.Vertical,
232         #                      Qt.ScrollBarAlwaysOff)
233         #main_layout.addWidget(webview)
234
235         self.setWindowTitle(self.tr('Spesmilo'))
236         if parent is not None:
237             self.setWindowIcon(parent.bitcoin_icon)
238         self.setAttribute(Qt.WA_DeleteOnClose, False)
239
240         self.transactions_table.final_confirmation = 6
241         self.txload_initial = 0x1000
242         self.txload_poll = 8
243         self.txload_waste = 8
244
245         refresh_info_timer = QTimer(self)
246         refresh_info_timer.timeout.connect(self.refresh_info)
247         refresh_info_timer.start(1000)
248         # Stores last transaction added to the table
249         self.last_tx = None
250         self.last_tx_with_confirmations = None
251         # Used for updating number of confirms
252         self.unconfirmed_tx = []
253         #   key=txid, category  val=row, confirms
254         self.trans_lookup = {}
255         self.refresh_info()
256         #self.transactions_table.add_transaction_entry({'confirmations': 3, 'time': 1223332, 'address': 'fake', 'amount': 111, 'category': 'send'})
257         #self.transactions_table.add_transaction_entry({'confirmations': 0, 'time': 1223332, 'address': 'fake', 'amount': 111, 'category': 'send'})
258
259         self.resize(900, 300)
260
261     def refresh_info(self):
262         self.refresh_balance()
263         self.refresh_transactions()
264
265     def __etxid(self, t):
266         txid = t['txid']
267         category = t['category']
268         etxid = "%s/%s" % (txid, category)
269         return etxid
270
271     def refresh_transactions(self):
272         fetchtx = self.txload_initial
273         utx = {}
274         if not self.last_tx is None:
275             # Figure out just how many fetches are needed to comfortably update new unconfirmed tx
276             fetchtx = 0
277             for etxid, status_item in self.unconfirmed_tx:
278                 row = self.transactions_table.row(status_item)
279                 utx[etxid] = [row, None]
280                 # Allow up to 5 wasted refetches in between unconfirmed refetches
281                 if row <= fetchtx + self.txload_waste:
282                     fetchtx = row + 1
283             fetchtx += self.txload_poll
284         while True:
285             transactions = self.core.transactions('*', fetchtx)
286
287             # Sort through fetched transactions, updating confirmation counts
288             ttf = len(transactions)
289             transactions.reverse()
290             otl = []
291             nltwc = None
292             nomore = False
293             for i in xrange(ttf):
294                 t = transactions[i]
295                 if 'txid' not in t:
296                     continue
297                 txid = t['txid'] if 'txid' in t else False
298                 if 'confirmations' in t:
299                     confirms = t['confirmations']
300                     if nltwc is None and confirms >= self.transactions_table.final_confirmation:
301                         nltwc = t
302                     if txid == self.last_tx_with_confirmations:
303                         ci = confirms - self.last_tx_with_confirmations_n
304                         if ci:
305                             self.transactions_table.update_confirmations(ci)
306                         self.last_tx_with_confirmations_n = confirms
307                     etxid = self.__etxid(t)
308                     if etxid in utx:
309                         utx[etxid][1] = (t,)
310                 if nomore:
311                     continue
312                 if txid == self.last_tx:
313                     nomore = True
314                     if i >= self.txload_poll:
315                         self.txload_poll = i + 1
316                     continue
317                 otl.append(t)
318             transactions = otl
319
320             if nomore or fetchtx > ttf: break
321
322             # If we get here, that means we didn't fetch enough to see our last confirmed tx... retry, this time getting more
323             fetchtx *= 2
324
325         if not nltwc is None:
326             self.last_tx_with_confirmations = nltwc['txid']
327             self.last_tx_with_confirmations_n = nltwc['confirmations']
328
329         if transactions:
330             transactions.reverse()
331
332             # Add any new transactions
333             for t in transactions:
334                 status_item = self.transactions_table.add_transaction_entry(t)
335                 if 'confirmations' not in t: continue
336                 if t['confirmations'] < self.transactions_table.final_confirmation:
337                     etxid = self.__etxid(t)
338                     self.unconfirmed_tx.insert(0, (etxid, status_item) )
339             self.last_tx = transactions[-1]['txid']
340
341         # Finally, fetch individual tx info for any old unconfirmed tx
342         while len(utx):
343             etxid, data = utx.items()[0]
344             status_item, transactions = data
345             txid = etxid[:etxid.index('/')]
346             if transactions is None:
347                 transactions = self.core.get_transaction(txid)
348             for t in transactions:
349                 etxid = self.__etxid(t)
350                 if etxid in utx:
351                     confirms = t['confirmations']
352                     status_item = utx[etxid][0]
353                     self.transactions_table.update_confirmation(status_item, confirms, adjustment=False)
354                     del utx[etxid]
355                     if t['confirmations'] >= self.transactions_table.final_confirmation:
356                         for i in xrange(len(self.unconfirmed_tx)):
357                             if self.unconfirmed_tx[i][0] == etxid:
358                                 self.unconfirmed_tx[i:i+1] = ()
359                                 break
360
361     def refresh_balance(self):
362         bltext = self.tr('Balance: %s') % (humanAmount(self.core.balance(), wantTLA=True),)
363         self.balance_label.setText(bltext)
364
365     def create_actions(self):
366         icon = lambda s: QIcon('./icons/' + s)
367
368         self.send_act = QAction(icon('forward.png'), self.tr('Send'),
369             self, toolTip=self.tr('Send bitcoins to another person'),
370             triggered=self.new_send_dialog)
371         self.newaddy_act = QAction(icon('document_new.png'),
372             self.tr('New address'), self,
373             toolTip=self.tr('Create new address for accepting bitcoins'),
374             triggered=self.new_address)
375         self.copyaddy_act = QAction(icon('klipper.png'),
376             self.tr('Copy address'),
377             self, toolTip=self.tr('Copy address to clipboard'),
378             triggered=self.copy_address)
379
380     def new_send_dialog(self):
381         if self.parent() is not None:
382             send_dialog = send.SendDialog(self.core, self.parent())
383         else:
384             send_dialog = send.SendDialog(self.core, self)
385
386     def new_address(self):
387         self.addy.setText(self.core.new_address())
388
389     def copy_address(self):
390         self.clipboard.setText(self.addy.text())
391
392 if __name__ == '__main__':
393     import os
394     import sys
395     import core_interface
396     from settings import SpesmiloSettings
397     os.system('/home/genjix/src/bitcoin/bitcoind')
398     app = QApplication(sys.argv)
399     SpesmiloSettings.loadTranslator()
400     uri = SpesmiloSettings.getEffectiveURI()
401     core = core_interface.CoreInterface(uri)
402     clipboard = qApp.clipboard()
403     cashier = Cashier(core, clipboard)
404     cashier.show()
405     sys.exit(app.exec_())