2
################################################################################
4
# Copyright (C) 2011-2014, Armory Technologies, Inc. #
5
# Distributed under the GNU Affero General Public License (AGPL v3) #
6
# See LICENSE or http://www.gnu.org/licenses/agpl.html #
8
################################################################################
10
from datetime import datetime
27
from copy import deepcopy
29
from PyQt4.QtCore import *
30
from PyQt4.QtGui import *
31
from twisted.internet.defer import Deferred
32
from twisted.internet.protocol import Protocol, ClientFactory
34
import CppBlockUtils as Cpp
35
from armoryengine.ALL import *
36
from armorycolors import Colors, htmlColor, QAPP
37
from armorymodels import *
38
from ui.toolsDialogs import MessageSigningVerificationDialog
39
import qrc_img_resources
40
from qtdefines import *
41
from qtdialogs import *
42
from ui.Wizards import WalletWizard, TxWizard
43
from ui.VerifyOfflinePackage import VerifyOfflinePackageDialog
44
from ui.UpgradeDownloader import UpgradeDownloaderDialog
46
from jasvet import verifySignature, readSigBlock
47
from announcefetch import AnnounceDataFetcher, ANNOUNCE_URL, ANNOUNCE_URL_BACKUP,\
48
DEFAULT_FETCH_INTERVAL
49
from armoryengine.parseAnnounce import *
50
from armoryengine.PyBtcWalletRecovery import WalletConsistencyCheck
52
from armoryengine.MultiSigUtils import MultiSigLockbox
53
from ui.MultiSigDialogs import DlgSelectMultiSigOption, DlgLockboxManager, \
54
DlgMergePromNotes, DlgCreatePromNote, DlgImportAsciiBlock
55
from armoryengine.Decorators import RemoveRepeatingExtensions
56
from armoryengine.Block import PyBlock
58
# HACK ALERT: Qt has a bug in OS X where the system font settings will override
59
# the app's settings when a window is activated (e.g., Armory starts, the user
60
# switches to another app, and then switches back to Armory). There is a
61
# workaround, as used by TeXstudio and other programs.
62
# https://bugreports.qt-project.org/browse/QTBUG-5469 - Bug discussion.
63
# http://sourceforge.net/p/texstudio/bugs/594/?page=1 - Fix is mentioned.
64
# http://pyqt.sourceforge.net/Docs/PyQt4/qapplication.html#setDesktopSettingsAware
65
# - Mentions that this must be called before the app (QAPP) is created.
67
QApplication.setDesktopSettingsAware(False)
70
# All the twisted/networking functionality
75
class ArmoryMainWindow(QMainWindow):
76
""" The primary Armory window """
78
#############################################################################
80
def __init__(self, parent=None):
81
super(ArmoryMainWindow, self).__init__(parent)
84
# Load the settings file
85
self.settingsPath = CLI_OPTIONS.settingsPath
86
self.settings = SettingsFile(self.settingsPath)
88
# SETUP THE WINDOWS DECORATIONS
89
self.lblLogoIcon = QLabel()
91
self.setWindowTitle('Armory - Bitcoin Wallet Management [TESTNET]')
92
self.iconfile = ':/armory_icon_green_32x32.png'
93
self.lblLogoIcon.setPixmap(QPixmap(':/armory_logo_green_h56.png'))
95
self.lblLogoIcon.setPixmap(QPixmap(':/armory_logo_white_text_green_h56.png'))
97
self.setWindowTitle('Armory - Bitcoin Wallet Management')
98
self.iconfile = ':/armory_icon_32x32.png'
99
self.lblLogoIcon.setPixmap(QPixmap(':/armory_logo_h44.png'))
100
if Colors.isDarkBkgd:
101
self.lblLogoIcon.setPixmap(QPixmap(':/armory_logo_white_text_h56.png'))
102
self.setWindowIcon(QIcon(self.iconfile))
103
self.lblLogoIcon.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
105
self.netMode = NETWORKMODE.Offline
106
self.abortLoad = False
107
self.memPoolInit = False
108
self.dirtyLastTime = False
109
self.needUpdateAfterScan = True
110
self.sweepAfterScanList = []
111
self.newWalletList = []
112
self.newZeroConfSinceLastUpdate = []
113
self.lastBDMState = ['Uninitialized', None]
114
self.lastSDMState = 'Uninitialized'
115
self.doShutdown = False
116
self.downloadDict = {}
117
self.notAvailErrorCount = 0
118
self.satoshiVerWarnAlready = False
119
self.satoshiLatestVer = None
121
self.downloadDict = {}
122
self.satoshiHomePath = None
123
self.satoshiExeSearchPath = None
124
self.initSyncCircBuff = []
126
self.lastVersionsTxtHash = ''
127
self.dlgCptWlt = None
128
self.torrentFinished = False
129
self.torrentCircBuffer = []
130
self.lastAskedUserStopTorrent = 0
131
self.wasSynchronizing = False
132
self.announceIsSetup = False
133
self.entropyAccum = []
134
self.allLockboxes = []
135
self.lockboxIDMap = {}
136
self.cppLockboxWltMap = {}
138
# Full list of notifications, and notify IDs that should trigger popups
139
# when sending or receiving.
140
self.lastAnnounceUpdate = {}
142
self.downloadLinks = {}
143
self.almostFullNotificationList = {}
144
self.notifyOnSend = set()
145
self.notifyonRecv = set()
146
self.versionNotification = {}
147
self.notifyIgnoreLong = []
148
self.notifyIgnoreShort = []
149
self.maxPriorityID = None
150
self.satoshiVersions = ['',''] # [curr, avail]
151
self.armoryVersions = [getVersionString(BTCARMORY_VERSION), '']
152
self.NetworkingFactory = None
155
# Kick off announcement checking, unless they explicitly disabled it
156
# The fetch happens in the background, we check the results periodically
157
self.announceFetcher = None
158
self.setupAnnouncementFetcher()
160
#delayed URI parsing dict
161
self.delayedURIData = {}
162
self.delayedURIData['qLen'] = 0
164
#Setup the signal to spawn progress dialogs from the main thread
165
self.connect(self, SIGNAL('initTrigger') , self.initTrigger)
166
self.connect(self, SIGNAL('execTrigger'), self.execTrigger)
167
self.connect(self, SIGNAL('checkForNegImports'), self.checkForNegImports)
169
# We want to determine whether the user just upgraded to a new version
170
self.firstLoadNewVersion = False
171
currVerStr = 'v'+getVersionString(BTCARMORY_VERSION)
172
if self.settings.hasSetting('LastVersionLoad'):
173
lastVerStr = self.settings.get('LastVersionLoad')
174
if not lastVerStr==currVerStr:
175
LOGINFO('First load of new version: %s', currVerStr)
176
self.firstLoadNewVersion = True
177
self.settings.set('LastVersionLoad', currVerStr)
179
# Because dynamically retrieving addresses for querying transaction
180
# comments can be so slow, I use this txAddrMap to cache the mappings
181
# between tx's and addresses relevant to our wallets. It really only
182
# matters for massive tx with hundreds of outputs -- but such tx do
183
# exist and this is needed to accommodate wallets with lots of them.
187
self.loadWalletsAndSettings()
189
eulaAgreed = self.getSettingOrSetDefault('Agreed_to_EULA', False)
191
DlgEULA(self,self).exec_()
194
if not self.abortLoad:
195
self.setupNetworking()
197
# setupNetworking may have set this flag if something went wrong
199
LOGWARN('Armory startup was aborted. Closing.')
202
# We need to query this once at the beginning, to avoid having
203
# strange behavior if the user changes the setting but hasn't
205
self.doAutoBitcoind = \
206
self.getSettingOrSetDefault('ManageSatoshi', not OS_MACOSX)
209
# If we're going into online mode, start loading blockchain
210
if self.doAutoBitcoind:
211
self.startBitcoindIfNecessary()
213
self.loadBlockchainIfNecessary()
215
# Setup system tray and register "bitcoin:" URLs with the OS
216
self.setupSystemTray()
217
self.setupUriRegistration()
220
self.extraHeartbeatSpecial = []
221
self.extraHeartbeatAlways = []
222
self.extraHeartbeatOnline = []
223
self.extraNewTxFunctions = []
224
self.extraNewBlockFunctions = []
225
self.extraShutdownFunctions = []
226
self.extraGoOnlineFunctions = []
229
pass a function to extraHeartbeatAlways to run on every heartbeat.
230
pass a list for more control on the function, as
231
[func, [args], keep_running],
234
[args] is a list of arguments
235
keep_running is a bool, pass False to remove the function from
236
extraHeartbeatAlways on the next iteration
240
self.lblArmoryStatus = QRichLabel('<font color=%s>Offline</font> ' %
241
htmlColor('TextWarn'), doWrap=False)
243
self.statusBar().insertPermanentWidget(0, self.lblArmoryStatus)
245
# Table for all the wallets
246
self.walletModel = AllWalletsDispModel(self)
247
self.walletsView = QTableView()
249
w,h = tightSizeNChar(self.walletsView, 55)
252
viewHeight = 4.4*sectionSz
254
self.walletsView.setModel(self.walletModel)
255
self.walletsView.setSelectionBehavior(QTableView.SelectRows)
256
self.walletsView.setSelectionMode(QTableView.SingleSelection)
257
self.walletsView.verticalHeader().setDefaultSectionSize(sectionSz)
258
self.walletsView.setMinimumSize(viewWidth, viewHeight)
259
self.walletsView.setItemDelegate(AllWalletsCheckboxDelegate(self))
260
self.walletsView.horizontalHeader().setResizeMode(0, QHeaderView.Fixed)
264
self.walletsView.hideColumn(0)
265
if self.usermode == USERMODE.Standard:
266
initialColResize(self.walletsView, [20, 0, 0.35, 0.2, 0.2])
268
initialColResize(self.walletsView, [20, 0.15, 0.30, 0.2, 0.20])
271
if self.settings.hasSetting('LastFilterState'):
272
if self.settings.get('LastFilterState')==4:
273
self.walletsView.showColumn(0)
276
self.connect(self.walletsView, SIGNAL('doubleClicked(QModelIndex)'),
277
self.execDlgWalletDetails)
278
self.connect(self.walletsView, SIGNAL('clicked(QModelIndex)'),
281
self.walletsView.setColumnWidth(WLTVIEWCOLS.Visible, 20)
282
w,h = tightSizeNChar(GETFONT('var'), 100)
285
# Prepare for tableView slices (i.e. "Showing 1 to 100 of 382", etc)
286
self.numShowOpts = [100,250,500,1000,'All']
287
self.sortLedgOrder = Qt.AscendingOrder
290
self.currLedgMax = 100
291
self.currLedgWidth = 100
293
# Table to display ledger/activity
294
self.ledgerTable = []
295
self.ledgerModel = LedgerDispModelSimple(self.ledgerTable, self, self)
297
self.ledgerView = QTableView()
298
self.ledgerView.setModel(self.ledgerModel)
299
self.ledgerView.setSortingEnabled(True)
300
self.ledgerView.setItemDelegate(LedgerDispDelegate(self))
301
self.ledgerView.setSelectionBehavior(QTableView.SelectRows)
302
self.ledgerView.setSelectionMode(QTableView.SingleSelection)
304
self.ledgerView.verticalHeader().setDefaultSectionSize(sectionSz)
305
self.ledgerView.verticalHeader().hide()
306
self.ledgerView.horizontalHeader().setResizeMode(0, QHeaderView.Fixed)
307
self.ledgerView.horizontalHeader().setResizeMode(3, QHeaderView.Fixed)
309
self.ledgerView.hideColumn(LEDGERCOLS.isOther)
310
self.ledgerView.hideColumn(LEDGERCOLS.UnixTime)
311
self.ledgerView.hideColumn(LEDGERCOLS.WltID)
312
self.ledgerView.hideColumn(LEDGERCOLS.TxHash)
313
self.ledgerView.hideColumn(LEDGERCOLS.isCoinbase)
314
self.ledgerView.hideColumn(LEDGERCOLS.toSelf)
315
self.ledgerView.hideColumn(LEDGERCOLS.DoubleSpend)
317
# Another table and model, for lockboxes
318
self.lockboxLedgTable = []
319
self.lockboxLedgModel = LedgerDispModelSimple(self.lockboxLedgTable,
320
self, self, isLboxModel=True)
322
dateWidth = tightSizeStr(self.ledgerView, '_9999-Dec-99 99:99pm__')[0]
323
nameWidth = tightSizeStr(self.ledgerView, '9'*32)[0]
324
cWidth = 20 # num-confirm icon width
325
tWidth = 72 # date icon width
326
initialColResize(self.ledgerView, [cWidth, 0, dateWidth, tWidth, 0.30, 0.40, 0.3])
328
self.connect(self.ledgerView, SIGNAL('doubleClicked(QModelIndex)'), \
331
self.ledgerView.setContextMenuPolicy(Qt.CustomContextMenu)
332
self.ledgerView.customContextMenuRequested.connect(self.showContextMenuLedger)
334
btnAddWallet = QPushButton("Create Wallet")
335
btnImportWlt = QPushButton("Import or Restore Wallet")
336
self.connect(btnAddWallet, SIGNAL('clicked()'), self.startWalletWizard)
337
self.connect(btnImportWlt, SIGNAL('clicked()'), self.execImportWallet)
339
# Put the Wallet info into it's own little box
340
lblAvail = QLabel("<b>Available Wallets:</b>")
341
viewHeader = makeLayoutFrame(HORIZONTAL, [lblAvail, \
346
wltFrame.setFrameStyle(QFrame.Box|QFrame.Sunken)
347
wltLayout = QGridLayout()
348
wltLayout.addWidget(viewHeader, 0,0, 1,3)
349
wltLayout.addWidget(self.walletsView, 1,0, 1,3)
350
wltFrame.setLayout(wltLayout)
354
# Make the bottom 2/3 a tabwidget
355
self.mainDisplayTabs = QTabWidget()
357
# Put the labels into scroll areas just in case window size is small.
358
self.tabDashboard = QWidget()
359
self.setupDashboard()
362
# Combo box to filter ledger display
363
self.comboWltSelect = QComboBox()
364
self.populateLedgerComboBox()
365
self.connect(self.ledgerView.horizontalHeader(), \
366
SIGNAL('sortIndicatorChanged(int,Qt::SortOrder)'), \
367
self.changeLedgerSorting)
370
self.connect(self.comboWltSelect, SIGNAL('activated(int)'),
371
self.changeWltFilter)
373
self.lblTot = QRichLabel('<b>Maximum Funds:</b>', doWrap=False);
374
self.lblSpd = QRichLabel('<b>Spendable Funds:</b>', doWrap=False);
375
self.lblUcn = QRichLabel('<b>Unconfirmed:</b>', doWrap=False);
377
self.lblTotalFunds = QRichLabel('-'*12, doWrap=False)
378
self.lblSpendFunds = QRichLabel('-'*12, doWrap=False)
379
self.lblUnconfFunds = QRichLabel('-'*12, doWrap=False)
380
self.lblTotalFunds.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
381
self.lblSpendFunds.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
382
self.lblUnconfFunds.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
384
self.lblTot.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
385
self.lblSpd.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
386
self.lblUcn.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
388
self.lblBTC1 = QRichLabel('<b>BTC</b>', doWrap=False)
389
self.lblBTC2 = QRichLabel('<b>BTC</b>', doWrap=False)
390
self.lblBTC3 = QRichLabel('<b>BTC</b>', doWrap=False)
391
self.ttipTot = self.createToolTipWidget( \
392
'Funds if all current transactions are confirmed. '
393
'Value appears gray when it is the same as your spendable funds.')
394
self.ttipSpd = self.createToolTipWidget( 'Funds that can be spent <i>right now</i>')
395
self.ttipUcn = self.createToolTipWidget( \
396
'Funds that have less than 6 confirmations, and thus should not '
397
'be considered <i>yours</i>, yet.')
400
frmTotals.setFrameStyle(STYLE_NONE)
401
frmTotalsLayout = QGridLayout()
402
frmTotalsLayout.addWidget(self.lblTot, 0,0)
403
frmTotalsLayout.addWidget(self.lblSpd, 1,0)
404
frmTotalsLayout.addWidget(self.lblUcn, 2,0)
406
frmTotalsLayout.addWidget(self.lblTotalFunds, 0,1)
407
frmTotalsLayout.addWidget(self.lblSpendFunds, 1,1)
408
frmTotalsLayout.addWidget(self.lblUnconfFunds, 2,1)
410
frmTotalsLayout.addWidget(self.lblBTC1, 0,2)
411
frmTotalsLayout.addWidget(self.lblBTC2, 1,2)
412
frmTotalsLayout.addWidget(self.lblBTC3, 2,2)
414
frmTotalsLayout.addWidget(self.ttipTot, 0,3)
415
frmTotalsLayout.addWidget(self.ttipSpd, 1,3)
416
frmTotalsLayout.addWidget(self.ttipUcn, 2,3)
418
frmTotals.setLayout(frmTotalsLayout)
422
# Will fill this in when ledgers are created & combined
423
self.lblLedgShowing = QRichLabel('Showing:', hAlign=Qt.AlignHCenter)
424
self.lblLedgRange = QRichLabel('', hAlign=Qt.AlignHCenter)
425
self.lblLedgTotal = QRichLabel('', hAlign=Qt.AlignHCenter)
426
self.comboNumShow = QComboBox()
427
for s in self.numShowOpts:
428
self.comboNumShow.addItem( str(s) )
429
self.comboNumShow.setCurrentIndex(0)
430
self.comboNumShow.setMaximumWidth( tightSizeStr(self, '_9999_')[0]+25 )
433
self.btnLedgUp = QLabelButton('')
434
self.btnLedgUp.setMaximumHeight(20)
435
self.btnLedgUp.setPixmap(QPixmap(':/scroll_up_18.png'))
436
self.btnLedgUp.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
437
self.btnLedgUp.setVisible(False)
439
self.btnLedgDn = QLabelButton('')
440
self.btnLedgDn.setMaximumHeight(20)
441
self.btnLedgDn.setPixmap(QPixmap(':/scroll_down_18.png'))
442
self.btnLedgDn.setAlignment(Qt.AlignVCenter | Qt.AlignHCenter)
445
self.connect(self.comboNumShow, SIGNAL('activated(int)'), self.changeNumShow)
446
self.connect(self.btnLedgUp, SIGNAL('clicked()'), self.clickLedgUp)
447
self.connect(self.btnLedgDn, SIGNAL('clicked()'), self.clickLedgDn)
449
frmFilter = makeVertFrame([QLabel('Filter:'), self.comboWltSelect, 'Stretch'])
451
self.frmLedgUpDown = QFrame()
452
layoutUpDown = QGridLayout()
453
layoutUpDown.addWidget(self.lblLedgShowing,0,0)
454
layoutUpDown.addWidget(self.lblLedgRange, 1,0)
455
layoutUpDown.addWidget(self.lblLedgTotal, 2,0)
456
layoutUpDown.addWidget(self.btnLedgUp, 0,1)
457
layoutUpDown.addWidget(self.comboNumShow, 1,1)
458
layoutUpDown.addWidget(self.btnLedgDn, 2,1)
459
layoutUpDown.setVerticalSpacing(2)
460
self.frmLedgUpDown.setLayout(layoutUpDown)
461
self.frmLedgUpDown.setFrameStyle(STYLE_SUNKEN)
464
frmLower = makeHorizFrame([ frmFilter, \
466
self.frmLedgUpDown, \
470
# Now add the ledger to the bottom of the window
472
ledgFrame.setFrameStyle(QFrame.Box|QFrame.Sunken)
473
ledgLayout = QGridLayout()
474
ledgLayout.addWidget(self.ledgerView, 1,0)
475
ledgLayout.addWidget(frmLower, 2,0)
476
ledgLayout.setRowStretch(0, 0)
477
ledgLayout.setRowStretch(1, 1)
478
ledgLayout.setRowStretch(2, 0)
479
ledgFrame.setLayout(ledgLayout)
481
self.tabActivity = QWidget()
482
self.tabActivity.setLayout(ledgLayout)
484
self.tabAnnounce = QWidget()
485
self.setupAnnounceTab()
488
# Add the available tabs to the main tab widget
489
self.MAINTABS = enum('Dash','Ledger','Announce')
491
self.mainDisplayTabs.addTab(self.tabDashboard, 'Dashboard')
492
self.mainDisplayTabs.addTab(self.tabActivity, 'Transactions')
493
self.mainDisplayTabs.addTab(self.tabAnnounce, 'Announcements')
495
##########################################################################
496
if USE_TESTNET and not CLI_OPTIONS.disableModules:
497
self.loadArmoryModules()
498
##########################################################################
501
btnSendBtc = QPushButton(tr("Send Bitcoins"))
502
btnRecvBtc = QPushButton(tr("Receive Bitcoins"))
503
btnWltProps = QPushButton(tr("Wallet Properties"))
504
btnOfflineTx = QPushButton(tr("Offline Transactions"))
505
btnMultisig = QPushButton(tr("Lockboxes (Multi-Sig)"))
507
self.connect(btnWltProps, SIGNAL('clicked()'), self.execDlgWalletDetails)
508
self.connect(btnRecvBtc, SIGNAL('clicked()'), self.clickReceiveCoins)
509
self.connect(btnSendBtc, SIGNAL('clicked()'), self.clickSendBitcoins)
510
self.connect(btnOfflineTx,SIGNAL('clicked()'), self.execOfflineTx)
511
self.connect(btnMultisig, SIGNAL('clicked()'), self.browseLockboxes)
513
verStr = 'Armory %s / %s User' % (getVersionString(BTCARMORY_VERSION),
514
UserModeStr(self.usermode))
515
lblInfo = QRichLabel(verStr, doWrap=False)
516
lblInfo.setFont(GETFONT('var',10))
517
lblInfo.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
520
logoBtnFrame.append(self.lblLogoIcon)
521
logoBtnFrame.append(btnSendBtc)
522
logoBtnFrame.append(btnRecvBtc)
523
logoBtnFrame.append(btnWltProps)
524
if self.usermode in (USERMODE.Advanced, USERMODE.Expert):
525
logoBtnFrame.append(btnOfflineTx)
526
if self.usermode in (USERMODE.Expert,):
527
logoBtnFrame.append(btnMultisig)
528
logoBtnFrame.append(lblInfo)
529
logoBtnFrame.append('Stretch')
531
btnFrame = makeVertFrame(logoBtnFrame, STYLE_SUNKEN)
533
btnFrame.sizeHint = lambda: QSize(logoWidth*1.0, 10)
534
btnFrame.setMaximumWidth(logoWidth*1.2)
535
btnFrame.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
537
layout = QGridLayout()
538
layout.addWidget(btnFrame, 0, 0, 1, 1)
539
layout.addWidget(wltFrame, 0, 1, 1, 1)
540
layout.addWidget(self.mainDisplayTabs, 1, 0, 1, 2)
541
layout.setRowStretch(0, 1)
542
layout.setRowStretch(1, 5)
544
# Attach the layout to the frame that will become the central widget
546
mainFrame.setLayout(layout)
547
self.setCentralWidget(mainFrame)
548
self.setMinimumSize(750,500)
550
# Start the user at the dashboard
551
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash)
554
##########################################################################
555
# Set up menu and actions
556
#MENUS = enum('File', 'Wallet', 'User', "Tools", "Network")
557
currmode = self.getSettingOrSetDefault('User_Mode', 'Advanced')
558
MENUS = enum('File', 'User', 'Tools', 'Addresses', 'Wallets', \
560
self.menu = self.menuBar()
562
self.menusList.append( self.menu.addMenu('&File') )
563
self.menusList.append( self.menu.addMenu('&User') )
564
self.menusList.append( self.menu.addMenu('&Tools') )
565
self.menusList.append( self.menu.addMenu('&Addresses') )
566
self.menusList.append( self.menu.addMenu('&Wallets') )
567
self.menusList.append( self.menu.addMenu('&MultiSig') )
568
self.menusList.append( self.menu.addMenu('&Help') )
569
#self.menusList.append( self.menu.addMenu('&Network') )
573
if not TheBDM.getBDMState()=='BlockchainReady':
574
QMessageBox.warning(self, 'Transactions Unavailable', \
575
'Transaction history cannot be collected until Armory is '
576
'in online mode. Please try again when Armory is online. ',
580
DlgExportTxHistory(self,self).exec_()
583
actExportTx = self.createAction('&Export Transactions...', exportTx)
584
actSettings = self.createAction('&Settings...', self.openSettings)
585
actMinimApp = self.createAction('&Minimize Armory', self.minimizeArmory)
586
actExportLog = self.createAction('Export &Log File...', self.exportLogFile)
587
actCloseApp = self.createAction('&Quit Armory', self.closeForReal)
588
self.menusList[MENUS.File].addAction(actExportTx)
589
self.menusList[MENUS.File].addAction(actSettings)
590
self.menusList[MENUS.File].addAction(actMinimApp)
591
self.menusList[MENUS.File].addAction(actExportLog)
592
self.menusList[MENUS.File].addAction(actCloseApp)
596
if b: self.setUserMode(USERMODE.Standard)
598
if b: self.setUserMode(USERMODE.Advanced)
600
if b: self.setUserMode(USERMODE.Expert)
602
modeActGrp = QActionGroup(self)
603
actSetModeStd = self.createAction('&Standard', chngStd, True)
604
actSetModeAdv = self.createAction('&Advanced', chngAdv, True)
605
actSetModeDev = self.createAction('&Expert', chngDev, True)
607
modeActGrp.addAction(actSetModeStd)
608
modeActGrp.addAction(actSetModeAdv)
609
modeActGrp.addAction(actSetModeDev)
611
self.menusList[MENUS.User].addAction(actSetModeStd)
612
self.menusList[MENUS.User].addAction(actSetModeAdv)
613
self.menusList[MENUS.User].addAction(actSetModeDev)
617
LOGINFO('Usermode: %s', currmode)
618
self.firstModeSwitch=True
619
if currmode=='Standard':
620
self.usermode = USERMODE.Standard
621
actSetModeStd.setChecked(True)
622
elif currmode=='Advanced':
623
self.usermode = USERMODE.Advanced
624
actSetModeAdv.setChecked(True)
625
elif currmode=='Expert':
626
self.usermode = USERMODE.Expert
627
actSetModeDev.setChecked(True)
629
def openMsgSigning():
630
MessageSigningVerificationDialog(self,self).exec_()
632
def openBlindBroad():
633
if not satoshiIsAvailable():
634
QMessageBox.warning(self, tr("Not Online"), tr("""
635
Bitcoin Core is not available, so Armory will not be able
636
to broadcast any transactions for you."""), QMessageBox.Ok)
638
DlgBroadcastBlindTx(self,self).exec_()
642
actOpenSigner = self.createAction('&Message Signing/Verification...', openMsgSigning)
643
if currmode=='Expert':
644
actOpenTools = self.createAction('&EC Calculator...', lambda: DlgECDSACalc(self,self, 1).exec_())
645
actBlindBroad = self.createAction('&Broadcast Raw Transaction...', openBlindBroad)
647
self.menusList[MENUS.Tools].addAction(actOpenSigner)
648
if currmode=='Expert':
649
self.menusList[MENUS.Tools].addAction(actOpenTools)
650
self.menusList[MENUS.Tools].addAction(actBlindBroad)
653
if not TheBDM.getBDMState()=='BlockchainReady':
654
QMessageBox.warning(self, tr('Offline'), tr("""
655
Armory is currently offline, and cannot determine what funds are
656
available for simulfunding. Please try again when Armory is in
657
online mode."""), QMessageBox.Ok)
659
DlgCreatePromNote(self, self).exec_()
663
title = tr('Import Multi-Spend Transaction')
665
Import a signature-collector text block for review and signing.
666
It is usually a block of text with "TXSIGCOLLECT" in the first line,
667
or a <i>*.sigcollect.tx</i> file.""")
668
ftypes = ['Signature Collectors (*.sigcollect.tx)']
669
dlgImport = DlgImportAsciiBlock(self, self, title, descr, ftypes,
672
if dlgImport.returnObj:
673
DlgMultiSpendReview(self, self, dlgImport.returnObj).exec_()
676
simulMerge = lambda: DlgMergePromNotes(self, self).exec_()
677
actMakeProm = self.createAction('Simulfund &Promissory Note', mkprom)
678
actPromCollect = self.createAction('Simulfund &Collect && Merge', simulMerge)
679
actMultiSpend = self.createAction('Simulfund &Review && Sign', msrevsign)
681
if not self.usermode==USERMODE.Expert:
682
self.menusList[MENUS.MultiSig].menuAction().setVisible(False)
686
actAddrBook = self.createAction('View &Address Book...', self.execAddressBook)
687
actSweepKey = self.createAction('&Sweep Private Key/Address...', self.menuSelectSweepKey)
688
actImportKey = self.createAction('&Import Private Key/Address...', self.menuSelectImportKey)
690
self.menusList[MENUS.Addresses].addAction(actAddrBook)
691
if not currmode=='Standard':
692
self.menusList[MENUS.Addresses].addAction(actImportKey)
693
self.menusList[MENUS.Addresses].addAction(actSweepKey)
695
actCreateNew = self.createAction('&Create New Wallet', self.startWalletWizard)
696
actImportWlt = self.createAction('&Import or Restore Wallet', self.execImportWallet)
697
actAddressBook = self.createAction('View &Address Book', self.execAddressBook)
698
actRecoverWlt = self.createAction('&Fix Damaged Wallet', self.RecoverWallet)
699
#actRescanOnly = self.createAction('Rescan Blockchain', self.forceRescanDB)
700
#actRebuildAll = self.createAction('Rescan with Database Rebuild', self.forceRebuildAndRescan)
702
self.menusList[MENUS.Wallets].addAction(actCreateNew)
703
self.menusList[MENUS.Wallets].addAction(actImportWlt)
704
self.menusList[MENUS.Wallets].addSeparator()
705
self.menusList[MENUS.Wallets].addAction(actRecoverWlt)
706
#self.menusList[MENUS.Wallets].addAction(actRescanOnly)
707
#self.menusList[MENUS.Wallets].addAction(actRebuildAll)
709
#self.menusList[MENUS.Wallets].addAction(actMigrateSatoshi)
710
#self.menusList[MENUS.Wallets].addAction(actAddressBook)
713
self.explicitCheckAnnouncements()
714
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Announce)
716
execAbout = lambda: DlgHelpAbout(self).exec_()
717
execTrouble = lambda: webbrowser.open('https://bitcoinarmory.com/troubleshooting/')
718
execBugReport = lambda: DlgBugReport(self, self).exec_()
721
execVerifySigned = lambda: VerifyOfflinePackageDialog(self, self).exec_()
722
actAboutWindow = self.createAction(tr('&About Armory...'), execAbout)
723
actVersionCheck = self.createAction(tr('Armory Version'), execVersion)
724
actDownloadUpgrade = self.createAction(tr('Update Software...'), self.openDownloaderAll)
725
actVerifySigned = self.createAction(tr('Verify Signed Package...'), execVerifySigned)
726
actTroubleshoot = self.createAction(tr('Troubleshooting Armory'), execTrouble)
727
actSubmitBug = self.createAction(tr('Submit Bug Report'), execBugReport)
728
actClearMemPool = self.createAction(tr('Clear All Unconfirmed'), self.clearMemoryPool)
729
actRescanDB = self.createAction(tr('Rescan Databases'), self.rescanNextLoad)
730
actRebuildDB = self.createAction(tr('Rebuild and Rescan Databases'), self.rebuildNextLoad)
731
actFactoryReset = self.createAction(tr('Factory Reset'), self.factoryReset)
732
actPrivacyPolicy = self.createAction(tr('Armory Privacy Policy'), self.showPrivacyGeneric)
734
self.menusList[MENUS.Help].addAction(actAboutWindow)
735
self.menusList[MENUS.Help].addAction(actVersionCheck)
736
self.menusList[MENUS.Help].addAction(actDownloadUpgrade)
737
self.menusList[MENUS.Help].addAction(actVerifySigned)
738
self.menusList[MENUS.Help].addSeparator()
739
self.menusList[MENUS.Help].addAction(actTroubleshoot)
740
self.menusList[MENUS.Help].addAction(actSubmitBug)
741
self.menusList[MENUS.Help].addAction(actPrivacyPolicy)
742
self.menusList[MENUS.Help].addSeparator()
743
self.menusList[MENUS.Help].addAction(actClearMemPool)
744
self.menusList[MENUS.Help].addAction(actRescanDB)
745
self.menusList[MENUS.Help].addAction(actRebuildDB)
746
self.menusList[MENUS.Help].addAction(actFactoryReset)
750
execMSHack = lambda: DlgSelectMultiSigOption(self,self).exec_()
751
execBrowse = lambda: DlgLockboxManager(self,self).exec_()
752
actMultiHacker = self.createAction(tr('Multi-Sig Lockboxes'), execMSHack)
753
actBrowseLockboxes = self.createAction(tr('Lockbox &Manager...'), execBrowse)
754
#self.menusList[MENUS.MultiSig].addAction(actMultiHacker)
755
self.menusList[MENUS.MultiSig].addAction(actBrowseLockboxes)
756
self.menusList[MENUS.MultiSig].addAction(actMakeProm)
757
self.menusList[MENUS.MultiSig].addAction(actPromCollect)
758
self.menusList[MENUS.MultiSig].addAction(actMultiSpend)
762
# Restore any main-window geometry saved in the settings file
763
hexgeom = self.settings.get('MainGeometry')
764
hexledgsz = self.settings.get('MainLedgerCols')
765
hexwltsz = self.settings.get('MainWalletCols')
767
geom = QByteArray.fromHex(hexgeom)
768
self.restoreGeometry(geom)
770
restoreTableView(self.walletsView, hexwltsz)
772
restoreTableView(self.ledgerView, hexledgsz)
773
self.ledgerView.setColumnWidth(LEDGERCOLS.NumConf, 20)
774
self.ledgerView.setColumnWidth(LEDGERCOLS.TxDir, 72)
778
BDMcurrentBlock[1] = 1
783
self.setDashboardDetails()
785
from twisted.internet import reactor
786
reactor.callLater(0.1, self.execIntroDialog)
787
reactor.callLater(1, self.Heartbeat)
789
if self.getSettingOrSetDefault('MinimizeOnOpen', False) and not CLI_ARGS:
790
LOGINFO('MinimizeOnOpen is True')
791
reactor.callLater(0, self.minimizeArmory)
795
reactor.callLater(1, self.uriLinkClicked, CLI_ARGS[0])
798
####################################################
799
def getWatchingOnlyWallets(self):
801
for wltID in self.walletIDList:
802
if self.walletMap[wltID].watchingOnly:
807
####################################################
808
def changeWltFilter(self):
810
currIdx = max(self.comboWltSelect.currentIndex(), 0)
811
currText = str(self.comboWltSelect.currentText()).lower()
813
if currText.lower().startswith('custom filter'):
814
self.walletsView.showColumn(0)
816
self.walletsView.hideColumn(0)
819
# If "custom" is selected, do nothing...
823
for i in range(len(self.walletVisibleList)):
824
self.walletVisibleList[i] = False
825
self.setWltSetting(self.walletIDList[i], 'LedgerShow', False)
827
# If a specific wallet is selected, just set that and you're done
829
self.walletVisibleList[currIdx-7] = True
830
self.setWltSetting(self.walletIDList[currIdx-7], 'LedgerShow', True)
832
# Else we walk through the wallets and flag the particular ones
833
typelist = [[wid, determineWalletType(self.walletMap[wid], self)[0]] \
834
for wid in self.walletIDList]
836
for i,winfo in enumerate(typelist):
840
doShow = wtype in [WLTTYPES.Offline,WLTTYPES.Crypt,WLTTYPES.Plain]
841
self.walletVisibleList[i] = doShow
842
self.setWltSetting(wid, 'LedgerShow', doShow)
845
doShow = winfo[1] in [WLTTYPES.Offline]
846
self.walletVisibleList[i] = doShow
847
self.setWltSetting(wid, 'LedgerShow', doShow)
850
doShow = winfo[1] in [WLTTYPES.WatchOnly]
851
self.walletVisibleList[i] = doShow
852
self.setWltSetting(wid, 'LedgerShow', doShow)
855
self.walletVisibleList[i] = True
856
self.setWltSetting(wid, 'LedgerShow', True)
858
self.walletsView.reset()
859
self.createCombinedLedger()
860
if self.frmLedgUpDown.isVisible():
864
############################################################################
865
def loadArmoryModules(self):
867
This method checks for any .py files in the exec directory
869
moduleDir = os.path.join(GetExecDir(), 'modules')
870
if not moduleDir or not os.path.exists(moduleDir):
873
LOGWARN('Attempting to load modules from: %s' % moduleDir)
875
from dynamicImport import getModuleList, dynamicImport
877
# This call does not eval any code in the modules. It simply
878
# loads the python files as raw chunks of text so we can
879
# check hashes and signatures
880
modMap = getModuleList(moduleDir)
881
for name,infoMap in modMap.iteritems():
882
modPath = os.path.join(infoMap['SourceDir'], infoMap['Filename'])
883
modHash = binary_to_hex(sha256(infoMap['SourceCode']))
885
isSignedByATI = False
886
if 'Signature' in infoMap:
888
Signature file contains multiple lines, of the form "key=value\n"
889
The last line is the hex-encoded signature, which is over the
890
source code + everything in the sig file up to the last line.
891
The key-value lines may contain properties such as signature
892
validity times/expiration, contact info of author, etc.
894
sigFile = infoMap['SigData']
895
sigLines = [line.strip() for line in sigFile.strip().split('\n')]
896
properties = dict([line.split('=') for line in sigLines[:-1]])
897
msgSigned = infoMap['SourceCode'] + '\x00' + '\n'.join(sigLines[:1])
899
sbdMsg = SecureBinaryData(sha256(msgSigned))
900
sbdSig = SecureBinaryData(hex_to_binary(sigLines[-1]))
901
sbdPub = SecureBinaryData(hex_to_binary(ARMORY_INFO_SIGN_PUBLICKEY))
902
isSignedByATI = CryptoECDSA().VerifyData(sbdMsg, sbdSig, sbdPub)
903
LOGWARN('Sig on "%s" is valid: %s' % (name, str(isSignedByATI)))
906
if not isSignedByATI and not USE_TESTNET:
907
reply = QMessageBox.warning(self, tr("UNSIGNED Module"), tr("""
908
Armory detected the following module which is
909
<font color="%s"><b>unsigned</b></font> and may be dangerous:
911
<b>Module Name:</b> %s<br>
912
<b>Module Path:</b> %s<br>
913
<b>Module Hash:</b> %s<br>
915
You should <u>never</u> trust unsigned modules! At this time,
916
Armory will not allow you to run this module unless you are
917
in testnet mode.""") % \
918
(name, modPath, modHash[:16]), QMessageBox.Ok)
920
if not reply==QMessageBox.Yes:
924
module = dynamicImport(moduleDir, name, globals())
925
plugObj = module.PluginObject(self)
927
if not hasattr(plugObj,'getTabToDisplay') or \
928
not hasattr(plugObj,'tabName'):
929
LOGERROR('Module is malformed! No tabToDisplay or tabName attrs')
930
QMessageBox.critical(self, tr("Bad Module"), tr("""
931
The module you attempted to load (%s) is malformed. It is
932
missing attributes that are needed for Armory to load it.
933
It will be skipped.""") % name, QMessageBox.Ok)
936
verPluginInt = getVersionInt(readVersionString(plugObj.maxVersion))
937
verArmoryInt = getVersionInt(BTCARMORY_VERSION)
938
if verArmoryInt >verPluginInt:
939
reply = QMessageBox.warning(self, tr("Outdated Module"), tr("""
940
Module "%s" is only specified to work up to Armory version %s.
941
You are using Armory version %s. Please remove the module if
942
you experience any problems with it, or contact the maintainer
945
Do you want to continue loading the module?"""),
946
QMessageBox.Yes | QMessageBox.No)
948
if not reply==QMessageBox.Yes:
951
# All plugins should have "tabToDisplay" and "tabName" attributes
952
LOGWARN('Adding module to tab list: "' + plugObj.tabName + '"')
953
self.mainDisplayTabs.addTab(plugObj.getTabToDisplay(), plugObj.tabName)
955
# Also inject any extra methods that will be
957
['injectHeartbeatAlwaysFunc', 'extraHeartbeatAlways'],
958
['injectHeartbeatOnlineFunc', 'extraHeartbeatOnline'],
959
['injectGoOnlineFunc', 'extraGoOnlineFunctions'],
960
['injectNewTxFunc', 'extraNewTxFunctions'],
961
['injectNewBlockFunc', 'extraNewBlockFunctions'],
962
['injectShutdownFunc', 'extraShutdownFunctions'] ]
965
for plugFuncName,funcListName in injectFuncList:
966
if not hasattr(plugObj, plugFuncName):
969
if not hasattr(self, funcListName):
970
LOGERROR('Missing an ArmoryQt list variable: %s' % funcListName)
973
LOGINFO('Found module function: %s' % plugFuncName)
974
funcList = getattr(self, funcListName)
975
plugFunc = getattr(plugObj, plugFuncName)
976
funcList.append(plugFunc)
979
############################################################################
980
def factoryReset(self):
982
reply = QMessageBox.information(self,'Factory Reset', \
983
'You are about to revert all Armory settings '
984
'to the state they were in when Armory was first installed. '
986
'If you click "Yes," Armory will exit after settings are '
987
'reverted. You will have to manually start Armory again.'
989
'Do you want to continue? ', \
990
QMessageBox.Yes | QMessageBox.No)
992
if reply==QMessageBox.Yes:
993
self.removeSettingsOnClose = True
997
if DlgFactoryReset(self,self).exec_():
998
# The dialog already wrote all the flag files, just close now
1002
####################################################
1003
def showPrivacyGeneric(self):
1004
DlgPrivacyPolicy().exec_()
1006
####################################################
1007
def clearMemoryPool(self):
1008
touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag') )
1010
The next time you restart Armory, all unconfirmed transactions will
1011
be cleared allowing you to retry any stuck transactions.""")
1012
if not self.doAutoBitcoind:
1014
<br><br>Make sure you also restart Bitcoin-Qt
1015
(or bitcoind) and let it synchronize again before you restart
1016
Armory. Doing so will clear its memory pool, as well""")
1017
QMessageBox.information(self, tr('Memory Pool'), msg, QMessageBox.Ok)
1021
####################################################
1022
def registerWidgetActivateTime(self, widget):
1023
# This is a bit of a hack, but it's a very isolated method to make
1024
# it easy to link widgets to my entropy accumulator
1026
# I just realized this doesn't do exactly what I originally intended...
1027
# I wanted it to work on arbitrary widgets like QLineEdits, but using
1028
# super is not the answer. What I want is the original class method
1029
# to be called after logging keypress, not its superclass method.
1030
# Nonetheless, it does do what I need it to, as long as you only
1031
# registered frames and dialogs, not individual widgets/controls.
1034
def newKPE(wself, event=None):
1035
mainWindow.logEntropy()
1036
super(wself.__class__, wself).keyPressEvent(event)
1038
def newKRE(wself, event=None):
1039
mainWindow.logEntropy()
1040
super(wself.__class__, wself).keyReleaseEvent(event)
1042
def newMPE(wself, event=None):
1043
mainWindow.logEntropy()
1044
super(wself.__class__, wself).mousePressEvent(event)
1046
def newMRE(wself, event=None):
1047
mainWindow.logEntropy()
1048
super(wself.__class__, wself).mouseReleaseEvent(event)
1050
from types import MethodType
1051
widget.keyPressEvent = MethodType(newKPE, widget)
1052
widget.keyReleaseEvent = MethodType(newKRE, widget)
1053
widget.mousePressEvent = MethodType(newMPE, widget)
1054
widget.mouseReleaseEvent = MethodType(newMRE, widget)
1057
####################################################
1058
def logEntropy(self):
1060
self.entropyAccum.append(RightNow())
1061
self.entropyAccum.append(QCursor.pos().x())
1062
self.entropyAccum.append(QCursor.pos().y())
1064
LOGEXCEPT('Error logging keypress entropy')
1066
####################################################
1067
def getExtraEntropyForKeyGen(self):
1068
# The entropyAccum var has all the timestamps, down to the microsecond,
1069
# of every keypress and mouseclick made during the wallet creation
1070
# wizard. Also logs mouse positions on every press, though it will
1071
# be constant while typing. Either way, even, if they change no text
1072
# and use a 5-char password, we will still pickup about 40 events.
1073
# Then we throw in the [name,time,size] triplets of some volatile
1074
# system directories, and the hash of a file in that directory that
1075
# is expected to have timestamps and system-dependent parameters.
1076
# Finally, take a desktop screenshot...
1077
# All three of these source are likely to have sufficient entropy alone.
1078
source1,self.entropyAccum = self.entropyAccum,[]
1081
LOGERROR('Error getting extra entropy from mouse & key presses')
1087
tempDir = os.getenv('TEMP')
1090
tempDir = '/var/log'
1091
extraFiles = ['/var/log/Xorg.0.log']
1093
tempDir = '/var/log'
1094
extraFiles = ['/var/log/system.log']
1096
# A simple listing of the directory files, sizes and times is good
1097
if os.path.exists(tempDir):
1098
for fname in os.listdir(tempDir):
1099
fullpath = os.path.join(tempDir, fname)
1100
sz = os.path.getsize(fullpath)
1101
tm = os.path.getmtime(fullpath)
1102
source2.append([fname, sz, tm])
1104
# On Linux we also throw in Xorg.0.log
1105
for f in extraFiles:
1106
if os.path.exists(f):
1107
with open(f,'rb') as infile:
1108
source2.append(hash256(infile.read()))
1111
LOGWARN('Second source of supplemental entropy will be empty')
1114
LOGEXCEPT('Error getting extra entropy from filesystem')
1119
pixDesk = QPixmap.grabWindow(QApplication.desktop().winId())
1120
pixRaw = QByteArray()
1121
pixBuf = QBuffer(pixRaw)
1122
pixBuf.open(QIODevice.WriteOnly)
1123
pixDesk.save(pixBuf, 'PNG')
1124
source3 = pixBuf.buffer().toHex()
1126
LOGEXCEPT('Third source of entropy (desktop screenshot) failed')
1129
LOGWARN('Error getting extra entropy from screenshot')
1131
LOGINFO('Adding %d keypress events to the entropy pool', len(source1)/3)
1132
LOGINFO('Adding %s bytes of filesystem data to the entropy pool',
1133
bytesToHumanSize(len(str(source2))))
1134
LOGINFO('Adding %s bytes from desktop screenshot to the entropy pool',
1135
bytesToHumanSize(len(str(source3))/2))
1138
allEntropy = ''.join([str(a) for a in [source1, source1, source3]])
1139
return SecureBinaryData(HMAC256('Armory Entropy', allEntropy))
1144
####################################################
1145
def rescanNextLoad(self):
1146
reply = QMessageBox.warning(self, tr('Queue Rescan?'), tr("""
1147
The next time you restart Armory, it will rescan the blockchain
1148
database, and reconstruct your wallet histories from scratch.
1149
The rescan will take 10-60 minutes depending on your system.
1151
Do you wish to force a rescan on the next Armory restart?"""), \
1152
QMessageBox.Yes | QMessageBox.No)
1153
if reply==QMessageBox.Yes:
1154
touchFile( os.path.join(ARMORY_HOME_DIR, 'rescan.flag') )
1156
####################################################
1157
def rebuildNextLoad(self):
1158
reply = QMessageBox.warning(self, tr('Queue Rebuild?'), tr("""
1159
The next time you restart Armory, it will rebuild and rescan
1160
the entire blockchain database. This operation can take between
1161
30 minutes and 4 hours depending on you system speed.
1163
Do you wish to force a rebuild on the next Armory restart?"""), \
1164
QMessageBox.Yes | QMessageBox.No)
1165
if reply==QMessageBox.Yes:
1166
touchFile( os.path.join(ARMORY_HOME_DIR, 'rebuild.flag') )
1168
####################################################
1169
def loadFailedManyTimesFunc(self, nFail):
1171
For now, if the user is having trouble loading the blockchain, all
1172
we do is delete mempool.bin (which is frequently corrupted but not
1173
detected as such. However, we may expand this in the future, if
1174
it's determined that more-complicated things are necessary.
1176
LOGERROR('%d attempts to load blockchain failed. Remove mempool.bin.' % nFail)
1177
mempoolfile = os.path.join(ARMORY_HOME_DIR,'mempool.bin')
1178
if os.path.exists(mempoolfile):
1179
os.remove(mempoolfile)
1181
LOGERROR('File mempool.bin does not exist. Nothing deleted.')
1183
####################################################
1184
def menuSelectImportKey(self):
1185
QMessageBox.information(self, 'Select Wallet', \
1186
'You must import an address into a specific wallet. If '
1187
'you do not want to import the key into any available wallet, '
1188
'it is recommeneded you make a new wallet for this purpose.'
1190
'Double-click on the desired wallet from the main window, then '
1191
'click on "Import/Sweep Private Keys" on the bottom-right '
1192
'of the properties window.'
1194
'Keys cannot be imported into watching-only wallets, only full '
1195
'wallets.', QMessageBox.Ok)
1197
####################################################
1198
def menuSelectSweepKey(self):
1199
QMessageBox.information(self, 'Select Wallet', \
1200
'You must select a wallet into which funds will be swept. '
1201
'Double-click on the desired wallet from the main window, then '
1202
'click on "Import/Sweep Private Keys" on the bottom-right '
1203
'of the properties window to sweep to that wallet.'
1205
'Keys cannot be swept into watching-only wallets, only full '
1206
'wallets.', QMessageBox.Ok)
1208
####################################################
1209
def changeNumShow(self):
1210
prefWidth = self.numShowOpts[self.comboNumShow.currentIndex()]
1211
if prefWidth=='All':
1212
self.currLedgMin = 1;
1213
self.currLedgMax = self.ledgerSize
1214
self.currLedgWidth = -1;
1216
self.currLedgMax = self.currLedgMin + prefWidth - 1
1217
self.currLedgWidth = prefWidth
1219
self.applyLedgerRange()
1222
####################################################
1223
def clickLedgUp(self):
1224
self.currLedgMin -= self.currLedgWidth
1225
self.currLedgMax -= self.currLedgWidth
1226
self.applyLedgerRange()
1228
####################################################
1229
def clickLedgDn(self):
1230
self.currLedgMin += self.currLedgWidth
1231
self.currLedgMax += self.currLedgWidth
1232
self.applyLedgerRange()
1235
####################################################
1236
def applyLedgerRange(self):
1237
if self.currLedgMin < 1:
1238
toAdd = 1 - self.currLedgMin
1239
self.currLedgMin += toAdd
1240
self.currLedgMax += toAdd
1242
if self.currLedgMax > self.ledgerSize:
1243
toSub = self.currLedgMax - self.ledgerSize
1244
self.currLedgMin -= toSub
1245
self.currLedgMax -= toSub
1247
self.currLedgMin = max(self.currLedgMin, 1)
1249
self.btnLedgUp.setVisible(self.currLedgMin!=1)
1250
self.btnLedgDn.setVisible(self.currLedgMax!=self.ledgerSize)
1252
self.createCombinedLedger()
1256
####################################################
1257
def openSettings(self):
1258
LOGDEBUG('openSettings')
1259
dlgSettings = DlgSettings(self, self)
1262
####################################################
1263
def setupSystemTray(self):
1264
LOGDEBUG('setupSystemTray')
1265
# Creating a QSystemTray
1266
self.sysTray = QSystemTrayIcon(self)
1267
self.sysTray.setIcon( QIcon(self.iconfile) )
1268
self.sysTray.setVisible(True)
1269
self.sysTray.setToolTip('Armory' + (' [Testnet]' if USE_TESTNET else ''))
1270
self.connect(self.sysTray, SIGNAL('messageClicked()'), self.bringArmoryToFront)
1271
self.connect(self.sysTray, SIGNAL('activated(QSystemTrayIcon::ActivationReason)'), \
1272
self.sysTrayActivated)
1276
self.bringArmoryToFront()
1277
self.clickSendBitcoins()
1280
self.bringArmoryToFront()
1281
self.clickReceiveCoins()
1283
actShowArmory = self.createAction('Show Armory', self.bringArmoryToFront)
1284
actSendBtc = self.createAction('Send Bitcoins', traySend)
1285
actRcvBtc = self.createAction('Receive Bitcoins', trayRecv)
1286
actClose = self.createAction('Quit Armory', self.closeForReal)
1287
# Create a short menu of options
1288
menu.addAction(actShowArmory)
1289
menu.addAction(actSendBtc)
1290
menu.addAction(actRcvBtc)
1292
menu.addAction(actClose)
1293
self.sysTray.setContextMenu(menu)
1294
self.notifyQueue = []
1295
self.notifyBlockedUntil = 0
1297
#############################################################################
1299
def registerBitcoinWithFF(self):
1300
#the 3 nodes needed to add to register bitcoin as a protocol in FF
1301
rdfschemehandler = 'about=\"urn:scheme:handler:bitcoin\"'
1302
rdfscheme = 'about=\"urn:scheme:bitcoin\"'
1303
rdfexternalApp = 'about=\"urn:scheme:externalApplication:bitcoin\"'
1305
#find mimeTypes.rdf file
1306
home = os.getenv('HOME')
1307
out,err = execAndWait('find %s -type f -name \"mimeTypes.rdf\"' % home)
1309
for rdfs in out.split('\n'):
1312
FFrdf = open(rdfs, 'r+')
1316
ct = FFrdf.readlines()
1323
if rdfschemehandler in line:
1325
elif rdfscheme in line:
1327
elif rdfexternalApp in line:
1331
#seek to end of file
1335
#add the missing nodes
1337
FFrdf.write(' <RDF:Description RDF:about=\"urn:scheme:handler:bitcoin\"\n')
1338
FFrdf.write(' NC:alwaysAsk=\"false\">\n')
1339
FFrdf.write(' <NC:externalApplication RDF:resource=\"urn:scheme:externalApplication:bitcoin\"/>\n')
1340
FFrdf.write(' <NC:possibleApplication RDF:resource=\"urn:handler:local:/usr/bin/xdg-open\"/>\n')
1341
FFrdf.write(' </RDF:Description>\n')
1345
FFrdf.write(' <RDF:Description RDF:about=\"urn:scheme:bitcoin\"\n')
1346
FFrdf.write(' NC:value=\"bitcoin\">\n')
1347
FFrdf.write(' <NC:handlerProp RDF:resource=\"urn:scheme:handler:bitcoin\"/>\n')
1348
FFrdf.write(' </RDF:Description>\n')
1352
FFrdf.write(' <RDF:Description RDF:about=\"urn:scheme:externalApplication:bitcoin\"\n')
1353
FFrdf.write(' NC:prettyName=\"xdg-open\"\n')
1354
FFrdf.write(' NC:path=\"/usr/bin/xdg-open\" />\n')
1358
FFrdf.write('</RDF:RDF>\n')
1362
#############################################################################
1363
def setupUriRegistration(self, justDoIt=False):
1365
Setup Armory as the default application for handling bitcoin: links
1367
LOGINFO('setupUriRegistration')
1373
out,err = execAndWait('gconftool-2 --get /desktop/gnome/url-handlers/bitcoin/command')
1374
out2,err = execAndWait('xdg-mime query default x-scheme-handler/bitcoin')
1376
#check FF protocol association
1377
#checkFF_thread = threading.Thread(target=self.registerBitcoinWithFF)
1378
#checkFF_thread.start()
1379
self.registerBitcoinWithFF(async=True)
1382
LOGINFO('Setting up Armory as default URI handler...')
1383
execAndWait('gconftool-2 -t string -s /desktop/gnome/url-handlers/bitcoin/command "python /usr/lib/armory/ArmoryQt.py \"%s\""')
1384
execAndWait('gconftool-2 -s /desktop/gnome/url-handlers/bitcoin/needs_terminal false -t bool')
1385
execAndWait('gconftool-2 -t bool -s /desktop/gnome/url-handlers/bitcoin/enabled true')
1386
execAndWait('xdg-mime default armory.desktop x-scheme-handler/bitcoin')
1389
if ('no value' in out.lower() or 'no value' in err.lower()) and not 'armory.desktop' in out2.lower():
1390
# Silently add Armory if it's never been set before
1392
elif (not 'armory' in out.lower() or not 'armory.desktop' in out2.lower()) and not self.firstLoad:
1393
# If another application has it, ask for permission to change it
1394
# Don't bother the user on the first load with it if verification is
1395
# needed. They have enough to worry about with this weird new program...
1396
if not self.getSettingOrSetDefault('DNAA_DefaultApp', False):
1397
reply = MsgBoxWithDNAA(MSGBOX.Question, 'Default URL Handler', \
1398
'Armory is not set as your default application for handling '
1399
'"bitcoin:" links. Would you like to use Armory as the '
1400
'default?', 'Do not ask this question again')
1404
self.writeSetting('DNAA_DefaultApp', True)
1407
# Check for existing registration (user first, then root, if necessary)
1408
action = 'DoNothing'
1409
modulepathname = '"'
1410
if getattr(sys, 'frozen', False):
1411
app_dir = os.path.dirname(sys.executable)
1412
app_path = os.path.join(app_dir, sys.executable)
1414
return #running from a .py script, not gonna register URI on Windows
1418
GetModuleFileNameW = ctypes.windll.kernel32.GetModuleFileNameW
1419
GetModuleFileNameW.restype = ctypes.c_int
1420
app_path = ctypes.create_string_buffer(1024)
1421
rtlength = ctypes.c_int()
1422
rtlength = GetModuleFileNameW(None, ctypes.byref(app_path), 1024)
1423
passstr = str(app_path.raw)
1425
modulepathname += unicode(passstr[0:(rtlength*2)], encoding='utf16') + u'" "%1"'
1426
modulepathname = modulepathname.encode('utf8')
1428
rootKey = 'bitcoin\\shell\\open\\command'
1430
userKey = 'Software\\Classes\\' + rootKey
1431
registryKey = OpenKey(HKEY_CURRENT_USER, userKey, 0, KEY_READ)
1432
val,code = QueryValueEx(registryKey, '')
1433
if 'armory' in val.lower():
1434
if val.lower()==modulepathname.lower():
1435
LOGINFO('Armory already registered for current user. Done!')
1438
action = 'DoIt' #armory is registered, but to another path
1440
# Already set to something (at least created, which is enough)
1443
# No user-key set, check if root-key is set
1445
registryKey = OpenKey(HKEY_CLASSES_ROOT, rootKey, 0, KEY_READ)
1446
val,code = QueryValueEx(registryKey, '')
1447
if 'armory' in val.lower():
1448
LOGINFO('Armory already registered at admin level. Done!')
1451
# Root key is set (or at least created, which is enough)
1456
dontAsk = self.getSettingOrSetDefault('DNAA_DefaultApp', False)
1457
dontAskDefault = self.getSettingOrSetDefault('AlwaysArmoryURI', False)
1459
LOGINFO('URL-register: just doing it')
1461
elif dontAsk and dontAskDefault:
1462
LOGINFO('URL-register: user wants to do it by default')
1464
elif action=='AskUser' and not self.firstLoad and not dontAsk:
1465
# If another application has it, ask for permission to change it
1466
# Don't bother the user on the first load with it if verification is
1467
# needed. They have enough to worry about with this weird new program...
1468
reply = MsgBoxWithDNAA(MSGBOX.Question, 'Default URL Handler', \
1469
'Armory is not set as your default application for handling '
1470
'"bitcoin:" links. Would you like to use Armory as the '
1471
'default?', 'Do not ask this question again')
1474
LOGINFO('URL-register: do not ask again: always %s', str(reply[0]))
1475
self.writeSetting('DNAA_DefaultApp', True)
1476
self.writeSetting('AlwaysArmoryURI', reply[0])
1481
LOGINFO('User requested not to use Armory as URI handler')
1484
# Finally, do it if we're supposed to!
1485
LOGINFO('URL-register action: %s', action)
1488
LOGINFO('Registering Armory for current user')
1489
baseDir = os.path.dirname(unicode(passstr[0:(rtlength*2)], encoding='utf16'))
1491
regKeys.append(['Software\\Classes\\bitcoin', '', 'URL:bitcoin Protocol'])
1492
regKeys.append(['Software\\Classes\\bitcoin', 'URL Protocol', ""])
1493
regKeys.append(['Software\\Classes\\bitcoin\\shell', '', None])
1494
regKeys.append(['Software\\Classes\\bitcoin\\shell\\open', '', None])
1496
for key,name,val in regKeys:
1497
dkey = '%s\\%s' % (key,name)
1498
LOGINFO('\tWriting key: [HKEY_CURRENT_USER\\] ' + dkey)
1499
registryKey = CreateKey(HKEY_CURRENT_USER, key)
1500
SetValueEx(registryKey, name, 0, REG_SZ, val)
1501
CloseKey(registryKey)
1504
regKeysU.append(['Software\\Classes\\bitcoin\\shell\\open\\command', '', \
1506
regKeysU.append(['Software\\Classes\\bitcoin\\DefaultIcon', '', \
1507
'"%s\\armory48x48.ico"' % baseDir])
1508
for key,name,val in regKeysU:
1509
dkey = '%s\\%s' % (key,name)
1510
LOGINFO('\tWriting key: [HKEY_CURRENT_USER\\] ' + dkey)
1511
registryKey = CreateKey(HKEY_CURRENT_USER, key)
1512
#hKey = ctypes.c_int(registryKey.handle)
1513
#ctypes.windll.Advapi32.RegSetValueEx(hKey, None, 0, REG_SZ, val, (len(val)+1))
1514
SetValueEx(registryKey, name, 0, REG_SZ, val)
1515
CloseKey(registryKey)
1518
#############################################################################
1519
def warnNewUSTXFormat(self):
1520
if not self.getSettingOrSetDefault('DNAA_Version092Warn', False):
1521
reply = MsgBoxWithDNAA(MSGBOX.Warning, tr("Version Warning"), tr("""
1522
Since Armory version 0.92 the formats for offline transaction
1523
operations has changed to accommodate multi-signature
1524
transactions. This format is <u>not</u> compatible with
1525
versions of Armory before 0.92.
1527
To continue, the other system will need to be upgraded to
1528
to version 0.92 or later. If you cannot upgrade the other
1529
system, you will need to reinstall an older version of Armory
1530
on this system."""), dnaaMsg='Do not show this warning again')
1531
self.writeSetting('DNAA_Version092Warn', reply[1])
1534
#############################################################################
1535
def execOfflineTx(self):
1536
self.warnNewUSTXFormat()
1538
dlgSelect = DlgOfflineSelect(self, self)
1539
if dlgSelect.exec_():
1541
# If we got here, one of three buttons was clicked.
1542
if dlgSelect.do_create:
1543
DlgSendBitcoins(self.getSelectedWallet(), self, self,
1544
onlyOfflineWallets=True).exec_()
1545
elif dlgSelect.do_broadc:
1546
DlgSignBroadcastOfflineTx(self,self).exec_()
1549
#############################################################################
1551
return QSize(1000, 650)
1553
#############################################################################
1554
def openToolsDlg(self):
1555
QMessageBox.information(self, 'No Tools Yet!', \
1556
'The developer tools are not available yet, but will be added '
1557
'soon. Regardless, developer-mode still offers lots of '
1558
'extra information and functionality that is not available in '
1559
'Standard or Advanced mode.', QMessageBox.Ok)
1563
#############################################################################
1564
def execIntroDialog(self):
1565
if not self.getSettingOrSetDefault('DNAA_IntroDialog', False):
1566
dlg = DlgIntroMessage(self, self)
1567
result = dlg.exec_()
1569
if dlg.chkDnaaIntroDlg.isChecked():
1570
self.writeSetting('DNAA_IntroDialog', True)
1572
if dlg.requestCreate:
1573
self.startWalletWizard()
1575
if dlg.requestImport:
1576
self.execImportWallet()
1580
#############################################################################
1581
def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=False):
1582
'''Create a digital backup of your wallet.'''
1584
LOGERROR('Changing password is not implemented yet!')
1585
raise NotImplementedError
1587
# Set the file name.
1588
if copyType.lower()=='pkcc':
1589
fn = 'armory_%s.%s' % (wlt.uniqueIDB58, suffix)
1591
fn = 'armory_%s_%s.wallet' % (wlt.uniqueIDB58, suffix)
1593
if wlt.watchingOnly and copyType.lower() != 'pkcc':
1594
fn = 'armory_%s_%s.watchonly.wallet' % (wlt.uniqueIDB58, suffix)
1595
savePath = unicode(self.getFileSave(defaultFilename=fn))
1596
if not len(savePath)>0:
1599
# Create the file based on the type you want.
1600
if copyType.lower()=='same':
1601
wlt.writeFreshWalletFile(savePath)
1602
elif copyType.lower()=='decrypt':
1603
if wlt.useEncryption:
1604
dlg = DlgUnlockWallet(wlt, parent, self, 'Unlock Private Keys')
1607
# Wallet should now be unlocked
1608
wlt.makeUnencryptedWalletCopy(savePath)
1609
elif copyType.lower()=='encrypt':
1611
if not wlt.useEncryption:
1612
dlgCrypt = DlgChangePassphrase(parent, self, not wlt.useEncryption)
1613
if not dlgCrypt.exec_():
1614
QMessageBox.information(parent, tr('Aborted'), tr("""
1615
No passphrase was selected for the encrypted backup.
1616
No backup was created"""), QMessageBox.Ok)
1617
newPassphrase = SecureBinaryData(str(dlgCrypt.edtPasswd1.text()))
1619
wlt.makeEncryptedWalletCopy(savePath, newPassphrase)
1620
elif copyType.lower() == 'pkcc':
1621
wlt.writePKCCFile(savePath)
1623
LOGERROR('Invalid "copyType" supplied to makeWalletCopy: %s', copyType)
1626
QMessageBox.information(parent, tr('Backup Complete'), tr("""
1627
Your wallet was successfully backed up to the following
1628
location:<br><br>%s""") % savePath, QMessageBox.Ok)
1632
#############################################################################
1633
def createAction(self, txt, slot, isCheckable=False, \
1634
ttip=None, iconpath=None, shortcut=None):
1636
Modeled from the "Rapid GUI Programming with Python and Qt" book, page 174
1640
icon = QIcon(iconpath)
1642
theAction = QAction(icon, txt, self)
1645
theAction.setCheckable(True)
1646
self.connect(theAction, SIGNAL('toggled(bool)'), slot)
1648
self.connect(theAction, SIGNAL('triggered()'), slot)
1651
theAction.setToolTip(ttip)
1652
theAction.setStatusTip(ttip)
1655
theAction.setShortcut(shortcut)
1660
#############################################################################
1661
def setUserMode(self, mode):
1662
LOGINFO('Changing usermode:')
1663
LOGINFO(' From: %s', self.settings.get('User_Mode'))
1664
self.usermode = mode
1665
if mode==USERMODE.Standard:
1666
self.writeSetting('User_Mode', 'Standard')
1667
if mode==USERMODE.Advanced:
1668
self.writeSetting('User_Mode', 'Advanced')
1669
if mode==USERMODE.Expert:
1670
self.writeSetting('User_Mode', 'Expert')
1671
LOGINFO(' To: %s', self.settings.get('User_Mode'))
1673
if not self.firstModeSwitch:
1674
QMessageBox.information(self,'Restart Armory', \
1675
'You may have to restart Armory for all aspects of '
1676
'the new usermode to go into effect.', QMessageBox.Ok)
1678
self.firstModeSwitch = False
1682
#############################################################################
1683
def getPreferredDateFormat(self):
1684
# Treat the format as "binary" to make sure any special symbols don't
1685
# interfere with the SettingsFile symbols
1686
globalDefault = binary_to_hex(DEFAULT_DATE_FORMAT)
1687
fmt = self.getSettingOrSetDefault('DateFormat', globalDefault)
1688
return hex_to_binary(str(fmt)) # short hex strings could look like int()
1690
#############################################################################
1691
def setPreferredDateFormat(self, fmtStr):
1692
# Treat the format as "binary" to make sure any special symbols don't
1693
# interfere with the SettingsFile symbols
1695
unixTimeToFormatStr(1000000000, fmtStr)
1697
QMessageBox.warning(self, 'Invalid Date Format', \
1698
'The date format you specified was not valid. Please re-enter '
1699
'it using only the strftime symbols shown in the help text.', \
1703
self.writeSetting('DateFormat', binary_to_hex(fmtStr))
1708
#############################################################################
1709
def setupAnnouncementFetcher(self):
1710
# Decide if disable OS/version reporting sent with announce fetches
1711
skipStats1 = self.getSettingOrSetDefault('SkipStatsReport', False)
1712
skipStats2 = CLI_OPTIONS.skipStatsReport
1713
self.skipStatsReport = skipStats1 or skipStats2
1715
# This determines if we should disable all of it
1716
skipChk1 = self.getSettingOrSetDefault('SkipAnnounceCheck', False)
1717
skipChk2 = CLI_OPTIONS.skipAnnounceCheck
1718
skipChk3 = CLI_OPTIONS.offline and not CLI_OPTIONS.testAnnounceCode
1719
skipChk4 = CLI_OPTIONS.useTorSettings
1720
skipChk5 = self.getSettingOrSetDefault('UseTorSettings', False)
1721
self.skipAnnounceCheck = \
1722
skipChk1 or skipChk2 or skipChk3 or skipChk4 or skipChk5
1726
url2 = ANNOUNCE_URL_BACKUP
1727
fetchPath = os.path.join(ARMORY_HOME_DIR, 'atisignedannounce')
1728
if self.announceFetcher is None:
1730
# We keep an ID in the settings file that can be used by ATI's
1731
# statistics aggregator to remove duplicate reports. We store
1732
# the month&year that the ID was generated, so that we can change
1733
# it every month for privacy reasons
1734
idData = self.getSettingOrSetDefault('MonthlyID', '0000_00000000')
1735
storedYM,currID = idData.split('_')
1736
monthyear = unixTimeToFormatStr(RightNow(), '%m%y')
1737
if not storedYM == monthyear:
1738
currID = SecureBinaryData().GenerateRandom(4).toHexStr()
1739
self.settings.set('MonthlyID', '%s_%s' % (monthyear, currID))
1741
self.announceFetcher = AnnounceDataFetcher(url1, url2, fetchPath, currID)
1742
self.announceFetcher.setStatsDisable(self.skipStatsReport)
1743
self.announceFetcher.setFullyDisabled(self.skipAnnounceCheck)
1744
self.announceFetcher.start()
1746
# Set last-updated vals to zero to force processing at startup
1747
for fid in ['changelog, downloads','notify','bootstrap']:
1748
self.lastAnnounceUpdate[fid] = 0
1750
# If we recently updated the settings to enable or disable checking...
1751
if not self.announceFetcher.isRunning() and not self.skipAnnounceCheck:
1752
self.announceFetcher.setFullyDisabled(False)
1753
self.announceFetcher.setFetchInterval(DEFAULT_FETCH_INTERVAL)
1754
self.announceFetcher.start()
1755
elif self.announceFetcher.isRunning() and self.skipAnnounceCheck:
1756
self.announceFetcher.setFullyDisabled(True)
1757
self.announceFetcher.shutdown()
1761
#############################################################################
1762
def processAnnounceData(self, forceCheck=False, forceWait=5):
1764
adf = self.announceFetcher
1768
# The ADF always fetches everything all the time. If forced, do the
1769
# regular fetch first, then examine the individual files without forcing
1771
adf.fetchRightNow(forceWait)
1773
# Check each of the individual files for recent modifications
1775
['announce', self.updateAnnounceTab],
1776
['changelog', self.processChangelog],
1777
['downloads', self.processDownloads],
1778
['notify', self.processNotifications],
1779
['bootstrap', self.processBootstrap] ]
1781
# If modified recently
1782
for fid,func in idFuncPairs:
1783
if not fid in self.lastAnnounceUpdate or \
1784
adf.getFileModTime(fid) > self.lastAnnounceUpdate[fid]:
1785
self.lastAnnounceUpdate[fid] = RightNow()
1786
fileText = adf.getAnnounceFile(fid)
1792
#############################################################################
1793
def processChangelog(self, txt):
1795
clp = changelogParser()
1796
self.changelog = clp.parseChangelogText(txt)
1798
# Don't crash on an error, but do log what happened
1799
LOGEXCEPT('Failed to parse changelog data')
1803
#############################################################################
1804
def processDownloads(self, txt):
1806
dlp = downloadLinkParser()
1807
self.downloadLinks = dlp.parseDownloadList(txt)
1809
if self.downloadLinks is None:
1812
thisVer = getVersionInt(BTCARMORY_VERSION)
1814
# Check ARMORY versions
1815
if not 'Armory' in self.downloadLinks:
1816
LOGWARN('No Armory links in the downloads list')
1819
self.versionNotification = {}
1820
for verStr,vermap in self.downloadLinks['Armory'].iteritems():
1821
dlVer = getVersionInt(readVersionString(verStr))
1824
self.armoryVersions[1] = verStr
1825
if thisVer >= maxVer:
1828
shortDescr = tr('Armory version %s is now available!') % verStr
1829
notifyID = binary_to_hex(hash256(shortDescr)[:4])
1830
self.versionNotification['UNIQUEID'] = notifyID
1831
self.versionNotification['VERSION'] = '0'
1832
self.versionNotification['STARTTIME'] = '0'
1833
self.versionNotification['EXPIRES'] = '%d' % long(UINT64_MAX)
1834
self.versionNotification['CANCELID'] = '[]'
1835
self.versionNotification['MINVERSION'] = '*'
1836
self.versionNotification['MAXVERSION'] = '<%s' % verStr
1837
self.versionNotification['PRIORITY'] = '3072'
1838
self.versionNotification['ALERTTYPE'] = 'Upgrade'
1839
self.versionNotification['NOTIFYSEND'] = 'False'
1840
self.versionNotification['NOTIFYRECV'] = 'False'
1841
self.versionNotification['SHORTDESCR'] = shortDescr
1842
self.versionNotification['LONGDESCR'] = \
1843
self.getVersionNotifyLongDescr(verStr).replace('\n','<br>')
1845
if 'ArmoryTesting' in self.downloadLinks:
1846
for verStr,vermap in self.downloadLinks['ArmoryTesting'].iteritems():
1847
dlVer = getVersionInt(readVersionString(verStr))
1850
self.armoryVersions[1] = verStr
1851
if thisVer >= maxVer:
1854
shortDescr = tr('Armory Testing version %s is now available!') % verStr
1855
notifyID = binary_to_hex(hash256(shortDescr)[:4])
1856
self.versionNotification['UNIQUEID'] = notifyID
1857
self.versionNotification['VERSION'] = '0'
1858
self.versionNotification['STARTTIME'] = '0'
1859
self.versionNotification['EXPIRES'] = '%d' % long(UINT64_MAX)
1860
self.versionNotification['CANCELID'] = '[]'
1861
self.versionNotification['MINVERSION'] = '*'
1862
self.versionNotification['MAXVERSION'] = '<%s' % verStr
1863
self.versionNotification['PRIORITY'] = '1024'
1864
self.versionNotification['ALERTTYPE'] = 'upgrade-testing'
1865
self.versionNotification['NOTIFYSEND'] = 'False'
1866
self.versionNotification['NOTIFYRECV'] = 'False'
1867
self.versionNotification['SHORTDESCR'] = shortDescr
1868
self.versionNotification['LONGDESCR'] = \
1869
self.getVersionNotifyLongDescr(verStr, True).replace('\n','<br>')
1872
# For Satoshi updates, we don't trigger any notifications like we
1873
# do for Armory above -- we will release a proper announcement if
1874
# necessary. But we want to set a flag to
1875
if not 'Satoshi' in self.downloadLinks:
1876
LOGWARN('No Satoshi links in the downloads list')
1880
for verStr,vermap in self.downloadLinks['Satoshi'].iteritems():
1881
dlVer = getVersionInt(readVersionString(verStr))
1884
self.satoshiVersions[1] = verStr
1886
if not self.NetworkingFactory:
1889
# This is to detect the running versions of Bitcoin-Qt/bitcoind
1890
thisVerStr = self.NetworkingFactory.proto.peerInfo['subver']
1891
thisVerStr = thisVerStr.strip('/').split(':')[-1]
1893
if sum([0 if c in '0123456789.' else 1 for c in thisVerStr]) > 0:
1896
self.satoshiVersions[0] = thisVerStr
1905
# Don't crash on an error, but do log what happened
1906
LOGEXCEPT('Failed to parse download link data')
1909
#############################################################################
1910
def getVersionNotifyLongDescr(self, verStr, testing=False):
1919
webURL = 'https://bitcoinarmory.com/download/'
1920
if shortOS is not None:
1921
webURL += '#' + shortOS
1925
A new testing version of Armory is out. You can upgrade to version
1926
%s through our secure downloader inside Armory (link at the bottom
1927
of this notification window).
1931
Your version of Armory is now outdated. Please upgrade to version
1932
%s through our secure downloader inside Armory (link at the bottom
1933
of this notification window). Alternatively, you can get the new
1934
version from our website downloads page at:
1936
<a href="%s">%s</a> """) % (verStr, webURL, webURL)
1940
#############################################################################
1941
def processBootstrap(self, binFile):
1942
# Nothing to process, actually. We'll grab the bootstrap from its
1943
# current location, if needed
1948
#############################################################################
1949
def notificationIsRelevant(self, notifyID, notifyMap):
1950
currTime = RightNow()
1951
thisVerInt = getVersionInt(BTCARMORY_VERSION)
1953
# Ignore transactions below the requested priority
1954
minPriority = self.getSettingOrSetDefault('NotifyMinPriority', 2048)
1955
if int(notifyMap['PRIORITY']) < minPriority:
1958
# Ignore version upgrade notifications if disabled in the settings
1959
if 'upgrade' in notifyMap['ALERTTYPE'].lower() and \
1960
self.getSettingOrSetDefault('DisableUpgradeNotify', False):
1963
if notifyID in self.notifyIgnoreShort:
1966
if notifyMap['STARTTIME'].isdigit():
1967
if currTime < long(notifyMap['STARTTIME']):
1970
if notifyMap['EXPIRES'].isdigit():
1971
if currTime > long(notifyMap['EXPIRES']):
1976
minVerStr = notifyMap['MINVERSION']
1977
minExclude = minVerStr.startswith('>')
1978
minVerStr = minVerStr[1:] if minExclude else minVerStr
1979
minVerInt = getVersionInt(readVersionString(minVerStr))
1980
minVerInt += 1 if minExclude else 0
1981
if thisVerInt < minVerInt:
1988
maxVerStr = notifyMap['MAXVERSION']
1989
maxExclude = maxVerStr.startswith('<')
1990
maxVerStr = maxVerStr[1:] if maxExclude else maxVerStr
1991
maxVerInt = getVersionInt(readVersionString(maxVerStr))
1992
maxVerInt -= 1 if maxExclude else 0
1993
if thisVerInt > maxVerInt:
2001
#############################################################################
2002
def processNotifications(self, txt):
2004
# Keep in mind this will always be run on startup with a blank slate, as
2005
# well as every 30 min while Armory is running. All notifications are
2006
# "new" on startup (though we will allow the user to do-not-show-again
2007
# and store the notification ID in the settings file).
2009
np = notificationParser()
2010
currNotificationList = np.parseNotificationText(txt)
2012
# Don't crash on an error, but do log what happened
2013
LOGEXCEPT('Failed to parse notifications')
2015
if currNotificationList is None:
2016
currNotificationList = {}
2018
# If we have a new-version notification, it's not ignroed, and such
2019
# notifications are not disabled, add it to the list
2020
vnotify = self.versionNotification
2021
if vnotify and 'UNIQUEID' in vnotify:
2022
currNotificationList[vnotify['UNIQUEID']] = deepcopy(vnotify)
2024
# Create a copy of almost all the notifications we have.
2025
# All notifications >= 2048, unless they've explictly allowed testing
2026
# notifications. This will be shown on the "Announcements" tab.
2027
self.almostFullNotificationList = {}
2028
currMin = self.getSettingOrSetDefault('NotifyMinPriority', \
2029
DEFAULT_MIN_PRIORITY)
2030
minmin = min(currMin, DEFAULT_MIN_PRIORITY)
2031
for nid,valmap in currNotificationList.iteritems():
2032
if int(valmap['PRIORITY']) >= minmin:
2033
self.almostFullNotificationList[nid] = deepcopy(valmap)
2037
self.maxPriorityID = None
2039
# Check for new notifications
2040
addedNotifyIDs = set()
2041
irrelevantIDs = set()
2042
for nid,valmap in currNotificationList.iteritems():
2043
if not self.notificationIsRelevant(nid, valmap):
2044
# Can't remove while iterating over the map
2045
irrelevantIDs.add(nid)
2046
self.notifyIgnoreShort.add(nid)
2049
if valmap['PRIORITY'].isdigit():
2050
if int(valmap['PRIORITY']) > tabPriority:
2051
tabPriority = int(valmap['PRIORITY'])
2052
self.maxPriorityID = nid
2054
if not nid in self.almostFullNotificationList:
2055
addedNotifyIDs.append(nid)
2057
# Now remove them from the set that we are working with
2058
for nid in irrelevantIDs:
2059
del currNotificationList[nid]
2061
# Check for notifications we had before but no long have
2062
removedNotifyIDs = []
2063
for nid,valmap in self.almostFullNotificationList.iteritems():
2064
if not nid in currNotificationList:
2065
removedNotifyIDs.append(nid)
2068
#for nid in removedNotifyIDs:
2069
#self.notifyIgnoreShort.discard(nid)
2070
#self.notifyIgnoreLong.discard(nid)
2074
# Change the "Announcements" tab color if something important is there
2075
tabWidgetBar = self.mainDisplayTabs.tabBar()
2076
tabColor = Colors.Foreground
2077
if tabPriority >= 5120:
2078
tabColor = Colors.TextRed
2079
elif tabPriority >= 4096:
2080
tabColor = Colors.TextRed
2081
elif tabPriority >= 3072:
2082
tabColor = Colors.TextBlue
2083
elif tabPriority >= 2048:
2084
tabColor = Colors.TextBlue
2086
tabWidgetBar.setTabTextColor(self.MAINTABS.Announce, tabColor)
2087
self.updateAnnounceTab()
2089
# We only do popups for notifications >=4096, AND upgrade notify
2090
if tabPriority >= 3072:
2091
DlgNotificationWithDNAA(self, self, self.maxPriorityID, \
2092
currNotificationList[self.maxPriorityID]).show()
2094
if not vnotify['UNIQUEID'] in self.notifyIgnoreShort:
2095
DlgNotificationWithDNAA(self,self,vnotify['UNIQUEID'],vnotify).show()
2103
#############################################################################
2105
def setupNetworking(self):
2106
LOGINFO('Setting up networking...')
2107
self.internetAvail = False
2109
# Prevent Armory from being opened twice
2110
from twisted.internet import reactor
2112
def uriClick_partial(a):
2113
self.uriLinkClicked(a)
2115
if CLI_OPTIONS.interport > 1:
2117
self.InstanceListener = ArmoryListenerFactory(self.bringArmoryToFront, \
2119
reactor.listenTCP(CLI_OPTIONS.interport, self.InstanceListener)
2120
except twisted.internet.error.CannotListenError:
2121
LOGWARN('Socket already occupied! This must be a duplicate Armory')
2122
QMessageBox.warning(self, tr('Already Open'), tr("""
2123
Armory is already running! You can only have one Armory open
2124
at a time. Exiting..."""), QMessageBox.Ok)
2127
LOGWARN('*** Listening port is disabled. URI-handling will not work')
2130
settingSkipCheck = self.getSettingOrSetDefault('SkipOnlineCheck', False)
2131
useTor = self.getSettingOrSetDefault('UseTorSettings', False)
2132
self.forceOnline = CLI_OPTIONS.forceOnline or settingSkipCheck or useTor
2134
# Check general internet connection
2135
self.internetAvail = False
2136
if self.forceOnline:
2137
LOGINFO('Skipping online check, forcing online mode')
2141
response=urllib2.urlopen('http://google.com', timeout=CLI_OPTIONS.nettimeout)
2142
self.internetAvail = True
2144
LOGERROR('No module urllib2 -- cannot determine if internet is available')
2145
except urllib2.URLError:
2146
# In the extremely rare case that google might be down (or just to try again...)
2148
response=urllib2.urlopen('http://microsoft.com', timeout=CLI_OPTIONS.nettimeout)
2150
LOGEXCEPT('Error checking for internet connection')
2151
LOGERROR('Run --skip-online-check if you think this is an error')
2152
self.internetAvail = False
2154
LOGEXCEPT('Error checking for internet connection')
2155
LOGERROR('Run --skip-online-check if you think this is an error')
2156
self.internetAvail = False
2159
LOGINFO('Internet connection is Available: %s', self.internetAvail)
2160
LOGINFO('Bitcoin-Qt/bitcoind is Available: %s', satoshiIsAvailable())
2161
LOGINFO('The first blk*.dat was Available: %s', str(self.checkHaveBlockfiles()))
2162
LOGINFO('Online mode currently possible: %s', self.onlineModeIsPossible())
2168
#############################################################################
2169
def manageBitcoindAskTorrent(self):
2171
if not satoshiIsAvailable():
2172
reply = MsgBoxCustom(MSGBOX.Question, tr('BitTorrent Option'), tr("""
2173
You are currently configured to run the core Bitcoin software
2174
yourself (Bitcoin-Qt or bitcoind). <u>Normally</u>, you should
2175
start the Bitcoin software first and wait for it to synchronize
2176
with the network before starting Armory.
2178
<b>However</b>, Armory can shortcut most of this initial
2180
for you using BitTorrent. If your firewall allows it,
2181
using BitTorrent can be an order of magnitude faster (2x to 20x)
2182
than letting the Bitcoin software download it via P2P.
2184
<u>To synchronize using BitTorrent (recommended):</u>
2185
Click "Use BitTorrent" below, and <u>do not</u> start the Bitcoin
2186
software until after it is complete.
2188
<u>To synchronize using Bitcoin P2P (fallback):</u>
2189
Click "Cancel" below, then close Armory and start Bitcoin-Qt
2190
(or bitcoind). Do not start Armory until you see a green checkmark
2191
in the bottom-right corner of the Bitcoin-Qt window."""), \
2192
wCancel=True, yesStr='Use BitTorrent')
2195
QMessageBox.warning(self, tr('Synchronize'), tr("""
2196
When you are ready to start synchronization, close Armory and
2197
start Bitcoin-Qt or bitcoind. Restart Armory only when
2198
synchronization is complete. If using Bitcoin-Qt, you will see
2199
a green checkmark in the bottom-right corner"""), QMessageBox.Ok)
2203
reply = MsgBoxCustom(MSGBOX.Question, tr('BitTorrent Option'), tr("""
2204
You are currently running the core Bitcoin software, but it
2205
is not fully synchronized with the network, yet. <u>Normally</u>,
2206
you should close Armory until Bitcoin-Qt (or bitcoind) is
2209
<b><u>However</u></b>, Armory can speed up this initial
2210
synchronization for you using BitTorrent. If your firewall
2211
allows it, using BitTorrent can be an order of magnitude
2213
than letting the Bitcoin software download it via P2P.
2215
<u>To synchronize using BitTorrent (recommended):</u>
2216
Close the running Bitcoin software <b>right now</b>. When it is
2217
closed, click "Use BitTorrent" below. Restart the Bitcoin software
2218
when Armory indicates it is complete.
2220
<u>To synchronize using Bitcoin P2P (fallback):</u>
2221
Click "Cancel" below, and then close Armory until the Bitcoin
2222
software is finished synchronizing. If using Bitcoin-Qt, you
2223
will see a green checkmark in the bottom-right corner of the
2224
main window."""), QMessageBox.Ok)
2227
if satoshiIsAvailable():
2228
QMessageBox.warning(self, tr('Still Running'), tr("""
2229
The Bitcoin software still appears to be open!
2230
Close it <b>right now</b>
2231
before clicking "Ok." The BitTorrent engine will start
2232
as soon as you do."""), QMessageBox.Ok)
2234
QMessageBox.warning(self, tr('Synchronize'), tr("""
2235
You chose to finish synchronizing with the network using
2236
the Bitcoin software which is already running. Please close
2237
Armory until it is finished. If you are running Bitcoin-Qt,
2238
you will see a green checkmark in the bottom-right corner,
2239
when it is time to open Armory again."""), QMessageBox.Ok)
2245
############################################################################
2246
def findTorrentFileForSDM(self, forceWaitTime=0):
2248
Hopefully the announcement fetcher has already gotten one for us,
2249
or at least we have a default.
2252
# Only do an explicit announce check if we have no bootstrap at all
2253
# (don't need to spend time doing an explicit check if we have one)
2254
if self.announceFetcher.getFileModTime('bootstrap') == 0:
2256
self.explicitCheckAnnouncements(forceWaitTime)
2258
# If it's still not there, look for a default file
2259
if self.announceFetcher.getFileModTime('bootstrap') == 0:
2260
LOGERROR('Could not get announce bootstrap; using default')
2261
srcTorrent = os.path.join(GetExecDir(), 'default_bootstrap.torrent')
2263
srcTorrent = self.announceFetcher.getAnnounceFilePath('bootstrap')
2265
# Maybe we still don't have a torrent for some reason
2266
if not srcTorrent or not os.path.exists(srcTorrent):
2269
torrentPath = os.path.join(ARMORY_HOME_DIR, 'bootstrap.dat.torrent')
2270
LOGINFO('Using torrent file: ' + torrentPath)
2271
shutil.copy(srcTorrent, torrentPath)
2279
############################################################################
2280
def startBitcoindIfNecessary(self):
2281
LOGINFO('startBitcoindIfNecessary')
2282
if not (self.forceOnline or self.internetAvail) or CLI_OPTIONS.offline:
2283
LOGWARN('Not online, will not start bitcoind')
2286
if not self.doAutoBitcoind:
2287
LOGWARN('Tried to start bitcoind, but ManageSatoshi==False')
2290
if satoshiIsAvailable():
2291
LOGWARN('Tried to start bitcoind, but satoshi already running')
2294
self.setSatoshiPaths()
2295
TheSDM.setDisabled(False)
2297
torrentIsDisabled = self.getSettingOrSetDefault('DisableTorrent', False)
2299
# Give the SDM the torrent file...it will use it if it makes sense
2300
if not torrentIsDisabled and TheSDM.shouldTryBootstrapTorrent():
2301
torrentFile = self.findTorrentFileForSDM(2)
2302
if not torrentFile or not os.path.exists(torrentFile):
2303
LOGERROR('Could not find torrent file')
2305
TheSDM.tryToSetupTorrentDL(torrentFile)
2309
# "satexe" is actually just the install directory, not the direct
2310
# path the executable. That dir tree will be searched for bitcoind
2311
TheSDM.setupSDM(extraExeSearch=self.satoshiExeSearchPath)
2312
TheSDM.startBitcoind()
2313
LOGDEBUG('Bitcoind started without error')
2316
LOGEXCEPT('Failed to setup SDM')
2317
self.switchNetworkMode(NETWORKMODE.Offline)
2320
############################################################################
2321
def setSatoshiPaths(self):
2322
LOGINFO('setSatoshiPaths')
2324
# We skip the getSettingOrSetDefault call, because we don't want to set
2325
# it if it doesn't exist
2326
if self.settings.hasSetting('SatoshiExe'):
2327
if not os.path.exists(self.settings.get('SatoshiExe')):
2328
LOGERROR('Bitcoin installation setting is a non-existent directory')
2329
self.satoshiExeSearchPath = [self.settings.get('SatoshiExe')]
2331
self.satoshiExeSearchPath = []
2334
self.satoshiHomePath = BTC_HOME_DIR
2335
if self.settings.hasSetting('SatoshiDatadir') and \
2336
CLI_OPTIONS.satoshiHome=='DEFAULT':
2337
# Setting override BTC_HOME_DIR only if it wasn't explicitly
2338
# set as the command line.
2339
self.satoshiHomePath = self.settings.get('SatoshiDatadir')
2340
LOGINFO('Setting satoshi datadir = %s' % self.satoshiHomePath)
2342
TheBDM.setSatoshiDir(self.satoshiHomePath)
2343
TheSDM.setSatoshiDir(self.satoshiHomePath)
2344
TheTDM.setSatoshiDir(self.satoshiHomePath)
2347
############################################################################
2348
def loadBlockchainIfNecessary(self):
2349
LOGINFO('loadBlockchainIfNecessary')
2350
if CLI_OPTIONS.offline:
2351
if self.forceOnline:
2352
LOGERROR('Cannot mix --force-online and --offline options! Using offline mode.')
2353
self.switchNetworkMode(NETWORKMODE.Offline)
2354
TheBDM.setOnlineMode(False, wait=False)
2355
elif self.onlineModeIsPossible():
2356
# Track number of times we start loading the blockchain.
2357
# We will decrement the number when loading finishes
2358
# We can use this to detect problems with mempool or blkxxxx.dat
2359
self.numTriesOpen = self.getSettingOrSetDefault('FailedLoadCount', 0)
2360
if self.numTriesOpen>2:
2361
self.loadFailedManyTimesFunc(self.numTriesOpen)
2362
self.settings.set('FailedLoadCount', self.numTriesOpen+1)
2364
self.switchNetworkMode(NETWORKMODE.Full)
2365
#self.resetBdmBeforeScan()
2366
TheBDM.setOnlineMode(True, wait=False)
2369
self.switchNetworkMode(NETWORKMODE.Offline)
2370
TheBDM.setOnlineMode(False, wait=False)
2373
#############################################################################
2374
def checkHaveBlockfiles(self):
2375
return os.path.exists(os.path.join(TheBDM.btcdir, 'blocks'))
2377
#############################################################################
2378
def onlineModeIsPossible(self):
2379
return ((self.internetAvail or self.forceOnline) and \
2380
satoshiIsAvailable() and \
2381
self.checkHaveBlockfiles())
2384
#############################################################################
2385
def switchNetworkMode(self, newMode):
2386
LOGINFO('Setting netmode: %s', newMode)
2387
self.netMode=newMode
2388
if newMode in (NETWORKMODE.Offline, NETWORKMODE.Disconnected):
2389
self.NetworkingFactory = FakeClientFactory()
2391
elif newMode==NETWORKMODE.Full:
2393
# Actually setup the networking, now
2394
from twisted.internet import reactor
2396
def showOfflineMsg():
2397
self.netMode = NETWORKMODE.Disconnected
2398
self.setDashboardDetails()
2399
self.lblArmoryStatus.setText( \
2400
'<font color=%s><i>Disconnected</i></font>' % htmlColor('TextWarn'))
2401
if not self.getSettingOrSetDefault('NotifyDiscon', not OS_MACOSX):
2405
self.sysTray.showMessage('Disconnected', \
2406
'Connection to Bitcoin-Qt client lost! Armory cannot send \n'
2407
'or receive bitcoins until connection is re-established.', \
2408
QSystemTrayIcon.Critical, 10000)
2410
LOGEXCEPT('Failed to show disconnect notification')
2413
self.connectCount = 0
2414
def showOnlineMsg():
2415
self.netMode = NETWORKMODE.Full
2416
self.setDashboardDetails()
2417
self.lblArmoryStatus.setText(\
2418
'<font color=%s>Connected (%s blocks)</font> ' %
2419
(htmlColor('TextGreen'), self.currBlockNum))
2420
if not self.getSettingOrSetDefault('NotifyReconn', not OS_MACOSX):
2424
if self.connectCount>0:
2425
self.sysTray.showMessage('Connected', \
2426
'Connection to Bitcoin-Qt re-established', \
2427
QSystemTrayIcon.Information, 10000)
2428
self.connectCount += 1
2430
LOGEXCEPT('Failed to show reconnect notification')
2433
self.NetworkingFactory = ArmoryClientFactory( \
2435
func_loseConnect=showOfflineMsg, \
2436
func_madeConnect=showOnlineMsg, \
2437
func_newTx=self.newTxFunc)
2438
#func_newTx=newTxFunc)
2439
reactor.callWhenRunning(reactor.connectTCP, '127.0.0.1', \
2440
BITCOIN_PORT, self.NetworkingFactory)
2445
#############################################################################
2446
def newTxFunc(self, pytxObj):
2447
if TheBDM.getBDMState() in ('Offline','Uninitialized') or self.doShutdown:
2450
TheBDM.addNewZeroConfTx(pytxObj.serialize(), long(RightNow()), True, wait=True)
2451
self.newZeroConfSinceLastUpdate.append(pytxObj.serialize())
2452
#LOGDEBUG('Added zero-conf tx to pool: ' + binary_to_hex(pytxObj.thisHash))
2454
# All extra tx functions take one arg: the PyTx object of the new ZC tx
2455
for txFunc in self.extraNewTxFunctions:
2460
#############################################################################
2461
def parseUriLink(self, uriStr, clickOrEnter='click'):
2463
QMessageBox.critical(self, 'No URL String', \
2464
'You have not entered a URL String yet. '
2465
'Please go back and enter a URL String.', \
2468
ClickOrEnter = clickOrEnter[0].upper() + clickOrEnter[1:]
2469
LOGINFO('URI link clicked!')
2470
LOGINFO('The following URI string was parsed:')
2471
LOGINFO(uriStr.replace('%','%%'))
2473
uriDict = parseBitcoinURI(uriStr)
2474
if TheBDM.getBDMState() in ('Offline','Uninitialized'):
2475
LOGERROR('%sed "bitcoin:" link in offline mode.' % ClickOrEnter)
2476
self.bringArmoryToFront()
2477
QMessageBox.warning(self, 'Offline Mode',
2478
'You %sed on a "bitcoin:" link, but Armory is in '
2479
'offline mode, and is not capable of creating transactions. '
2480
'%sing links will only work if Armory is connected '
2481
'to the Bitcoin network!' % (clickOrEnter, ClickOrEnter), \
2486
warnMsg = ('It looks like you just %sed a "bitcoin:" link, but '
2487
'that link is malformed. ' % clickOrEnter)
2488
if self.usermode == USERMODE.Standard:
2489
warnMsg += ('Please check the source of the link and enter the '
2490
'transaction manually.')
2492
warnMsg += 'The raw URI string is:<br><br>' + uriStr
2493
QMessageBox.warning(self, 'Invalid URI', warnMsg, QMessageBox.Ok)
2497
if not uriDict.has_key('address'):
2498
QMessageBox.warning(self, 'The "bitcoin:" link you just %sed '
2499
'does not even contain an address! There is nothing that '
2500
'Armory can do with this link!' % clickOrEnter, QMessageBox.Ok)
2501
LOGERROR('No address in "bitcoin:" link! Nothing to do!')
2504
# Verify the URI is for the same network as this Armory instnance
2505
theAddrByte = checkAddrType(base58_to_binary(uriDict['address']))
2506
if theAddrByte!=-1 and not theAddrByte in [ADDRBYTE, P2SHBYTE]:
2507
net = 'Unknown Network'
2508
if NETWORKS.has_key(theAddrByte):
2509
net = NETWORKS[theAddrByte]
2510
QMessageBox.warning(self, 'Wrong Network!', \
2511
'The address for the "bitcoin:" link you just %sed is '
2512
'for the wrong network! You are on the <b>%s</b> '
2513
'and the address you supplied is for the the '
2514
'<b>%s</b>!' % (clickOrEnter, NETWORKS[ADDRBYTE], net), \
2516
LOGERROR('URI link is for the wrong network!')
2519
# If the URI contains "req-" strings we don't recognize, throw error
2520
recognized = ['address','version','amount','label','message']
2521
for key,value in uriDict.iteritems():
2522
if key.startswith('req-') and not key[4:] in recognized:
2523
QMessageBox.warning(self,'Unsupported URI', 'The "bitcoin:" link '
2524
'you just %sed contains fields that are required but not '
2525
'recognized by Armory. This may be an older version of Armory, '
2526
'or the link you %sed on uses an exotic, unsupported format.'
2527
'<br><br>The action cannot be completed.' % (clickOrEnter, clickOrEnter), \
2529
LOGERROR('URI link contains unrecognized req- fields.')
2536
#############################################################################
2537
def uriLinkClicked(self, uriStr):
2538
LOGINFO('uriLinkClicked')
2539
if TheBDM.getBDMState()=='Offline':
2540
QMessageBox.warning(self, 'Offline', \
2541
'You just clicked on a "bitcoin:" link, but Armory is offline '
2542
'and cannot send transactions. Please click the link '
2543
'again when Armory is online.', \
2546
elif not TheBDM.getBDMState()=='BlockchainReady':
2547
# BDM isnt ready yet, saved URI strings in the delayed URIDict to
2548
# call later through finishLoadBlockChainGUI
2549
qLen = self.delayedURIData['qLen']
2551
self.delayedURIData[qLen] = uriStr
2553
self.delayedURIData['qLen'] = qLen
2556
uriDict = self.parseUriLink(uriStr, 'click')
2559
self.bringArmoryToFront()
2560
return self.uriSendBitcoins(uriDict)
2563
#############################################################################
2565
def loadWalletsAndSettings(self):
2566
LOGINFO('loadWalletsAndSettings')
2568
self.getSettingOrSetDefault('First_Load', True)
2569
self.getSettingOrSetDefault('Load_Count', 0)
2570
self.getSettingOrSetDefault('User_Mode', 'Advanced')
2571
self.getSettingOrSetDefault('UnlockTimeout', 10)
2572
self.getSettingOrSetDefault('DNAA_UnlockTimeout', False)
2575
# Determine if we need to do new-user operations, increment load-count
2576
self.firstLoad = False
2577
if self.getSettingOrSetDefault('First_Load', True):
2578
self.firstLoad = True
2579
self.writeSetting('First_Load', False)
2580
self.writeSetting('First_Load_Date', long(RightNow()))
2581
self.writeSetting('Load_Count', 1)
2582
self.writeSetting('AdvFeature_UseCt', 0)
2584
self.writeSetting('Load_Count', (self.settings.get('Load_Count')+1) % 100)
2585
firstDate = self.getSettingOrSetDefault('First_Load_Date', RightNow())
2586
daysSinceFirst = (RightNow() - firstDate) / (60*60*24)
2589
# Set the usermode, default to standard
2590
self.usermode = USERMODE.Standard
2591
if self.settings.get('User_Mode') == 'Advanced':
2592
self.usermode = USERMODE.Advanced
2593
elif self.settings.get('User_Mode') == 'Expert':
2594
self.usermode = USERMODE.Expert
2597
# The user may have asked to never be notified of a particular
2598
# notification again. We have a short-term list (wiped on every
2599
# load), and a long-term list (saved in settings). We simply
2600
# initialize the short-term list with the long-term list, and add
2601
# short-term ignore requests to it
2602
notifyStr = self.getSettingOrSetDefault('NotifyIgnore', '')
2603
nsz = len(notifyStr)
2604
self.notifyIgnoreLong = set(notifyStr[8*i:8*(i+1)] for i in range(nsz/8))
2605
self.notifyIgnoreShort = set(notifyStr[8*i:8*(i+1)] for i in range(nsz/8))
2608
# Load wallets found in the .armory directory
2610
self.walletIndices = {}
2611
self.walletIDSet = set()
2613
# I need some linear lists for accessing by index
2614
self.walletIDList = []
2615
self.walletVisibleList = []
2616
self.combinedLedger = []
2618
self.ledgerTable = []
2620
self.currBlockNum = 0
2622
LOGINFO('Loading wallets...')
2623
wltPaths = readWalletFiles()
2625
wltExclude = self.settings.get('Excluded_Wallets', expectList=True)
2626
wltOffline = self.settings.get('Offline_WalletIDs', expectList=True)
2627
for fpath in wltPaths:
2629
wltLoad = PyBtcWallet().readWalletFile(fpath)
2630
wltID = wltLoad.uniqueIDB58
2631
if fpath in wltExclude or wltID in wltExclude:
2634
if wltID in self.walletIDSet:
2635
LOGWARN('***WARNING: Duplicate wallet detected, %s', wltID)
2636
wo1 = self.walletMap[wltID].watchingOnly
2637
wo2 = wltLoad.watchingOnly
2639
prevWltPath = self.walletMap[wltID].walletPath
2640
self.walletMap[wltID] = wltLoad
2641
LOGWARN('First wallet is more useful than the second one...')
2642
LOGWARN(' Wallet 1 (loaded): %s', fpath)
2643
LOGWARN(' Wallet 2 (skipped): %s', prevWltPath)
2645
LOGWARN('Second wallet is more useful than the first one...')
2646
LOGWARN(' Wallet 1 (skipped): %s', fpath)
2647
LOGWARN(' Wallet 2 (loaded): %s', self.walletMap[wltID].walletPath)
2649
# Update the maps/dictionaries
2650
self.walletMap[wltID] = wltLoad
2651
self.walletIndices[wltID] = len(self.walletMap)-1
2653
# Maintain some linear lists of wallet info
2654
self.walletIDSet.add(wltID)
2655
self.walletIDList.append(wltID)
2656
wtype = determineWalletType(wltLoad, self)[0]
2657
notWatch = (not wtype == WLTTYPES.WatchOnly)
2658
defaultVisible = self.getWltSetting(wltID, 'LedgerShow', notWatch)
2659
self.walletVisibleList.append(defaultVisible)
2660
wltLoad.mainWnd = self
2662
LOGEXCEPT( '***WARNING: Wallet could not be loaded: %s (skipping)',
2668
LOGINFO('Number of wallets read in: %d', len(self.walletMap))
2669
for wltID, wlt in self.walletMap.iteritems():
2670
dispStr = (' Wallet (%s):' % wlt.uniqueIDB58).ljust(25)
2671
dispStr += '"'+wlt.labelName.ljust(32)+'" '
2672
dispStr += '(Encrypted)' if wlt.useEncryption else '(No Encryption)'
2674
# Register all wallets with TheBDM
2675
TheBDM.registerWallet( wlt.cppWallet )
2676
TheBDM.bdm.registerWallet(wlt.cppWallet)
2679
# Create one wallet per lockbox to make sure we can query individual
2680
# lockbox histories easily.
2681
if self.usermode==USERMODE.Expert:
2682
LOGINFO('Loading Multisig Lockboxes')
2683
self.loadLockboxesFromFile(MULTISIG_FILE)
2686
# Get the last directory
2687
savedDir = self.settings.get('LastDirectory')
2688
if len(savedDir)==0 or not os.path.exists(savedDir):
2689
savedDir = ARMORY_HOME_DIR
2690
self.lastDirectory = savedDir
2691
self.writeSetting('LastDirectory', savedDir)
2694
#############################################################################
2695
@RemoveRepeatingExtensions
2696
def getFileSave(self, title='Save Wallet File', \
2697
ffilter=['Wallet files (*.wallet)'], \
2698
defaultFilename=None):
2699
LOGDEBUG('getFileSave')
2700
startPath = self.settings.get('LastDirectory')
2701
if len(startPath)==0 or not os.path.exists(startPath):
2702
startPath = ARMORY_HOME_DIR
2704
if not defaultFilename==None:
2705
startPath = os.path.join(startPath, defaultFilename)
2708
types.append('All files (*)')
2709
typesStr = ';; '.join(types)
2711
# Found a bug with Swig+Threading+PyQt+OSX -- save/load file dialogs freeze
2712
# User picobit discovered this is avoided if you use the Qt dialogs, instead
2713
# of the native OS dialogs. Use native for all except OSX...
2715
fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr))
2717
fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr,
2718
options=QFileDialog.DontUseNativeDialog))
2721
fdir,fname = os.path.split(fullPath)
2723
self.writeSetting('LastDirectory', fdir)
2727
#############################################################################
2728
def getFileLoad(self, title='Load Wallet File', \
2729
ffilter=['Wallet files (*.wallet)'], \
2732
LOGDEBUG('getFileLoad')
2734
if defaultDir is None:
2735
defaultDir = self.settings.get('LastDirectory')
2736
if len(defaultDir)==0 or not os.path.exists(defaultDir):
2737
defaultDir = ARMORY_HOME_DIR
2740
types = list(ffilter)
2741
types.append(tr('All files (*)'))
2742
typesStr = ';; '.join(types)
2743
# Found a bug with Swig+Threading+PyQt+OSX -- save/load file dialogs freeze
2744
# User picobit discovered this is avoided if you use the Qt dialogs, instead
2745
# of the native OS dialogs. Use native for all except OSX...
2747
fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr))
2749
fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr, \
2750
options=QFileDialog.DontUseNativeDialog))
2752
self.writeSetting('LastDirectory', os.path.split(fullPath)[0])
2755
##############################################################################
2756
def getWltSetting(self, wltID, propName, defaultValue=''):
2757
# Sometimes we need to settings specific to individual wallets -- we will
2758
# prefix the settings name with the wltID.
2759
wltPropName = 'Wallet_%s_%s' % (wltID, propName)
2760
if self.settings.hasSetting(wltPropName):
2761
return self.settings.get(wltPropName)
2763
if not defaultValue=='':
2764
self.setWltSetting(wltID, propName, defaultValue)
2767
#############################################################################
2768
def setWltSetting(self, wltID, propName, value):
2769
wltPropName = 'Wallet_%s_%s' % (wltID, propName)
2770
self.writeSetting(wltPropName, value)
2773
#############################################################################
2774
def toggleIsMine(self, wltID):
2775
alreadyMine = self.getWltSetting(wltID, 'IsMine')
2777
self.setWltSetting(wltID, 'IsMine', False)
2779
self.setWltSetting(wltID, 'IsMine', True)
2782
#############################################################################
2783
def loadLockboxesFromFile(self, fn):
2784
self.allLockboxes = []
2785
self.cppLockboxWltMap = {}
2786
if not os.path.exists(fn):
2789
lbList = readLockboxesFile(fn)
2791
self.updateOrAddLockbox(lb)
2794
#############################################################################
2795
def updateOrAddLockbox(self, lbObj, isFresh=False):
2797
lbID = lbObj.uniqueIDB58
2798
index = self.lockboxIDMap.get(lbID)
2800
# Add new lockbox to list
2801
self.allLockboxes.append(lbObj)
2802
self.lockboxIDMap[lbID] = len(self.allLockboxes)-1
2804
# Create new wallet to hold the lockbox, register it with BDM
2805
self.cppLockboxWltMap[lbID] = BtcWallet()
2806
scraddrReg = script_to_scrAddr(lbObj.binScript)
2807
scraddrP2SH = script_to_scrAddr(script_to_p2sh_script(lbObj.binScript))
2808
TheBDM.registerWallet(self.cppLockboxWltMap[lbID], isFresh)
2809
TheBDM.bdm.registerWallet(self.cppLockboxWltMap[lbID], isFresh)
2811
self.cppLockboxWltMap[lbID].addScrAddress_1_(scraddrReg)
2812
self.cppLockboxWltMap[lbID].addScrAddress_1_(scraddrP2SH)
2814
self.cppLockboxWltMap[lbID].addNewScrAddress(scraddrReg)
2815
self.cppLockboxWltMap[lbID].addNewScrAddress(scraddrP2SH)
2817
# Save the scrAddr histories again to make sure no rescan nexttime
2818
if TheBDM.getBDMState()=='BlockchainReady':
2819
TheBDM.saveScrAddrHistories()
2821
# Replace the original
2822
self.allLockboxes[index] = lbObj
2824
writeLockboxesFile(self.allLockboxes, MULTISIG_FILE)
2826
LOGEXCEPT('Failed to add/update lockbox')
2829
#############################################################################
2830
def removeLockbox(self, lbObj):
2831
lbID = lbObj.uniqueIDB58
2832
index = self.lockboxIDMap.get(lbID)
2834
LOGERROR('Tried to remove lockbox that DNE: %s', lbID)
2836
del self.allLockboxes[index]
2837
self.reconstructLockboxMaps()
2838
writeLockboxesFile(self.allLockboxes, MULTISIG_FILE)
2841
#############################################################################
2842
def reconstructLockboxMaps(self):
2843
self.lockboxIDMap.clear()
2844
for i,box in enumerate(self.allLockboxes):
2845
self.lockboxIDMap[box.uniqueIDB58] = i
2847
#############################################################################
2848
def getLockboxByID(self, boxID):
2849
index = self.lockboxIDMap.get(boxID)
2850
return None if index is None else self.allLockboxes[index]
2852
################################################################################
2853
# Get the lock box ID if the p2shAddrString is found in one of the lockboxes
2854
# otherwise it returns None
2855
def getLockboxByP2SHAddrStr(self, p2shAddrStr):
2856
for lboxId in self.lockboxIDMap.keys():
2857
lbox = self.allLockboxes[self.lockboxIDMap[lboxId]]
2858
if p2shAddrStr == binScript_to_p2shAddrStr(lbox.binScript):
2863
#############################################################################
2864
def browseLockboxes(self):
2865
DlgLockboxManager(self,self).exec_()
2869
#############################################################################
2870
def getContribStr(self, binScript, contribID='', contribLabel=''):
2872
This is used to display info for the lockbox interface. It might also be
2873
useful as a general script_to_user_string method, where you have a
2874
binScript and you want to tell the user something about it. However,
2875
it is verbose, so it won't fit in a send-confirm dialog, necessarily.
2877
We should extract as much information as possible without contrib*. This
2878
at least guarantees that we see the correct data for our own wallets
2879
and lockboxes, even if the data for other parties is incorrect.
2882
displayInfo = self.getDisplayStringForScript(binScript, 60, 2)
2883
if displayInfo['WltID'] is not None:
2884
return displayInfo['String'], ('WLT:%s' % displayInfo['WltID'])
2885
elif displayInfo['LboxID'] is not None:
2886
return displayInfo['String'], ('LB:%s' % displayInfo['LboxID'])
2888
scriptType = getTxOutScriptType(binScript)
2889
scrAddr = script_to_scrAddr(binScript)
2892
# At this point, we can use the contrib ID (and know we can't sign it)
2893
if contribID or contribLabel:
2896
outStr = 'Contributor "%s" (%s)' % (contribLabel, contribID)
2898
outStr = 'Contributor %s' % contribID
2901
outStr = 'Contributor "%s"' % contribLabel
2903
outStr = 'Unknown Contributor'
2904
LOGERROR('How did we get to this impossible else-statement?')
2906
return outStr, ('CID:%s' % contribID)
2908
# If no contrib ID, then salvage anything
2909
astr = displayInfo['AddrStr']
2911
if scriptType == CPP_TXOUT_MULTISIG:
2912
M,N,a160s,pubs = getMultisigScriptInfo(binScript)
2913
dispStr = 'Unrecognized Multisig %d-of-%d: P2SH=%s' % (M,N,astr)
2914
cid = 'MS:%s' % astr
2915
elif scriptType == CPP_TXOUT_P2SH:
2916
dispStr = 'Unrecognized P2SH: %s' % astr
2917
cid = 'P2SH:%s' % astr
2918
elif scriptType in CPP_TXOUT_HAS_ADDRSTR:
2919
dispStr = 'Address: %s' % astr
2920
cid = 'ADDR:%s' % astr
2922
dispStr = 'Non-standard: P2SH=%s' % astr
2923
cid = 'NS:%s' % astr
2929
#############################################################################
2930
def getWalletForAddr160(self, addr160):
2931
for wltID, wlt in self.walletMap.iteritems():
2932
if wlt.hasAddr(addr160):
2936
#############################################################################
2937
def getWalletForScrAddr(self, scrAddr):
2938
for wltID, wlt in self.walletMap.iteritems():
2939
if wlt.hasScrAddr(scrAddr):
2943
#############################################################################
2944
def getSettingOrSetDefault(self, settingName, defaultVal):
2945
s = self.settings.getSettingOrSetDefault(settingName, defaultVal)
2948
#############################################################################
2949
def writeSetting(self, settingName, val):
2950
self.settings.set(settingName, val)
2952
#############################################################################
2953
def startRescanBlockchain(self, forceFullScan=False):
2954
if TheBDM.getBDMState() in ('Offline','Uninitialized'):
2955
LOGWARN('Rescan requested but Armory is in offline mode')
2958
if TheBDM.getBDMState()=='Scanning':
2959
LOGINFO('Queueing rescan after current scan completes.')
2961
LOGINFO('Starting blockchain rescan...')
2964
# Start it in the background
2965
TheBDM.rescanBlockchain('AsNeeded', wait=False)
2966
self.needUpdateAfterScan = True
2967
self.setDashboardDetails()
2969
#############################################################################
2970
def forceRescanDB(self):
2971
self.needUpdateAfterScan = True
2972
self.lblDashModeBuild.setText( 'Build Databases', \
2973
size=4, bold=True, color='DisableFG')
2974
self.lblDashModeScan.setText( 'Scanning Transaction History', \
2975
size=4, bold=True, color='Foreground')
2976
TheBDM.rescanBlockchain('ForceRescan', wait=False)
2977
self.setDashboardDetails()
2979
#############################################################################
2980
def forceRebuildAndRescan(self):
2981
self.needUpdateAfterScan = True
2982
self.lblDashModeBuild.setText( 'Preparing Databases', \
2983
size=4, bold=True, color='Foreground')
2984
self.lblDashModeScan.setText( 'Scan Transaction History', \
2985
size=4, bold=True, color='DisableFG')
2986
#self.resetBdmBeforeScan() # this resets BDM and then re-registeres wlts
2987
TheBDM.rescanBlockchain('ForceRebuild', wait=False)
2988
self.setDashboardDetails()
2994
#############################################################################
2996
def initialWalletSync(self):
2997
for wltID in self.walletMap.iterkeys():
2998
LOGINFO('Syncing wallet: %s', wltID)
2999
self.walletMap[wltID].setBlockchainSyncFlag(BLOCKCHAIN_READONLY)
3000
# Used to do "sync-lite" when we had to rescan for new addresses,
3001
self.walletMap[wltID].syncWithBlockchainLite(0)
3002
#self.walletMap[wltID].syncWithBlockchain(0)
3003
self.walletMap[wltID].detectHighestUsedIndex(True) # expand wlt if necessary
3004
self.walletMap[wltID].fillAddressPool()
3006
for lbID,cppWallet in self.cppLockboxWltMap.iteritems():
3007
TheBDM.scanRegisteredTxForWallet(cppWallet, wait=True)
3011
# NB: armoryd has a similar function (Armory_Daemon::start()), and both share
3012
# common functionality in ArmoryUtils (finishLoadBlockchainCommon). If you
3013
# mod this function, please be mindful of what goes where, and make sure
3014
# any critical functionality makes it into armoryd.
3015
def finishLoadBlockchainGUI(self):
3016
# Let's populate the wallet info after finishing loading the blockchain.
3017
if TheBDM.isInitialized():
3018
self.setDashboardDetails()
3019
(self.currBlockNum, self.memPoolInit) = \
3020
TheBDM.finishLoadBlockchainCommon(self.walletMap, \
3021
self.cppLockboxWltMap, \
3023
self.statusBar().showMessage('Blockchain loaded. Wallets synced!', 10000)
3025
# We still need to put together various bits of info.
3026
self.createCombinedLedger()
3027
self.ledgerSize = len(self.combinedLedger)
3028
if self.netMode==NETWORKMODE.Full:
3029
LOGINFO('Current block number: %d', self.currBlockNum)
3030
self.lblArmoryStatus.setText(\
3031
'<font color=%s>Connected (%s blocks)</font> ' %
3032
(htmlColor('TextGreen'), self.currBlockNum))
3034
self.blkReceived = TheBDM.getTopBlockHeader().getTimestamp()
3035
self.writeSetting('LastBlkRecv', self.currBlockNum)
3036
self.writeSetting('LastBlkRecvTime', self.blkReceived)
3038
currSyncSuccess = self.getSettingOrSetDefault("SyncSuccessCount", 0)
3039
self.writeSetting('SyncSuccessCount', min(currSyncSuccess+1, 10))
3041
# If there are missing blocks, continue, but throw up a huge warning.
3042
vectMissingBlks = TheBDM.missingBlockHashes()
3043
LOGINFO('Blockfile corruption check: Missing blocks: %d', \
3044
len(vectMissingBlks))
3045
if len(vectMissingBlks) > 0:
3046
LOGINFO('Missing blocks: %d', len(vectMissingBlks))
3047
QMessageBox.critical(self, tr('Blockdata Error'), tr("""
3048
Armory has detected an error in the blockchain database
3049
maintained by the third-party Bitcoin software (Bitcoin-Qt
3050
or bitcoind). This error is not fatal, but may lead to
3051
incorrect balances, inability to send coins, or application
3054
It is unlikely that the error affects your wallets,
3055
but it <i>is</i> possible. If you experience crashing,
3056
or see incorrect balances on any wallets, it is strongly
3057
recommended you re-download the blockchain using:
3058
"<i>Help</i>"\xe2\x86\x92"<i>Factory Reset</i>"."""), \
3061
# If necessary, throw up a window stating the the blockchain's loaded.
3062
if self.getSettingOrSetDefault('NotifyBlkFinish',True):
3063
reply,remember = MsgBoxWithDNAA(MSGBOX.Info, \
3064
'Blockchain Loaded!', 'Blockchain loading is complete. '
3065
'Your balances and transaction history are now available '
3066
'under the "Transactions" tab. You can also send and '
3067
'receive bitcoins.', \
3068
dnaaMsg='Do not show me this notification again ', yesStr='OK')
3071
self.writeSetting('NotifyBlkFinish',False)
3073
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger)
3075
# Execute any extra functions we may have.
3076
for fn in self.extraGoOnlineFunctions:
3077
fn(self.currBlockNum)
3079
self.netMode = NETWORKMODE.Full
3080
self.settings.set('FailedLoadCount', 0)
3082
self.statusBar().showMessage('! Blockchain loading failed !', 10000)
3085
# This will force the table to refresh with new data
3086
self.setDashboardDetails()
3087
self.updateAnnounceTab() # make sure satoshi version info is up to date
3088
self.removeBootstrapDat() # if we got here, we're *really* done with it
3089
self.walletModel.reset()
3091
qLen = self.delayedURIData['qLen']
3093
#delayed URI parses, feed them back to the uri parser now
3094
for i in range(0, qLen):
3095
uriStr = self.delayedURIData[qLen-i-1]
3096
self.delayedURIData['qLen'] = qLen -i -1
3097
self.uriLinkClicked(uriStr)
3100
#############################################################################
3101
def removeBootstrapDat(self):
3102
bfile = os.path.join(BTC_HOME_DIR, 'bootstrap.dat.old')
3103
if os.path.exists(bfile):
3106
#############################################################################
3107
def changeLedgerSorting(self, col, order):
3109
The direct sorting was implemented to avoid having to search for comment
3110
information for every ledger entry. Therefore, you can't sort by comments
3111
without getting them first, which is the original problem to avoid.
3113
if col in (LEDGERCOLS.NumConf, LEDGERCOLS.DateStr, \
3114
LEDGERCOLS.Comment, LEDGERCOLS.Amount, LEDGERCOLS.WltName):
3115
self.sortLedgCol = col
3116
self.sortLedgOrder = order
3117
self.createCombinedLedger()
3119
#############################################################################
3121
def createCombinedLedger(self, wltIDList=None, withZeroConf=True):
3123
Create a ledger to display on the main screen, that consists of ledger
3124
entries of any SUBSET of available wallets.
3128
currIdx = max(self.comboWltSelect.currentIndex(), 0)
3130
for i,vis in enumerate(self.walletVisibleList):
3132
wltIDList.append(self.walletIDList[i])
3133
self.writeSetting('LastFilterState', currIdx)
3139
self.combinedLedger = []
3143
currBlk = 0xffffffff
3144
if TheBDM.isInitialized():
3145
currBlk = TheBDM.getTopBlockHeight()
3147
for wltID in wltIDList:
3148
wlt = self.walletMap[wltID]
3149
id_le_pairs = [[wltID, le] for le in wlt.getTxLedger('Full')]
3150
self.combinedLedger.extend(id_le_pairs)
3151
totalFunds += wlt.getBalance('Total')
3152
spendFunds += wlt.getBalance('Spendable')
3153
unconfFunds += wlt.getBalance('Unconfirmed')
3156
def keyFuncNumConf(x):
3157
numConf = x[1].getBlockNum() - currBlk # returns neg for reverse sort
3158
txTime = x[1].getTxTime()
3159
txhash = x[1].getTxHash()
3160
value = x[1].getValue()
3161
return (numConf, txTime, txhash, value)
3163
def keyFuncTxTime(x):
3164
numConf = x[1].getBlockNum() - currBlk # returns neg for reverse sort
3165
txTime = x[1].getTxTime()
3166
txhash = x[1].getTxHash()
3167
value = x[1].getValue()
3168
return (txTime, numConf, txhash, value)
3170
# Apply table sorting -- this is very fast
3171
sortDir = (self.sortLedgOrder == Qt.AscendingOrder)
3172
if self.sortLedgCol == LEDGERCOLS.NumConf:
3173
self.combinedLedger.sort(key=keyFuncNumConf, reverse=sortDir)
3174
if self.sortLedgCol == LEDGERCOLS.DateStr:
3175
self.combinedLedger.sort(key=keyFuncTxTime, reverse=sortDir)
3176
if self.sortLedgCol == LEDGERCOLS.WltName:
3177
self.combinedLedger.sort(key=lambda x: self.walletMap[x[0]].labelName, reverse=sortDir)
3178
if self.sortLedgCol == LEDGERCOLS.Comment:
3179
self.combinedLedger.sort(key=lambda x: self.getCommentForLE(x[0],x[1]), reverse=sortDir)
3180
if self.sortLedgCol == LEDGERCOLS.Amount:
3181
self.combinedLedger.sort(key=lambda x: abs(x[1].getValue()), reverse=sortDir)
3183
self.ledgerSize = len(self.combinedLedger)
3185
# Hide the ledger slicer if our data set is smaller than the slice width
3186
self.frmLedgUpDown.setVisible(self.ledgerSize>self.currLedgWidth)
3187
self.lblLedgRange.setText('%d to %d' % (self.currLedgMin, self.currLedgMax))
3188
self.lblLedgTotal.setText('(of %d)' % self.ledgerSize)
3190
# Many MainWindow objects haven't been created yet...
3191
# let's try to update them and fail silently if they don't exist
3193
if TheBDM.getBDMState() in ('Offline', 'Scanning'):
3194
self.lblTotalFunds.setText( '-'*12 )
3195
self.lblSpendFunds.setText( '-'*12 )
3196
self.lblUnconfFunds.setText('-'*12 )
3199
uncolor = htmlColor('MoneyNeg') if unconfFunds>0 else htmlColor('Foreground')
3200
btccolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('MoneyPos')
3201
lblcolor = htmlColor('DisableFG') if spendFunds==totalFunds else htmlColor('Foreground')
3202
goodColor= htmlColor('TextGreen')
3203
self.lblTotalFunds.setText( '<b><font color="%s">%s</font></b>' % (btccolor,coin2str(totalFunds)))
3204
self.lblTot.setText('<b><font color="%s">Maximum Funds:</font></b>' % lblcolor)
3205
self.lblBTC1.setText('<b><font color="%s">BTC</font></b>' % lblcolor)
3206
self.lblSpendFunds.setText( '<b><font color=%s>%s</font></b>' % (goodColor, coin2str(spendFunds)))
3207
self.lblUnconfFunds.setText('<b><font color="%s">%s</font></b>' % \
3208
(uncolor, coin2str(unconfFunds)))
3210
# Finally, update the ledger table
3211
rmin,rmax = self.currLedgMin-1, self.currLedgMax
3212
self.ledgerTable = self.convertLedgerToTable(self.combinedLedger[rmin:rmax])
3213
self.ledgerModel.ledger = self.ledgerTable
3214
self.ledgerModel.reset()
3216
except AttributeError:
3220
if not self.usermode==USERMODE.Expert:
3223
# In expert mode, we're updating the lockbox info, too
3226
for lbID,cppWlt in self.cppLockboxWltMap.iteritems():
3228
zcLedger = cppWlt.getZeroConfLedger()
3229
for i in range(len(zcLedger)):
3230
lockboxTable.append([lbID, zcLedger[i]])
3232
ledger = cppWlt.getTxLedger()
3233
for i in range(len(ledger)):
3234
lockboxTable.append([lbID, ledger[i]])
3236
self.lockboxLedgTable = self.convertLedgerToTable(lockboxTable)
3237
self.lockboxLedgModel.ledger = self.lockboxLedgTable
3238
self.lockboxLedgModel.reset()
3240
LOGEXCEPT('Failed to update lockbox ledger')
3242
#############################################################################
3243
def getCommentForLockboxTx(self, lboxId, le):
3244
commentSet = set([])
3245
lbox = self.allLockboxes[self.lockboxIDMap[lboxId]]
3246
for a160 in lbox.a160List:
3247
wltID = self.getWalletForAddr160(a160)
3249
commentSet.add(self.walletMap[wltID].getCommentForLE(le))
3250
return ' '.join(commentSet)
3252
#############################################################################
3254
def convertLedgerToTable(self, ledger, showSentToSelfAmt=True):
3256
datefmt = self.getPreferredDateFormat()
3257
for wltID,le in ledger:
3260
wlt = self.walletMap.get(wltID)
3263
isWatch = (determineWalletType(wlt, self)[0] == WLTTYPES.WatchOnly)
3264
wltName = wlt.labelName
3265
dispComment = self.getCommentForLE(wltID, le)
3268
lbox = self.getLockboxByID(lboxId)
3272
wltName = '%s-of-%s: %s (%s)' % (lbox.M, lbox.N, lbox.shortName, lboxId)
3273
dispComment = self.getCommentForLockboxTx(lboxId, le)
3275
nConf = self.currBlockNum - le.getBlockNum()+1
3276
if le.getBlockNum()>=0xffffffff:
3279
# If this was sent-to-self... we should display the actual specified
3280
# value when the transaction was executed. This is pretty difficult
3281
# when both "recipient" and "change" are indistinguishable... but
3282
# They're actually not because we ALWAYS generate a new address to
3283
# for change , which means the change address MUST have a higher
3286
if le.isSentToSelf() and wlt and showSentToSelfAmt:
3287
amt = determineSentToSelfAmt(le, wlt)[0]
3292
# UnixTime (needed for sorting)
3293
row.append(le.getTxTime())
3296
row.append(unixTimeToFormatStr(le.getTxTime(), datefmt))
3298
# TxDir (actually just the amt... use the sign of the amt to determine dir)
3299
row.append(coin2str(le.getValue(), maxZeros=2))
3305
row.append(dispComment)
3308
row.append(coin2str(amt, maxZeros=2))
3310
# Is this money mine?
3313
# ID to display (this might be the lockbox ID)
3317
row.append( binary_to_hex(le.getTxHash() ))
3319
# Is this a coinbase/generation transaction
3320
row.append( le.isCoinbase() )
3323
row.append( le.isSentToSelf() )
3325
# Tx was invalidated! (double=spend!)
3326
row.append( not le.isValid())
3328
# Finally, attach the row to the table
3334
#############################################################################
3336
def walletListChanged(self):
3337
self.walletModel.reset()
3338
self.populateLedgerComboBox()
3339
self.createCombinedLedger()
3342
#############################################################################
3344
def populateLedgerComboBox(self):
3345
self.comboWltSelect.clear()
3346
self.comboWltSelect.addItem( 'My Wallets' )
3347
self.comboWltSelect.addItem( 'Offline Wallets' )
3348
self.comboWltSelect.addItem( 'Other\'s wallets' )
3349
self.comboWltSelect.addItem( 'All Wallets' )
3350
self.comboWltSelect.addItem( 'Custom Filter' )
3351
for wltID in self.walletIDList:
3352
self.comboWltSelect.addItem( self.walletMap[wltID].labelName )
3353
self.comboWltSelect.insertSeparator(5)
3354
self.comboWltSelect.insertSeparator(5)
3355
comboIdx = self.getSettingOrSetDefault('LastFilterState', 0)
3356
self.comboWltSelect.setCurrentIndex(comboIdx)
3358
#############################################################################
3359
def execDlgWalletDetails(self, index=None):
3360
if len(self.walletMap)==0:
3361
reply = QMessageBox.information(self, 'No Wallets!', \
3362
'You currently do not have any wallets. Would you like to '
3363
'create one, now?', QMessageBox.Yes | QMessageBox.No)
3364
if reply==QMessageBox.Yes:
3365
self.startWalletWizard()
3369
index = self.walletsView.selectedIndexes()
3370
if len(self.walletMap)==1:
3371
self.walletsView.selectRow(0)
3372
index = self.walletsView.selectedIndexes()
3374
QMessageBox.warning(self, 'Select a Wallet', \
3375
'Please select a wallet on the right, to see its properties.', \
3380
wlt = self.walletMap[self.walletIDList[index.row()]]
3381
dialog = DlgWalletDetails(wlt, self.usermode, self, self)
3383
#self.walletListChanged()
3385
#############################################################################
3386
def execClickRow(self, index=None):
3387
row,col = index.row(), index.column()
3388
if not col==WLTVIEWCOLS.Visible:
3391
wltID = self.walletIDList[row]
3392
currEye = self.walletVisibleList[row]
3393
self.walletVisibleList[row] = not currEye
3394
self.setWltSetting(wltID, 'LedgerShow', not currEye)
3396
# Set it to "Custom Filter"
3397
self.comboWltSelect.setCurrentIndex(4)
3399
if TheBDM.getBDMState()=='BlockchainReady':
3400
self.createCombinedLedger()
3401
self.ledgerModel.reset()
3402
self.walletModel.reset()
3405
#############################################################################
3406
def updateTxCommentFromView(self, view):
3407
index = view.selectedIndexes()[0]
3408
row, col = index.row(), index.column()
3409
currComment = str(view.model().index(row, LEDGERCOLS.Comment).data().toString())
3410
wltID = str(view.model().index(row, LEDGERCOLS.WltID ).data().toString())
3411
txHash = str(view.model().index(row, LEDGERCOLS.TxHash ).data().toString())
3413
dialog = DlgSetComment(self, self, currComment, 'Transaction')
3415
newComment = str(dialog.edtComment.text())
3416
self.walletMap[wltID].setComment(hex_to_binary(txHash), newComment)
3417
self.walletListChanged()
3420
#############################################################################
3421
def updateAddressCommentFromView(self, view, wlt):
3422
index = view.selectedIndexes()[0]
3423
row, col = index.row(), index.column()
3424
currComment = str(view.model().index(row, ADDRESSCOLS.Comment).data().toString())
3425
addrStr = str(view.model().index(row, ADDRESSCOLS.Address).data().toString())
3427
dialog = DlgSetComment(self, self, currComment, 'Address')
3429
newComment = str(dialog.edtComment.text())
3430
atype, addr160 = addrStr_to_hash160(addrStr)
3432
LOGWARN('Setting comment for P2SH address: %s' % addrStr)
3433
wlt.setComment(addr160, newComment)
3437
#############################################################################
3439
def getAddrCommentIfAvailAll(self, txHash):
3440
if not TheBDM.isInitialized():
3444
appendedComments = []
3445
for wltID,wlt in self.walletMap.iteritems():
3446
cmt = wlt.getAddrCommentIfAvail(txHash)
3448
appendedComments.append(cmt)
3450
return '; '.join(appendedComments)
3454
#############################################################################
3455
def getCommentForLE(self, wltID, le):
3456
# Smart comments for LedgerEntry objects: get any direct comments ...
3457
# if none, then grab the one for any associated addresses.
3459
return self.walletMap[wltID].getCommentForLE(le)
3461
txHash = le.getTxHash()
3462
if wlt.commentsMap.has_key(txHash):
3463
comment = wlt.commentsMap[txHash]
3465
# [[ COMMENTS ]] are not meant to be displayed on main ledger
3466
comment = self.getAddrCommentIfAvail(txHash)
3467
if comment.startswith('[[') and comment.endswith(']]'):
3473
#############################################################################
3474
def addWalletToApplication(self, newWallet, walletIsNew=True):
3475
LOGINFO('addWalletToApplication')
3476
# Update the maps/dictionaries
3477
newWltID = newWallet.uniqueIDB58
3479
if self.walletMap.has_key(newWltID):
3482
self.walletMap[newWltID] = newWallet
3483
self.walletIndices[newWltID] = len(self.walletMap)-1
3485
# Maintain some linear lists of wallet info
3486
self.walletIDSet.add(newWltID)
3487
self.walletIDList.append(newWltID)
3488
showByDefault = (determineWalletType(newWallet, self)[0] != WLTTYPES.WatchOnly)
3489
self.walletVisibleList.append(showByDefault)
3490
self.setWltSetting(newWltID, 'LedgerShow', showByDefault)
3493
self.walletListChanged()
3497
#############################################################################
3498
def removeWalletFromApplication(self, wltID):
3499
LOGINFO('removeWalletFromApplication')
3502
idx = self.walletIndices[wltID]
3504
LOGERROR('Invalid wallet ID passed to "removeWalletFromApplication"')
3505
raise WalletExistsError
3507
del self.walletMap[wltID]
3508
del self.walletIndices[wltID]
3509
self.walletIDSet.remove(wltID)
3510
del self.walletIDList[idx]
3511
del self.walletVisibleList[idx]
3513
# Reconstruct walletIndices
3514
for i,wltID in enumerate(self.walletIDList):
3515
self.walletIndices[wltID] = i
3517
self.walletListChanged()
3519
#############################################################################
3520
def RecoverWallet(self):
3521
DlgWltRecoverWallet(self, self).promptWalletRecovery()
3524
#############################################################################
3525
def createSweepAddrTx(self, sweepFromAddrObjList, sweepToScript):
3527
This method takes a list of addresses (likely just created from private
3528
key data), finds all their unspent TxOuts, and creates a signed tx that
3529
transfers 100% of the funds to the sweepTO160 address. It doesn't
3530
actually execute the transaction, but it will return a broadcast-ready
3531
PyTx object that the user can confirm. TxFee is automatically calc'd
3532
and deducted from the output value, if necessary.
3536
LOGINFO('createSweepAddrTx')
3537
if not isinstance(sweepFromAddrObjList, (list, tuple)):
3538
sweepFromAddrObjList = [sweepFromAddrObjList]
3541
addr160List = [a.getAddr160() for a in sweepFromAddrObjList]
3542
utxoList = getUnspentTxOutsForAddr160List(addr160List, 'Sweep', 0)
3543
if len(utxoList)==0:
3546
outValue = sumTxOutList(utxoList)
3551
for utxo in utxoList:
3552
# The PyCreateAndSignTx method require PyTx and PyBtcAddress objects
3553
rawTx = TheBDM.getTxByHash(utxo.getTxHash()).serialize()
3554
PyPrevTx = PyTx().unserialize(rawTx)
3555
a160 = CheckHash160(utxo.getRecipientScrAddr())
3556
for aobj in sweepFromAddrObjList:
3557
if a160 == aobj.getAddr160():
3558
pubKey = aobj.binPublicKey65.toBinStr()
3559
txoIdx = utxo.getTxOutIndex()
3560
inputSide.append(UnsignedTxInput(rawTx, txoIdx, None, pubKey))
3563
minFee = calcMinSuggestedFees(utxoList, outValue, 0, 1)[1]
3566
LOGDEBUG( 'Subtracting fee from Sweep-output')
3570
return [None, outValue, minFee]
3572
# Creating the output list is pretty easy...
3574
outputSide.append(DecoratedTxOut(sweepToScript, outValue))
3577
# Make copies, destroy them in the finally clause
3579
for addrObj in sweepFromAddrObjList:
3580
scrAddr = SCRADDR_P2PKH_BYTE + addrObj.getAddr160()
3581
privKeyMap[scrAddr] = addrObj.binPrivKey32_Plain.copy()
3583
pytx = PyCreateAndSignTx(inputSide, outputSide, privKeyMap)
3584
return (pytx, outValue, minFee)
3587
for scraddr in privKeyMap:
3588
privKeyMap[scraddr].destroy()
3591
# Try with zero fee and exactly one output
3592
minFee = calcMinSuggestedFees(utxoList, outValue, 0, 1)[1]
3595
LOGDEBUG( 'Subtracting fee from Sweep-output')
3599
return [None, outValue, minFee]
3602
outputSide.append( [PyBtcAddress().createFromPublicKeyHash160(sweepTo160), \
3605
pytx = PyCreateAndSignTx(inputSide, outputSide)
3606
return (pytx, outValue, minFee)
3613
#############################################################################
3614
def confirmSweepScan(self, pybtcaddrList, targAddr160):
3615
LOGINFO('confirmSweepScan')
3616
gt1 = len(self.sweepAfterScanList)>1
3618
if len(self.sweepAfterScanList) > 0:
3619
QMessageBox.critical(self, 'Already Sweeping',
3620
'You are already in the process of scanning the blockchain for '
3621
'the purposes of sweeping other addresses. You cannot initiate '
3622
'sweeping new addresses until the current operation completes. '
3624
'In the future, you may select "Multiple Keys" when entering '
3625
'addresses to sweep. There is no limit on the number that can be '
3626
'specified, but they must all be entered at once.', QMessageBox.Ok)
3627
# Destroy the private key data
3628
for addr in pybtcaddrList:
3629
addr.binPrivKey32_Plain.destroy()
3634
if TheBDM.getBDMState() in ('Offline', 'Uninitialized'):
3635
#LOGERROR('Somehow ended up at confirm-sweep while in offline mode')
3636
#QMessageBox.info(self, 'Armory is Offline', \
3637
#'Armory is currently in offline mode. You must be in online '
3638
#'mode to initiate the sweep operation.')
3639
nkey = len(self.sweepAfterScanList)
3640
strPlur = 'addresses' if nkey>1 else 'address'
3641
QMessageBox.info(self, 'Armory is Offline', \
3642
'You have chosen to sweep %d %s, but Armory is currently '
3643
'in offline mode. The sweep will be performed the next time you '
3644
'go into online mode. You can initiate online mode (if available) '
3645
'from the dashboard in the main window.' (nkey,strPlur), QMessageBox.Ok)
3650
'Armory must scan the global transaction history in order to '
3651
'find any bitcoins associated with the %s you supplied. '
3652
'Armory will go into offline mode temporarily while the scan '
3653
'is performed, and you will not have access to balances or be '
3654
'able to create transactions. The scan may take several minutes.'
3655
'<br><br>' % ('keys' if gt1 else 'key'))
3657
if TheBDM.getBDMState()=='Scanning':
3659
'There is currently another scan operation being performed. '
3660
'Would you like to start the sweep operation after it completes? ')
3661
elif TheBDM.getBDMState()=='BlockchainReady':
3663
'<b>Would you like to start the scan operation right now?</b>')
3665
msgConfirm += ('<br><br>Clicking "No" will abort the sweep operation')
3667
confirmed = QMessageBox.question(self, 'Confirm Rescan', msgConfirm, \
3668
QMessageBox.Yes | QMessageBox.No)
3670
if confirmed==QMessageBox.Yes:
3671
for addr in pybtcaddrList:
3672
TheBDM.registerImportedScrAddr(Hash160ToScrAddr(addr.getAddr160()))
3673
self.sweepAfterScanList = pybtcaddrList
3674
self.sweepAfterScanTarg = targAddr160
3675
#TheBDM.rescanBlockchain('AsNeeded', wait=False)
3676
self.startRescanBlockchain()
3677
self.setDashboardDetails()
3681
#############################################################################
3682
def finishSweepScan(self):
3683
LOGINFO('finishSweepScan')
3684
sweepList, self.sweepAfterScanList = self.sweepAfterScanList,[]
3686
#######################################################################
3687
# The createSweepTx method will return instantly because the blockchain
3688
# has already been rescanned, as described above
3689
targScript = scrAddr_to_script(SCRADDR_P2PKH_BYTE + self.sweepAfterScanTarg)
3690
finishedTx, outVal, fee = self.createSweepAddrTx(sweepList, targScript)
3692
gt1 = len(sweepList)>1
3694
if finishedTx==None:
3695
if (outVal,fee)==(0,0):
3696
QMessageBox.critical(self, 'Nothing to do', \
3697
'The private %s you have provided does not appear to contain '
3698
'any funds. There is nothing to sweep.' % ('keys' if gt1 else 'key'), \
3702
pladdr = ('addresses' if gt1 else 'address')
3703
QMessageBox.critical(self, 'Cannot sweep',\
3704
'You cannot sweep the funds from the %s you specified, because '
3705
'the transaction fee would be equal to or greater than the amount '
3708
'<b>Balance of %s:</b> %s<br>'
3709
'<b>Fee to sweep %s:</b> %s'
3710
'<br><br>The sweep operation has been canceled.' % (pladdr, pladdr, \
3711
coin2str(outVal+fee,maxZeros=0), pladdr, coin2str(fee,maxZeros=0)), \
3713
LOGERROR('Sweep amount (%s) is less than fee needed for sweeping (%s)', \
3714
coin2str(outVal+fee, maxZeros=0), coin2str(fee, maxZeros=0))
3717
wltID = self.getWalletForAddr160(self.sweepAfterScanTarg)
3718
wlt = self.walletMap[wltID]
3720
# Finally, if we got here, we're ready to broadcast!
3722
dispIn = 'multiple addresses'
3724
dispIn = 'address <b>%s</b>' % sweepList[0].getAddrStr()
3726
dispOut = 'wallet <b>"%s"</b> (%s) ' % (wlt.labelName, wlt.uniqueIDB58)
3727
if DlgVerifySweep(dispIn, dispOut, outVal, fee).exec_():
3728
self.broadcastTransaction(finishedTx, dryRun=False)
3730
if TheBDM.getBDMState()=='BlockchainReady':
3731
wlt.syncWithBlockchain(0)
3733
self.walletListChanged()
3735
#############################################################################
3736
def broadcastTransaction(self, pytx, dryRun=False, withOldSigWarning=True):
3739
#DlgDispTxInfo(pytx, None, self, self).exec_()
3742
modified, newTx = pytx.minimizeDERSignaturePadding()
3743
if modified and withOldSigWarning:
3744
reply = QMessageBox.warning(self, 'Old signature format detected', \
3745
'The transaction that you are about to execute '
3746
'has been signed with an older version Bitcoin Armory '
3747
'that has added unnecessary padding to the signature. '
3748
'If you are running version Bitcoin 0.8.2 or later the unnecessary '
3749
'the unnecessary signature padding will not be broadcast. '
3750
'Note that removing the unnecessary padding will change the hash value '
3751
'of the transaction. Do you want to remove the unnecessary padding?', QMessageBox.Yes | QMessageBox.No)
3752
if reply == QMessageBox.Yes:
3754
LOGRAWDATA(pytx.serialize(), logging.INFO)
3755
LOGPPRINT(pytx, logging.INFO)
3756
newTxHash = pytx.getHash()
3757
LOGINFO('Sending Tx, %s', binary_to_hex(newTxHash))
3758
self.NetworkingFactory.sendTx(pytx)
3759
LOGINFO('Transaction sent to Satoshi client...!')
3762
def sendGetDataMsg():
3763
msg = PyMessage('getdata')
3764
msg.payload.invList.append( [MSG_INV_TX, newTxHash] )
3765
self.NetworkingFactory.sendMessage(msg)
3767
def checkForTxInBDM():
3768
# The sleep/delay makes sure we have time to receive a response
3769
# but it also gives the user a chance to SEE the change to their
3770
# balance occur. In some cases, that may be more satisfying than
3771
# just seeing the updated balance when they get back to the main
3773
if not TheBDM.getTxByHash(newTxHash).isInitialized():
3774
LOGERROR('Transaction was not accepted by the Satoshi client')
3775
LOGERROR('Raw transaction:')
3776
LOGRAWDATA(pytx.serialize(), logging.ERROR)
3777
LOGERROR('Transaction details')
3778
LOGPPRINT(pytx, logging.ERROR)
3779
searchstr = binary_to_hex(newTxHash, BIGENDIAN)
3781
supportURL = 'https://bitcoinarmory.com/support'
3782
blkexplURL = BLOCKEXPLORE_URL_TX % searchstr
3783
blkexplURL_short = BLOCKEXPLORE_URL_TX % searchstr[:20]
3785
QMessageBox.warning(self, tr('Transaction Not Accepted'), tr("""
3786
The transaction that you just executed, does not
3787
appear to have been accepted by the Bitcoin network.
3788
This can happen for a variety of reasons, but it is
3789
usually due to a bug in the Armory software.
3790
<br><br>On some occasions the transaction actually did succeed
3791
and this message is the bug itself! To confirm whether the
3792
the transaction actually succeeded, you can try this direct link
3795
<a href="%s">%s...</a>
3797
If you do not see the
3798
transaction on that webpage within one minute, it failed and you
3799
should attempt to re-send it.
3800
If it <i>does</i> show up, then you do not need to do anything
3801
else -- it will show up in Armory as soon as it receives one
3803
<br><br>If the transaction did fail, please consider
3804
reporting this error the the Armory developers.
3805
From the main window, go to "<i>Help</i>" and select
3806
"<i>Submit Bug Report</i>". Or use "<i>File</i>" ->
3807
"<i>Export Log File</i>" and then attach it to a support
3809
<a href="%s">%s</a>""") % (BLOCKEXPLORE_NAME, blkexplURL,
3810
blkexplURL_short, supportURL, supportURL), QMessageBox.Ok)
3812
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger)
3814
# Send the Tx after a short delay, give the system time to see the Tx
3815
# on the network and process it, and check to see if the Tx was seen.
3816
# We may change this setup in the future, but for now....
3817
reactor.callLater(3, sendGetDataMsg)
3818
reactor.callLater(7, checkForTxInBDM)
3821
#############################################################################
3822
def warnNoImportWhileScan(self):
3824
if not self.usermode==USERMODE.Standard:
3825
extraMsg = ('<br><br>'
3826
'In the future, you may avoid scanning twice by '
3827
'starting Armory in offline mode (--offline), and '
3828
'perform the import before switching to online mode.')
3829
QMessageBox.warning(self, 'Armory is Busy', \
3830
'Wallets and addresses cannot be imported while Armory is in '
3831
'the middle of an existing blockchain scan. Please wait for '
3832
'the scan to finish. ' + extraMsg, QMessageBox.Ok)
3836
#############################################################################
3837
def execImportWallet(self):
3838
sdm = TheSDM.getSDMState()
3839
bdm = TheBDM.getBDMState()
3840
if sdm in ['BitcoindInitializing', \
3841
'BitcoindSynchronizing', \
3842
'TorrentSynchronizing'] or \
3843
bdm in ['Scanning']:
3844
QMessageBox.warning(self, tr('Scanning'), tr("""
3845
Armory is currently in the middle of scanning the blockchain for
3846
your existing wallets. New wallets cannot be imported until this
3847
operation is finished."""), QMessageBox.Ok)
3850
DlgUniversalRestoreSelect(self, self).exec_()
3853
#############################################################################
3854
def execGetImportWltName(self):
3855
fn = self.getFileLoad('Import Wallet File')
3856
if not os.path.exists(fn):
3859
wlt = PyBtcWallet().readWalletFile(fn, verifyIntegrity=False, \
3861
wltID = wlt.uniqueIDB58
3864
if self.walletMap.has_key(wltID):
3865
QMessageBox.warning(self, 'Duplicate Wallet!', \
3866
'You selected a wallet that has the same ID as one already '
3867
'in your wallet (%s)! If you would like to import it anyway, '
3868
'please delete the duplicate wallet in Armory, first.'%wltID, \
3872
fname = self.getUniqueWalletFilename(fn)
3873
newpath = os.path.join(ARMORY_HOME_DIR, fname)
3875
LOGINFO('Copying imported wallet to: %s', newpath)
3876
shutil.copy(fn, newpath)
3877
newWlt = PyBtcWallet().readWalletFile(newpath)
3878
newWlt.fillAddressPool()
3880
self.addWalletToAppAndAskAboutRescan(newWlt)
3882
""" I think the addWalletToAppAndAskAboutRescan replaces this...
3883
if TheBDM.getBDMState() in ('Uninitialized', 'Offline'):
3884
self.addWalletToApplication(newWlt, walletIsNew=False)
3887
if TheBDM.getBDMState()=='BlockchainReady':
3888
doRescanNow = QMessageBox.question(self, 'Rescan Needed', \
3889
'The wallet was imported successfully, but cannot be displayed '
3890
'until the global transaction history is '
3891
'searched for previous transactions. This scan will potentially '
3892
'take much longer than a regular rescan, and the wallet cannot '
3893
'be shown on the main display until this rescan is complete.'
3895
'<b>Would you like to go into offline mode to start this scan now?'
3896
'</b> If you click "No" the scan will be aborted, and the wallet '
3897
'will not be added to Armory.', \
3898
QMessageBox.Yes | QMessageBox.No)
3900
doRescanNow = QMessageBox.question(self, 'Rescan Needed', \
3901
'The wallet was imported successfully, but its balance cannot '
3902
'be determined until Armory performs a "recovery scan" for the '
3903
'wallet. This scan potentially takes much longer than a regular '
3904
'scan, and must be completed for all imported wallets. '
3906
'Armory is already in the middle of a scan and cannot be interrupted. '
3907
'Would you like to start the recovery scan when it is done?'
3909
'</b> If you click "No," the wallet import will be aborted '
3910
'and you must re-import the wallet when you '
3911
'are able to wait for the recovery scan.', \
3912
QMessageBox.Yes | QMessageBox.No)
3914
if doRescanNow == QMessageBox.Yes:
3915
LOGINFO('User requested rescan after wallet import')
3916
#TheBDM.startWalletRecoveryScan(newWlt) # TODO: re-enable this later
3917
#TheBDM.rescanBlockchain('AsNeeded', wait=False)
3918
self.startRescanBlockchain()
3919
self.setDashboardDetails()
3921
LOGINFO('User aborted the wallet-import scan')
3922
QMessageBox.warning(self, 'Import Failed', \
3923
'The wallet was not imported.', QMessageBox.Ok)
3925
# The wallet cannot exist without also being on disk.
3926
# If the user aborted, we should remove the disk data.
3927
thepath = newWlt.getWalletPath()
3928
thepathBackup = newWlt.getWalletPath('backup')
3930
os.remove(thepathBackup)
3933
self.addWalletToApplication(newWlt, walletIsNew=False)
3934
self.newWalletList.append([newWlt, False])
3935
LOGINFO('Import Complete!')
3941
#############################################################################
3942
def addWalletToAppAndAskAboutRescan(self, newWallet):
3943
LOGINFO('Raw import successful.')
3945
# If we are offline, then we can't assume there will ever be a
3946
# rescan. Just add the wallet to the application
3947
if TheBDM.getBDMState() in ('Uninitialized', 'Offline'):
3948
TheBDM.registerWallet(newWallet.cppWallet)
3949
self.addWalletToApplication(newWallet, walletIsNew=False)
3952
""" TODO: Temporarily removed recovery-rescan operations
3953
elif TheBDM.getBDMState()=='BlockchainReady':
3954
doRescanNow = QMessageBox.question(self, 'Rescan Needed', \
3955
'The wallet was recovered successfully, but cannot be displayed '
3956
'until the global transaction history is '
3957
'searched for previous transactions. This scan will potentially '
3958
'take much longer than a regular rescan, and the wallet cannot '
3959
'be shown on the main display until this rescan is complete.'
3961
'<b>Would you like to go into offline mode to start this scan now?'
3962
'</b> If you click "No" the scan will be aborted, and the wallet '
3963
'will not be added to Armory.', \
3964
QMessageBox.Yes | QMessageBox.No)
3965
doRescanNow = QMessageBox.question(self, 'Rescan Needed', \
3966
'The wallet was recovered successfully, but cannot be displayed '
3967
'until a special kind of rescan is performed to find previous '
3968
'transactions. However, Armory is currently in the middle of '
3969
'a scan. Would you like to start the recovery scan immediately '
3972
'</b> If you click "No" the scan will be aborted, and the wallet '
3973
'will not be added to Armory. Restore the wallet again when you '
3974
'are able to wait for the recovery scan.', \
3975
QMessageBox.Yes | QMessageBox.No)
3978
doRescanNow = QMessageBox.Cancel
3980
if TheBDM.getBDMState()=='BlockchainReady':
3981
doRescanNow = QMessageBox.question(self, tr('Rescan Needed'), \
3982
tr("""The wallet was restored successfully but its balance
3983
cannot be displayed until the blockchain is rescanned.
3984
Armory will need to go into offline mode for 5-20 minutes.
3986
Would you like to do the scan now? Clicking "No" will
3987
abort the restore/import operation."""), \
3988
QMessageBox.Yes | QMessageBox.No)
3990
doRescanNow = QMessageBox.question(self, tr('Rescan Needed'), \
3991
tr("""The wallet was restored successfully but its balance
3992
cannot be displayed until the blockchain is rescanned.
3993
However, Armory is currently in the middle of a rescan
3994
operation right now. Would you like to start a new scan
3995
as soon as this one is finished?
3997
Clicking "No" will abort adding the wallet to Armory."""), \
3998
QMessageBox.Yes | QMessageBox.No)
4001
if doRescanNow == QMessageBox.Yes:
4002
LOGINFO('User requested rescan after wallet restore')
4003
#TheBDM.startWalletRecoveryScan(newWallet)
4004
TheBDM.registerWallet(newWallet.cppWallet)
4005
self.startRescanBlockchain()
4006
self.setDashboardDetails()
4008
LOGINFO('User aborted the wallet-recovery scan')
4009
QMessageBox.warning(self, 'Import Failed', \
4010
'The wallet was not restored. To restore the wallet, reenter '
4011
'the "Restore Wallet" dialog again when you are able to wait '
4012
'for the rescan operation. ', QMessageBox.Ok)
4013
# The wallet cannot exist without also being on disk.
4014
# If the user aborted, we should remove the disk data.
4015
thepath = newWallet.getWalletPath()
4016
thepathBackup = newWallet.getWalletPath('backup')
4018
os.remove(thepathBackup)
4021
self.addWalletToApplication(newWallet, walletIsNew=False)
4022
LOGINFO('Import Complete!')
4025
#############################################################################
4026
def digitalBackupWarning(self):
4027
reply = QMessageBox.warning(self, 'Be Careful!', tr("""
4028
<font color="red"><b>WARNING:</b></font> You are about to make an
4029
<u>unencrypted</u> backup of your wallet. It is highly recommended
4030
that you do <u>not</u> ever save unencrypted wallets to your regular
4031
hard drive. This feature is intended for saving to a USB key or
4032
other removable media."""), QMessageBox.Ok | QMessageBox.Cancel)
4033
return (reply==QMessageBox.Ok)
4036
#############################################################################
4037
def execAddressBook(self):
4038
if TheBDM.getBDMState()=='Scanning':
4039
QMessageBox.warning(self, 'Blockchain Not Ready', \
4040
'The address book is created from transaction data available in '
4041
'the blockchain, which has not finished loading. The address '
4042
'book will become available when Armory is online.', QMessageBox.Ok)
4043
elif TheBDM.getBDMState() in ('Uninitialized','Offline'):
4044
QMessageBox.warning(self, 'Blockchain Not Ready', \
4045
'The address book is created from transaction data available in '
4046
'the blockchain, but Armory is currently offline. The address '
4047
'book will become available when Armory is online.', QMessageBox.Ok)
4049
if len(self.walletMap)==0:
4050
QMessageBox.warning(self, 'No wallets!', 'You have no wallets so '
4051
'there is no address book to display.', QMessageBox.Ok)
4053
DlgAddressBook(self, self, None, None, None).exec_()
4056
#############################################################################
4057
def getUniqueWalletFilename(self, wltPath):
4058
root,fname = os.path.split(wltPath)
4059
base,ext = os.path.splitext(fname)
4060
if not ext=='.wallet':
4061
fname = base+'.wallet'
4062
currHomeList = os.listdir(ARMORY_HOME_DIR)
4064
while fname in currHomeList:
4065
# If we already have a wallet by this name, must adjust name
4066
base,ext = os.path.splitext(fname)
4067
fname='%s_%02d.wallet'%(base, newIndex)
4070
raise WalletExistsError('Cannot find unique filename for wallet.'
4071
'Too many duplicates!')
4075
#############################################################################
4076
def addrViewDblClicked(self, index, wlt):
4077
uacfv = lambda x: self.updateAddressCommentFromView(self.wltAddrView, self.wlt)
4080
#############################################################################
4081
def dblClickLedger(self, index):
4082
if index.column()==LEDGERCOLS.Comment:
4083
self.updateTxCommentFromView(self.ledgerView)
4088
#############################################################################
4089
def showLedgerTx(self):
4090
row = self.ledgerView.selectedIndexes()[0].row()
4091
txHash = str(self.ledgerView.model().index(row, LEDGERCOLS.TxHash).data().toString())
4092
wltID = str(self.ledgerView.model().index(row, LEDGERCOLS.WltID).data().toString())
4093
txtime = unicode(self.ledgerView.model().index(row, LEDGERCOLS.DateStr).data().toString())
4096
txHashBin = hex_to_binary(txHash)
4097
if TheBDM.isInitialized():
4098
cppTx = TheBDM.getTxByHash(txHashBin)
4099
if cppTx.isInitialized():
4100
pytx = PyTx().unserialize(cppTx.serialize())
4103
QMessageBox.critical(self, 'Invalid Tx:',
4104
'The transaction you requested be displayed does not exist in '
4105
'in Armory\'s database. This is unusual...', QMessageBox.Ok)
4108
DlgDispTxInfo( pytx, self.walletMap[wltID], self, self, txtime=txtime).exec_()
4111
#############################################################################
4112
def showContextMenuLedger(self):
4113
menu = QMenu(self.ledgerView)
4115
if len(self.ledgerView.selectedIndexes())==0:
4118
row = self.ledgerView.selectedIndexes()[0].row()
4120
txHash = str(self.ledgerView.model().index(row, LEDGERCOLS.TxHash).data().toString())
4121
txHash = hex_switchEndian(txHash)
4122
wltID = str(self.ledgerView.model().index(row, LEDGERCOLS.WltID).data().toString())
4125
actViewTx = menu.addAction("View Details")
4126
actViewBlkChn = menu.addAction("View on %s" % BLOCKEXPLORE_NAME)
4127
actComment = menu.addAction("Change Comment")
4128
actCopyTxID = menu.addAction("Copy Transaction ID")
4129
actOpenWallet = menu.addAction("Open Relevant Wallet")
4130
action = menu.exec_(QCursor.pos())
4132
if action==actViewTx:
4134
elif action==actViewBlkChn:
4136
webbrowser.open(BLOCKEXPLORE_URL_TX % txHash)
4138
LOGEXCEPT('Failed to open webbrowser')
4139
QMessageBox.critical(self, 'Could not open browser', \
4140
'Armory encountered an error opening your web browser. To view '
4141
'this transaction on blockchain.info, please copy and paste '
4142
'the following URL into your browser: '
4143
'<br><br>%s' % (BLOCKEXPLORE_URL_TX % txHash), QMessageBox.Ok)
4144
elif action==actCopyTxID:
4145
clipb = QApplication.clipboard()
4147
clipb.setText(txHash)
4148
elif action==actComment:
4149
self.updateTxCommentFromView(self.ledgerView)
4150
elif action==actOpenWallet:
4151
DlgWalletDetails(self.getSelectedWallet(), self.usermode, self, self).exec_()
4153
#############################################################################
4155
def getSelectedWallet(self):
4157
if len(self.walletMap) > 0:
4158
wltID = self.walletMap.keys()[0]
4159
wltSelect = self.walletsView.selectedIndexes()
4160
if len(wltSelect) > 0:
4161
row = wltSelect[0].row()
4162
wltID = str(self.walletsView.model().index(row, WLTVIEWCOLS.ID).data().toString())
4163
# Starting the send dialog with or without a wallet
4164
return None if wltID == None else self.walletMap[wltID]
4166
def clickSendBitcoins(self):
4167
if TheBDM.getBDMState() in ('Offline', 'Uninitialized'):
4168
QMessageBox.warning(self, 'Offline Mode', \
4169
'Armory is currently running in offline mode, and has no '
4170
'ability to determine balances or create transactions. '
4172
'In order to send coins from this wallet you must use a '
4173
'full copy of this wallet from an online computer, '
4174
'or initiate an "offline transaction" using a watching-only '
4175
'wallet on an online computer.', QMessageBox.Ok)
4177
elif TheBDM.getBDMState()=='Scanning':
4178
QMessageBox.warning(self, 'Armory Not Ready', \
4179
'Armory is currently scanning the blockchain to collect '
4180
'the information needed to create transactions. This typically '
4181
'takes between one and five minutes. Please wait until your '
4182
'balance appears on the main window, then try again.', \
4186
selectionMade = True
4187
if len(self.walletMap)==0:
4188
reply = QMessageBox.information(self, 'No Wallets!', \
4189
'You cannot send any bitcoins until you create a wallet and '
4190
'receive some coins. Would you like to create a wallet?', \
4191
QMessageBox.Yes | QMessageBox.No)
4192
if reply==QMessageBox.Yes:
4193
self.startWalletWizard()
4195
DlgSendBitcoins(self.getSelectedWallet(), self, self).exec_()
4198
#############################################################################
4199
def uriSendBitcoins(self, uriDict):
4200
# Because Bitcoin-Qt doesn't store the message= field we have to assume
4201
# that the label field holds the Tx-info. So we concatenate them for
4202
# the display message
4203
uri_has = lambda s: uriDict.has_key(s)
4205
haveLbl = uri_has('label')
4206
haveMsg = uri_has('message')
4209
if haveLbl and haveMsg:
4210
newMsg = uriDict['label'] + ': ' + uriDict['message']
4211
elif not haveLbl and haveMsg:
4212
newMsg = uriDict['message']
4213
elif haveLbl and not haveMsg:
4214
newMsg = uriDict['label']
4217
descrStr = ('You just clicked on a "bitcoin:" link requesting bitcoins '
4218
'to be sent to the following address:<br> ')
4220
descrStr += '<br>--<b>Address</b>:\t%s ' % uriDict['address']
4222
#if uri_has('label'):
4223
#if len(uriDict['label'])>30:
4224
#descrStr += '(%s...)' % uriDict['label'][:30]
4226
#descrStr += '(%s)' % uriDict['label']
4229
if uri_has('amount'):
4230
amt = uriDict['amount']
4231
amtstr = coin2str(amt, maxZeros=1)
4232
descrStr += '<br>--<b>Amount</b>:\t%s BTC' % amtstr
4237
descrStr += '<br>--<b>Message</b>:\t%s...' % newMsg[:60]
4239
descrStr += '<br>--<b>Message</b>:\t%s' % newMsg
4241
uriDict['message'] = newMsg
4243
if not uri_has('amount'):
4244
descrStr += ('<br><br>There is no amount specified in the link, so '
4245
'you can decide the amount after selecting a wallet to use '
4246
'for this this transaction. ')
4248
descrStr += ('<br><br><b>The specified amount <u>can</u> be changed</b> on the '
4249
'next screen before hitting the "Send" button. ')
4252
selectedWalletID = None
4253
if len(self.walletMap)==0:
4254
reply = QMessageBox.information(self, 'No Wallets!', \
4255
'You just clicked on a "bitcoin:" link to send money, but you '
4256
'currently have no wallets! Would you like to create a wallet '
4257
'now?', QMessageBox.Yes | QMessageBox.No)
4258
if reply==QMessageBox.Yes:
4259
self.startWalletWizard()
4262
DlgSendBitcoins(self.getSelectedWallet(), self, self, uriDict).exec_()
4266
#############################################################################
4267
def clickReceiveCoins(self):
4268
LOGDEBUG('Clicked "Receive Bitcoins Button"')
4270
selectionMade = True
4271
if len(self.walletMap)==0:
4272
reply = QMessageBox.information(self, 'No Wallets!', \
4273
'You have not created any wallets which means there is nowhere to '
4274
'store you bitcoins! Would you like to create a wallet now?', \
4275
QMessageBox.Yes | QMessageBox.No)
4276
if reply==QMessageBox.Yes:
4277
self.startWalletWizard()
4279
elif len(self.walletMap)==1:
4280
wltID = self.walletMap.keys()[0]
4282
wltSelect = self.walletsView.selectedIndexes()
4283
if len(wltSelect)>0:
4284
row = wltSelect[0].row()
4285
wltID = str(self.walletsView.model().index(row, WLTVIEWCOLS.ID).data().toString())
4286
dlg = DlgWalletSelect(self, self, 'Receive coins with wallet...', '', \
4287
firstSelect=wltID, onlyMyWallets=False)
4289
wltID = dlg.selectedID
4291
selectionMade = False
4294
wlt = self.walletMap[wltID]
4295
wlttype = determineWalletType(wlt, self)[0]
4296
if showRecvCoinsWarningIfNecessary(wlt, self):
4297
DlgNewAddressDisp(wlt, self, self).exec_()
4301
#############################################################################
4302
def sysTrayActivated(self, reason):
4303
if reason==QSystemTrayIcon.DoubleClick:
4304
self.bringArmoryToFront()
4308
#############################################################################
4309
def bringArmoryToFront(self):
4311
self.setWindowState(Qt.WindowActive)
4312
self.activateWindow()
4315
#############################################################################
4316
def minimizeArmory(self):
4317
LOGDEBUG('Minimizing Armory')
4321
#############################################################################
4322
def startWalletWizard(self):
4323
walletWizard = WalletWizard(self, self)
4324
walletWizard.exec_()
4326
#############################################################################
4327
def startTxWizard(self, prefill=None, onlyOfflineWallets=False):
4328
txWizard = TxWizard(self, self, self.getSelectedWallet(), prefill, onlyOfflineWallets=onlyOfflineWallets)
4331
#############################################################################
4332
def exportLogFile(self):
4333
LOGDEBUG('exportLogFile')
4334
reply = QMessageBox.warning(self, tr('Bug Reporting'), tr("""
4335
As of version 0.91, Armory now includes a form for reporting
4336
problems with the software. Please use
4337
<i>"Help"</i>\xe2\x86\x92<i>"Submit Bug Report"</i>
4338
to send a report directly to the Armory team, which will include
4339
your log file automatically."""), QMessageBox.Ok | QMessageBox.Cancel)
4341
if not reply==QMessageBox.Ok:
4344
if self.logFilePrivacyWarning(wCancel=True):
4345
self.saveCombinedLogFile()
4347
#############################################################################
4348
def getUserAgreeToPrivacy(self, getAgreement=False):
4349
ptype = 'submitbug' if getAgreement else 'generic'
4350
dlg = DlgPrivacyPolicy(self, self, ptype)
4354
return dlg.chkUserAgrees.isChecked()
4356
#############################################################################
4357
def logFileTriplePrivacyWarning(self):
4358
return MsgBoxCustom(MSGBOX.Warning, tr('Privacy Warning'), tr("""
4359
<b><u><font size=4>ATI Privacy Policy</font></u></b>
4361
You should review the <a href="%s">Armory Technologies, Inc. privacy
4362
policy</a> before sending any data to ATI servers.
4365
<b><u><font size=3>Wallet Analysis Log Files</font></u></b>
4367
The wallet analysis logs contain no personally-identifiable
4368
information, only a record of errors and inconsistencies
4369
found in your wallet file. No private keys or even public
4373
<b><u><font size=3>Regular Log Files</font></u></b>
4375
The regular log files do not contain any <u>security</u>-sensitive
4376
information, but some users may consider the information to be
4377
<u>privacy</u>-sensitive. The log files may identify some addresses
4378
and transactions that are related to your wallets. It is always
4379
recommended you include your log files with any request to the
4380
Armory team, unless you are uncomfortable with the privacy
4384
<b><u><font size=3>Watching-only Wallet</font></u></b>
4386
A watching-only wallet is a copy of a regular wallet that does not
4387
contain any signing keys. This allows the holder to see the balance
4388
and transaction history of the wallet, but not spend any of the funds.
4390
You may be requested to submit a watching-only copy of your wallet
4391
to <i>Armory Technologies, Inc.</i> to make sure that there is no
4392
risk to the security of your funds. You should not even consider
4394
watching-only wallet unless it was specifically requested by an
4395
Armory representative.""") % PRIVACY_URL, yesStr="&Ok")
4398
#############################################################################
4399
def logFilePrivacyWarning(self, wCancel=False):
4400
return MsgBoxCustom(MSGBOX.Warning, tr('Privacy Warning'), tr("""
4401
<b><u><font size=4>ATI Privacy Policy</font></u></b>
4403
You should review the <a href="%s">Armory Technologies, Inc. privacy
4404
policy</a> before sending any data to ATI servers.
4407
Armory log files do not contain any <u>security</u>-sensitive
4408
information, but some users may consider the information to be
4409
<u>privacy</u>-sensitive. The log files may identify some addresses
4410
and transactions that are related to your wallets.
4413
<b>No signing-key data is ever written to the log file</b>.
4414
Only enough data is there to help the Armory developers
4415
track down bugs in the software, but it may still be considered
4416
sensitive information to some users.
4419
Please do not send the log file to the Armory developers if you
4420
are not comfortable with the privacy implications! However, if you
4421
do not send the log file, it may be very difficult or impossible
4422
for us to help you with your problem.
4424
<br><br><b><u>Advanced tip:</u></b> You can use
4425
"<i>File</i>"\xe2\x86\x92"<i>Export Log File</i>" from the main
4426
window to save a copy of the log file that you can manually
4427
review."""), wCancel=wCancel, yesStr="&Ok")
4430
#############################################################################
4431
def saveCombinedLogFile(self, saveFile=None):
4432
if saveFile is None:
4433
# TODO: Interleave the C++ log and the python log.
4434
# That could be a lot of work!
4435
defaultFN = 'armorylog_%s.txt' % \
4436
unixTimeToFormatStr(RightNow(),'%Y%m%d_%H%M')
4437
saveFile = self.getFileSave(title='Export Log File', \
4438
ffilter=['Text Files (*.txt)'], \
4439
defaultFilename=defaultFN)
4441
if len(unicode(saveFile)) > 0:
4442
fout = open(saveFile, 'wb')
4443
fout.write(getLastBytesOfFile(ARMORY_LOG_FILE, 256*1024))
4444
fout.write(getLastBytesOfFile(ARMCPP_LOG_FILE, 256*1024))
4447
LOGINFO('Log saved to %s', saveFile)
4449
#############################################################################
4450
def blinkTaskbar(self):
4451
self.activateWindow()
4454
#############################################################################
4455
def lookForBitcoind(self):
4456
LOGDEBUG('lookForBitcoind')
4457
if satoshiIsAvailable():
4460
self.setSatoshiPaths()
4463
TheSDM.setupSDM(extraExeSearch=self.satoshiExeSearchPath)
4465
LOGEXCEPT('Error setting up SDM')
4468
if TheSDM.failedFindExe:
4469
return 'StillMissing'
4473
#############################################################################
4474
def executeModeSwitch(self):
4475
LOGDEBUG('executeModeSwitch')
4477
if TheSDM.getSDMState() == 'BitcoindExeMissing':
4478
bitcoindStat = self.lookForBitcoind()
4479
if bitcoindStat=='Running':
4480
result = QMessageBox.warning(self, tr('Already running!'), tr("""
4481
The Bitcoin software appears to be installed now, but it
4482
needs to be closed for Armory to work. Would you like Armory
4483
to close it for you?"""), QMessageBox.Yes | QMessageBox.No)
4484
if result==QMessageBox.Yes:
4485
self.closeExistingBitcoin()
4486
self.startBitcoindIfNecessary()
4487
elif bitcoindStat=='StillMissing':
4488
QMessageBox.warning(self, tr('Still Missing'), tr("""
4489
The Bitcoin software still appears to be missing. If you
4490
just installed it, then please adjust your settings to point
4491
to the installation directory."""), QMessageBox.Ok)
4492
self.startBitcoindIfNecessary()
4493
elif self.doAutoBitcoind and not TheSDM.isRunningBitcoind():
4494
if satoshiIsAvailable():
4495
result = QMessageBox.warning(self, tr('Still Running'), tr("""
4496
'Bitcoin-Qt is still running. Armory cannot start until
4497
'it is closed. Do you want Armory to close it for you?"""), \
4498
QMessageBox.Yes | QMessageBox.No)
4499
if result==QMessageBox.Yes:
4500
self.closeExistingBitcoin()
4501
self.startBitcoindIfNecessary()
4503
self.startBitcoindIfNecessary()
4504
elif TheBDM.getBDMState() == 'BlockchainReady' and TheBDM.isDirty():
4505
#self.resetBdmBeforeScan()
4506
self.startRescanBlockchain()
4507
elif TheBDM.getBDMState() in ('Offline','Uninitialized'):
4508
#self.resetBdmBeforeScan()
4509
TheBDM.setOnlineMode(True)
4510
self.switchNetworkMode(NETWORKMODE.Full)
4512
LOGERROR('ModeSwitch button pressed when it should be disabled')
4514
self.setDashboardDetails()
4519
#############################################################################
4521
def resetBdmBeforeScan(self):
4522
if TheBDM.getBDMState()=='Scanning':
4523
LOGINFO('Aborting load')
4524
touchFile(os.path.join(ARMORY_HOME_DIR,'abortload.txt'))
4525
os.remove(os.path.join(ARMORY_HOME_DIR,'blkfiles.txt'))
4527
TheBDM.Reset(wait=False)
4528
for wid,wlt in self.walletMap.iteritems():
4529
TheBDM.registerWallet(wlt.cppWallet)
4533
#############################################################################
4534
def setupDashboard(self):
4535
LOGDEBUG('setupDashboard')
4536
self.lblBusy = QLabel('')
4538
# Unfortunately, QMovie objects don't work in Windows with py2exe
4539
# had to create my own little "Busy" icon and hook it up to the
4541
self.lblBusy.setPixmap(QPixmap(':/loadicon_0.png'))
4542
self.numHeartBeat = 0
4543
def loadBarUpdate():
4544
if self.lblBusy.isVisible():
4545
self.numHeartBeat += 1
4546
self.lblBusy.setPixmap(QPixmap(':/loadicon_%d.png' % \
4547
(self.numHeartBeat%6)))
4548
self.extraHeartbeatAlways.append(loadBarUpdate)
4550
self.qmov = QMovie(':/busy.gif')
4551
self.lblBusy.setMovie( self.qmov )
4555
self.btnModeSwitch = QPushButton('')
4556
self.connect(self.btnModeSwitch, SIGNAL('clicked()'), \
4557
self.executeModeSwitch)
4560
# Will switch this to array/matrix of widgets if I get more than 2 rows
4561
self.lblDashModeTorrent = QRichLabel('',doWrap=False)
4562
self.lblDashModeSync = QRichLabel('',doWrap=False)
4563
self.lblDashModeBuild = QRichLabel('',doWrap=False)
4564
self.lblDashModeScan = QRichLabel('',doWrap=False)
4566
self.lblDashModeTorrent.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
4567
self.lblDashModeSync.setAlignment( Qt.AlignLeft | Qt.AlignVCenter)
4568
self.lblDashModeBuild.setAlignment( Qt.AlignLeft | Qt.AlignVCenter)
4569
self.lblDashModeScan.setAlignment( Qt.AlignLeft | Qt.AlignVCenter)
4571
self.barProgressTorrent = QProgressBar(self)
4572
self.barProgressSync = QProgressBar(self)
4573
self.barProgressBuild = QProgressBar(self)
4574
self.barProgressScan = QProgressBar(self)
4576
self.barProgressTorrent.setRange(0,100)
4577
self.barProgressSync.setRange(0,100)
4578
self.barProgressBuild.setRange(0,100)
4579
self.barProgressScan.setRange(0,100)
4582
self.lblTorrentStats = QRichLabel('', hAlign=Qt.AlignHCenter)
4584
twid = relaxedSizeStr(self,'99 seconds')[0]
4585
self.lblTimeLeftTorrent = QRichLabel('')
4586
self.lblTimeLeftSync = QRichLabel('')
4587
self.lblTimeLeftBuild = QRichLabel('')
4588
self.lblTimeLeftScan = QRichLabel('')
4590
self.lblTimeLeftSync.setMinimumWidth(twid)
4591
self.lblTimeLeftScan.setMinimumWidth(twid)
4593
self.lblStatsTorrent = QRichLabel('')
4595
layoutDashMode = QGridLayout()
4596
layoutDashMode.addWidget(self.lblDashModeTorrent, 0,0)
4597
layoutDashMode.addWidget(self.barProgressTorrent, 0,1)
4598
layoutDashMode.addWidget(self.lblTimeLeftTorrent, 0,2)
4599
layoutDashMode.addWidget(self.lblTorrentStats, 1,0)
4601
layoutDashMode.addWidget(self.lblDashModeSync, 2,0)
4602
layoutDashMode.addWidget(self.barProgressSync, 2,1)
4603
layoutDashMode.addWidget(self.lblTimeLeftSync, 2,2)
4605
layoutDashMode.addWidget(self.lblDashModeBuild, 3,0)
4606
layoutDashMode.addWidget(self.barProgressBuild, 3,1)
4607
layoutDashMode.addWidget(self.lblTimeLeftBuild, 3,2)
4609
layoutDashMode.addWidget(self.lblDashModeScan, 4,0)
4610
layoutDashMode.addWidget(self.barProgressScan, 4,1)
4611
layoutDashMode.addWidget(self.lblTimeLeftScan, 4,2)
4613
layoutDashMode.addWidget(self.lblBusy, 0,3, 5,1)
4614
layoutDashMode.addWidget(self.btnModeSwitch, 0,3, 5,1)
4616
self.frmDashModeSub = QFrame()
4617
self.frmDashModeSub.setFrameStyle(STYLE_SUNKEN)
4618
self.frmDashModeSub.setLayout(layoutDashMode)
4619
self.frmDashMode = makeHorizFrame(['Stretch', \
4620
self.frmDashModeSub, \
4624
self.lblDashDescr1 = QRichLabel('')
4625
self.lblDashDescr2 = QRichLabel('')
4626
for lbl in [self.lblDashDescr1, self.lblDashDescr2]:
4627
# One textbox above buttons, one below
4628
lbl.setStyleSheet('padding: 5px')
4629
qpal = lbl.palette()
4630
qpal.setColor(QPalette.Base, Colors.Background)
4631
lbl.setPalette(qpal)
4632
lbl.setOpenExternalLinks(True)
4634
# Set up an array of buttons in the middle of the dashboard, to be used
4635
# to help the user install bitcoind.
4636
self.lblDashBtnDescr = QRichLabel('')
4637
self.lblDashBtnDescr.setOpenExternalLinks(True)
4638
BTN,LBL,TTIP = range(3)
4639
self.dashBtns = [[None]*3 for i in range(5)]
4640
self.dashBtns[DASHBTNS.Close ][BTN] = QPushButton('Close Bitcoin Process')
4641
self.dashBtns[DASHBTNS.Install ][BTN] = QPushButton('Download Bitcoin')
4642
self.dashBtns[DASHBTNS.Browse ][BTN] = QPushButton('Open www.bitcoin.org')
4643
self.dashBtns[DASHBTNS.Instruct][BTN] = QPushButton('Installation Instructions')
4644
self.dashBtns[DASHBTNS.Settings][BTN] = QPushButton('Change Settings')
4648
def openBitcoinOrg():
4649
webbrowser.open('http://www.bitcoin.org/en/download')
4655
webbrowser.open('https://www.bitcoinarmory.com/install-windows/')
4657
webbrowser.open('https://www.bitcoinarmory.com/install-linux/')
4659
webbrowser.open('https://www.bitcoinarmory.com/install-macosx/')
4666
self.connect(self.dashBtns[DASHBTNS.Close][BTN], SIGNAL('clicked()'), \
4667
self.closeExistingBitcoin)
4668
self.connect(self.dashBtns[DASHBTNS.Install][BTN], SIGNAL('clicked()'), \
4670
self.connect(self.dashBtns[DASHBTNS.Browse][BTN], SIGNAL('clicked()'), \
4672
self.connect(self.dashBtns[DASHBTNS.Settings][BTN], SIGNAL('clicked()'), \
4674
#self.connect(self.dashBtns[DASHBTNS.Instruct][BTN], SIGNAL('clicked()'), \
4675
#self.openInstructWindow)
4677
self.dashBtns[DASHBTNS.Close][LBL] = QRichLabel( \
4678
'Stop existing Bitcoin processes so that Armory can open its own')
4679
self.dashBtns[DASHBTNS.Browse][LBL] = QRichLabel( \
4680
'Open browser to Bitcoin webpage to download and install Bitcoin software')
4681
self.dashBtns[DASHBTNS.Instruct][LBL] = QRichLabel( \
4682
'Instructions for manually installing Bitcoin for operating system')
4683
self.dashBtns[DASHBTNS.Settings][LBL] = QRichLabel( \
4684
'Open Armory settings window to change Bitcoin software management')
4687
self.dashBtns[DASHBTNS.Browse][TTIP] = self.createToolTipWidget( \
4688
'Will open your default browser to http://www.bitcoin.org where you can '
4689
'download the latest version of Bitcoin-Qt, and get other information '
4690
'and links about Bitcoin, in general.')
4691
self.dashBtns[DASHBTNS.Instruct][TTIP] = self.createToolTipWidget( \
4692
'Instructions are specific to your operating system and include '
4693
'information to help you verify you are installing the correct software')
4694
self.dashBtns[DASHBTNS.Settings][TTIP] = self.createToolTipWidget(
4695
'Change Bitcoin-Qt/bitcoind management settings or point Armory to '
4696
'a non-standard Bitcoin installation')
4697
self.dashBtns[DASHBTNS.Close][TTIP] = self.createToolTipWidget( \
4698
'Armory has detected a running Bitcoin-Qt or bitcoind instance and '
4699
'will force it to exit')
4701
self.dashBtns[DASHBTNS.Install][BTN].setEnabled(False)
4702
self.dashBtns[DASHBTNS.Install][LBL] = QRichLabel('')
4703
self.dashBtns[DASHBTNS.Install][LBL].setText( \
4704
'This option is not yet available yet!', color='DisableFG')
4705
self.dashBtns[DASHBTNS.Install][TTIP] = QRichLabel('') # disabled
4709
self.dashBtns[DASHBTNS.Install][BTN].setEnabled(True)
4710
self.dashBtns[DASHBTNS.Install][LBL] = QRichLabel('')
4711
self.dashBtns[DASHBTNS.Install][LBL].setText( \
4712
'Securely download Bitcoin software for Windows %s' % OS_VARIANT[0])
4713
self.dashBtns[DASHBTNS.Install][TTIP] = self.createToolTipWidget( \
4714
'The downloaded files are cryptographically verified. '
4715
'Using this option will start the installer, you will '
4716
'have to click through it to complete installation.')
4718
#self.lblDashInstallForMe = QRichLabel( \
4719
#'Armory will download, verify, and start the Bitcoin installer for you')
4720
#self.ttipInstallForMe = self.createToolTipWidget( \
4721
#'Armory will download the latest version of the Bitcoin software '
4722
#'for Windows and verify its digital signatures. You will have to '
4723
#'click through the installation options.<u></u>')
4725
# Only display the install button if using a debian-based distro
4726
dist = platform.linux_distribution()
4727
if dist[0] in ['Ubuntu','LinuxMint'] or 'debian' in dist:
4728
self.dashBtns[DASHBTNS.Install][BTN].setEnabled(True)
4729
self.dashBtns[DASHBTNS.Install][LBL] = QRichLabel( tr("""
4730
Download and Install Bitcoin Core for Ubuntu/Debian"""))
4731
self.dashBtns[DASHBTNS.Install][TTIP] = self.createToolTipWidget( tr("""
4732
'Will download and Bitcoin software and cryptographically verify it"""))
4736
LOGERROR('Unrecognized OS!')
4739
self.frmDashMgmtButtons = QFrame()
4740
self.frmDashMgmtButtons.setFrameStyle(STYLE_SUNKEN)
4741
layoutButtons = QGridLayout()
4742
layoutButtons.addWidget(self.lblDashBtnDescr, 0,0, 1,3)
4746
wMin = tightSizeNChar(self, 50)[0]
4747
self.dashBtns[r][c].setMinimumWidth(wMin)
4748
layoutButtons.addWidget(self.dashBtns[r][c], r+1,c)
4750
self.frmDashMgmtButtons.setLayout(layoutButtons)
4751
self.frmDashMidButtons = makeHorizFrame(['Stretch', \
4752
self.frmDashMgmtButtons,
4755
dashLayout = QVBoxLayout()
4756
dashLayout.addWidget(self.frmDashMode)
4757
dashLayout.addWidget(self.lblDashDescr1)
4758
dashLayout.addWidget(self.frmDashMidButtons )
4759
dashLayout.addWidget(self.lblDashDescr2)
4761
frmInner.setLayout(dashLayout)
4763
self.dashScrollArea = QScrollArea()
4764
self.dashScrollArea.setWidgetResizable(True)
4765
self.dashScrollArea.setWidget(frmInner)
4766
scrollLayout = QVBoxLayout()
4767
scrollLayout.addWidget(self.dashScrollArea)
4768
self.tabDashboard.setLayout(scrollLayout)
4772
#############################################################################
4773
def setupAnnounceTab(self):
4775
self.lblAlertStr = QRichLabel(tr("""
4776
<font size=4><b>Announcements and alerts from <i>Armory Technologies,
4777
Inc.</i></b></font>"""), doWrap=False, hAlign=Qt.AlignHCenter)
4780
lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime()
4781
self.explicitCheckAnnouncements(5)
4782
lastUpdate2 = self.announceFetcher.getLastSuccessfulFetchTime()
4783
if lastUpdate==lastUpdate2:
4784
QMessageBox.warning(self, tr('Not Available'), tr("""
4785
Could not access the <font color="%s"><b>Armory
4786
Technologies, Inc.</b></font> announcement feeder.
4787
Try again in a couple minutes.""") % \
4788
htmlColor('TextGreen'), QMessageBox.Ok)
4790
QMessageBox.warning(self, tr('Update'), tr("""
4791
Announcements are now up to date!"""), QMessageBox.Ok)
4794
self.lblLastUpdated = QRichLabel('', doWrap=False)
4795
self.btnCheckForUpdates = QPushButton(tr('Check for Updates'))
4796
self.connect(self.btnCheckForUpdates, SIGNAL(CLICKED), checkUpd)
4799
frmLastUpdate = makeHorizFrame(['Stretch', \
4800
self.lblLastUpdated, \
4801
self.btnCheckForUpdates, \
4804
self.icoArmorySWVersion = QLabel('')
4805
self.lblArmorySWVersion = QRichLabel(tr("""
4806
No version information is available"""), doWrap=False)
4807
self.icoSatoshiSWVersion = QLabel('')
4808
self.lblSatoshiSWVersion = QRichLabel('', doWrap=False)
4810
self.btnSecureDLArmory = QPushButton(tr('Secure Downloader'))
4811
self.btnSecureDLSatoshi = QPushButton(tr('Secure Downloader'))
4812
self.btnSecureDLArmory.setVisible(False)
4813
self.btnSecureDLSatoshi.setVisible(False)
4814
self.connect(self.btnSecureDLArmory, SIGNAL(CLICKED), self.openDLArmory)
4815
self.connect(self.btnSecureDLSatoshi, SIGNAL(CLICKED), self.openDLSatoshi)
4818
frmVersions = QFrame()
4819
layoutVersions = QGridLayout()
4820
layoutVersions.addWidget(self.icoArmorySWVersion, 0,0)
4821
layoutVersions.addWidget(self.lblArmorySWVersion, 0,1)
4822
layoutVersions.addWidget(self.btnSecureDLArmory, 0,2)
4823
layoutVersions.addWidget(self.icoSatoshiSWVersion, 1,0)
4824
layoutVersions.addWidget(self.lblSatoshiSWVersion, 1,1)
4825
layoutVersions.addWidget(self.btnSecureDLSatoshi, 1,2)
4826
layoutVersions.setColumnStretch(0,0)
4827
layoutVersions.setColumnStretch(1,1)
4828
layoutVersions.setColumnStretch(2,0)
4829
frmVersions.setLayout(layoutVersions)
4830
frmVersions.setFrameStyle(STYLE_RAISED)
4832
lblVerHeader = QRichLabel(tr("""<font size=4><b>
4833
Software Version Updates:</b></font>"""), doWrap=False, \
4834
hAlign=Qt.AlignHCenter)
4835
lblTableHeader = QRichLabel(tr("""<font size=4><b>
4836
All Available Notifications:</b></font>"""), doWrap=False, \
4837
hAlign=Qt.AlignHCenter)
4840
# We need to generate popups when a widget is clicked, and be able
4841
# change that particular widget's target, when the table is updated.
4842
# Create one of these DlgGen objects for each of the 10 rows, simply
4843
# update it's nid and notifyMap when the table is updated
4845
def setParams(self, parent, nid, notifyMap):
4846
self.parent = parent
4848
self.notifyMap = notifyMap
4851
return DlgNotificationWithDNAA(self.parent, self.parent, \
4852
self.nid, self.notifyMap, False).exec_()
4854
self.announceTableWidgets = \
4855
[[QLabel(''), QRichLabel(''), QLabelButton('+'), DlgGen()] \
4860
layoutTable = QGridLayout()
4863
layoutTable.addWidget(self.announceTableWidgets[i][j], i,j)
4864
self.connect(self.announceTableWidgets[i][2], SIGNAL(CLICKED), \
4865
self.announceTableWidgets[i][3])
4867
layoutTable.setColumnStretch(0,0)
4868
layoutTable.setColumnStretch(1,1)
4869
layoutTable.setColumnStretch(2,0)
4872
frmTable.setLayout(layoutTable)
4873
frmTable.setFrameStyle(STYLE_SUNKEN)
4875
self.updateAnnounceTable()
4878
frmEverything = makeVertFrame( [ self.lblAlertStr,
4888
frmEverything.setMinimumWidth(300)
4889
frmEverything.setMaximumWidth(800)
4891
frmFinal = makeHorizFrame(['Stretch', frmEverything, 'Stretch'])
4893
self.announceScrollArea = QScrollArea()
4894
self.announceScrollArea.setWidgetResizable(True)
4895
self.announceScrollArea.setWidget(frmFinal)
4896
scrollLayout = QVBoxLayout()
4897
scrollLayout.addWidget(self.announceScrollArea)
4898
self.tabAnnounce.setLayout(scrollLayout)
4900
self.announceIsSetup = True
4903
#############################################################################
4904
def openDownloaderAll(self):
4905
dl,cl = self.getDownloaderData()
4906
if not dl is None and not cl is None:
4907
UpgradeDownloaderDialog(self, self, None, dl, cl).exec_()
4909
#############################################################################
4910
def openDLArmory(self):
4911
dl,cl = self.getDownloaderData()
4912
if not dl is None and not cl is None:
4913
UpgradeDownloaderDialog(self, self, 'Armory', dl, cl).exec_()
4915
#############################################################################
4916
def openDLSatoshi(self):
4917
dl,cl = self.getDownloaderData()
4918
if not dl is None and not cl is None:
4919
UpgradeDownloaderDialog(self, self, 'Satoshi', dl, cl).exec_()
4922
#############################################################################
4923
def getDownloaderData(self):
4924
dl = self.announceFetcher.getAnnounceFile('downloads')
4925
cl = self.announceFetcher.getAnnounceFile('changelog')
4927
dlObj = downloadLinkParser().parseDownloadList(dl)
4928
clObj = changelogParser().parseChangelogText(cl)
4930
if dlObj is None or clObj is None:
4931
QMessageBox.warning(self, tr('No Data'), tr("""
4932
The secure downloader has not received any download
4933
data to display. Either the <font color="%s"><b>Armory
4934
Technologies, Inc.</b></font> announcement feeder is
4935
down, or this computer cannot access the server.""") % \
4936
htmlColor('TextGreen'), QMessageBox.Ok)
4939
lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime()
4940
sinceLastUpd = RightNow() - lastUpdate
4941
if lastUpdate < RightNow()-1*WEEK:
4942
QMessageBox.warning(self, tr('Old Data'), tr("""
4943
The last update retrieved from the <font color="%s"><b>Armory
4944
Technologies, Inc.</b></font> announcement feeder was <b>%s</b>
4945
ago. The following downloads may not be the latest
4946
available.""") % (htmlColor("TextGreen"), \
4947
secondsToHumanTime(sinceLastUpd)), QMessageBox.Ok)
4949
dl = self.announceFetcher.getAnnounceFile('downloads')
4950
cl = self.announceFetcher.getAnnounceFile('changelog')
4956
#############################################################################
4957
def updateAnnounceTab(self, *args):
4959
if not self.announceIsSetup:
4962
iconArmory = ':/armory_icon_32x32.png'
4963
iconSatoshi = ':/bitcoinlogo.png'
4964
iconInfoFile = ':/MsgBox_info48.png'
4965
iconGoodFile = ':/MsgBox_good48.png'
4966
iconWarnFile = ':/MsgBox_warning48.png'
4967
iconCritFile = ':/MsgBox_critical24.png'
4969
lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime()
4970
noAnnounce = (lastUpdate == 0)
4973
self.lblLastUpdated.setText(tr("No announcement data was found!"))
4974
self.btnSecureDLArmory.setVisible(False)
4975
self.icoArmorySWVersion.setVisible(True)
4976
self.lblArmorySWVersion.setText(tr(""" You are running Armory
4977
version %s""") % getVersionString(BTCARMORY_VERSION))
4979
updTimeStr = unixTimeToFormatStr(lastUpdate)
4980
self.lblLastUpdated.setText(tr("<u>Last Updated</u>: %s") % updTimeStr)
4983
verStrToInt = lambda s: getVersionInt(readVersionString(s))
4985
# Notify of Armory updates
4986
self.icoArmorySWVersion.setPixmap(QPixmap(iconArmory).scaled(24,24))
4987
self.icoSatoshiSWVersion.setPixmap(QPixmap(iconSatoshi).scaled(24,24))
4990
armCurrent = verStrToInt(self.armoryVersions[0])
4991
armLatest = verStrToInt(self.armoryVersions[1])
4992
if armCurrent >= armLatest:
4993
dispIcon = QPixmap(iconArmory).scaled(24,24)
4994
self.icoArmorySWVersion.setPixmap(dispIcon)
4995
self.btnSecureDLArmory.setVisible(False)
4996
self.lblArmorySWVersion.setText(tr("""
4997
You are using the latest version of Armory"""))
4999
dispIcon = QPixmap(iconWarnFile).scaled(24,24)
5000
self.icoArmorySWVersion.setPixmap(dispIcon)
5001
self.btnSecureDLArmory.setVisible(True)
5002
self.lblArmorySWVersion.setText(tr("""
5003
<b>There is a newer version of Armory available!</b>"""))
5004
self.btnSecureDLArmory.setVisible(True)
5005
self.icoArmorySWVersion.setVisible(True)
5007
self.btnSecureDLArmory.setVisible(False)
5008
self.lblArmorySWVersion.setText(tr(""" You are running Armory
5009
version %s""") % getVersionString(BTCARMORY_VERSION))
5013
satCurrStr,satLastStr = self.satoshiVersions
5014
satCurrent = verStrToInt(satCurrStr) if satCurrStr else 0
5015
satLatest = verStrToInt(satLastStr) if satLastStr else 0
5017
# Show CoreBTC updates
5018
if satCurrent and satLatest:
5019
if satCurrent >= satLatest:
5020
dispIcon = QPixmap(iconGoodFile).scaled(24,24)
5021
self.btnSecureDLSatoshi.setVisible(False)
5022
self.icoSatoshiSWVersion.setPixmap(dispIcon)
5023
self.lblSatoshiSWVersion.setText(tr(""" You are using
5024
the latest version of core Bitcoin (%s)""") % satCurrStr)
5026
dispIcon = QPixmap(iconWarnFile).scaled(24,24)
5027
self.btnSecureDLSatoshi.setVisible(True)
5028
self.icoSatoshiSWVersion.setPixmap(dispIcon)
5029
self.lblSatoshiSWVersion.setText(tr("""
5030
<b>There is a newer version of the core Bitcoin software
5033
# satLatest is not available
5034
dispIcon = QPixmap(iconGoodFile).scaled(24,24)
5035
self.btnSecureDLSatoshi.setVisible(False)
5036
self.icoSatoshiSWVersion.setPixmap(None)
5037
self.lblSatoshiSWVersion.setText(tr(""" You are using
5038
core Bitcoin version %s""") % satCurrStr)
5040
# only satLatest is avail (maybe offline)
5041
dispIcon = QPixmap(iconSatoshi).scaled(24,24)
5042
self.btnSecureDLSatoshi.setVisible(True)
5043
self.icoSatoshiSWVersion.setPixmap(dispIcon)
5044
self.lblSatoshiSWVersion.setText(tr("""Core Bitcoin version
5045
%s is available.""") % satLastStr)
5047
# only satLatest is avail (maybe offline)
5048
dispIcon = QPixmap(iconSatoshi).scaled(24,24)
5049
self.btnSecureDLSatoshi.setVisible(False)
5050
self.icoSatoshiSWVersion.setPixmap(dispIcon)
5051
self.lblSatoshiSWVersion.setText(tr("""No version information
5052
is available for core Bitcoin""") )
5057
#self.btnSecureDLSatoshi.setVisible(False)
5058
#if self.satoshiVersions[0]:
5059
#self.lblSatoshiSWVersion.setText(tr(""" You are running
5060
#core Bitcoin software version %s""") % self.satoshiVersions[0])
5062
#self.lblSatoshiSWVersion.setText(tr("""No information is
5063
#available for the core Bitcoin software"""))
5065
LOGEXCEPT('Failed to process satoshi versions')
5068
self.updateAnnounceTable()
5071
#############################################################################
5072
def updateAnnounceTable(self):
5074
# Default: Make everything non-visible except first row, middle column
5077
self.announceTableWidgets[i][j].setVisible(i==0 and j==1)
5079
if len(self.almostFullNotificationList)==0:
5080
self.announceTableWidgets[0][1].setText(tr("""
5081
There are no announcements or alerts to display"""))
5085
alertsForSorting = []
5086
for nid,nmap in self.almostFullNotificationList.iteritems():
5087
alertsForSorting.append([nid, int(nmap['PRIORITY'])])
5089
sortedAlerts = sorted(alertsForSorting, key=lambda a: -a[1])[:10]
5092
for nid,priority in sortedAlerts:
5094
pixm = QPixmap(':/MsgBox_critical64.png')
5095
elif priority>=3072:
5096
pixm = QPixmap(':/MsgBox_warning48.png')
5097
elif priority>=2048:
5098
pixm = QPixmap(':/MsgBox_info48.png')
5100
pixm = QPixmap(':/MsgBox_info48.png')
5103
shortDescr = self.almostFullNotificationList[nid]['SHORTDESCR']
5105
shortDescr = '<font color="%s">' + shortDescr + '</font>'
5106
shortDescr = shortDescr % htmlColor('TextWarn')
5108
self.announceTableWidgets[i][0].setPixmap(pixm.scaled(24,24))
5109
self.announceTableWidgets[i][1].setText(shortDescr)
5110
self.announceTableWidgets[i][2].setVisible(True)
5111
self.announceTableWidgets[i][3].setParams(self, nid, \
5112
self.almostFullNotificationList[nid])
5115
self.announceTableWidgets[i][j].setVisible(True)
5119
#############################################################################
5120
def explicitCheckAnnouncements(self, waitTime=3):
5121
self.announceFetcher.fetchRightNow(waitTime)
5122
self.processAnnounceData()
5123
self.updateAnnounceTab()
5125
#############################################################################
5126
def closeExistingBitcoin(self):
5127
for proc in psutil.process_iter():
5128
if proc.name.lower() in ['bitcoind.exe','bitcoin-qt.exe',\
5129
'bitcoind','bitcoin-qt']:
5130
killProcess(proc.pid)
5134
# If got here, never found it
5135
QMessageBox.warning(self, 'Not Found', \
5136
'Attempted to kill the running Bitcoin-Qt/bitcoind instance, '
5137
'but it was not found. ', QMessageBox.Ok)
5139
#############################################################################
5140
def getPercentageFinished(self, maxblk, lastblk):
5141
curr = EstimateCumulativeBlockchainSize(lastblk)
5142
maxb = EstimateCumulativeBlockchainSize(maxblk)
5143
return float(curr)/float(maxb)
5145
#############################################################################
5146
def updateSyncProgress(self):
5148
if TheTDM.getTDMState()=='Downloading':
5150
dlSpeed = TheTDM.getLastStats('downRate')
5151
timeEst = TheTDM.getLastStats('timeEst')
5152
fracDone = TheTDM.getLastStats('fracDone')
5153
numSeeds = TheTDM.getLastStats('numSeeds')
5154
numPeers = TheTDM.getLastStats('numPeers')
5156
self.barProgressTorrent.setVisible(True)
5157
self.lblDashModeTorrent.setVisible(True)
5158
self.lblTimeLeftTorrent.setVisible(True)
5159
self.lblTorrentStats.setVisible(True)
5160
self.barProgressTorrent.setFormat('%p%')
5162
self.lblDashModeSync.setVisible(True)
5163
self.barProgressSync.setVisible(True)
5164
self.barProgressSync.setValue(0)
5165
self.lblTimeLeftSync.setVisible(True)
5166
self.barProgressSync.setFormat('')
5168
self.lblDashModeBuild.setVisible(True)
5169
self.barProgressBuild.setVisible(True)
5170
self.barProgressBuild.setValue(0)
5171
self.lblTimeLeftBuild.setVisible(True)
5172
self.barProgressBuild.setFormat('')
5174
self.lblDashModeScan.setVisible(True)
5175
self.barProgressScan.setVisible(True)
5176
self.barProgressScan.setValue(0)
5177
self.lblTimeLeftScan.setVisible(True)
5178
self.barProgressScan.setFormat('')
5181
self.barProgressTorrent.setValue(0)
5182
self.lblTimeLeftTorrent.setText('')
5183
self.lblTorrentStats.setText('')
5185
self.lblDashModeTorrent.setText(tr('Initializing Torrent Engine'), \
5186
size=4, bold=True, color='Foreground')
5188
self.lblTorrentStats.setVisible(False)
5190
self.lblDashModeTorrent.setText(tr('Downloading via Armory CDN'), \
5191
size=4, bold=True, color='Foreground')
5194
self.barProgressTorrent.setValue(int(99.9*fracDone))
5197
self.lblTimeLeftTorrent.setText(secondsToHumanTime(timeEst))
5199
self.lblTorrentStats.setText(tr("""
5200
Bootstrap Torrent: %s/sec from %d peers""") % \
5201
(bytesToHumanSize(dlSpeed), numSeeds+numPeers))
5203
self.lblTorrentStats.setVisible(True)
5207
elif TheBDM.getBDMState()=='Scanning':
5208
self.barProgressTorrent.setVisible(TheTDM.isStarted())
5209
self.lblDashModeTorrent.setVisible(TheTDM.isStarted())
5210
self.barProgressTorrent.setValue(100)
5211
self.lblTimeLeftTorrent.setVisible(False)
5212
self.lblTorrentStats.setVisible(False)
5213
self.barProgressTorrent.setFormat('')
5215
self.lblDashModeSync.setVisible(self.doAutoBitcoind)
5216
self.barProgressSync.setVisible(self.doAutoBitcoind)
5217
self.barProgressSync.setValue(100)
5218
self.lblTimeLeftSync.setVisible(False)
5219
self.barProgressSync.setFormat('')
5221
self.lblDashModeBuild.setVisible(True)
5222
self.barProgressBuild.setVisible(True)
5223
self.lblTimeLeftBuild.setVisible(True)
5225
self.lblDashModeScan.setVisible(True)
5226
self.barProgressScan.setVisible(True)
5227
self.lblTimeLeftScan.setVisible(True)
5229
# Scan time is super-simple to predict: it's pretty much linear
5230
# with the number of bytes remaining.
5232
phase,pct,rate,tleft = TheBDM.predictLoadTime()
5234
self.lblDashModeBuild.setText( 'Building Databases', \
5235
size=4, bold=True, color='Foreground')
5236
self.lblDashModeScan.setText( 'Scan Transaction History', \
5237
size=4, bold=True, color='DisableFG')
5238
self.barProgressBuild.setFormat('%p%')
5239
self.barProgressScan.setFormat('')
5242
self.lblDashModeBuild.setText( 'Build Databases', \
5243
size=4, bold=True, color='DisableFG')
5244
self.lblDashModeScan.setText( 'Scanning Transaction History', \
5245
size=4, bold=True, color='Foreground')
5246
self.lblTimeLeftBuild.setVisible(False)
5247
self.barProgressBuild.setFormat('')
5248
self.barProgressBuild.setValue(100)
5249
self.barProgressScan.setFormat('%p%')
5251
self.lblDashModeScan.setText( 'Global Blockchain Index', \
5252
size=4, bold=True, color='Foreground')
5254
tleft15 = (int(tleft-1)/15 + 1)*15
5259
tstring = secondsToHumanTime(tleft15)
5263
self.lblTimeLeftBuild.setText(tstring)
5264
self.barProgressBuild.setValue(pvalue)
5266
self.lblTimeLeftScan.setText(tstring)
5267
self.barProgressScan.setValue(pvalue)
5269
elif TheSDM.getSDMState() in ['BitcoindInitializing','BitcoindSynchronizing']:
5271
self.barProgressTorrent.setVisible(TheTDM.isStarted())
5272
self.lblDashModeTorrent.setVisible(TheTDM.isStarted())
5273
self.barProgressTorrent.setValue(100)
5274
self.lblTimeLeftTorrent.setVisible(False)
5275
self.lblTorrentStats.setVisible(False)
5276
self.barProgressTorrent.setFormat('')
5278
self.lblDashModeSync.setVisible(True)
5279
self.barProgressSync.setVisible(True)
5280
self.lblTimeLeftSync.setVisible(True)
5281
self.barProgressSync.setFormat('%p%')
5283
self.lblDashModeBuild.setVisible(True)
5284
self.barProgressBuild.setVisible(True)
5285
self.lblTimeLeftBuild.setVisible(False)
5286
self.barProgressBuild.setValue(0)
5287
self.barProgressBuild.setFormat('')
5289
self.lblDashModeScan.setVisible(True)
5290
self.barProgressScan.setVisible(True)
5291
self.lblTimeLeftScan.setVisible(False)
5292
self.barProgressScan.setValue(0)
5293
self.barProgressScan.setFormat('')
5295
ssdm = TheSDM.getSDMState()
5296
lastBlkNum = self.getSettingOrSetDefault('LastBlkRecv', 0)
5297
lastBlkTime = self.getSettingOrSetDefault('LastBlkRecvTime', 0)
5299
# Get data from SDM if it has it
5300
info = TheSDM.getTopBlockInfo()
5301
if len(info['tophash'])>0:
5302
lastBlkNum = info['numblks']
5303
lastBlkTime = info['toptime']
5305
# Use a reference point if we are starting from scratch
5306
refBlock = max(290746, lastBlkNum)
5307
refTime = max(1394922889, lastBlkTime)
5310
# Ten min/block is pretty accurate, even from genesis (about 1% slow)
5311
# And it gets better as we sync past the reference block above
5312
self.approxMaxBlock = refBlock + int((RightNow() - refTime) / (10*MINUTE))
5313
self.approxBlkLeft = self.approxMaxBlock - lastBlkNum
5314
self.approxPctSoFar = self.getPercentageFinished(self.approxMaxBlock, \
5317
self.initSyncCircBuff.append([RightNow(), self.approxPctSoFar])
5318
if len(self.initSyncCircBuff)>30:
5319
# There's always a couple wacky measurements up front, start at 10
5320
t0,p0 = self.initSyncCircBuff[10]
5321
t1,p1 = self.initSyncCircBuff[-1]
5322
dt,dp = t1-t0, p1-p0
5324
self.initSyncCircBuff = self.initSyncCircBuff[1:]
5328
if lastBlkNum < 200000:
5329
dpPerSec = dpPerSec / 2
5330
timeRemain = (1 - self.approxPctSoFar) / dpPerSec
5331
#timeRemain = min(timeRemain, 8*HOUR)
5338
intPct = int(100*self.approxPctSoFar)
5339
strPct = '%d%%' % intPct
5342
self.barProgressSync.setFormat('%p%')
5343
if ssdm == 'BitcoindReady':
5344
return (0,0,0.99) # because it's probably not completely done...
5345
self.lblTimeLeftSync.setText('Almost Done...')
5346
self.barProgressSync.setValue(99)
5347
elif ssdm == 'BitcoindSynchronizing':
5348
sdmPercent = int(99.9*self.approxPctSoFar)
5349
if self.approxBlkLeft < 10000:
5350
if self.approxBlkLeft < 200:
5351
self.lblTimeLeftSync.setText('%d blocks' % self.approxBlkLeft)
5353
# If we're within 10k blocks, estimate based on blkspersec
5354
if info['blkspersec'] > 0:
5355
timeleft = int(self.approxBlkLeft/info['blkspersec'])
5356
self.lblTimeLeftSync.setText(secondsToHumanTime(timeleft))
5358
# If we're more than 10k blocks behind...
5360
timeRemain = min(24*HOUR, timeRemain)
5361
self.lblTimeLeftSync.setText(secondsToHumanTime(timeRemain))
5363
self.lblTimeLeftSync.setText('')
5364
elif ssdm == 'BitcoindInitializing':
5366
self.barProgressSync.setFormat('')
5367
self.barProgressBuild.setFormat('')
5368
self.barProgressScan.setFormat('')
5370
LOGERROR('Should not predict sync info in non init/sync SDM state')
5371
return ('UNKNOWN','UNKNOWN', 'UNKNOWN')
5373
self.barProgressSync.setValue(sdmPercent)
5375
LOGWARN('Called updateSyncProgress while not sync\'ing')
5378
#############################################################################
5379
def GetDashFunctionalityText(self, func):
5381
Outsourcing all the verbose dashboard text to here, to de-clutter the
5382
logic paths in the setDashboardDetails function
5384
LOGINFO('Switching Armory functional mode to "%s"', func)
5385
if func.lower() == 'scanning':
5387
'The following functionality is available while scanning in offline mode:'
5389
'<li>Create new wallets</li>'
5390
'<li>Generate receiving addresses for your wallets</li>'
5391
'<li>Create backups of your wallets (printed or digital)</li>'
5392
'<li>Change wallet encryption settings</li>'
5393
'<li>Sign transactions created from an online system</li>'
5394
'<li>Sign messages</li>'
5396
'<br><br><b>NOTE:</b> The Bitcoin network <u>will</u> process transactions '
5397
'to your addresses, even if you are offline. It is perfectly '
5398
'okay to create and distribute payment addresses while Armory is offline, '
5399
'you just won\'t be able to verify those payments until the next time '
5400
'Armory is online.')
5401
elif func.lower() == 'offline':
5403
'The following functionality is available in offline mode:'
5405
'<li>Create, import or recover wallets</li>'
5406
'<li>Generate new receiving addresses for your wallets</li>'
5407
'<li>Create backups of your wallets (printed or digital)</li>'
5408
'<li>Import private keys to wallets</li>'
5409
'<li>Change wallet encryption settings</li>'
5410
'<li>Sign messages</li>'
5411
'<li><b>Sign transactions created from an online system</b></li>'
5413
'<br><br><b>NOTE:</b> The Bitcoin network <u>will</u> process transactions '
5414
'to your addresses, regardless of whether you are online. It is perfectly '
5415
'okay to create and distribute payment addresses while Armory is offline, '
5416
'you just won\'t be able to verify those payments until the next time '
5417
'Armory is online.')
5418
elif func.lower() == 'online':
5421
'<li>Create, import or recover Armory wallets</li>'
5422
'<li>Generate new addresses to receive coins</li>'
5423
'<li>Send bitcoins to other people</li>'
5424
'<li>Create one-time backups of your wallets (in printed or digital form)</li>'
5425
'<li>Click on "bitcoin:" links in your web browser '
5426
'(not supported on all operating systems)</li>'
5427
'<li>Import private keys to wallets</li>'
5428
'<li>Monitor payments to watching-only wallets and create '
5429
'unsigned transactions</li>'
5430
'<li>Sign messages</li>'
5431
'<li><b>Create transactions with watching-only wallets, '
5432
'to be signed by an offline wallets</b></li>'
5436
#############################################################################
5437
def GetDashStateText(self, mgmtMode, state):
5439
Outsourcing all the verbose dashboard text to here, to de-clutter the
5440
logic paths in the setDashboardDetails function
5442
LOGINFO('Switching Armory state text to Mgmt:%s, State:%s', mgmtMode, state)
5444
# A few states don't care which mgmtMode you are in...
5445
if state == 'NewUserInfo':
5447
For more information about Armory, and even Bitcoin itself, you should
5448
visit the <a href="https://bitcoinarmory.com/faqs/">frequently
5449
asked questions page</a>. If
5450
you are experiencing problems using this software, please visit the
5451
<a href="https://bitcoinarmory.com/troubleshooting/">Armory
5452
troubleshooting webpage</a>. It will be updated frequently with
5453
solutions to common problems.
5455
<b><u>IMPORTANT:</u></b> Make a backup of your wallet(s)! Paper
5456
backups protect you <i>forever</i> against forgotten passwords,
5457
hard-drive failure, and make it easy for your family to recover
5458
your funds if something terrible happens to you. <i>Each wallet
5459
only needs to be backed up once, ever!</i> Without it, you are at
5460
risk of losing all of your Bitcoins! For more information,
5461
visit the <a href="https://bitcoinarmory.com/armory-backups-are-forever/">Armory
5464
To learn about improving your security through the use of offline
5466
<a href="https://bitcoinarmory.com/using-our-wallet">Armory
5467
Quick Start Guide</a>, and the
5468
<a href="https://bitcoinarmory.com/using-our-wallet/#offlinewallet">Offline
5469
Wallet Tutorial</a>.<br><br> """)
5470
elif state == 'OnlineFull1':
5472
'<p><b>You now have access to all the features Armory has to offer!</b><br>'
5473
'To see your balances and transaction history, please click '
5474
'on the "Transactions" tab above this text. <br>'
5475
'Here\'s some things you can do with Armory Bitcoin Client:'
5477
elif state == 'OnlineFull2':
5479
('If you experience any performance issues with Armory, '
5480
'please confirm that Bitcoin-Qt is running and <i>fully '
5481
'synchronized with the Bitcoin network</i>. You will see '
5482
'a green checkmark in the bottom right corner of the '
5483
'Bitcoin-Qt window if it is synchronized. If not, it is '
5484
'recommended you close Armory and restart it only when you '
5485
'see that checkmark.'
5486
'<br><br>' if not self.doAutoBitcoind else '') + (
5487
'<b>Please backup your wallets!</b> Armory wallets are '
5488
'"deterministic", meaning they only need to be backed up '
5489
'one time (unless you have imported external addresses/keys). '
5490
'Make a backup and keep it in a safe place! All funds from '
5491
'Armory-generated addresses will always be recoverable with '
5492
'a paper backup, any time in the future. Use the "Backup '
5493
'Individual Keys" option for each wallet to backup imported '
5495
elif state == 'OnlineNeedSweep':
5497
'Armory is currently online, but you have requested a sweep operation '
5498
'on one or more private keys. This requires searching the global '
5499
'transaction history for the available balance of the keys to be '
5502
'Press the button to start the blockchain scan, which '
5503
'will also put Armory into offline mode for a few minutes '
5504
'until the scan operation is complete')
5505
elif state == 'OnlineDirty':
5507
'<b>Wallet balances may '
5508
'be incorrect until the rescan operation is performed!</b>'
5510
'Armory is currently online, but addresses/keys have been added '
5511
'without rescanning the blockchain. You may continue using '
5512
'Armory in online mode, but any transactions associated with the '
5513
'new addresses will not appear in the ledger. '
5515
'Pressing the button above will put Armory into offline mode '
5516
'for a few minutes until the scan operation is complete.')
5517
elif state == 'OfflineNoSatoshiNoInternet':
5519
'There is no connection to the internet, and there is no other '
5520
'Bitcoin software running. Most likely '
5521
'you are here because this is a system dedicated '
5522
'to manage offline wallets! '
5524
'<b>If you expected Armory to be in online mode</b>, '
5525
'please verify your internet connection is active, '
5526
'then restart Armory. If you think the lack of internet '
5527
'connection is in error (such as if you are using Tor), '
5528
'then you can restart Armory with the "--skip-online-check" '
5529
'option, or change it in the Armory settings.'
5531
'If you do not have Bitcoin-Qt installed, you can '
5532
'download it from <a href="http://www.bitcoin.org">'
5533
'http://www.bitcoin.org</a>.')
5535
# Branch the available display text based on which Satoshi-Management
5536
# mode Armory is using. It probably wasn't necessary to branch the
5537
# the code like this, but it helped me organize the seemingly-endless
5538
# number of dashboard screens I need
5539
if mgmtMode.lower()=='user':
5540
if state == 'OfflineButOnlinePossible':
5542
'You are currently in offline mode, but can '
5543
'switch to online mode by pressing the button above. However, '
5544
'it is not recommended that you switch until '
5545
'Bitcoin-Qt/bitcoind is fully synchronized with the bitcoin network. '
5546
'You will see a green checkmark in the bottom-right corner of '
5547
'the Bitcoin-Qt window when it is finished.'
5549
'Switching to online mode will give you access '
5550
'to more Armory functionality, including sending and receiving '
5551
'bitcoins and viewing the balances and transaction histories '
5552
'of each of your wallets.<br><br>')
5553
elif state == 'OfflineNoSatoshi':
5554
bitconf = os.path.join(BTC_HOME_DIR, 'bitcoin.conf')
5556
'You are currently in offline mode because '
5557
'Bitcoin-Qt is not running. To switch to online '
5558
'mode, start Bitcoin-Qt and let it synchronize with the network '
5559
'-- you will see a green checkmark in the bottom-right corner when '
5560
'it is complete. If Bitcoin-Qt is already running and you believe '
5561
'the lack of connection is an error (especially if using proxies), '
5562
'please see <a href="'
5563
'https://bitcointalk.org/index.php?topic=155717.msg1719077#msg1719077">'
5564
'this link</a> for options.'
5566
'<b>If you prefer to have Armory do this for you</b>, '
5567
'then please check "Let Armory run '
5568
'Bitcoin-Qt in the background" under "File"->"Settings."'
5570
'If you are new to Armory and/or Bitcoin-Qt, '
5571
'please visit the Armory '
5572
'webpage for more information. Start at '
5573
'<a href="https://bitcoinarmory.com/armory-and-bitcoin-qt">'
5574
'Why Armory needs Bitcoin-Qt</a> or go straight to our <a '
5575
'href="https://bitcoinarmory.com/faqs/">'
5576
'frequently asked questions</a> page for more general information. '
5577
'If you already know what you\'re doing and simply need '
5578
'to fetch the latest version of Bitcoin-Qt, you can download it from '
5579
'<a href="http://www.bitcoin.org">http://www.bitcoin.org</a>.')
5580
elif state == 'OfflineNoInternet':
5582
'You are currently in offline mode because '
5583
'Armory could not detect an internet connection. '
5584
'If you think this is in error, then '
5585
'restart Armory using the " --skip-online-check" option, '
5586
'or adjust the Armory settings. Then restart Armory.'
5588
'If this is intended to be an offline computer, note '
5589
'that it is not necessary to have Bitcoin-Qt or bitcoind '
5591
elif state == 'OfflineNoBlkFiles':
5593
'You are currently in offline mode because '
5594
'Armory could not find the blockchain files produced '
5595
'by Bitcoin-Qt. Do you run Bitcoin-Qt (or bitcoind) '
5596
'from a non-standard directory? Armory expects to '
5597
'find the blkXXXX.dat files in <br><br>%s<br><br> '
5598
'If you know where they are located, please restart '
5599
'Armory using the " --satoshi-datadir=[path]" '
5600
'to notify Armory where to find them.') % BLKFILE_DIR
5601
elif state == 'Disconnected':
5603
'Armory was previously online, but the connection to Bitcoin-Qt/'
5604
'bitcoind was interrupted. You will not be able to send bitcoins '
5605
'or confirm receipt of bitcoins until the connection is '
5606
'reestablished. br><br>Please check that Bitcoin-Qt is open '
5607
'and synchronized with the network. Armory will <i>try to '
5608
'reconnect</i> automatically when the connection is available '
5609
'again. If Bitcoin-Qt is available again, and reconnection does '
5610
'not happen, please restart Armory.<br><br>')
5611
elif state == 'ScanNoWallets':
5613
'Please wait while the global transaction history is scanned. '
5614
'Armory will go into online mode automatically, as soon as '
5615
'the scan is complete.')
5616
elif state == 'ScanWithWallets':
5618
'Armory is scanning the global transaction history to retrieve '
5619
'information about your wallets. The "Transactions" tab will '
5620
'be updated with wallet balance and history as soon as the scan is '
5621
'complete. You may manage your wallets while you wait.<br><br>')
5623
LOGERROR('Unrecognized dashboard state: Mgmt:%s, State:%s', \
5626
elif mgmtMode.lower()=='auto':
5627
if state == 'OfflineBitcoindRunning':
5629
'It appears you are already running Bitcoin software '
5630
'(Bitcoin-Qt or bitcoind). '
5631
'Unlike previous versions of Armory, you should <u>not</u> run '
5632
'this software yourself -- Armory '
5633
'will run it in the background for you. Either close the '
5634
'Bitcoin application or adjust your settings. If you change '
5635
'your settings, then please restart Armory.')
5636
if state == 'OfflineNeedBitcoinInst':
5638
'<b>Only one more step to getting online with Armory!</b> You '
5639
'must install the Bitcoin software from www.bitcoin.org in order '
5640
'for Armory to communicate with the Bitcoin network. If the '
5641
'Bitcoin software is already installed and/or you would prefer '
5642
'to manage it yourself, please adjust your settings and '
5644
if state == 'InitializingLongTime':
5646
<b>To maximize your security, the Bitcoin engine is downloading
5647
and verifying the global transaction ledger. <u>This will take
5648
several hours, but only needs to be done once</u>!</b> It is
5649
usually best to leave it running over night for this
5650
initialization process. Subsequent loads will only take a few
5653
<b>Please Note:</b> Between Armory and the underlying Bitcoin
5654
engine, you need to have 40-50 GB of spare disk space available
5655
to hold the global transaction history.
5657
While you wait, you can manage your wallets. Make new wallets,
5658
make digital or paper backups, create Bitcoin addresses to receive
5660
sign messages, and/or import private keys. You will always
5661
receive Bitcoin payments regardless of whether you are online,
5662
but you will have to verify that payment through another service
5663
until Armory is finished this initialization.""")
5664
if state == 'InitializingDoneSoon':
5666
'The software is downloading and processing the latest activity '
5667
'on the network related to your wallet%s. This should take only '
5668
'a few minutes. While you wait, you can manage your wallets. '
5670
'Now would be a good time to make paper (or digital) backups of '
5671
'your wallet%s if you have not done so already! You are protected '
5672
'<i>forever</i> from hard-drive loss, or forgetting you password. '
5673
'If you do not have a backup, you could lose all of your '
5674
'Bitcoins forever! See the <a href="https://bitcoinarmory.com/">'
5675
'Armory Backups page</a> for more info.' % \
5676
(('' if len(self.walletMap)==1 else 's',)*2))
5677
if state == 'OnlineDisconnected':
5679
'Armory\'s communication with the Bitcoin network was interrupted. '
5680
'This usually does not happen unless you closed the process that '
5681
'Armory was using to communicate with the network. Armory requires '
5682
'%s to be running in the background, and this error pops up if it '
5684
'<br><br>You may continue in offline mode, or you can close '
5685
'all Bitcoin processes and restart Armory.' \
5686
% os.path.basename(TheSDM.executable))
5687
if state == 'OfflineBadConnection':
5689
'Armory has experienced an issue trying to communicate with the '
5690
'Bitcoin software. The software is running in the background, '
5691
'but Armory cannot communicate with it through RPC as it expects '
5692
'to be able to. If you changed any settings in the Bitcoin home '
5693
'directory, please make sure that RPC is enabled and that it is '
5694
'accepting connections from localhost. '
5696
'If you have not changed anything, please export the log file '
5697
'(from the "File" menu) and send it to support@bitcoinarmory.com')
5698
if state == 'OfflineSatoshiAvail':
5700
'Armory does not detect internet access, but it does detect '
5701
'running Bitcoin software. Armory is in offline-mode. <br><br>'
5702
'If you are intending to run an offline system, you will not '
5703
'need to have the Bitcoin software installed on the offline '
5704
'computer. It is only needed for the online computer. '
5705
'If you expected to be online and '
5706
'the absence of internet is an error, please restart Armory '
5707
'using the "--skip-online-check" option. ')
5708
if state == 'OfflineForcedButSatoshiAvail':
5710
'Armory was started in offline-mode, but detected you are '
5711
'running Bitcoin software. If you are intending to run an '
5712
'offline system, you will <u>not</u> need to have the Bitcoin '
5713
'software installed or running on the offline '
5714
'computer. It is only required for being online. ')
5715
if state == 'OfflineBadDBEnv':
5717
'The Bitcoin software indicates there '
5718
'is a problem with its databases. This can occur when '
5719
'Bitcoin-Qt/bitcoind is upgraded or downgraded, or sometimes '
5720
'just by chance after an unclean shutdown.'
5722
'You can either revert your installed Bitcoin software to the '
5723
'last known working version (but not earlier than version 0.8.1) '
5724
'or delete everything <b>except</b> "wallet.dat" from the your Bitcoin '
5725
'home directory:<br><br>'
5726
'<font face="courier"><b>%s</b></font>'
5728
'If you choose to delete the contents of the Bitcoin home '
5729
'directory, you will have to do a fresh download of the blockchain '
5730
'again, which will require a few hours the first '
5731
'time.' % self.satoshiHomePath)
5732
if state == 'OfflineBtcdCrashed':
5733
sout = '' if TheSDM.btcOut==None else str(TheSDM.btcOut)
5734
serr = '' if TheSDM.btcErr==None else str(TheSDM.btcErr)
5735
soutHtml = '<br><br>' + '<br>'.join(sout.strip().split('\n'))
5736
serrHtml = '<br><br>' + '<br>'.join(serr.strip().split('\n'))
5737
soutDisp = '<b><font face="courier">StdOut: %s</font></b>' % soutHtml
5738
serrDisp = '<b><font face="courier">StdErr: %s</font></b>' % serrHtml
5739
if len(sout)>0 or len(serr)>0:
5741
There was an error starting the underlying Bitcoin engine.
5742
This should not normally happen. Usually it occurs when you
5743
have been using Bitcoin-Qt prior to using Armory, especially
5744
if you have upgraded or downgraded Bitcoin-Qt recently.
5745
Output from bitcoind:<br>""") + \
5746
(soutDisp if len(sout)>0 else '') + \
5747
(serrDisp if len(serr)>0 else '') )
5750
There was an error starting the underlying Bitcoin engine.
5751
This should not normally happen. Usually it occurs when you
5752
have been using Bitcoin-Qt prior to using Armory, especially
5753
if you have upgraded or downgraded Bitcoin-Qt recently.
5755
Unfortunately, this error is so strange, Armory does not
5756
recognize it. Please go to "Export Log File" from the "File"
5757
menu and email at as an attachment to <a href="mailto:
5758
support@bitcoinarmory.com?Subject=Bitcoind%20Crash">
5759
support@bitcoinarmory.com</a>. We apologize for the
5763
#############################################################################
5765
def setDashboardDetails(self, INIT=False):
5767
We've dumped all the dashboard text into the above 2 methods in order
5768
to declutter this method.
5770
onlineAvail = self.onlineModeIsPossible()
5772
sdmState = TheSDM.getSDMState()
5773
bdmState = TheBDM.getBDMState()
5774
tdmState = TheTDM.getTDMState()
5779
# Methods for showing/hiding groups of widgets on the dashboard
5780
def setBtnRowVisible(r, visBool):
5782
self.dashBtns[r][c].setVisible(visBool)
5784
def setSyncRowVisible(b):
5785
self.lblDashModeSync.setVisible(b)
5786
self.barProgressSync.setVisible(b)
5787
self.lblTimeLeftSync.setVisible(b)
5790
def setTorrentRowVisible(b):
5791
self.lblDashModeTorrent.setVisible(b)
5792
self.barProgressTorrent.setVisible(b)
5793
self.lblTimeLeftTorrent.setVisible(b)
5794
self.lblTorrentStats.setVisible(b)
5796
def setBuildRowVisible(b):
5797
self.lblDashModeBuild.setVisible(b)
5798
self.barProgressBuild.setVisible(b)
5799
self.lblTimeLeftBuild.setVisible(b)
5801
def setScanRowVisible(b):
5802
self.lblDashModeScan.setVisible(b)
5803
self.barProgressScan.setVisible(b)
5804
self.lblTimeLeftScan.setVisible(b)
5806
def setOnlyDashModeVisible():
5807
setTorrentRowVisible(False)
5808
setSyncRowVisible(False)
5809
setBuildRowVisible(False)
5810
setScanRowVisible(False)
5811
self.lblBusy.setVisible(False)
5812
self.btnModeSwitch.setVisible(False)
5813
self.lblDashModeSync.setVisible(True)
5815
def setBtnFrameVisible(b, descr=''):
5816
self.frmDashMidButtons.setVisible(b)
5817
self.lblDashBtnDescr.setVisible(len(descr)>0)
5818
self.lblDashBtnDescr.setText(descr)
5822
setBtnFrameVisible(False)
5823
setBtnRowVisible(DASHBTNS.Install, False)
5824
setBtnRowVisible(DASHBTNS.Browse, False)
5825
setBtnRowVisible(DASHBTNS.Instruct, False)
5826
setBtnRowVisible(DASHBTNS.Settings, False)
5827
setBtnRowVisible(DASHBTNS.Close, False)
5828
setOnlyDashModeVisible()
5829
self.btnModeSwitch.setVisible(False)
5831
# This keeps popping up for some reason!
5832
self.lblTorrentStats.setVisible(False)
5834
if self.doAutoBitcoind and not sdmState=='BitcoindReady':
5835
# User is letting Armory manage the Satoshi client for them.
5837
if not sdmState==self.lastSDMState:
5839
self.lblBusy.setVisible(False)
5840
self.btnModeSwitch.setVisible(False)
5842
# There's a whole bunch of stuff that has to be hidden/shown
5843
# depending on the state... set some reasonable defaults here
5844
setBtnFrameVisible(False)
5845
setBtnRowVisible(DASHBTNS.Install, False)
5846
setBtnRowVisible(DASHBTNS.Browse, False)
5847
setBtnRowVisible(DASHBTNS.Instruct, False)
5848
setBtnRowVisible(DASHBTNS.Settings, True)
5849
setBtnRowVisible(DASHBTNS.Close, False)
5851
if not (self.forceOnline or self.internetAvail) or CLI_OPTIONS.offline:
5852
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
5853
setOnlyDashModeVisible()
5854
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
5855
size=4, color='TextWarn', bold=True)
5856
if satoshiIsAvailable():
5857
self.frmDashMidButtons.setVisible(True)
5858
setBtnRowVisible(DASHBTNS.Close, True)
5859
if CLI_OPTIONS.offline:
5860
# Forced offline but bitcoind is running
5861
LOGINFO('Dashboard switched to auto-OfflineForcedButSatoshiAvail')
5862
descr1 += self.GetDashStateText('Auto', 'OfflineForcedButSatoshiAvail')
5863
descr2 += self.GetDashFunctionalityText('Offline')
5864
self.lblDashDescr1.setText(descr1)
5865
self.lblDashDescr2.setText(descr2)
5867
LOGINFO('Dashboard switched to auto-OfflineSatoshiAvail')
5868
descr1 += self.GetDashStateText('Auto', 'OfflineSatoshiAvail')
5869
descr2 += self.GetDashFunctionalityText('Offline')
5870
self.lblDashDescr1.setText(descr1)
5871
self.lblDashDescr2.setText(descr2)
5873
LOGINFO('Dashboard switched to auto-OfflineNoSatoshiNoInternet')
5874
setBtnFrameVisible(True, \
5875
'In case you actually do have internet access, use can use '
5876
'the following links to get Armory installed. Or change '
5878
setBtnRowVisible(DASHBTNS.Browse, True)
5879
setBtnRowVisible(DASHBTNS.Install, True)
5880
setBtnRowVisible(DASHBTNS.Settings, True)
5881
#setBtnRowVisible(DASHBTNS.Instruct, not OS_WINDOWS)
5882
descr1 += self.GetDashStateText('Auto','OfflineNoSatoshiNoInternet')
5883
descr2 += self.GetDashFunctionalityText('Offline')
5884
self.lblDashDescr1.setText(descr1)
5885
self.lblDashDescr2.setText(descr2)
5886
elif not TheSDM.isRunningBitcoind() and not TheTDM.isRunning():
5887
setOnlyDashModeVisible()
5888
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
5889
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
5890
size=4, color='TextWarn', bold=True)
5891
# Bitcoind is not being managed, but we want it to be
5892
if satoshiIsAvailable() or sdmState=='BitcoindAlreadyRunning':
5893
# But bitcoind/-qt is already running
5894
LOGINFO('Dashboard switched to auto-butSatoshiRunning')
5895
self.lblDashModeSync.setText(' Please close Bitcoin-Qt', \
5897
setBtnFrameVisible(True, '')
5898
setBtnRowVisible(DASHBTNS.Close, True)
5899
self.btnModeSwitch.setVisible(True)
5900
self.btnModeSwitch.setText('Check Again')
5901
#setBtnRowVisible(DASHBTNS.Close, True)
5902
descr1 += self.GetDashStateText('Auto', 'OfflineBitcoindRunning')
5903
descr2 += self.GetDashStateText('Auto', 'NewUserInfo')
5904
descr2 += self.GetDashFunctionalityText('Offline')
5905
self.lblDashDescr1.setText(descr1)
5906
self.lblDashDescr2.setText(descr2)
5907
#self.psutil_detect_bitcoin_exe_path()
5908
elif sdmState in ['BitcoindExeMissing', 'BitcoindHomeMissing']:
5909
LOGINFO('Dashboard switched to auto-cannotFindExeHome')
5910
if sdmState=='BitcoindExeMissing':
5911
self.lblDashModeSync.setText('Cannot find Bitcoin Installation', \
5914
self.lblDashModeSync.setText('Cannot find Bitcoin Home Directory', \
5916
setBtnRowVisible(DASHBTNS.Close, satoshiIsAvailable())
5917
setBtnRowVisible(DASHBTNS.Install, True)
5918
setBtnRowVisible(DASHBTNS.Browse, True)
5919
setBtnRowVisible(DASHBTNS.Settings, True)
5920
#setBtnRowVisible(DASHBTNS.Instruct, not OS_WINDOWS)
5921
self.btnModeSwitch.setVisible(True)
5922
self.btnModeSwitch.setText('Check Again')
5923
setBtnFrameVisible(True)
5924
descr1 += self.GetDashStateText('Auto', 'OfflineNeedBitcoinInst')
5925
descr2 += self.GetDashStateText('Auto', 'NewUserInfo')
5926
descr2 += self.GetDashFunctionalityText('Offline')
5927
self.lblDashDescr1.setText(descr1)
5928
self.lblDashDescr2.setText(descr2)
5929
elif sdmState in ['BitcoindDatabaseEnvError']:
5930
LOGINFO('Dashboard switched to auto-BadDBEnv')
5931
setOnlyDashModeVisible()
5932
setBtnRowVisible(DASHBTNS.Install, True)
5933
#setBtnRowVisible(DASHBTNS.Instruct, not OS_WINDOWS)
5934
setBtnRowVisible(DASHBTNS.Settings, True)
5935
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
5936
size=4, color='TextWarn', bold=True)
5937
descr1 += self.GetDashStateText('Auto', 'OfflineBadDBEnv')
5938
descr2 += self.GetDashFunctionalityText('Offline')
5939
self.lblDashDescr1.setText(descr1)
5940
self.lblDashDescr2.setText(descr2)
5941
setBtnFrameVisible(True, '')
5942
elif sdmState in ['BitcoindUnknownCrash']:
5943
LOGERROR('Should not usually get here')
5944
setOnlyDashModeVisible()
5945
setBtnFrameVisible(True, \
5946
'Try reinstalling the Bitcoin '
5947
'software then restart Armory. If you continue to have '
5948
'problems, please contact Armory\'s core developer at '
5949
'<a href="mailto:support@bitcoinarmory.com?Subject=Bitcoind%20Crash"'
5950
'>support@bitcoinarmory.com</a>.')
5951
setBtnRowVisible(DASHBTNS.Settings, True)
5952
setBtnRowVisible(DASHBTNS.Install, True)
5953
LOGINFO('Dashboard switched to auto-BtcdCrashed')
5954
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
5955
size=4, color='TextWarn', bold=True)
5956
descr1 += self.GetDashStateText('Auto', 'OfflineBtcdCrashed')
5957
descr2 += self.GetDashFunctionalityText('Offline')
5958
self.lblDashDescr1.setText(descr1)
5959
self.lblDashDescr2.setText(descr2)
5960
self.lblDashDescr1.setTextInteractionFlags( \
5961
Qt.TextSelectableByMouse | \
5962
Qt.TextSelectableByKeyboard)
5963
elif sdmState in ['BitcoindNotAvailable']:
5964
LOGERROR('BitcoindNotAvailable: should not happen...')
5965
self.notAvailErrorCount += 1
5966
#if self.notAvailErrorCount < 5:
5967
#LOGERROR('Auto-mode-switch')
5968
#self.executeModeSwitch()
5970
descr2 += self.GetDashFunctionalityText('Offline')
5971
self.lblDashDescr1.setText(descr1)
5972
self.lblDashDescr2.setText(descr2)
5974
setBtnFrameVisible(False)
5976
descr2 += self.GetDashFunctionalityText('Offline')
5977
self.lblDashDescr1.setText(descr1)
5978
self.lblDashDescr2.setText(descr2)
5979
else: # online detected/forced, and TheSDM has already been started
5980
if sdmState in ['BitcoindWrongPassword', 'BitcoindNotAvailable']:
5983
if not self.wasSynchronizing:
5984
setOnlyDashModeVisible()
5987
<b>Armory has lost connection to the
5988
core Bitcoin software. If you did not do anything
5989
that affects your network connection or the bitcoind
5990
process, it will probably recover on its own in a
5991
couple minutes</b><br><br>""")
5992
self.lblTimeLeftSync.setVisible(False)
5993
self.barProgressSync.setFormat('')
5996
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
5997
LOGINFO('Dashboard switched to auto-BadConnection')
5998
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
5999
size=4, color='TextWarn', bold=True)
6000
descr1 += self.GetDashStateText('Auto', 'OfflineBadConnection')
6001
descr2 += self.GetDashFunctionalityText('Offline')
6002
self.lblDashDescr1.setText(extraTxt + descr1)
6003
self.lblDashDescr2.setText(descr2)
6004
elif sdmState in ['BitcoindInitializing', \
6005
'BitcoindSynchronizing', \
6006
'TorrentSynchronizing']:
6007
self.wasSynchronizing = True
6008
LOGINFO('Dashboard switched to auto-InitSync')
6009
self.lblBusy.setVisible(True)
6010
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
6011
self.updateSyncProgress()
6014
# If torrent ever ran, leave it visible
6015
setSyncRowVisible(True)
6016
setScanRowVisible(True)
6017
setTorrentRowVisible(TheTDM.isStarted())
6019
if TheTDM.isRunning():
6020
self.lblDashModeTorrent.setText('Downloading via Armory CDN', \
6021
size=4, bold=True, color='Foreground')
6022
self.lblDashModeSync.setText( 'Synchronizing with Network', \
6023
size=4, bold=True, color='DisableFG')
6024
self.lblTorrentStats.setVisible(True)
6025
elif sdmState=='BitcoindInitializing':
6026
self.lblDashModeTorrent.setText('Download via Armory CDN', \
6027
size=4, bold=True, color='DisableFG')
6028
self.lblDashModeSync.setText( 'Initializing Bitcoin Engine', \
6029
size=4, bold=True, color='Foreground')
6030
self.lblTorrentStats.setVisible(False)
6032
self.lblDashModeTorrent.setText('Download via Armory CDN', \
6033
size=4, bold=True, color='DisableFG')
6034
self.lblDashModeSync.setText( 'Synchronizing with Network', \
6035
size=4, bold=True, color='Foreground')
6036
self.lblTorrentStats.setVisible(False)
6039
self.lblDashModeBuild.setText( 'Build Databases', \
6040
size=4, bold=True, color='DisableFG')
6041
self.lblDashModeScan.setText( 'Scan Transaction History', \
6042
size=4, bold=True, color='DisableFG')
6044
# If more than 10 days behind, or still downloading torrent
6045
if tdmState=='Downloading' or self.approxBlkLeft > 1440:
6046
descr1 += self.GetDashStateText('Auto', 'InitializingLongTime')
6047
descr2 += self.GetDashStateText('Auto', 'NewUserInfo')
6049
descr1 += self.GetDashStateText('Auto', 'InitializingDoneSoon')
6050
descr2 += self.GetDashStateText('Auto', 'NewUserInfo')
6052
setBtnRowVisible(DASHBTNS.Settings, True)
6053
setBtnFrameVisible(True, \
6054
'Since version 0.88, Armory runs bitcoind in the '
6055
'background. You can switch back to '
6056
'the old way in the Settings dialog. ')
6058
descr2 += self.GetDashFunctionalityText('Offline')
6059
self.lblDashDescr1.setText(descr1)
6060
self.lblDashDescr2.setText(descr2)
6062
# User is managing satoshi client, or bitcoind is already sync'd
6063
self.frmDashMidButtons.setVisible(False)
6064
if bdmState in ('Offline', 'Uninitialized'):
6065
if onlineAvail and not self.lastBDMState[1]==onlineAvail:
6066
LOGINFO('Dashboard switched to user-OfflineOnlinePoss')
6067
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
6068
setOnlyDashModeVisible()
6069
self.lblBusy.setVisible(False)
6070
self.btnModeSwitch.setVisible(True)
6071
self.btnModeSwitch.setEnabled(True)
6072
self.btnModeSwitch.setText('Go Online!')
6073
self.lblDashModeSync.setText('Armory is <u>offline</u>', size=4, bold=True)
6074
descr = self.GetDashStateText('User', 'OfflineButOnlinePossible')
6075
descr += self.GetDashFunctionalityText('Offline')
6076
self.lblDashDescr1.setText(descr)
6077
elif not onlineAvail and not self.lastBDMState[1]==onlineAvail:
6078
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
6079
setOnlyDashModeVisible()
6080
self.lblBusy.setVisible(False)
6081
self.btnModeSwitch.setVisible(False)
6082
self.btnModeSwitch.setEnabled(False)
6083
self.lblDashModeSync.setText( 'Armory is <u>offline</u>', \
6084
size=4, color='TextWarn', bold=True)
6086
if not satoshiIsAvailable():
6087
if self.internetAvail:
6088
descr = self.GetDashStateText('User','OfflineNoSatoshi')
6089
setBtnRowVisible(DASHBTNS.Settings, True)
6090
setBtnFrameVisible(True, \
6091
'If you would like Armory to manage the Bitcoin software '
6092
'for you (Bitcoin-Qt or bitcoind), then adjust your '
6093
'Armory settings, then restart Armory.')
6095
descr = self.GetDashStateText('User','OfflineNoSatoshiNoInternet')
6096
elif not self.internetAvail:
6097
descr = self.GetDashStateText('User', 'OfflineNoInternet')
6098
elif not self.checkHaveBlockfiles():
6099
descr = self.GetDashStateText('User', 'OfflineNoBlkFiles')
6102
descr += self.GetDashFunctionalityText('Offline')
6103
self.lblDashDescr1.setText(descr)
6105
elif bdmState == 'BlockchainReady':
6106
setOnlyDashModeVisible()
6107
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, True)
6108
self.lblBusy.setVisible(False)
6109
if self.netMode == NETWORKMODE.Disconnected:
6110
self.btnModeSwitch.setVisible(False)
6111
self.lblDashModeSync.setText( 'Armory is disconnected', size=4, color='TextWarn', bold=True)
6112
descr = self.GetDashStateText('User','Disconnected')
6113
descr += self.GetDashFunctionalityText('Offline')
6114
self.lblDashDescr1.setText(descr)
6115
elif TheBDM.isDirty():
6116
LOGINFO('Dashboard switched to online-but-dirty mode')
6117
self.btnModeSwitch.setVisible(True)
6118
self.btnModeSwitch.setText('Rescan Now')
6119
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash)
6120
self.lblDashModeSync.setText( 'Armory is online, but needs to rescan ' \
6121
'the blockchain</b>', size=4, color='TextWarn', bold=True)
6122
if len(self.sweepAfterScanList) > 0:
6123
self.lblDashDescr1.setText( self.GetDashStateText('User', 'OnlineNeedSweep'))
6125
self.lblDashDescr1.setText( self.GetDashStateText('User', 'OnlineDirty'))
6128
LOGINFO('Dashboard switched to fully-online mode')
6129
self.btnModeSwitch.setVisible(False)
6130
self.lblDashModeSync.setText( 'Armory is online!', color='TextGreen', size=4, bold=True)
6131
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, True)
6132
descr = self.GetDashStateText('User', 'OnlineFull1')
6133
descr += self.GetDashFunctionalityText('Online')
6134
descr += self.GetDashStateText('User', 'OnlineFull2')
6135
self.lblDashDescr1.setText(descr)
6136
#self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash)
6137
elif bdmState == 'Scanning':
6138
LOGINFO('Dashboard switched to "Scanning" mode')
6139
self.updateSyncProgress()
6140
self.lblDashModeScan.setVisible(True)
6141
self.barProgressScan.setVisible(True)
6142
self.lblTimeLeftScan.setVisible(True)
6143
self.lblBusy.setVisible(True)
6144
self.btnModeSwitch.setVisible(False)
6146
if TheSDM.getSDMState() == 'BitcoindReady':
6147
self.barProgressSync.setVisible(True)
6148
self.lblTimeLeftSync.setVisible(True)
6149
self.lblDashModeSync.setVisible(True)
6150
self.lblTimeLeftSync.setText('')
6151
self.lblDashModeSync.setText( 'Synchronizing with Network', \
6152
size=4, bold=True, color='DisableFG')
6154
self.barProgressSync.setVisible(False)
6155
self.lblTimeLeftSync.setVisible(False)
6156
self.lblDashModeSync.setVisible(False)
6158
if len(str(self.lblDashModeBuild.text()).strip()) == 0:
6159
self.lblDashModeBuild.setText( 'Preparing Databases', \
6160
size=4, bold=True, color='Foreground')
6162
if len(str(self.lblDashModeScan.text()).strip()) == 0:
6163
self.lblDashModeScan.setText( 'Scan Transaction History', \
6164
size=4, bold=True, color='DisableFG')
6166
self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
6168
if len(self.walletMap)==0:
6169
descr = self.GetDashStateText('User','ScanNoWallets')
6171
descr = self.GetDashStateText('User','ScanWithWallets')
6173
descr += self.GetDashStateText('Auto', 'NewUserInfo')
6174
descr += self.GetDashFunctionalityText('Scanning') + '<br>'
6175
self.lblDashDescr1.setText(descr)
6176
self.lblDashDescr2.setText('')
6177
self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash)
6179
LOGERROR('What the heck blockchain mode are we in? %s', bdmState)
6181
self.lastBDMState = [bdmState, onlineAvail]
6182
self.lastSDMState = sdmState
6183
self.lblDashModeTorrent.setContentsMargins( 50,5,50,5)
6184
self.lblDashModeSync.setContentsMargins( 50,5,50,5)
6185
self.lblDashModeBuild.setContentsMargins(50,5,50,5)
6186
self.lblDashModeScan.setContentsMargins( 50,5,50,5)
6187
vbar = self.dashScrollArea.verticalScrollBar()
6189
# On Macs, this causes the main window scroll area to keep bouncing back
6190
# to the top. Not setting the value seems to fix it. DR - 2014/02/12
6192
vbar.setValue(vbar.minimum())
6194
#############################################################################
6195
def createToolTipWidget(self, tiptext, iconSz=2):
6197
The <u></u> is to signal to Qt that it should be interpretted as HTML/Rich
6198
text even if no HTML tags are used. This appears to be necessary for Qt
6199
to wrap the tooltip text
6201
fgColor = htmlColor('ToolTipQ')
6202
lbl = QLabel('<font size=%d color=%s>(?)</font>' % (iconSz, fgColor))
6203
lbl.setMaximumWidth(relaxedSizeStr(lbl, '(?)')[0])
6205
def setAllText(wself, txt):
6207
QWhatsThis.showText(ev.globalPos(), txt, self)
6208
wself.mousePressEvent = pressEv
6209
wself.setToolTip('<u></u>' + txt)
6211
# Calling setText on this widget will update both the tooltip and QWT
6212
from types import MethodType
6213
lbl.setText = MethodType(setAllText, lbl)
6215
lbl.setText(tiptext)
6218
#############################################################################
6219
def createAddressEntryWidgets(self, parent, initString='', maxDetectLen=128,
6220
boldDetectParts=0, **cabbKWArgs):
6222
If you are putting the LBL_DETECT somewhere that is space-constrained,
6223
set maxDetectLen to a smaller value. It will limit the number of chars
6224
to be included in the autodetect label.
6226
"cabbKWArgs" is "create address book button kwargs"
6227
Here's the signature of that function... you can pass any named args
6228
to this function and they will be passed along to createAddrBookButton
6229
def createAddrBookButton(parent, targWidget, defaultWltID=None,
6230
actionStr="Select", selectExistingOnly=False,
6231
selectMineOnly=False, getPubKey=False,
6234
Returns three widgets that can be put into layouts:
6235
[[QLineEdit: addr/pubkey]] [[Button: Addrbook]]
6236
[[Label: Wallet/Lockbox/Addr autodetect]]
6240
addrEntryObjs['QLE_ADDR'] = QLineEdit()
6241
addrEntryObjs['QLE_ADDR'].setText(initString)
6242
addrEntryObjs['BTN_BOOK'] = createAddrBookButton(parent,
6243
addrEntryObjs['QLE_ADDR'],
6245
addrEntryObjs['LBL_DETECT'] = QRichLabel('')
6246
addrEntryObjs['CALLBACK_GETSCRIPT'] = None
6248
##########################################################################
6249
# Create a function that reads the user string and updates labels if
6250
# the entry is recognized. This will be used to automatically show the
6251
# user that what they entered is recognized and gives them more info
6253
# It's a little awkward to put this whole thing in here... this could
6254
# probably use some refactoring
6255
def updateAddrDetectLabels():
6257
enteredText = str(addrEntryObjs['QLE_ADDR'].text()).strip()
6259
scriptInfo = self.getScriptForUserString(enteredText)
6260
displayInfo = self.getDisplayStringForScript(
6261
scriptInfo['Script'], maxDetectLen, boldDetectParts,
6262
prefIDOverAddr=scriptInfo['ShowID'])
6264
dispStr = displayInfo['String']
6265
if displayInfo['WltID'] is None and displayInfo['LboxID'] is None:
6266
addrEntryObjs['LBL_DETECT'].setText(dispStr)
6268
addrEntryObjs['LBL_DETECT'].setText(dispStr, color='TextBlue')
6270
# No point in repeating what the user just entered
6271
addrEntryObjs['LBL_DETECT'].setVisible(enteredText != dispStr)
6272
addrEntryObjs['QLE_ADDR'].setCursorPosition(0)
6275
#LOGEXCEPT('Invalid recipient string')
6276
addrEntryObjs['LBL_DETECT'].setVisible(False)
6277
addrEntryObjs['LBL_DETECT'].setVisible(False)
6278
# End function to be connected
6279
##########################################################################
6281
# Now actually connect the entry widgets
6282
parent.connect(addrEntryObjs['QLE_ADDR'], SIGNAL('textChanged(QString)'),
6283
updateAddrDetectLabels)
6285
updateAddrDetectLabels()
6287
# Create a func that can be called to get the script that was entered
6288
# This uses getScriptForUserString() which actually returns 4 vals
6289
# rawScript, wltIDorNone, lboxIDorNone, addrStringEntered
6290
# (The last one is really only used to determine what info is most
6291
# relevant to display to the user...it can be ignored in most cases)
6293
entered = str(addrEntryObjs['QLE_ADDR'].text()).strip()
6294
return self.getScriptForUserString(entered)
6296
addrEntryObjs['CALLBACK_GETSCRIPT'] = getScript
6297
return addrEntryObjs
6301
#############################################################################
6302
def getScriptForUserString(self, userStr):
6303
return getScriptForUserString(userStr, self.walletMap, self.allLockboxes)
6306
#############################################################################
6307
def getDisplayStringForScript(self, binScript, maxChars=256,
6308
doBold=0, prefIDOverAddr=False,
6309
lblTrunc=12, lastTrunc=12):
6310
return getDisplayStringForScript(binScript, self.walletMap,
6311
self.allLockboxes, maxChars, doBold,
6312
prefIDOverAddr, lblTrunc, lastTrunc)
6315
#############################################################################
6317
def checkNewZeroConf(self):
6319
Function that looks at an incoming zero-confirmation transaction queue and
6320
determines if any incoming transactions were created by Armory. If so, the
6321
transaction will be passed along to a user notification queue.
6323
while len(self.newZeroConfSinceLastUpdate)>0:
6324
rawTx = self.newZeroConfSinceLastUpdate.pop()
6326
# Iterate through the Python wallets and create a ledger entry for the
6327
# transaction. If the transaction is for us, put it on the notification
6328
# queue, create the combined ledger, and reset the Qt table model.
6329
for wltID in self.walletMap.keys():
6330
wlt = self.walletMap[wltID]
6331
le = wlt.cppWallet.calcLedgerEntryForTxStr(rawTx)
6332
if not le.getTxHash() == '\x00' * 32:
6333
LOGDEBUG('ZerConf tx for wallet: %s. Adding to notify queue.' \
6335
notifyIn = self.getSettingOrSetDefault('NotifyBtcIn', \
6337
notifyOut = self.getSettingOrSetDefault('NotifyBtcOut', \
6339
if (le.getValue() <= 0 and notifyOut) or \
6340
(le.getValue() > 0 and notifyIn):
6341
# notifiedAlready = False,
6342
self.notifyQueue.append([wltID, le, False])
6343
self.createCombinedLedger()
6344
self.walletModel.reset()
6346
# Iterate through the C++ lockbox wallets and create a ledger entry for
6347
# the transaction. If the transaction is for us, put it on the
6348
# notification queue, create the combined ledger, and reset the Qt
6350
for lbID,cppWlt in self.cppLockboxWltMap.iteritems():
6351
le = cppWlt.calcLedgerEntryForTxStr(rawTx)
6352
if not le.getTxHash() == '\x00' * 32:
6353
LOGDEBUG('ZerConf tx for LOCKBOX: %s' % lbID)
6354
# notifiedAlready = False,
6355
self.notifyQueue.append([lbID, le, False])
6356
self.createCombinedLedger()
6357
self.walletModel.reset()
6358
self.lockboxLedgModel.reset()
6361
#############################################################################
6362
#############################################################################
6363
def Heartbeat(self, nextBeatSec=1):
6365
This method is invoked when the app is initialized, and will
6366
run every second, or whatever is specified in the nextBeatSec
6370
# Special heartbeat functions are for special windows that may need
6371
# to update every, say, every 0.1s
6372
# is all that matters at that moment, like a download progress window.
6373
# This is "special" because you are putting all other processing on
6374
# hold while this special window is active
6375
# IMPORTANT: Make sure that the special heartbeat function returns
6376
# a value below zero when it's done OR if it errors out!
6377
# Otherwise, it should return the next heartbeat delay,
6378
# which would probably be something like 0.1 for a rapidly
6379
# updating progress counter
6380
for fn in self.extraHeartbeatSpecial:
6384
reactor.callLater(nextBeat, self.Heartbeat)
6386
self.extraHeartbeatSpecial = []
6387
reactor.callLater(1, self.Heartbeat)
6389
LOGEXCEPT('Error in special heartbeat function')
6390
self.extraHeartbeatSpecial = []
6391
reactor.callLater(1, self.Heartbeat)
6395
# TorrentDownloadManager
6396
# SatoshiDaemonManager
6398
tdmState = TheTDM.getTDMState()
6399
sdmState = TheSDM.getSDMState()
6400
bdmState = TheBDM.getBDMState()
6401
#print '(SDM, BDM) State = (%s, %s)' % (sdmState, bdmState)
6403
self.processAnnounceData()
6406
for func in self.extraHeartbeatAlways:
6407
if isinstance(func, list):
6410
keep_running = func[2]
6411
if keep_running == False:
6412
self.extraHeartbeatAlways.remove(func)
6417
for idx,wltID in enumerate(self.walletIDList):
6418
self.walletMap[wltID].checkWalletLockTimeout()
6423
if self.doAutoBitcoind:
6424
if TheTDM.isRunning():
6425
if tdmState=='Downloading':
6426
self.updateSyncProgress()
6428
downRate = TheTDM.getLastStats('downRate')
6429
self.torrentCircBuffer.append(downRate if downRate else 0)
6431
# Assumes 1 sec heartbeat
6432
bufsz = len(self.torrentCircBuffer)
6433
if bufsz > 5*MINUTE:
6434
self.torrentCircBuffer = self.torrentCircBuffer[1:]
6436
if bufsz >= 4.99*MINUTE:
6437
# If dlrate is below 30 kB/s, offer the user a way to skip it
6438
avgDownRate = sum(self.torrentCircBuffer) / float(bufsz)
6439
if avgDownRate < 30*KILOBYTE:
6440
if (RightNow() - self.lastAskedUserStopTorrent) > 5*MINUTE:
6441
self.lastAskedUserStopTorrent = RightNow()
6442
reply = QMessageBox.warning(self, tr('Torrent'), tr("""
6443
Armory is attempting to use BitTorrent to speed up
6444
the initial synchronization, but it appears to be
6445
downloading slowly or not at all.
6447
If the torrent engine is not starting properly,
6448
or is not downloading
6449
at a reasonable speed for your internet connection,
6450
you should disable it in
6451
<i>File\xe2\x86\x92Settings</i> and then
6452
restart Armory."""), QMessageBox.Ok)
6454
# For now, just show once then disable
6455
self.lastAskedUserStopTorrent = UINT64_MAX
6457
if sdmState in ['BitcoindInitializing','BitcoindSynchronizing']:
6458
self.updateSyncProgress()
6459
elif sdmState == 'BitcoindReady':
6460
if bdmState == 'Uninitialized':
6461
LOGINFO('Starting load blockchain')
6462
self.loadBlockchainIfNecessary()
6463
elif bdmState == 'Offline':
6464
LOGERROR('Bitcoind is ready, but we are offline... ?')
6465
elif bdmState=='Scanning':
6466
self.updateSyncProgress()
6468
if not sdmState==self.lastSDMState or \
6469
not bdmState==self.lastBDMState[0]:
6470
self.setDashboardDetails()
6472
if bdmState in ('Offline','Uninitialized'):
6473
# This call seems out of place, but it's because if you are in offline
6474
# mode, it needs to check periodically for the existence of Bitcoin-Qt
6475
# so that it can enable the "Go Online" button
6476
self.setDashboardDetails()
6478
elif bdmState=='Scanning':
6479
self.updateSyncProgress()
6482
if self.netMode==NETWORKMODE.Disconnected:
6483
if self.onlineModeIsPossible():
6484
self.switchNetworkMode(NETWORKMODE.Full)
6486
if not TheBDM.isDirty() == self.dirtyLastTime:
6487
self.setDashboardDetails()
6488
self.dirtyLastTime = TheBDM.isDirty()
6491
if bdmState=='BlockchainReady':
6494
# Blockchain just finished loading. Do lots of stuff...
6495
if self.needUpdateAfterScan:
6496
LOGDEBUG('Running finishLoadBlockchainGUI')
6497
self.finishLoadBlockchainGUI()
6498
self.needUpdateAfterScan = False
6499
self.setDashboardDetails()
6502
# If we just rescanned to sweep an address, need to finish it
6503
if len(self.sweepAfterScanList)>0:
6504
LOGDEBUG('SweepAfterScanList is not empty -- exec finishSweepScan()')
6505
self.finishSweepScan()
6506
for addr in self.sweepAfterScanList:
6507
addr.binPrivKey32_Plain.destroy()
6508
self.sweepAfterScanList = []
6509
self.setDashboardDetails()
6512
# If we had initiated any wallet restoration scans, we need to add
6513
# Those wallets to the display
6514
if len(self.newWalletList)>0:
6515
LOGDEBUG('Wallet restore completed. Add to application.')
6516
while len(self.newWalletList)>0:
6517
wlt,isFresh = self.newWalletList.pop()
6518
LOGDEBUG('Registering %s wallet' % ('NEW' if isFresh else 'IMPORTED'))
6519
TheBDM.registerWallet(wlt.cppWallet, isFresh)
6520
self.addWalletToApplication(wlt, walletIsNew=isFresh)
6521
self.setDashboardDetails()
6524
# If there's a new block, use this to determine it affected our wallets
6525
prevLedgSize = dict([(wltID, len(self.walletMap[wltID].getTxLedger())) \
6526
for wltID in self.walletMap.keys()])
6529
# Now we start the normal array of heartbeat operations
6530
newBlocks = TheBDM.readBlkFileUpdate(wait=True)
6531
self.currBlockNum = TheBDM.getTopBlockHeight()
6532
if isinstance(self.currBlockNum, int): BDMcurrentBlock[0] = self.currBlockNum
6538
# If we have new zero-conf transactions, scan them and update ledger
6539
if len(self.newZeroConfSinceLastUpdate)>0:
6540
self.newZeroConfSinceLastUpdate.reverse()
6541
for wltID in self.walletMap.keys():
6542
wlt = self.walletMap[wltID]
6543
TheBDM.rescanWalletZeroConf(wlt.cppWallet, wait=True)
6545
for lbID,cppWlt in self.cppLockboxWltMap.iteritems():
6546
TheBDM.rescanWalletZeroConf(cppWlt, wait=True)
6549
self.checkNewZeroConf()
6551
# Trigger any notifications, if we have them...
6552
self.doTheSystemTrayThing()
6554
if newBlocks>0 and not TheBDM.isDirty():
6556
# This says "after scan", but works when new blocks appear, too
6557
TheBDM.updateWalletsAfterScan(wait=True)
6559
self.ledgerModel.reset()
6561
LOGINFO('New Block! : %d', self.currBlockNum)
6564
# LITE sync means it won't rescan if addresses have been imported
6565
didAffectUs = newBlockSyncRescanZC(TheBDM, self.walletMap, \
6569
LOGINFO('New Block contained a transaction relevant to us!')
6570
self.walletListChanged()
6571
notifyOnSurpriseTx(self.currBlockNum-newBlocks, \
6572
self.currBlockNum+1, self.walletMap, \
6573
self.cppLockboxWltMap, True, TheBDM, \
6574
self.notifyQueue, self.settings)
6576
self.createCombinedLedger()
6577
self.blkReceived = RightNow()
6578
self.writeSetting('LastBlkRecvTime', self.blkReceived)
6579
self.writeSetting('LastBlkRecv', self.currBlockNum)
6581
if self.netMode==NETWORKMODE.Full:
6582
LOGINFO('Current block number: %d', self.currBlockNum)
6583
self.lblArmoryStatus.setText(\
6584
'<font color=%s>Connected (%s blocks)</font> ' % \
6585
(htmlColor('TextGreen'), self.currBlockNum))
6588
# Update the wallet view to immediately reflect new balances
6589
self.walletModel.reset()
6591
# Any extra functions that may have been injected to be run
6592
# when new blocks are received.
6593
if len(self.extraNewBlockFunctions) > 0:
6594
cppHead = TheBDM.getMainBlockFromDB(self.currBlockNum)
6595
pyBlock = PyBlock().unserialize(cppHead.getSerializedBlock())
6596
for blockFunc in self.extraNewBlockFunctions:
6600
blkRecvAgo = RightNow() - self.blkReceived
6601
#blkStampAgo = RightNow() - TheBDM.getTopBlockHeader().getTimestamp()
6602
self.lblArmoryStatus.setToolTip('Last block received is %s ago' % \
6603
secondsToHumanTime(blkRecvAgo))
6606
for func in self.extraHeartbeatOnline:
6610
# When getting the error info, don't collect the traceback in order to
6611
# avoid circular references. https://docs.python.org/2/library/sys.html
6613
LOGEXCEPT('Error in heartbeat function')
6614
(errType, errVal) = sys.exc_info()[:2]
6615
errStr = 'Error Type: %s\nError Value: %s' % (errType, errVal)
6618
reactor.callLater(nextBeatSec, self.Heartbeat)
6621
#############################################################################
6622
def printAlert(self, moneyID, ledgerAmt, txAmt):
6624
Function that prints a notification for a transaction that affects an
6629
totalStr = coin2strNZS(txAmt)
6632
if moneyID in self.walletMap:
6633
wlt = self.walletMap[moneyID]
6634
if len(wlt.labelName) <= 20:
6635
dispName = '"%s"' % wlt.labelName
6637
dispName = '"%s..."' % wlt.labelName[:17]
6638
dispName = 'Wallet %s (%s)' % (dispName, wlt.uniqueIDB58)
6639
elif moneyID in self.cppLockboxWltMap:
6640
lbox = self.getLockboxByID(moneyID)
6641
if len(lbox.shortName) <= 20:
6642
dispName = '%d-of-%d "%s"' % (lbox.M, lbox.N, lbox.shortName)
6644
dispName = '%d-of-%d "%s..."' % (lbox.M, lbox.N, lbox.shortName[:17])
6645
dispName = 'Lockbox %s (%s)' % (dispName, lbox.uniqueIDB58)
6647
LOGERROR('Asked to show notification for wlt/lbox we do not have')
6650
# Collected everything we need to display, now construct it and do it.
6653
title = 'Bitcoins Received!'
6654
dispLines.append('Amount: %s BTC' % totalStr)
6655
dispLines.append('Recipient: %s' % dispName)
6658
title = 'Bitcoins Sent!'
6659
dispLines.append('Amount: %s BTC' % totalStr)
6660
dispLines.append('Sender: %s' % dispName)
6662
self.sysTray.showMessage(title, \
6663
'\n'.join(dispLines), \
6664
QSystemTrayIcon.Information, \
6669
#############################################################################
6671
def doTheSystemTrayThing(self):
6673
I named this method as it is because this is not just "show a message."
6674
I need to display all relevant transactions, in sequence that they were
6675
received. I will store them in self.notifyQueue, and this method will
6676
do nothing if it's empty.
6678
if not TheBDM.getBDMState()=='BlockchainReady' or \
6679
RightNow()<self.notifyBlockedUntil:
6682
# Notify queue input is: [WltID/LBID, LedgerEntry, alreadyNotified]
6683
for i in range(len(self.notifyQueue)):
6684
moneyID, le, alreadyNotified = self.notifyQueue[i]
6686
# Skip the ones we've notified of already.
6690
# Marke it alreadyNotified=True
6691
self.notifyQueue[i][2] = True
6693
# Catch condition that somehow the tx isn't related to us
6694
if le.getTxHash()=='\x00'*32:
6697
# Make sure the wallet ID or lockbox ID keys are actually valid before
6698
# using them to grab the appropriate C++ wallet.
6699
pywlt = self.walletMap.get(moneyID)
6700
lbox = self.getLockboxByID(moneyID)
6702
# If we couldn't find a matching wallet or lbox, bail
6703
if pywlt is None and lbox is None:
6704
LOGERROR('Could not find moneyID = %s; skipping notify' % moneyID)
6709
cppWlt = self.walletMap[moneyID].cppWallet
6710
wname = self.walletMap[moneyID].labelName
6712
wname = wname[:17] + '...'
6713
wltName = 'Wallet "%s" (%s)' % (wname, moneyID)
6715
cppWlt = self.cppLockboxWltMap[moneyID]
6716
lbox = self.getLockboxByID(moneyID)
6717
M = self.getLockboxByID(moneyID).M
6718
N = self.getLockboxByID(moneyID).N
6719
lname = self.getLockboxByID(moneyID).shortName
6721
lname = lname[:17] + '...'
6722
wltName = 'Lockbox %d-of-%d "%s" (%s)' % (M, N, lname, moneyID)
6725
if le.isSentToSelf():
6726
# Used to display the sent-to-self amount, but if this is a lockbox
6727
# we only have a cppWallet, and the determineSentToSelfAmt() func
6728
# only operates on python wallets. Oh well, the user can double-
6729
# click on the tx in their ledger if they want to see what's in it.
6730
# amt = determineSentToSelfAmt(le, cppWlt)[0]
6731
# self.sysTray.showMessage('Your bitcoins just did a lap!', \
6732
# 'Wallet "%s" (%s) just sent %s BTC to itself!' % \
6733
# (wlt.labelName, moneyID, coin2str(amt,maxZeros=1).strip()),
6734
self.sysTray.showMessage('Your bitcoins just did a lap!', \
6735
'%s just sent some BTC to itself!' % wltName,
6736
QSystemTrayIcon.Information, 10000)
6740
# If coins were either received or sent from the loaded wlt/lbox
6742
totalStr = coin2strNZS(abs(le.getValue()))
6743
if le.getValue() > 0:
6744
title = 'Bitcoins Received!'
6745
dispLines.append('Amount: %s BTC' % totalStr)
6746
dispLines.append('Recipient: %s' % wltName)
6747
elif le.getValue() < 0:
6748
# Also display the address of where they went
6749
txref = TheBDM.getTxByHash(le.getTxHash())
6750
nOut = txref.getNumTxOut()
6752
for i in range(nOut):
6753
script = txref.getTxOutCopy(i).getScript()
6754
if cppWlt.hasScrAddress(script_to_scrAddr(script)):
6756
if len(recipStr)==0:
6757
recipStr = self.getDisplayStringForScript(script, 45)['String']
6759
recipStr = '<Multiple Recipients>'
6761
title = 'Bitcoins Sent!'
6762
dispLines.append('Amount: %s BTC' % totalStr)
6763
dispLines.append('From: %s' % wltName)
6764
dispLines.append('To: %s' % recipStr)
6766
self.sysTray.showMessage(title, '\n'.join(dispLines),
6767
QSystemTrayIcon.Information, 10000)
6768
LOGINFO(title + '\n' + '\n'.join(dispLines))
6770
# Wait for 5 seconds before processing the next queue object.
6771
self.notifyBlockedUntil = RightNow() + 5
6775
#############################################################################
6776
def closeEvent(self, event=None):
6777
moc = self.getSettingOrSetDefault('MinimizeOrClose', 'DontKnow')
6778
doClose, doMinimize = False, False
6780
reply,remember = MsgBoxWithDNAA(MSGBOX.Question, 'Minimize or Close', \
6781
'Would you like to minimize Armory to the system tray instead '
6782
'of closing it?', dnaaMsg='Remember my answer', \
6783
yesStr='Minimize', noStr='Close')
6787
self.writeSetting('MinimizeOrClose', 'Minimize')
6791
self.writeSetting('MinimizeOrClose', 'Close')
6793
if doMinimize or moc=='Minimize':
6794
self.minimizeArmory()
6797
elif doClose or moc=='Close':
6798
self.doShutdown = True
6800
self.closeForReal(event)
6802
return # how would we get here?
6806
#############################################################################
6807
def unpackLinuxTarGz(self, targzFile, changeSettings=True):
6808
if targzFile is None:
6811
if not os.path.exists(targzFile):
6814
unpackDir = os.path.join(ARMORY_HOME_DIR, 'latestBitcoinInst')
6815
unpackDir2 = os.path.join(ARMORY_HOME_DIR, 'latestBitcoinInstOld')
6816
if os.path.exists(unpackDir):
6817
if os.path.exists(unpackDir2):
6818
shutil.rmtree(unpackDir2)
6819
shutil.move(unpackDir, unpackDir2)
6823
out,err = execAndWait('tar -zxf %s -C %s' % (targzFile, unpackDir), \
6826
LOGINFO('UNPACK STDOUT: "' + out + '"')
6827
LOGINFO('UNPACK STDERR: "' + err + '"')
6830
# There should only be one subdir
6831
unpackDirChild = None
6832
for fn in os.listdir(unpackDir):
6833
unpackDirChild = os.path.join(unpackDir, fn)
6835
if unpackDirChild is None:
6836
LOGERROR('There was apparently an error unpacking the file')
6839
finalDir = os.path.abspath(unpackDirChild)
6840
LOGWARN('Bitcoin Core unpacked into: %s', finalDir)
6843
self.settings.set('SatoshiExe', finalDir)
6849
#############################################################################
6850
def closeForReal(self, event=None):
6852
Unlike File->Quit or clicking the X on the window, which may actually
6853
minimize Armory, this method is for *really* closing Armory
6856
# Save the main window geometry in the settings file
6857
self.writeSetting('MainGeometry', str(self.saveGeometry().toHex()))
6858
self.writeSetting('MainWalletCols', saveTableView(self.walletsView))
6859
self.writeSetting('MainLedgerCols', saveTableView(self.ledgerView))
6861
if TheBDM.getBDMState()=='Scanning':
6862
LOGINFO('BDM state is scanning -- force shutdown BDM')
6863
TheBDM.execCleanShutdown(wait=False)
6865
LOGINFO('BDM is safe for clean shutdown')
6866
TheBDM.execCleanShutdown(wait=True)
6868
# This will do nothing if bitcoind isn't running.
6869
TheSDM.stopBitcoind()
6871
# Don't want a strange error here interrupt shutdown
6872
LOGEXCEPT('Strange error during shutdown')
6875
# Any extra shutdown activities, perhaps added by modules
6876
for fn in self.extraShutdownFunctions:
6880
LOGEXCEPT('Shutdown function failed. Skipping.')
6883
from twisted.internet import reactor
6884
LOGINFO('Attempting to close the main window!')
6891
#############################################################################
6892
def execTrigger(self, toSpawn):
6893
super(ArmoryDialog, toSpawn).exec_()
6896
#############################################################################
6897
def initTrigger(self, toInit):
6898
if isinstance(toInit, DlgProgress):
6903
#############################################################################
6904
def checkForNegImports(self):
6906
negativeImports = []
6908
for wlt in self.walletMap:
6909
if self.walletMap[wlt].hasNegativeImports:
6910
negativeImports.append(self.walletMap[wlt].uniqueIDB58)
6912
# If we detect any negative import
6913
if len(negativeImports) > 0:
6915
for wltID in negativeImports:
6916
if not wltID in self.walletMap:
6919
homedir = os.path.dirname(self.walletMap[wltID].walletPath)
6920
wltlogdir = os.path.join(homedir, wltID)
6921
if not os.path.exists(wltlogdir):
6924
for subdirname in os.listdir(wltlogdir):
6925
subdirpath = os.path.join(wltlogdir, subdirname)
6926
logDirs.append([wltID, subdirpath])
6929
DlgInconsistentWltReport(self, self, logDirs).exec_()
6932
#############################################################################
6933
def getAllRecoveryLogDirs(self, wltIDList):
6935
for wltID in wltIDList:
6936
if not wltID in self.walletMap:
6939
homedir = os.path.dirname(self.walletMap[wltID].walletPath)
6940
logdir = os.path.join(homedir, wltID)
6941
if not os.path.exists(logdir):
6944
self.logDirs.append([wltID, logdir])
6949
#############################################################################
6951
def CheckWalletConsistency(self, wallets, prgAt=None):
6957
statinfo = os.stat(wallets[wlt].walletPath)
6958
walletSize[wlt] = statinfo.st_size
6959
totalSize = totalSize + statinfo.st_size
6968
f = 10000*walletSize[wlt]/totalSize
6972
self.wltCstStatus = WalletConsistencyCheck(wallets[wlt], prgAt)
6973
if self.wltCstStatus[0] != 0:
6974
self.WltCstError(wallets[wlt], self.wltCstStatus[1], dlgrdy)
6975
while not dlgrdy[0]:
6977
nerrors = nerrors +1
6982
while prgAt[2] != 2:
6985
self.emit(SIGNAL('UWCS'), [1, 'All wallets are consistent', 10000, dlgrdy])
6986
self.emit(SIGNAL('checkForNegImports'))
6989
self.emit(SIGNAL('UWCS'), [1, 'Consistency Check Failed!', 0, dlgrdy])
6992
self.checkRdyForFix()
6995
def checkRdyForFix(self):
6998
self.dlgCptWlt.emit(SIGNAL('Show'))
7000
if TheBDM.getBDMState() == 'Scanning':
7002
The wallet analysis tool will become available
7003
as soon as Armory is done loading. You can close this
7004
window and it will reappear when ready.""")
7005
self.dlgCptWlt.UpdateCanFix([canFix])
7007
elif TheBDM.getBDMState() == 'Offline' or \
7008
TheBDM.getBDMState() == 'Uninitialized':
7009
TheSDM.setDisabled(True)
7010
CLI_OPTIONS.offline = True
7015
#check running dialogs
7016
self.dlgCptWlt.emit(SIGNAL('Show'))
7021
for dlg in runningList:
7022
if dlg not in runningDialogsList:
7023
runningList.remove(dlg)
7026
for dlg in runningDialogsList:
7027
if not isinstance(dlg, DlgCorruptWallet):
7028
if dlg not in runningList:
7029
runningList.append(dlg)
7032
if len(runningList):
7034
canFix.append(tr("""
7035
<b>The following windows need closed before you can
7036
run the wallet analysis tool:</b>"""))
7037
canFix.extend([str(myobj.windowTitle()) for myobj in runningList])
7038
self.dlgCptWlt.UpdateCanFix(canFix)
7044
canFix.append('Ready to analyze inconsistent wallets!')
7045
self.dlgCptWlt.UpdateCanFix(canFix, True)
7046
self.dlgCptWlt.exec_()
7048
def checkWallets(self):
7049
nwallets = len(self.walletMap)
7052
self.prgAt = [0, 0, 0]
7054
self.pbarWalletProgress = QProgressBar()
7055
self.pbarWalletProgress.setMaximum(10000)
7056
self.pbarWalletProgress.setMaximumSize(300, 22)
7057
self.pbarWalletProgress.setStyleSheet('text-align: center; margin-bottom: 2px; margin-left: 10px;')
7058
self.pbarWalletProgress.setFormat('Wallet Consistency Check: %p%')
7059
self.pbarWalletProgress.setValue(0)
7060
self.statusBar().addWidget(self.pbarWalletProgress)
7062
self.connect(self, SIGNAL('UWCS'), self.UpdateWalletConsistencyStatus)
7063
self.connect(self, SIGNAL('PWCE'), self.PromptWltCstError)
7064
self.CheckWalletConsistency(self.walletMap, self.prgAt, async=True)
7065
self.UpdateConsistencyCheckMessage(async = True)
7066
#self.extraHeartbeatAlways.append(self.UpdateWalletConsistencyPBar)
7069
def UpdateConsistencyCheckMessage(self):
7070
while self.prgAt[2] == 0:
7071
self.emit(SIGNAL('UWCS'), [0, self.prgAt[0]])
7074
self.emit(SIGNAL('UWCS'), [2])
7077
def UpdateWalletConsistencyStatus(self, msg):
7079
self.pbarWalletProgress.setValue(msg[1])
7081
self.statusBar().showMessage(msg[1], msg[2])
7084
self.pbarWalletProgress.hide()
7086
def WltCstError(self, wlt, status, dlgrdy):
7087
self.emit(SIGNAL('PWCE'), dlgrdy, wlt, status)
7088
LOGERROR('Wallet consistency check failed! (%s)', wlt.uniqueIDB58)
7090
def PromptWltCstError(self, dlgrdy, wallet=None, status='', mode=None):
7091
if not self.dlgCptWlt:
7092
self.dlgCptWlt = DlgCorruptWallet(wallet, status, self, self)
7095
self.dlgCptWlt.addStatus(wallet, status)
7098
self.dlgCptWlt.show()
7100
self.dlgCptWlt.exec_()
7103
############################################
7104
class ArmoryInstanceListener(Protocol):
7105
def connectionMade(self):
7106
LOGINFO('Another Armory instance just tried to open.')
7107
self.factory.func_conn_made()
7109
def dataReceived(self, data):
7110
LOGINFO('Received data from alternate Armory instance')
7111
self.factory.func_recv_data(data)
7112
self.transport.loseConnection()
7114
############################################
7115
class ArmoryListenerFactory(ClientFactory):
7116
protocol = ArmoryInstanceListener
7117
def __init__(self, fn_conn_made, fn_recv_data):
7118
self.func_conn_made = fn_conn_made
7119
self.func_recv_data = fn_recv_data
7123
############################################
7124
def checkForAlreadyOpen():
7126
LOGDEBUG('Checking for already open socket...')
7128
sock = socket.create_connection(('127.0.0.1',CLI_OPTIONS.interport), 0.1);
7129
# If we got here (no error), there's already another Armory open
7132
# Windows can be tricky, sometimes holds sockets even after closing
7133
checkForAlreadyOpenError()
7135
LOGERROR('Socket already in use. Sending CLI args to existing proc.')
7137
sock.send(CLI_ARGS[0])
7139
LOGERROR('Exiting...')
7142
# This is actually the normal condition: we expect this to be the
7143
# first/only instance of Armory and opening the socket will err out
7148
############################################
7149
def checkForAlreadyOpenError():
7150
LOGINFO('Already open error checking')
7151
# Sometimes in Windows, Armory actually isn't open, because it holds
7152
# onto the socket even after it's closed.
7155
aexe = os.path.basename(sys.argv[0])
7156
bexe = 'bitcoind.exe' if OS_WINDOWS else 'bitcoind'
7157
for proc in psutil.process_iter():
7158
if aexe in proc.name:
7159
LOGINFO('Found armory PID: %d', proc.pid)
7160
armoryExists.append(proc.pid)
7161
if bexe in proc.name:
7162
LOGINFO('Found bitcoind PID: %d', proc.pid)
7163
if ('testnet' in proc.name) == USE_TESTNET:
7164
bitcoindExists.append(proc.pid)
7166
if len(armoryExists)>0:
7167
LOGINFO('Not an error! Armory really is open')
7169
elif len(bitcoindExists)>0:
7170
# Strange condition where bitcoind doesn't get killed by Armory/guardian
7171
# (I've only seen this happen on windows, though)
7172
LOGERROR('Found zombie bitcoind process...killing it')
7173
for pid in bitcoindExists:
7179
############################################
7183
qt4reactor.install()
7185
if CLI_OPTIONS.interport > 1:
7186
checkForAlreadyOpen()
7188
pixLogo = QPixmap(':/splashlogo.png')
7190
pixLogo = QPixmap(':/splashlogo_testnet.png')
7191
SPLASH = QSplashScreen(pixLogo)
7192
SPLASH.setMask(pixLogo.mask())
7194
QAPP.processEvents()
7196
# Will make this customizable
7197
QAPP.setFont(GETFONT('var'))
7199
form = ArmoryMainWindow()
7204
from twisted.internet import reactor
7206
LOGINFO('Resetting BlockDataMgr, freeing memory')
7208
TheBDM.execCleanShutdown(wait=False)
7209
if reactor.threadpool is not None:
7210
reactor.threadpool.stop()
7214
QAPP.connect(form, SIGNAL("lastWindowClosed()"), endProgram)
7215
reactor.addSystemEventTrigger('before', 'shutdown', endProgram)
7216
QAPP.setQuitOnLastWindowClosed(True)
7218
os._exit(QAPP.exec_())