-WIP-electrum-btcp/client/gui_qt.py

677 lines
22 KiB
Python

import sys, time, datetime, re
# todo: see PySide
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import PyQt4.QtCore as QtCore
import PyQt4.QtGui as QtGui
from wallet import format_satoshis
from decimal import Decimal
def restore_create_dialog(wallet):
pass
class Sender(QtCore.QThread):
def run(self):
while True:
self.emit(QtCore.SIGNAL('testsignal'))
time.sleep(0.5)
class ElectrumWindow(QMainWindow):
def __init__(self, wallet):
QMainWindow.__init__(self)
self.wallet = wallet
tabs = QTabWidget(self)
tabs.addTab(self.create_history_tab(), 'History')
tabs.addTab(self.create_send_tab(), 'Send')
tabs.addTab(self.create_receive_tab(), 'Receive')
tabs.addTab(self.create_contacts_tab(),'Contacts')
tabs.addTab(self.create_wall_tab(), 'Wall')
tabs.setMinimumSize(600, 400)
tabs.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.setCentralWidget(tabs)
self.create_status_bar()
self.setGeometry(100,100,840,400)
self.setWindowTitle( 'Electrum ' + self.wallet.electrum_version + ' - Qt')
self.show()
QShortcut(QKeySequence("Ctrl+W"), self, self.close)
QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
QShortcut(QKeySequence("Ctrl+PgUp"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() - 1 )%tabs.count() ))
QShortcut(QKeySequence("Ctrl+PgDown"), self, lambda: tabs.setCurrentIndex( (tabs.currentIndex() + 1 )%tabs.count() ))
def connect_slots(self, sender):
self.connect(sender, QtCore.SIGNAL('testsignal'), self.update_wallet)
def update_wallet(self):
if self.wallet.interface.is_connected:
if self.wallet.interface.blocks == 0:
text = "Server not ready"
icon = QIcon("icons/status_disconnected.svg")
elif not self.wallet.interface.was_polled:
text = "Synchronizing..."
icon = QIcon("icons/status_waiting.svg")
else:
c, u = self.wallet.get_balance()
text = "Balance: %s "%( format_satoshis(c) )
if u: text += "[%s unconfirmed]"%( format_satoshis(u,True) )
icon = QIcon("icons/status_connected.png")
else:
text = "Not connected"
icon = QIcon("icons/status_disconnected.svg")
self.statusBar().showMessage(text)
self.status_button.setIcon( icon )
if self.wallet.interface.was_updated:
self.wallet.interface.was_updated = False
self.textbox.setText( self.wallet.interface.message )
self.update_history_tab()
self.update_receive_tab()
self.update_contacts_tab()
def create_history_tab(self):
self.history_list = w = QTreeWidget(self)
#print w.getContentsMargins()
w.setColumnCount(5)
w.setColumnWidth(0, 40)
w.setColumnWidth(1, 140)
w.setColumnWidth(2, 350)
w.setColumnWidth(3, 140)
w.setColumnWidth(4, 140)
w.setHeaderLabels( [ '', 'Date', 'Description', 'Amount', 'Balance'] )
self.connect(w, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.tx_details)
self.connect(w, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), self.tx_label_clicked)
self.connect(w, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), self.tx_label_changed)
return w
def tx_details(self, item, column):
tx_hash = str(item.toolTip(0))
tx = self.wallet.tx_history.get(tx_hash)
if tx['height']:
conf = self.wallet.interface.blocks - tx['height'] + 1
time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
else:
conf = 0
time_str = 'pending'
tx_details = "Transaction Details:\n\n" \
+ "Transaction ID:\n" + tx_hash + "\n\n" \
+ "Status: %d confirmations\n\n"%conf \
+ "Date: %s\n\n"%time_str \
+ "Inputs:\n-"+ '\n-'.join(tx['inputs']) + "\n\n" \
+ "Outputs:\n-"+ '\n-'.join(tx['outputs'])
r = self.wallet.receipts.get(tx_hash)
if r:
tx_details += "\n_______________________________________" \
+ '\n\nSigned URI: ' + r[2] \
+ "\n\nSigned by: " + r[0] \
+ '\n\nSignature: ' + r[1]
QMessageBox.information(self, 'Details', tx_details, 'OK')
def tx_label_clicked(self, item, column):
if column==2 and item.isSelected():
tx_hash = str(item.toolTip(0))
self.is_edit=True
#if not self.wallet.labels.get(tx_hash): item.setText(2,'')
item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
self.history_list.editItem( item, column )
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
self.is_edit=False
def tx_label_changed(self, item, column):
if self.is_edit:
return
self.is_edit=True
tx_hash = str(item.toolTip(0))
tx = self.wallet.tx_history.get(tx_hash)
s = self.wallet.labels.get(tx_hash)
text = str( item.text(2) )
if text:
self.wallet.labels[tx_hash] = text
item.setForeground(2, QBrush(QColor('black')))
else:
if s: self.wallet.labels.pop(tx_hash)
text = tx['default_label']
item.setText(2, text)
item.setForeground(2, QBrush(QColor('gray')))
self.is_edit=False
def address_label_clicked(self, item, column, l):
if column==1 and item.isSelected():
item.setFlags(Qt.ItemIsEditable|Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
l.editItem( item, column )
item.setFlags(Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled | Qt.ItemIsDragEnabled)
def address_label_changed(self, item, column, l):
addr = str(item.text(0))
text = str( item.text(1) )
if text:
self.wallet.labels[addr] = text
else:
s = self.wallet.labels.get(addr)
if s: self.wallet.labels.pop(addr)
self.update_history_tab()
def update_history_tab(self):
self.history_list.clear()
balance = 0
for tx in self.wallet.get_tx_history():
tx_hash = tx['tx_hash']
if tx['height']:
conf = self.wallet.interface.blocks - tx['height'] + 1
time_str = datetime.datetime.fromtimestamp( tx['nTime']).isoformat(' ')[:-3]
icon = QIcon("icons/confirmed.png")
else:
conf = 0
time_str = 'pending'
icon = QIcon("icons/unconfirmed.svg")
v = tx['value']
balance += v
label = self.wallet.labels.get(tx_hash)
is_default_label = (label == '') or (label is None)
if is_default_label: label = tx['default_label']
item = QTreeWidgetItem( [ '', time_str, label, format_satoshis(v,True), format_satoshis(balance)] )
item.setFont(2, QFont('monospace'))
item.setFont(3, QFont('monospace'))
item.setFont(4, QFont('monospace'))
item.setToolTip(0, tx_hash)
if is_default_label:
item.setForeground(2, QBrush(QColor('grey')))
item.setIcon(0, icon)
self.history_list.insertTopLevelItem(0,item)
def create_send_tab(self):
w = QWidget()
paytoEdit = QtGui.QLineEdit()
descriptionEdit = QtGui.QLineEdit()
amountEdit = QtGui.QLineEdit()
feeEdit = QtGui.QLineEdit()
grid = QtGui.QGridLayout()
grid.setSpacing(8)
grid.setColumnMinimumWidth(3,300)
grid.setColumnStretch(4,1)
grid.addWidget(QLabel('Pay to'), 1, 0)
grid.addWidget(paytoEdit, 1, 1, 1, 3)
grid.addWidget(QLabel('Description'), 2, 0)
grid.addWidget(descriptionEdit, 2, 1, 1, 3)
grid.addWidget(QLabel('Amount'), 3, 0)
grid.addWidget(amountEdit, 3, 1, 1, 2)
grid.addWidget(QLabel('Fee'), 4, 0)
grid.addWidget(feeEdit, 4, 1, 1, 2)
b = QPushButton("Send")
b.clicked.connect( lambda: self.do_send(paytoEdit,descriptionEdit,amountEdit,feeEdit ) )
grid.addWidget(b, 5, 1)
b = QPushButton("Clear")
b.clicked.connect( lambda: map( lambda x: x.setText(''), [paytoEdit,descriptionEdit,amountEdit,feeEdit] ) )
grid.addWidget(b, 5, 2)
w.setLayout(grid)
w.show()
w2 = QWidget()
vbox = QtGui.QVBoxLayout()
vbox.addWidget(w)
vbox.addStretch(1)
w2.setLayout(vbox)
return w2
def do_send(self, payto_entry, label_entry, amount_entry, fee_entry):
label = str( label_entry.text() )
r = str( payto_entry.text() )
r = r.strip()
m1 = re.match('^(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+)$', r)
m2 = re.match('(|([\w\-\.]+)@)((\w[\w\-]+\.)+[\w\-]+) \<([1-9A-HJ-NP-Za-km-z]{26,})\>', r)
if m1:
to_address = self.get_alias(r, interactive = True)
if not to_address:
return
elif m2:
to_address = m2.group(5)
else:
to_address = r
if not self.wallet.is_valid(to_address):
QMessageBox.warning(self, 'Error', 'Invalid Bitcoin Address:\n'+to_address, 'OK')
return
try:
amount = int( Decimal( str( amount_entry.text())) * 100000000 )
except:
QMessageBox.warning(self, 'Error', 'Invalid Amount', 'OK')
return
try:
fee = int( Decimal( str( fee_entry.text())) * 100000000 )
except:
QMessageBox.warning(self, 'Error', 'Invalid Fee', 'OK')
return
if self.wallet.use_encryption:
password = self.password_dialog()
if not password:
return
else:
password = None
try:
tx = self.wallet.mktx( to_address, amount, label, password, fee )
except BaseException, e:
self.show_message(e.message)
return
status, msg = self.wallet.sendtx( tx )
if status:
QMessageBox.information(self, '', 'Payment sent.\n'+msg, 'OK')
payto_entry.setText("")
label_entry.setText("")
amount_entry.setText("")
fee_entry.setText("")
self.update_contacts_tab()
else:
QMessageBox.warning(self, 'Error', msg, 'OK')
def make_address_list(self, is_recv):
l = QTreeWidget(self)
l.setColumnCount(3)
l.setColumnWidth(0, 350)
l.setColumnWidth(1, 330)
l.setColumnWidth(2, 20)
l.setHeaderLabels( ['Address', 'Label','Tx'])
vbox = QtGui.QVBoxLayout()
vbox.setMargin(0)
vbox.setSpacing(0)
vbox.addWidget(l)
hbox = QtGui.QHBoxLayout()
hbox.setMargin(0)
hbox.setSpacing(0)
qrButton = QtGui.QPushButton("QR")
copyButton = QtGui.QPushButton("Copy to Clipboard")
hbox.addWidget(qrButton)
hbox.addWidget(copyButton)
if not is_recv:
addButton = QtGui.QPushButton("New")
addButton.clicked.connect(self.newaddress_dialog)
paytoButton = QtGui.QPushButton("Pay to")
hbox.addWidget(addButton)
hbox.addWidget(paytoButton)
hbox.addStretch(1)
buttons = QWidget()
buttons.setLayout(hbox)
vbox.addWidget(buttons)
w = QWidget()
w.setLayout(vbox)
return w, l
def create_receive_tab(self):
w, l = self.make_address_list(True)
self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
self.receive_list = l
return w
def create_contacts_tab(self):
w, l = self.make_address_list(False)
self.connect(l, SIGNAL('itemDoubleClicked(QTreeWidgetItem*, int)'), lambda a, b: self.address_label_clicked(a,b,l))
self.connect(l, SIGNAL('itemChanged(QTreeWidgetItem*, int)'), lambda a,b: self.address_label_changed(a,b,l))
self.connect(l, SIGNAL('itemActivated(QTreeWidgetItem*, int)'), self.show_contact_details)
self.contacts_list = l
return w
def update_receive_tab(self):
self.receive_list.clear()
for address in self.wallet.all_addresses():
if self.wallet.is_change(address):continue
label = self.wallet.labels.get(address,'')
n = 0
h = self.wallet.history.get(address,[])
for item in h:
if not item['is_in'] : n=n+1
tx = "None" if n==0 else "%d"%n
item = QTreeWidgetItem( [ address, label, tx] )
item.setFont(0, QFont('monospace'))
self.receive_list.addTopLevelItem(item)
def show_contact_details(self, item, column):
m = str(item.text(0))
a = self.wallet.aliases.get(m)
if a:
if a[0] in self.wallet.authorities.keys():
s = self.wallet.authorities.get(a[0])
else:
s = "self-signed"
msg = 'Alias: '+ m + '\nTarget address: '+ a[1] + '\n\nSigned by: ' + s + '\nSigning address:' + a[0]
QMessageBox.information(self, 'Alias', msg, 'OK')
def update_contacts_tab(self):
self.contacts_list.clear()
for alias, v in self.wallet.aliases.items():
s, target = v
label = self.wallet.labels.get(alias,'')
item = QTreeWidgetItem( [ alias, label, '-'] )
self.contacts_list.addTopLevelItem(item)
for address in self.wallet.addressbook:
label = self.wallet.labels.get(address,'')
n = 0
for item in self.wallet.tx_history.values():
if address in item['outputs'] : n=n+1
tx = "None" if n==0 else "%d"%n
item = QTreeWidgetItem( [ address, label, tx] )
item.setFont(0, QFont('monospace'))
self.contacts_list.addTopLevelItem(item)
def create_wall_tab(self):
self.textbox = textbox = QTextEdit(self)
textbox.setReadOnly(True)
return textbox
def create_status_bar(self):
sb = QStatusBar()
sb.setFixedHeight(35)
hbox = QtGui.QHBoxLayout()
hbox.setMargin(0)
buttons = QWidget()
buttons.setLayout(hbox)
icon = QIcon("icons/lock.svg")
b = QPushButton( icon, '' )
b.setToolTip("Password")
b.setFlat(True)
b.setMaximumWidth(25)
b.clicked.connect(self.change_password_dialog)
hbox.addWidget(b)
icon = QIcon("icons/preferences.svg")
b = QPushButton( icon, '' )
b.setToolTip("Preferences")
b.setFlat(True)
b.setMaximumWidth(25)
b.clicked.connect(self.settings_dialog)
hbox.addWidget(b)
icon = QIcon("icons/seed.png")
b = QPushButton( icon, '' )
b.setToolTip("Seed")
b.setFlat(True)
b.setMaximumWidth(20)
b.clicked.connect(self.show_seed_dialog)
hbox.addWidget(b)
icon = QIcon("icons/status_disconnected.svg")
self.status_button = b = QPushButton( icon, '' )
b.setToolTip("Network")
b.setFlat(True)
b.setMaximumWidth(25)
b.clicked.connect(self.network_dialog)
hbox.addWidget(b)
sb.addPermanentWidget(buttons)
self.setStatusBar(sb)
def newaddress_dialog(self):
text, ok = QtGui.QInputDialog.getText(self, 'New Contact', 'Address:')
address = str(text)
if ok:
if self.wallet.is_valid(address):
self.wallet.addressbook.append(address)
self.wallet.save()
self.update_contacts_tab()
else:
QMessageBox.warning(self, 'Error', 'Invalid Address', 'OK')
def show_seed_dialog(self):
import mnemonic
if self.wallet.use_encryption:
password = self.password_dialog()
if not password: return
else:
password = None
try:
seed = self.wallet.pw_decode( self.wallet.seed, password)
except:
QMessageBox.warning(self, 'Error', 'Invalid Password', 'OK')
return
msg = "Your wallet generation seed is:\n\n" + seed \
+ "\n\nPlease keep it in a safe place; if you lose it, you will not be able to restore your wallet.\n\n" \
+ "Equivalently, your wallet seed can be stored and recovered with the following mnemonic code:\n\n\"" \
+ ' '.join(mnemonic.mn_encode(seed)) + "\""
QMessageBox.information(self, 'Seed', msg, 'OK')
def password_dialog(self):
d = QDialog(self)
d.setModal(1)
pw = QLineEdit()
pw.setEchoMode(2)
grid = QGridLayout()
grid.setSpacing(8)
msg = 'Please enter your password'
grid.addWidget(QLabel(msg), 0, 0, 1, 2)
grid.addWidget(QLabel('Password'), 1, 0)
grid.addWidget(pw, 1, 1)
b = QPushButton("Cancel")
grid.addWidget(b, 5, 1)
b.clicked.connect(d.reject)
b = QPushButton("OK")
grid.addWidget(b, 5, 2)
b.clicked.connect(d.accept)
d.setLayout(grid)
if not d.exec_(): return
return str(pw.text())
def change_password_dialog(self):
d = QDialog(self)
d.setModal(1)
pw = QLineEdit()
pw.setEchoMode(2)
new_pw = QLineEdit()
new_pw.setEchoMode(2)
conf_pw = QLineEdit()
conf_pw.setEchoMode(2)
grid = QGridLayout()
grid.setSpacing(8)
msg = 'Your wallet is encrypted. Use this dialog to change your password.\nTo disable wallet encryption, enter an empty new password.' if self.wallet.use_encryption else 'Your wallet keys are not encrypted'
grid.addWidget(QLabel(msg), 0, 0, 1, 2)
if self.wallet.use_encryption:
grid.addWidget(QLabel('Password'), 1, 0)
grid.addWidget(pw, 1, 1)
grid.addWidget(QLabel('New Password'), 2, 0)
grid.addWidget(new_pw, 2, 1)
grid.addWidget(QLabel('Confirm Password'), 3, 0)
grid.addWidget(conf_pw, 3, 1)
b = QPushButton("Cancel")
grid.addWidget(b, 5, 1)
b.clicked.connect(d.reject)
b = QPushButton("OK")
grid.addWidget(b, 5, 2)
b.clicked.connect(d.accept)
d.setLayout(grid)
if not d.exec_(): return
password = str(pw.text()) if self.wallet.use_encryption else None
new_password = str(new_pw.text())
new_password2 = str(conf_pw.text())
try:
seed = self.wallet.pw_decode( self.wallet.seed, password)
except:
QMessageBox.warning(self, 'Error', 'Incorrect Password', 'OK')
return
if new_password != new_password2:
QMessageBox.warning(self, 'Error', 'Passwords do not match', 'OK')
return
self.wallet.update_password(seed, new_password)
def settings_dialog(self):
d = QDialog(self)
d.setModal(1)
grid = QGridLayout()
grid.setSpacing(8)
msg = 'These are the settings of your wallet'
grid.addWidget(QLabel(msg), 0, 0, 1, 2)
fee_line = QLineEdit()
fee_line.setText("%s"% str( Decimal( self.wallet.fee)/100000000 ) )
grid.addWidget(QLabel('Fee'), 2, 0)
grid.addWidget(fee_line, 2, 1)
b = QPushButton("Cancel")
grid.addWidget(b, 5, 1)
b.clicked.connect(d.reject)
b = QPushButton("OK")
grid.addWidget(b, 5, 2)
b.clicked.connect(d.accept)
d.setLayout(grid)
if not d.exec_(): return
fee = str(fee_line.text())
try:
fee = int( 100000000 * Decimal(fee) )
except:
QMessageBox.warning(self, 'Error', 'Invalid value:%s'%fee, 'OK')
return
self.wallet.fee = fee
self.wallet.save()
def network_dialog(self, parent=True):
wallet = self.wallet
if parent:
if wallet.interface.is_connected:
status = "Connected to %s.\n%d blocks\nresponse time: %f"%(wallet.interface.host, wallet.interface.blocks, wallet.interface.rtime)
else:
status = "Not connected"
host = wallet.interface.host
port = wallet.interface.port
else:
import random
status = "Please choose a server."
host = random.choice( wallet.interface.servers )
port = 50000
d = QDialog(self)
d.setModal(1)
grid = QGridLayout()
grid.setSpacing(8)
grid.addWidget(QLabel(status), 0, 0, 1, 2)
host_line = QLineEdit()
host_line.setText("%s:%d"% (host,port) )
grid.addWidget(QLabel('Server'), 2, 0)
grid.addWidget(host_line, 2, 1)
b = QPushButton("Cancel")
grid.addWidget(b, 5, 1)
b.clicked.connect(d.reject)
b = QPushButton("OK")
grid.addWidget(b, 5, 2)
b.clicked.connect(d.accept)
d.setLayout(grid)
if not d.exec_(): return
hh = str( host_line.text() )
try:
if ':' in hh:
host, port = hh.split(':')
port = int(port)
else:
host = hh
port = 50000
except:
show_message("error")
if parent == None:
sys.exit(1)
else:
return
wallet.interface.set_server(host, port)
if parent:
wallet.save()
class BitcoinGUI():
def __init__(self, wallet):
self.wallet = wallet
def main(self):
s = Sender()
s.start()
app = QApplication(sys.argv)
w = ElectrumWindow(self.wallet)
w.connect_slots(s)
app.exec_()