PageRenderTime 71ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/joinmarket-qt.py

https://gitlab.com/yenny.prathivi/joinmarket
Python | 1226 lines | 1186 code | 21 blank | 19 comment | 12 complexity | 68290906bc0a50575a44cef9a9b416d0 MD5 | raw file
  1. '''
  2. Joinmarket GUI using PyQt for doing Sendpayment.
  3. Some widgets copied and modified from https://github.com/spesmilo/electrum
  4. The latest version of this code is currently maintained at:
  5. https://github.com/AdamISZ/joinmarket/tree/gui
  6. This program is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, either version 3 of the License, or
  9. (at your option) any later version.
  10. This program is distributed in the hope that it will be useful,
  11. but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. GNU General Public License for more details.
  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 sys, base64, textwrap, re, datetime, os, math, json, logging
  18. import Queue, platform
  19. from decimal import Decimal
  20. from functools import partial
  21. from collections import namedtuple
  22. from PyQt4 import QtCore
  23. from PyQt4.QtGui import *
  24. if platform.system() == 'Windows':
  25. MONOSPACE_FONT = 'Lucida Console'
  26. elif platform.system() == 'Darwin':
  27. MONOSPACE_FONT = 'Monaco'
  28. else:
  29. MONOSPACE_FONT = 'monospace'
  30. GREEN_BG = "QWidget {background-color:#80ff80;}"
  31. RED_BG = "QWidget {background-color:#ffcccc;}"
  32. RED_FG = "QWidget {color:red;}"
  33. BLUE_FG = "QWidget {color:blue;}"
  34. BLACK_FG = "QWidget {color:black;}"
  35. import bitcoin as btc
  36. JM_CORE_VERSION = '0.1.3'
  37. JM_GUI_VERSION = '3'
  38. from joinmarket import load_program_config, get_network, Wallet, encryptData, \
  39. get_p2pk_vbyte, jm_single, mn_decode, mn_encode, create_wallet_file, \
  40. validate_address, random_nick, get_log, IRCMessageChannel, \
  41. weighted_order_choose, get_blockchain_interface_instance, joinmarket_alert, \
  42. core_alert
  43. from sendpayment import SendPayment, PT
  44. log = get_log()
  45. donation_address = '1LT6rwv26bV7mgvRosoSCyGM7ttVRsYidP'
  46. donation_address_testnet = 'mz6FQosuiNe8135XaQqWYmXsa3aD8YsqGL'
  47. warnings = {"blockr_privacy": """You are using blockr as your method of
  48. connecting to the blockchain; this means
  49. that blockr.com can see the addresses you
  50. query. This is bad for privacy - consider
  51. using a Bitcoin Core node instead."""}
  52. #configuration types
  53. config_types = {'rpc_port': int,
  54. 'port': int,
  55. 'usessl': bool,
  56. 'socks5': bool,
  57. 'network': bool,
  58. 'socks5_port': int,
  59. 'maker_timeout_sec': int,
  60. 'tx_fees': int,
  61. 'gaplimit': int,
  62. 'check_high_fee': int,
  63. 'max_mix_depth': int,
  64. 'txfee_default': int,
  65. 'order_wait_time': int,
  66. 'privacy_warning': None}
  67. config_tips = {'blockchain_source':
  68. 'options: blockr, bitcoin-rpc',
  69. 'network':
  70. 'one of "testnet" or "mainnet"',
  71. 'rpc_host':
  72. 'the host for bitcoind; only used if blockchain_source is bitcoin-rpc',
  73. 'rpc_port':
  74. 'port for connecting to bitcoind over rpc',
  75. 'rpc_user':
  76. 'user for connecting to bitcoind over rpc',
  77. 'rpc_password':
  78. 'password for connecting to bitcoind over rpc',
  79. 'host':
  80. 'hostname for IRC server',
  81. 'channel':
  82. 'channel name on IRC server',
  83. 'port':
  84. 'port for connecting to IRC server',
  85. 'usessl':
  86. 'check to use SSL for connection to IRC',
  87. 'socks5':
  88. 'check to use SOCKS5 proxy for IRC connection',
  89. 'socks5_host':
  90. 'host for SOCKS5 proxy',
  91. 'socks5_port':
  92. 'port for SOCKS5 proxy',
  93. 'maker_timeout_sec':
  94. 'timeout for waiting for replies from makers',
  95. 'merge_algorithm':
  96. 'for dust sweeping, try merge_algorithm = gradual, \n'+
  97. 'for more rapid dust sweeping, try merge_algorithm = greedy \n'+
  98. 'for most rapid dust sweeping, try merge_algorithm = greediest \n' +
  99. ' but dont forget to bump your miner fees!',
  100. 'tx_fees':
  101. 'the fee estimate is based on a projection of how many satoshis \n'+
  102. 'per kB are needed to get in one of the next N blocks, N set here \n'+
  103. 'as the value of "tx_fees". This estimate is high if you set N=1, \n'+
  104. 'so we choose N=3 for a more reasonable figure, \n'+
  105. 'as our default. Note that for clients not using a local blockchain \n'+
  106. 'instance, we retrieve an estimate from the API at blockcypher.com, currently. \n',
  107. 'gaplimit': 'How far forward to search for used addresses in the HD wallet',
  108. 'check_high_fee': 'Percent fee considered dangerously high, default 2%',
  109. 'max_mix_depth': 'Total number of mixdepths in the wallet, default 5',
  110. 'txfee_default': 'Number of satoshis per counterparty for an initial\n'+
  111. 'tx fee estimate; this value is not usually used and is best left at\n'+
  112. 'the default of 5000',
  113. 'order_wait_time': 'How long to wait for orders to arrive on entering\n'+
  114. 'the message channel, default is 30s'
  115. }
  116. def update_config_for_gui():
  117. '''The default joinmarket config does not contain these GUI settings
  118. (they are generally set by command line flags or not needed).
  119. If they are set in the file, use them, else set the defaults.
  120. These *will* be persisted to joinmarket.cfg, but that will not affect
  121. operation of the command line version.
  122. '''
  123. gui_config_names = ['gaplimit', 'history_file', 'check_high_fee',
  124. 'max_mix_depth', 'txfee_default', 'order_wait_time']
  125. gui_config_default_vals = ['6', 'jm-tx-history.txt', '2', '5', '5000', '30']
  126. if "GUI" not in jm_single().config.sections():
  127. jm_single().config.add_section("GUI")
  128. gui_items = jm_single().config.items("GUI")
  129. for gcn, gcv in zip(gui_config_names, gui_config_default_vals):
  130. if gcn not in [_[0] for _ in gui_items]:
  131. jm_single().config.set("GUI", gcn, gcv)
  132. #Extra setting not exposed to the GUI, but only for the GUI app
  133. if 'privacy_warning' not in [_[0] for _ in gui_items]:
  134. print 'overwriting privacy_warning'
  135. jm_single().config.set("GUI", 'privacy_warning', '1')
  136. def persist_config():
  137. '''This loses all comments in the config file.
  138. TODO: possibly correct that.'''
  139. with open('joinmarket.cfg','w') as f:
  140. jm_single().config.write(f)
  141. class QtHandler(logging.Handler):
  142. def __init__(self):
  143. logging.Handler.__init__(self)
  144. def emit(self, record):
  145. record = self.format(record)
  146. if record: XStream.stdout().write('%s\n'%record)
  147. handler = QtHandler()
  148. handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
  149. log.addHandler(handler)
  150. class XStream(QtCore.QObject):
  151. _stdout = None
  152. _stderr = None
  153. messageWritten = QtCore.pyqtSignal(str)
  154. def flush( self ):
  155. pass
  156. def fileno( self ):
  157. return -1
  158. def write( self, msg ):
  159. if ( not self.signalsBlocked() ):
  160. self.messageWritten.emit(unicode(msg))
  161. @staticmethod
  162. def stdout():
  163. if ( not XStream._stdout ):
  164. XStream._stdout = XStream()
  165. sys.stdout = XStream._stdout
  166. return XStream._stdout
  167. @staticmethod
  168. def stderr():
  169. if ( not XStream._stderr ):
  170. XStream._stderr = XStream()
  171. sys.stderr = XStream._stderr
  172. return XStream._stderr
  173. class Buttons(QHBoxLayout):
  174. def __init__(self, *buttons):
  175. QHBoxLayout.__init__(self)
  176. self.addStretch(1)
  177. for b in buttons:
  178. self.addWidget(b)
  179. class CloseButton(QPushButton):
  180. def __init__(self, dialog):
  181. QPushButton.__init__(self, "Close")
  182. self.clicked.connect(dialog.close)
  183. self.setDefault(True)
  184. class CopyButton(QPushButton):
  185. def __init__(self, text_getter, app):
  186. QPushButton.__init__(self, "Copy")
  187. self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
  188. class CopyCloseButton(QPushButton):
  189. def __init__(self, text_getter, app, dialog):
  190. QPushButton.__init__(self, "Copy and Close")
  191. self.clicked.connect(lambda: app.clipboard().setText(text_getter()))
  192. self.clicked.connect(dialog.close)
  193. self.setDefault(True)
  194. class OkButton(QPushButton):
  195. def __init__(self, dialog, label=None):
  196. QPushButton.__init__(self, label or "OK")
  197. self.clicked.connect(dialog.accept)
  198. self.setDefault(True)
  199. class CancelButton(QPushButton):
  200. def __init__(self, dialog, label=None):
  201. QPushButton.__init__(self, label or "Cancel")
  202. self.clicked.connect(dialog.reject)
  203. class HelpLabel(QLabel):
  204. def __init__(self, text, help_text, wtitle):
  205. QLabel.__init__(self, text)
  206. self.help_text = help_text
  207. self.wtitle = wtitle
  208. self.font = QFont()
  209. self.setStyleSheet(BLUE_FG)
  210. def mouseReleaseEvent(self, x):
  211. QMessageBox.information(w, self.wtitle, self.help_text, 'OK')
  212. def enterEvent(self, event):
  213. self.font.setUnderline(True)
  214. self.setFont(self.font)
  215. app.setOverrideCursor(QCursor(QtCore.Qt.PointingHandCursor))
  216. return QLabel.enterEvent(self, event)
  217. def leaveEvent(self, event):
  218. self.font.setUnderline(False)
  219. self.setFont(self.font)
  220. app.setOverrideCursor(QCursor(QtCore.Qt.ArrowCursor))
  221. return QLabel.leaveEvent(self, event)
  222. def check_password_strength(password):
  223. '''
  224. Check the strength of the password entered by the user and return back the same
  225. :param password: password entered by user in New Password
  226. :return: password strength Weak or Medium or Strong
  227. '''
  228. password = unicode(password)
  229. n = math.log(len(set(password)))
  230. num = re.search("[0-9]", password) is not None and re.match("^[0-9]*$", password) is None
  231. caps = password != password.upper() and password != password.lower()
  232. extra = re.match("^[a-zA-Z0-9]*$", password) is None
  233. score = len(password)*( n + caps + num + extra)/20
  234. password_strength = {0:"Weak",1:"Medium",2:"Strong",3:"Very Strong"}
  235. return password_strength[min(3, int(score))]
  236. def update_password_strength(pw_strength_label,password):
  237. '''
  238. call the function check_password_strength and update the label pw_strength
  239. interactively as the user is typing the password
  240. :param pw_strength_label: the label pw_strength
  241. :param password: password entered in New Password text box
  242. :return: None
  243. '''
  244. if password:
  245. colors = {"Weak":"Red","Medium":"Blue","Strong":"Green",
  246. "Very Strong":"Green"}
  247. strength = check_password_strength(password)
  248. label = "Password Strength"+ ": "+"<font color=" + \
  249. colors[strength] + ">" + strength + "</font>"
  250. else:
  251. label = ""
  252. pw_strength_label.setText(label)
  253. def make_password_dialog(self, msg, new_pass=True):
  254. self.new_pw = QLineEdit()
  255. self.new_pw.setEchoMode(2)
  256. self.conf_pw = QLineEdit()
  257. self.conf_pw.setEchoMode(2)
  258. vbox = QVBoxLayout()
  259. label = QLabel(msg)
  260. label.setWordWrap(True)
  261. grid = QGridLayout()
  262. grid.setSpacing(8)
  263. grid.setColumnMinimumWidth(0, 70)
  264. grid.setColumnStretch(1,1)
  265. #TODO perhaps add an icon here
  266. logo = QLabel()
  267. lockfile = ":icons/lock.png"
  268. logo.setPixmap(QPixmap(lockfile).scaledToWidth(36))
  269. logo.setAlignment(QtCore.Qt.AlignCenter)
  270. grid.addWidget(logo, 0, 0)
  271. grid.addWidget(label, 0, 1, 1, 2)
  272. vbox.addLayout(grid)
  273. grid = QGridLayout()
  274. grid.setSpacing(8)
  275. grid.setColumnMinimumWidth(0, 250)
  276. grid.setColumnStretch(1,1)
  277. grid.addWidget(QLabel('New Password' if new_pass else 'Password'), 1, 0)
  278. grid.addWidget(self.new_pw, 1, 1)
  279. grid.addWidget(QLabel('Confirm Password'), 2, 0)
  280. grid.addWidget(self.conf_pw, 2, 1)
  281. vbox.addLayout(grid)
  282. #Password Strength Label
  283. self.pw_strength = QLabel()
  284. grid.addWidget(self.pw_strength, 3, 0, 1, 2)
  285. self.new_pw.textChanged.connect(lambda: update_password_strength(
  286. self.pw_strength, self.new_pw.text()))
  287. vbox.addStretch(1)
  288. vbox.addLayout(Buttons(CancelButton(self), OkButton(self)))
  289. return vbox
  290. class PasswordDialog(QDialog):
  291. def __init__(self):
  292. super(PasswordDialog, self).__init__()
  293. self.initUI()
  294. def initUI(self):
  295. self.setWindowTitle('Create a new password')
  296. msg = "Enter a new password"
  297. self.setLayout(make_password_dialog(self,msg))
  298. self.show()
  299. class MyTreeWidget(QTreeWidget):
  300. def __init__(self, parent, create_menu, headers, stretch_column=None,
  301. editable_columns=None):
  302. QTreeWidget.__init__(self, parent)
  303. self.parent = parent
  304. self.stretch_column = stretch_column
  305. self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
  306. self.customContextMenuRequested.connect(create_menu)
  307. self.setUniformRowHeights(True)
  308. # extend the syntax for consistency
  309. self.addChild = self.addTopLevelItem
  310. self.insertChild = self.insertTopLevelItem
  311. self.editor = None
  312. self.pending_update = False
  313. if editable_columns is None:
  314. editable_columns = [stretch_column]
  315. self.editable_columns = editable_columns
  316. self.itemActivated.connect(self.on_activated)
  317. self.update_headers(headers)
  318. def update_headers(self, headers):
  319. self.setColumnCount(len(headers))
  320. self.setHeaderLabels(headers)
  321. self.header().setStretchLastSection(False)
  322. for col in range(len(headers)):
  323. sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
  324. self.header().setResizeMode(col, sm)
  325. def editItem(self, item, column):
  326. if column in self.editable_columns:
  327. self.editing_itemcol = (item, column, unicode(item.text(column)))
  328. # Calling setFlags causes on_changed events for some reason
  329. item.setFlags(item.flags() | Qt.ItemIsEditable)
  330. QTreeWidget.editItem(self, item, column)
  331. item.setFlags(item.flags() & ~Qt.ItemIsEditable)
  332. def keyPressEvent(self, event):
  333. if event.key() == QtCore.Qt.Key_F2:
  334. self.on_activated(self.currentItem(), self.currentColumn())
  335. else:
  336. QTreeWidget.keyPressEvent(self, event)
  337. def permit_edit(self, item, column):
  338. return (column in self.editable_columns
  339. and self.on_permit_edit(item, column))
  340. def on_permit_edit(self, item, column):
  341. return True
  342. def on_activated(self, item, column):
  343. if self.permit_edit(item, column):
  344. self.editItem(item, column)
  345. else:
  346. pt = self.visualItemRect(item).bottomLeft()
  347. pt.setX(50)
  348. self.emit(QtCore.SIGNAL('customContextMenuRequested(const QPoint&)'), pt)
  349. def createEditor(self, parent, option, index):
  350. self.editor = QStyledItemDelegate.createEditor(self.itemDelegate(),
  351. parent, option, index)
  352. self.editor.connect(self.editor, QtCore.SIGNAL("editingFinished()"),
  353. self.editing_finished)
  354. return self.editor
  355. def editing_finished(self):
  356. # Long-time QT bug - pressing Enter to finish editing signals
  357. # editingFinished twice. If the item changed the sequence is
  358. # Enter key: editingFinished, on_change, editingFinished
  359. # Mouse: on_change, editingFinished
  360. # This mess is the cleanest way to ensure we make the
  361. # on_edited callback with the updated item
  362. if self.editor:
  363. (item, column, prior_text) = self.editing_itemcol
  364. if self.editor.text() == prior_text:
  365. self.editor = None # Unchanged - ignore any 2nd call
  366. elif item.text(column) == prior_text:
  367. pass # Buggy first call on Enter key, item not yet updated
  368. else:
  369. # What we want - the updated item
  370. self.on_edited(*self.editing_itemcol)
  371. self.editor = None
  372. # Now do any pending updates
  373. if self.editor is None and self.pending_update:
  374. self.pending_update = False
  375. self.on_update()
  376. def on_edited(self, item, column, prior):
  377. '''Called only when the text actually changes'''
  378. key = str(item.data(0, Qt.UserRole).toString())
  379. text = unicode(item.text(column))
  380. self.parent.wallet.set_label(key, text)
  381. if text:
  382. item.setForeground(column, QBrush(QColor('black')))
  383. else:
  384. text = self.parent.wallet.get_default_label(key)
  385. item.setText(column, text)
  386. item.setForeground(column, QBrush(QColor('gray')))
  387. self.parent.history_list.update()
  388. self.parent.update_completions()
  389. def update(self):
  390. # Defer updates if editing
  391. if self.editor:
  392. self.pending_update = True
  393. else:
  394. self.on_update()
  395. def on_update(self):
  396. pass
  397. def get_leaves(self, root):
  398. child_count = root.childCount()
  399. if child_count == 0:
  400. yield root
  401. for i in range(child_count):
  402. item = root.child(i)
  403. for x in self.get_leaves(item):
  404. yield x
  405. def filter(self, p, columns):
  406. p = unicode(p).lower()
  407. for item in self.get_leaves(self.invisibleRootItem()):
  408. item.setHidden(all([unicode(item.text(column)).lower().find(p) == -1
  409. for column in columns]))
  410. class SettingsTab(QDialog):
  411. def __init__(self):
  412. super(SettingsTab, self).__init__()
  413. self.initUI()
  414. def initUI(self):
  415. outerGrid = QGridLayout()
  416. sA = QScrollArea()
  417. sA.setWidgetResizable(True)
  418. frame = QFrame()
  419. grid = QGridLayout()
  420. self.settingsFields = []
  421. j = 0
  422. for i,section in enumerate(jm_single().config.sections()):
  423. pairs = jm_single().config.items(section)
  424. #an awkward design element from the core code: maker_timeout_sec
  425. #is set outside the config, if it doesn't exist in the config.
  426. #Add it here and it will be in the newly updated config file.
  427. if section=='MESSAGING' and 'maker_timeout_sec' not in [_[0] for _ in pairs]:
  428. jm_single().config.set(section, 'maker_timeout_sec', '60')
  429. pairs = jm_single().config.items(section)
  430. newSettingsFields = self.getSettingsFields(section,
  431. [_[0] for _ in pairs])
  432. self.settingsFields.extend(newSettingsFields)
  433. sL = QLabel(section)
  434. sL.setStyleSheet("QLabel {color: blue;}")
  435. grid.addWidget(sL)
  436. j += 1
  437. for k, ns in enumerate(newSettingsFields):
  438. grid.addWidget(ns[0],j,0)
  439. #try to find the tooltip for this label from config tips;
  440. #it might not be there
  441. if str(ns[0].text()) in config_tips:
  442. ttS = config_tips[str(ns[0].text())]
  443. ns[0].setToolTip(ttS)
  444. grid.addWidget(ns[1],j,1)
  445. sfindex = len(self.settingsFields)-len(newSettingsFields)+k
  446. if isinstance(ns[1], QCheckBox):
  447. ns[1].toggled.connect(lambda checked, s=section,
  448. q=sfindex: self.handleEdit(
  449. s, self.settingsFields[q], checked))
  450. else:
  451. ns[1].editingFinished.connect(
  452. lambda q=sfindex, s=section: self.handleEdit(s,
  453. self.settingsFields[q]))
  454. j+=1
  455. outerGrid.addWidget(sA)
  456. sA.setWidget(frame)
  457. frame.setLayout(grid)
  458. frame.adjustSize()
  459. self.setLayout(outerGrid)
  460. self.show()
  461. def handleEdit(self, section, t, checked=None):
  462. if isinstance(t[1], QCheckBox):
  463. if str(t[0].text()) == 'Testnet':
  464. oname = 'network'
  465. oval = 'testnet' if checked else 'mainnet'
  466. add = '' if not checked else ' - Testnet'
  467. w.setWindowTitle(appWindowTitle + add)
  468. else:
  469. oname = str(t[0].text())
  470. oval = 'true' if checked else 'false'
  471. log.debug('setting section: '+section+' and name: '+oname+' to: '+oval)
  472. jm_single().config.set(section,oname,oval)
  473. else: #currently there is only QLineEdit
  474. log.debug('setting section: '+section+' and name: '+
  475. str(t[0].text())+' to: '+str(t[1].text()))
  476. jm_single().config.set(section, str(t[0].text()),str(t[1].text()))
  477. if str(t[0].text())=='blockchain_source':
  478. jm_single().bc_interface = get_blockchain_interface_instance(
  479. jm_single().config)
  480. def getSettingsFields(self, section, names):
  481. results = []
  482. for name in names:
  483. val = jm_single().config.get(section, name)
  484. if name in config_types:
  485. t = config_types[name]
  486. if t == bool:
  487. qt = QCheckBox()
  488. if val=='testnet' or val.lower()=='true':
  489. qt.setChecked(True)
  490. elif not t:
  491. continue
  492. else:
  493. qt = QLineEdit(val)
  494. if t == int:
  495. qt.setValidator(QIntValidator(0, 65535))
  496. else:
  497. qt = QLineEdit(val)
  498. label = 'Testnet' if name=='network' else name
  499. results.append((QLabel(label), qt))
  500. return results
  501. class SpendTab(QWidget):
  502. def __init__(self):
  503. super(SpendTab, self).__init__()
  504. self.initUI()
  505. def initUI(self):
  506. vbox = QVBoxLayout(self)
  507. top = QFrame()
  508. top.setFrameShape(QFrame.StyledPanel)
  509. topLayout = QGridLayout()
  510. top.setLayout(topLayout)
  511. sA = QScrollArea()
  512. sA.setWidgetResizable(True)
  513. topLayout.addWidget(sA)
  514. iFrame = QFrame()
  515. sA.setWidget(iFrame)
  516. innerTopLayout = QGridLayout()
  517. innerTopLayout.setSpacing(4)
  518. iFrame.setLayout(innerTopLayout)
  519. donateLayout = QHBoxLayout()
  520. self.donateCheckBox = QCheckBox()
  521. self.donateCheckBox.setChecked(False)
  522. self.donateCheckBox.setMaximumWidth(30)
  523. self.donateLimitBox = QDoubleSpinBox()
  524. self.donateLimitBox.setMinimum(0.001)
  525. self.donateLimitBox.setMaximum(0.100)
  526. self.donateLimitBox.setSingleStep(0.001)
  527. self.donateLimitBox.setDecimals(3)
  528. self.donateLimitBox.setValue(0.010)
  529. self.donateLimitBox.setMaximumWidth(100)
  530. self.donateLimitBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
  531. donateLayout.addWidget(self.donateCheckBox)
  532. label1 = QLabel("Check to send change lower than: ")
  533. label1.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
  534. donateLayout.addWidget(label1)
  535. donateLayout.setAlignment(label1, QtCore.Qt.AlignLeft)
  536. donateLayout.addWidget(self.donateLimitBox)
  537. donateLayout.setAlignment(self.donateLimitBox, QtCore.Qt.AlignLeft)
  538. label2 = QLabel(" BTC as a donation.")
  539. donateLayout.addWidget(label2)
  540. label2.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
  541. donateLayout.setAlignment(label2, QtCore.Qt.AlignLeft)
  542. label3 = HelpLabel('More','\n'.join(
  543. ['If the calculated change for your transaction',
  544. 'is smaller than the value you choose (default 0.01 btc)',
  545. 'then that change is sent as a donation. If your change',
  546. 'is larger than that, there will be no donation.',
  547. '',
  548. 'As well as helping the developers, this feature can,',
  549. 'in certain circumstances, improve privacy, because there',
  550. 'is no change output that can be linked with your inputs later.']),
  551. 'About the donation feature')
  552. label3.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
  553. donateLayout.setAlignment(label3, QtCore.Qt.AlignLeft)
  554. donateLayout.addWidget(label3)
  555. donateLayout.addStretch(1)
  556. innerTopLayout.addLayout(donateLayout, 0, 0, 1, 2)
  557. self.widgets = self.getSettingsWidgets()
  558. for i, x in enumerate(self.widgets):
  559. innerTopLayout.addWidget(x[0], i+1, 0)
  560. innerTopLayout.addWidget(x[1], i+1, 1, 1, 2)
  561. self.widgets[0][1].editingFinished.connect(lambda : self.checkAddress(
  562. self.widgets[0][1].text()))
  563. self.startButton =QPushButton('Start')
  564. self.startButton.setToolTip('You will be prompted to decide whether to accept\n'+
  565. 'the transaction after connecting, and shown the\n'+
  566. 'fees to pay; you can cancel at that point if you wish.')
  567. self.startButton.clicked.connect(self.startSendPayment)
  568. #TODO: how to make the Abort button work, at least some of the time..
  569. self.abortButton = QPushButton('Abort')
  570. self.abortButton.setEnabled(False)
  571. buttons = QHBoxLayout()
  572. buttons.addStretch(1)
  573. buttons.addWidget(self.startButton)
  574. buttons.addWidget(self.abortButton)
  575. innerTopLayout.addLayout(buttons, len(self.widgets)+1, 0, 1, 2)
  576. splitter1 = QSplitter(QtCore.Qt.Vertical)
  577. self.textedit = QTextEdit()
  578. self.textedit.verticalScrollBar().rangeChanged.connect(self.resizeScroll)
  579. XStream.stdout().messageWritten.connect(self.updateConsoleText)
  580. XStream.stderr().messageWritten.connect(self.updateConsoleText)
  581. splitter1.addWidget(top)
  582. splitter1.addWidget(self.textedit)
  583. splitter1.setSizes([400, 200])
  584. self.setLayout(vbox)
  585. vbox.addWidget(splitter1)
  586. self.show()
  587. def updateConsoleText(self, txt):
  588. #these alerts are a bit suboptimal;
  589. #colored is better, and in the ultra-rare
  590. #case of getting both, one will be swallowed.
  591. #However, the transaction confirmation dialog
  592. #will at least show both in RED and BOLD, and they will be more prominent.
  593. if joinmarket_alert[0]:
  594. w.statusBar().showMessage("JOINMARKET ALERT: " + joinmarket_alert[0])
  595. if core_alert[0]:
  596. w.statusBar().showMessage("BITCOIN CORE ALERT: " + core_alert[0])
  597. self.textedit.insertPlainText(txt)
  598. def resizeScroll(self, mini, maxi):
  599. self.textedit.verticalScrollBar().setValue(maxi)
  600. def startSendPayment(self, ignored_makers = None):
  601. self.aborted = False
  602. if not self.validateSettings():
  603. return
  604. if jm_single().config.get("BLOCKCHAIN", "blockchain_source")=='blockr':
  605. res = self.showBlockrWarning()
  606. if res==True:
  607. return
  608. #all settings are valid; start
  609. QMessageBox.information(self,"Sendpayment","Connecting to IRC.\n"+
  610. "View real-time log in the lower pane.")
  611. self.startButton.setEnabled(False)
  612. self.abortButton.setEnabled(True)
  613. jm_single().nickname = random_nick()
  614. log.debug('starting sendpayment')
  615. w.statusBar().showMessage("Syncing wallet ...")
  616. jm_single().bc_interface.sync_wallet(w.wallet)
  617. self.irc = IRCMessageChannel(jm_single().nickname)
  618. self.destaddr = str(self.widgets[0][1].text())
  619. #convert from bitcoins (enforced by QDoubleValidator) to satoshis
  620. self.btc_amount_str = str(self.widgets[3][1].text())
  621. amount = int(Decimal(self.btc_amount_str)*Decimal('1e8'))
  622. makercount = int(self.widgets[1][1].text())
  623. mixdepth = int(self.widgets[2][1].text())
  624. self.taker = SendPayment(self.irc, w.wallet, self.destaddr, amount,
  625. makercount,
  626. jm_single().config.getint("GUI", "txfee_default"),
  627. jm_single().config.getint("GUI", "order_wait_time"),
  628. mixdepth, False, weighted_order_choose,
  629. isolated=True)
  630. self.pt = PT(self.taker)
  631. if ignored_makers:
  632. self.pt.ignored_makers.extend(ignored_makers)
  633. thread = TaskThread(self)
  634. thread.add(self.runIRC, on_done=self.cleanUp)
  635. w.statusBar().showMessage("Connecting to IRC ...")
  636. thread2 = TaskThread(self)
  637. thread2.add(self.createTxThread, on_done=self.doTx)
  638. def createTxThread(self):
  639. self.orders, self.total_cj_fee, self.cjamount, self.utxos = self.pt.create_tx()
  640. log.debug("Finished create_tx")
  641. #TODO this can't be done in a thread as currently built;
  642. #how else? or fix?
  643. #w.statusBar().showMessage("Found counterparties...")
  644. def doTx(self):
  645. if not self.orders:
  646. QMessageBox.warning(self,"Error","Not enough matching orders found.")
  647. self.giveUp()
  648. return
  649. total_fee_pc = 1.0 * self.total_cj_fee / self.cjamount
  650. #reset the btc amount display string if it's a sweep:
  651. if self.taker.amount == 0:
  652. self.btc_amount_str = str((Decimal(self.cjamount)/Decimal('1e8')))
  653. mbinfo = []
  654. if joinmarket_alert[0]:
  655. mbinfo.append("<b><font color=red>JOINMARKET ALERT: " +
  656. joinmarket_alert[0] + "</font></b>")
  657. mbinfo.append(" ")
  658. if core_alert[0]:
  659. mbinfo.append("<b><font color=red>BITCOIN CORE ALERT: " +
  660. core_alert[0] + "</font></b>")
  661. mbinfo.append(" ")
  662. mbinfo.append("Sending amount: " + self.btc_amount_str + " BTC")
  663. mbinfo.append("to address: " + self.destaddr)
  664. mbinfo.append(" ")
  665. mbinfo.append("Counterparties chosen:")
  666. mbinfo.append('Name, Order id, Coinjoin fee (sat.)')
  667. for k,o in self.orders.iteritems():
  668. if o['ordertype']=='relorder':
  669. display_fee = int(self.cjamount*float(o['cjfee'])) - int(o['txfee'])
  670. elif o['ordertype'] == 'absorder':
  671. display_fee = int(o['cjfee']) - int(o['txfee'])
  672. else:
  673. log.debug("Unsupported order type: " + str(
  674. o['ordertype']) + ", aborting.")
  675. self.giveUp()
  676. return
  677. mbinfo.append(k + ', ' + str(o['oid']) + ', ' + str(display_fee))
  678. mbinfo.append('Total coinjoin fee = ' +str(
  679. self.total_cj_fee) + ' satoshis, or ' + str(float('%.3g' % (
  680. 100.0 * total_fee_pc))) + '%')
  681. title = 'Check Transaction'
  682. if total_fee_pc * 100 > jm_single().config.getint("GUI","check_high_fee"):
  683. title += ': WARNING: Fee is HIGH!!'
  684. reply = QMessageBox.question(self,
  685. title,'\n'.join([m + '<p>' for m in mbinfo]),
  686. QMessageBox.Yes,QMessageBox.No)
  687. if reply == QMessageBox.Yes:
  688. log.debug('You agreed, transaction proceeding')
  689. w.statusBar().showMessage("Building transaction...")
  690. thread3 = TaskThread(self)
  691. log.debug("Trigger is: "+str(self.donateLimitBox.value()))
  692. if get_network()=='testnet':
  693. da = donation_address_testnet
  694. else:
  695. da = donation_address
  696. thread3.add(partial(self.pt.do_tx,self.total_cj_fee, self.orders,
  697. self.cjamount, self.utxos,
  698. self.donateCheckBox.isChecked(),
  699. self.donateLimitBox.value(),
  700. da),
  701. on_done=None)
  702. else:
  703. self.giveUp()
  704. return
  705. def giveUp(self):
  706. self.aborted = True
  707. log.debug("Transaction aborted.")
  708. self.taker.msgchan.shutdown()
  709. self.abortButton.setEnabled(False)
  710. self.startButton.setEnabled(True)
  711. w.statusBar().showMessage("Transaction aborted.")
  712. def cleanUp(self):
  713. if not self.taker.txid:
  714. if not self.aborted:
  715. if not self.pt.ignored_makers:
  716. w.statusBar().showMessage("Transaction failed.")
  717. QMessageBox.warning(self,"Failed","Transaction was not completed.")
  718. else:
  719. reply = QMessageBox.question(self, "Transaction not completed.",
  720. '\n'.join(["The following counterparties did not respond: ",
  721. ','.join(self.pt.ignored_makers),
  722. "This sometimes happens due to bad network connections.",
  723. "",
  724. "If you would like to try again, ignoring those",
  725. "counterparties, click Yes."]), QMessageBox.Yes, QMessageBox.No)
  726. if reply == QMessageBox.Yes:
  727. self.startSendPayment(ignored_makers=self.pt.ignored_makers)
  728. else:
  729. self.giveUp()
  730. return
  731. else:
  732. w.statusBar().showMessage("Transaction completed successfully.")
  733. QMessageBox.information(self,"Success",
  734. "Transaction has been broadcast.\n"+
  735. "Txid: "+str(self.taker.txid))
  736. #persist the transaction to history
  737. with open(jm_single().config.get("GUI", "history_file"),'ab') as f:
  738. f.write(','.join([self.destaddr, self.btc_amount_str,
  739. self.taker.txid,
  740. datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")]))
  741. f.write('\n') #TODO: Windows
  742. #update the TxHistory tab
  743. txhist = w.centralWidget().widget(3)
  744. txhist.updateTxInfo()
  745. self.startButton.setEnabled(True)
  746. self.abortButton.setEnabled(False)
  747. def runIRC(self):
  748. try:
  749. log.debug('starting irc')
  750. self.irc.run()
  751. except:
  752. log.debug('CRASHING, DUMPING EVERYTHING')
  753. debug_dump_object(w.wallet, ['addr_cache', 'keys', 'wallet_name', 'seed'])
  754. debug_dump_object(self.taker)
  755. import traceback
  756. log.debug(traceback.format_exc())
  757. def finishPayment(self):
  758. log.debug("Done")
  759. def validateSettings(self):
  760. valid, errmsg = validate_address(self.widgets[0][1].text())
  761. if not valid:
  762. QMessageBox.warning(self,"Error", errmsg)
  763. return False
  764. errs = ["Number of counterparties must be provided.",
  765. "Mixdepth must be chosen.",
  766. "Amount, in bitcoins, must be provided."
  767. ]
  768. for i in range(1,4):
  769. if self.widgets[i][1].text().size()==0:
  770. QMessageBox.warning(self, "Error",errs[i-1])
  771. return False
  772. if not w.wallet:
  773. QMessageBox.warning(self,"Error","There is no wallet loaded.")
  774. return False
  775. return True
  776. def showBlockrWarning(self):
  777. if jm_single().config.getint("GUI", "privacy_warning") == 0:
  778. return False
  779. qmb = QMessageBox()
  780. qmb.setIcon(QMessageBox.Warning)
  781. qmb.setWindowTitle("Privacy Warning")
  782. qcb = QCheckBox("Don't show this warning again.")
  783. lyt = qmb.layout()
  784. lyt.addWidget(QLabel(warnings['blockr_privacy']), 0, 1)
  785. lyt.addWidget(qcb, 1, 1)
  786. qmb.addButton(QPushButton("Continue"), QMessageBox.YesRole)
  787. qmb.addButton(QPushButton("Cancel"), QMessageBox.NoRole)
  788. qmb.exec_()
  789. switch_off_warning = '0' if qcb.isChecked() else '1'
  790. jm_single().config.set("GUI","privacy_warning", switch_off_warning)
  791. res = qmb.buttonRole(qmb.clickedButton())
  792. if res == QMessageBox.YesRole:
  793. return False
  794. elif res == QMessageBox.NoRole:
  795. return True
  796. else:
  797. log.debug("GUI error: unrecognized button, canceling.")
  798. return True
  799. def checkAddress(self, addr):
  800. valid, errmsg = validate_address(str(addr))
  801. if not valid:
  802. QMessageBox.warning(self, "Error","Bitcoin address not valid.\n"+errmsg)
  803. def getSettingsWidgets(self):
  804. results = []
  805. sN = ['Recipient address', 'Number of counterparties',
  806. 'Mixdepth','Amount in bitcoins (BTC)']
  807. sH = ['The address you want to send the payment to',
  808. 'How many other parties to send to; if you enter 4\n'+
  809. ', there will be 5 participants, including you',
  810. 'The mixdepth of the wallet to send the payment from',
  811. 'The amount IN BITCOINS to send.\n'+
  812. 'If you enter 0, a SWEEP transaction\nwill be performed,'+
  813. ' spending all the coins \nin the given mixdepth.']
  814. sT = [str, int, int, float]
  815. #todo maxmixdepth
  816. sMM = ['',(2,20),(0,jm_single().config.getint("GUI","max_mix_depth")-1),
  817. (0.00000001,100.0,8)]
  818. sD = ['', '3', '0', '']
  819. for x in zip(sN, sH, sT, sD, sMM):
  820. ql = QLabel(x[0])
  821. ql.setToolTip(x[1])
  822. qle = QLineEdit(x[3])
  823. if x[2]==int:
  824. qle.setValidator(QIntValidator(*x[4]))
  825. if x[2]==float:
  826. qle.setValidator(QDoubleValidator(*x[4]))
  827. results.append((ql, qle))
  828. return results
  829. class TxHistoryTab(QWidget):
  830. def __init__(self):
  831. super(TxHistoryTab, self).__init__()
  832. self.initUI()
  833. def initUI(self):
  834. self.tHTW = MyTreeWidget(self,
  835. self.create_menu, self.getHeaders())
  836. self.tHTW.setSelectionMode(QAbstractItemView.ExtendedSelection)
  837. self.tHTW.header().setResizeMode(QHeaderView.Interactive)
  838. self.tHTW.header().setStretchLastSection(False)
  839. self.tHTW.on_update = self.updateTxInfo
  840. vbox = QVBoxLayout()
  841. self.setLayout(vbox)
  842. vbox.setMargin(0)
  843. vbox.setSpacing(0)
  844. vbox.addWidget(self.tHTW)
  845. self.updateTxInfo()
  846. self.show()
  847. def getHeaders(self):
  848. '''Function included in case dynamic in future'''
  849. return ['Receiving address','Amount in BTC','Transaction id','Date']
  850. def updateTxInfo(self, txinfo=None):
  851. self.tHTW.clear()
  852. if not txinfo:
  853. txinfo = self.getTxInfoFromFile()
  854. for t in txinfo:
  855. t_item = QTreeWidgetItem(t)
  856. self.tHTW.addChild(t_item)
  857. for i in range(4):
  858. self.tHTW.resizeColumnToContents(i)
  859. def getTxInfoFromFile(self):
  860. hf = jm_single().config.get("GUI", "history_file")
  861. if not os.path.isfile(hf):
  862. if w:
  863. w.statusBar().showMessage("No transaction history found.")
  864. return []
  865. txhist = []
  866. with open(hf,'rb') as f:
  867. txlines = f.readlines()
  868. for tl in txlines:
  869. txhist.append(tl.strip().split(','))
  870. if not len(txhist[-1])==4:
  871. QMessageBox.warning(self,"Error",
  872. "Incorrectedly formatted file "+hf)
  873. w.statusBar().showMessage("No transaction history found.")
  874. return []
  875. return txhist[::-1] #appended to file in date order, window shows reverse
  876. def create_menu(self, position):
  877. item = self.tHTW.currentItem()
  878. if not item:
  879. return
  880. address_valid = False
  881. if item:
  882. address = str(item.text(0))
  883. try:
  884. btc.b58check_to_hex(address)
  885. address_valid = True
  886. except AssertionError:
  887. log.debug('no btc address found, not creating menu item')
  888. menu = QMenu()
  889. if address_valid:
  890. menu.addAction("Copy address to clipboard",
  891. lambda: app.clipboard().setText(address))
  892. menu.addAction("Copy transaction id to clipboard",
  893. lambda: app.clipboard().setText(str(item.text(2))))
  894. menu.addAction("Copy full tx info to clipboard",
  895. lambda: app.clipboard().setText(
  896. ','.join([str(item.text(_)) for _ in range(4)])))
  897. menu.exec_(self.tHTW.viewport().mapToGlobal(position))
  898. class JMWalletTab(QWidget):
  899. def __init__(self):
  900. super(JMWalletTab, self).__init__()
  901. self.wallet_name = 'NONE'
  902. self.initUI()
  903. def initUI(self):
  904. self.label1 = QLabel(
  905. "CURRENT WALLET: "+self.wallet_name + ', total balance: 0.0',
  906. self)
  907. v = MyTreeWidget(self, self.create_menu, self.getHeaders())
  908. v.setSelectionMode(QAbstractItemView.ExtendedSelection)
  909. v.on_update = self.updateWalletInfo
  910. self.history = v
  911. vbox = QVBoxLayout()
  912. self.setLayout(vbox)
  913. vbox.setMargin(0)
  914. vbox.setSpacing(0)
  915. vbox.addWidget(self.label1)
  916. vbox.addWidget(v)
  917. buttons = QWidget()
  918. vbox.addWidget(buttons)
  919. self.updateWalletInfo()
  920. #vBoxLayout.addWidget(self.label2)
  921. #vBoxLayout.addWidget(self.table)
  922. self.show()
  923. def getHeaders(self):
  924. '''Function included in case dynamic in future'''
  925. return ['Address','Index','Balance','Used/New']
  926. def create_menu(self, position):
  927. item = self.history.currentItem()
  928. address_valid = False
  929. if item:
  930. address = str(item.text(0))
  931. try:
  932. btc.b58check_to_hex(address)
  933. address_valid = True
  934. except AssertionError:
  935. log.debug('no btc address found, not creating menu item')
  936. menu = QMenu()
  937. if address_valid:
  938. menu.addAction("Copy address to clipboard",
  939. lambda: app.clipboard().setText(address))
  940. menu.addAction("Resync wallet from blockchain", lambda: w.resyncWallet())
  941. #TODO add more items to context menu
  942. menu.exec_(self.history.viewport().mapToGlobal(position))
  943. def updateWalletInfo(self, walletinfo=None):
  944. l = self.history
  945. l.clear()
  946. if walletinfo:
  947. self.mainwindow = self.parent().parent().parent()
  948. rows, mbalances, total_bal = walletinfo
  949. if get_network() == 'testnet':
  950. self.wallet_name = self.mainwindow.wallet.seed
  951. else:
  952. self.wallet_name = os.path.basename(self.mainwindow.wallet.path)
  953. self.label1.setText(
  954. "CURRENT WALLET: "+self.wallet_name + ', total balance: '+total_bal)
  955. for i in range(jm_single().config.getint("GUI","max_mix_depth")):
  956. if walletinfo:
  957. mdbalance = mbalances[i]
  958. else:
  959. mdbalance = "{0:.8f}".format(0)
  960. m_item = QTreeWidgetItem(["Mixdepth " +str(i) + " , balance: "+mdbalance,
  961. '','','',''])
  962. l.addChild(m_item)
  963. for forchange in [0,1]:
  964. heading = 'EXTERNAL' if forchange==0 else 'INTERNAL'
  965. heading_end = ' addresses m/0/%d/%d/' % (i, forchange)
  966. heading += heading_end
  967. seq_item = QTreeWidgetItem([ heading, '', '', '', ''])
  968. m_item.addChild(seq_item)
  969. if not forchange:
  970. seq_item.setExpanded(True)
  971. if not walletinfo:
  972. item = QTreeWidgetItem(['None', '', '', ''])
  973. seq_item.addChild(item)
  974. else:
  975. for j in range(len(rows[i][forchange])):
  976. item = QTreeWidgetItem(rows[i][forchange][j])
  977. item.setFont(0,QFont(MONOSPACE_FONT))
  978. if rows[i][forchange][j][3] == 'used':
  979. item.setForeground(3, QBrush(QColor('red')))
  980. seq_item.addChild(item)
  981. class TaskThread(QtCore.QThread):
  982. '''Thread that runs background tasks. Callbacks are guaranteed
  983. to happen in the context of its parent.'''
  984. Task = namedtuple("Task", "task cb_success cb_done cb_error")
  985. doneSig = QtCore.pyqtSignal(object, object, object)
  986. def __init__(self, parent, on_error=None):
  987. super(TaskThread, self).__init__(parent)
  988. self.on_error = on_error
  989. self.tasks = Queue.Queue()
  990. self.doneSig.connect(self.on_done)
  991. self.start()
  992. def add(self, task, on_success=None, on_done=None, on_error=None):
  993. on_error = on_error or self.on_error
  994. self.tasks.put(TaskThread.Task(task, on_success, on_done, on_error))
  995. def run(self):
  996. while True:
  997. task = self.tasks.get()
  998. if not task:
  999. break
  1000. try:
  1001. result = task.task()
  1002. self.doneSig.emit(result, task.cb_done, task.cb_success)
  1003. except BaseException:
  1004. self.doneSig.emit(sys.exc_info(), task.cb_done, task.cb_error)
  1005. def on_done(self, result, cb_done, cb):
  1006. # This runs in the parent's thread.
  1007. if cb_done:
  1008. cb_done()
  1009. if cb:
  1010. cb(result)
  1011. def stop(self):
  1012. self.tasks.put(None)
  1013. class JMMainWindow(QMainWindow):
  1014. def __init__(self):
  1015. super(JMMainWindow, self).__init__()
  1016. self.wallet=None
  1017. self.initUI()
  1018. def closeEvent(self, event):
  1019. quit_msg = "Are you sure you want to quit?"
  1020. reply = QMessageBox.question(self, appWindowTitle, quit_msg,
  1021. QMessageBox.Yes, QMessageBox.No)
  1022. if reply == QMessageBox.Yes:
  1023. persist_config()
  1024. event.accept()
  1025. else:
  1026. event.ignore()
  1027. def initUI(self):
  1028. self.statusBar().showMessage("Ready")
  1029. self.setGeometry(300,300,250,150)
  1030. exitAction = QAction(QIcon('exit.png'), '&Exit', self)
  1031. exitAction.setShortcut('Ctrl+Q')
  1032. exitAction.setStatusTip('Exit application')
  1033. exitAction.triggered.connect(qApp.quit)
  1034. generateAction = QAction('&Generate', self)
  1035. generateAction.setStatusTip('Generate new wallet')
  1036. generateAction.triggered.connect(self.generateWallet)
  1037. loadAction = QAction('&Load', self)
  1038. loadAction.setStatusTip('Load wallet from file')
  1039. loadAction.triggered.connect(self.selectWallet)
  1040. recoverAction = QAction('&Recover', self)
  1041. recoverAction.setStatusTip('Recover wallet from seedphrase')
  1042. recoverAction.triggered.connect(self.recoverWallet)
  1043. aboutAction = QAction('About Joinmarket', self)
  1044. aboutAction.triggered.connect(self.showAboutDialog)
  1045. menubar = QMenuBar()
  1046. walletMenu = menubar.addMenu('&Wallet')
  1047. walletMenu.addAction(loadAction)
  1048. walletMenu.addAction(generateAction)
  1049. walletMenu.addAction(recoverAction)
  1050. walletMenu.addAction(exitAction)
  1051. aboutMenu = menubar.addMenu('&About')
  1052. aboutMenu.addAction(aboutAction)
  1053. self.setMenuBar(menubar)
  1054. self.show()
  1055. def showAboutDialog(self):
  1056. msgbox = QDialog(self)
  1057. lyt = QVBoxLayout(msgbox)
  1058. msgbox.setWindowTitle(appWindowTitle)
  1059. label1 = QLabel()
  1060. label1.setText("<a href="+
  1061. "'https://github.com/joinmarket-org/joinmarket/wiki'>"+
  1062. "Read more about Joinmarket</a><p>"+
  1063. "<p>".join(["Joinmarket core software version: "+JM_CORE_VERSION,
  1064. "JoinmarketQt version: "+JM_GUI_VERSION,
  1065. "Messaging protocol version:"+" %s" % (
  1066. str(jm_single().JM_VERSION)),
  1067. "Help us support Bitcoin fungibility -",
  1068. "donate here: "]))
  1069. label2 = QLabel(donation_address)
  1070. for l in [label1, label2]:
  1071. l.setTextFormat(QtCore.Qt.RichText)
  1072. l.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction)
  1073. l.setOpenExternalLinks(True)
  1074. label2.setText("<a href='bitcoin:"+donation_address+"'>"+donation_address+"</a>")
  1075. lyt.addWidget(label1)
  1076. lyt.addWidget(label2)
  1077. btnbox = QDialogButtonBox(msgbox)
  1078. btnbox.setStandardButtons(QDialogButtonBox.Ok)
  1079. btnbox.accepted.connect(msgbox.accept)
  1080. lyt.addWidget(btnbox)
  1081. msgbox.exec_()
  1082. def recoverWallet(self):
  1083. if get_network()=='testnet':
  1084. QMessageBox.information(self, 'Error',
  1085. 'recover from seedphrase not supported for testnet')
  1086. return
  1087. d = QDialog(self)
  1088. d.setModal(1)
  1089. d.setWindowTitle('Recover from seed')
  1090. layout = QGridLayout(d)
  1091. message_e = QTextEdit()
  1092. layout.addWidget(QLabel('Enter 12 words'), 0, 0)
  1093. layout.addWidget(message_e, 1, 0)
  1094. hbox = QHBoxLayout()
  1095. buttonBox = QDialogButtonBox(self)
  1096. buttonBox.setStandardButtons(QDial