~ubuntu-branches/debian/jessie/armory/jessie

« back to all changes in this revision

Viewing changes to ArmoryQt.py

  • Committer: Package Import Robot
  • Author(s): Joseph Bisch
  • Date: 2014-10-07 10:22:45 UTC
  • Revision ID: package-import@ubuntu.com-20141007102245-2s3x3rhjxg689hek
Tags: upstream-0.92.3
ImportĀ upstreamĀ versionĀ 0.92.3

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
################################################################################
 
3
#                                                                              #
 
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                         #
 
7
#                                                                              #
 
8
################################################################################
 
9
 
 
10
from datetime import datetime
 
11
import hashlib
 
12
import logging
 
13
import math
 
14
import os
 
15
import platform
 
16
import random
 
17
import shutil
 
18
import signal
 
19
import socket
 
20
import subprocess
 
21
import sys
 
22
import threading
 
23
import time
 
24
import traceback
 
25
import webbrowser
 
26
import psutil
 
27
from copy import deepcopy
 
28
 
 
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
 
33
 
 
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
 
45
 
 
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
 
51
 
 
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
 
57
 
 
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.
 
66
if OS_MACOSX:
 
67
   QApplication.setDesktopSettingsAware(False)
 
68
 
 
69
# PyQt4 Imports
 
70
# All the twisted/networking functionality
 
71
if OS_WINDOWS:
 
72
   from _winreg import *
 
73
 
 
74
 
 
75
class ArmoryMainWindow(QMainWindow):
 
76
   """ The primary Armory window """
 
77
 
 
78
   #############################################################################
 
79
   @TimeThisFunction
 
80
   def __init__(self, parent=None):
 
81
      super(ArmoryMainWindow, self).__init__(parent)
 
82
 
 
83
 
 
84
      # Load the settings file
 
85
      self.settingsPath = CLI_OPTIONS.settingsPath
 
86
      self.settings = SettingsFile(self.settingsPath)
 
87
 
 
88
      # SETUP THE WINDOWS DECORATIONS
 
89
      self.lblLogoIcon = QLabel()
 
90
      if USE_TESTNET:
 
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'))
 
94
         if Colors.isDarkBkgd:
 
95
            self.lblLogoIcon.setPixmap(QPixmap(':/armory_logo_white_text_green_h56.png'))
 
96
      else:
 
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)
 
104
 
 
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
 
120
      self.latestVer = {}
 
121
      self.downloadDict = {}
 
122
      self.satoshiHomePath = None
 
123
      self.satoshiExeSearchPath = None
 
124
      self.initSyncCircBuff = []
 
125
      self.latestVer = {}
 
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 = {}
 
137
 
 
138
      # Full list of notifications, and notify IDs that should trigger popups
 
139
      # when sending or receiving.
 
140
      self.lastAnnounceUpdate = {}
 
141
      self.changelog = []
 
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
 
153
 
 
154
 
 
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()
 
159
 
 
160
      #delayed URI parsing dict
 
161
      self.delayedURIData = {}
 
162
      self.delayedURIData['qLen'] = 0
 
163
 
 
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)
 
168
 
 
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)
 
178
 
 
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.
 
184
      self.txAddrMap = {}
 
185
 
 
186
 
 
187
      self.loadWalletsAndSettings()
 
188
 
 
189
      eulaAgreed = self.getSettingOrSetDefault('Agreed_to_EULA', False)
 
190
      if not eulaAgreed:
 
191
         DlgEULA(self,self).exec_()
 
192
 
 
193
 
 
194
      if not self.abortLoad:
 
195
         self.setupNetworking()
 
196
 
 
197
      # setupNetworking may have set this flag if something went wrong
 
198
      if self.abortLoad:
 
199
         LOGWARN('Armory startup was aborted.  Closing.')
 
200
         os._exit(0)
 
201
 
 
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
 
204
      # restarted yet...
 
205
      self.doAutoBitcoind = \
 
206
            self.getSettingOrSetDefault('ManageSatoshi', not OS_MACOSX)
 
207
 
 
208
 
 
209
      # If we're going into online mode, start loading blockchain
 
210
      if self.doAutoBitcoind:
 
211
         self.startBitcoindIfNecessary()
 
212
      else:
 
213
         self.loadBlockchainIfNecessary()
 
214
 
 
215
      # Setup system tray and register "bitcoin:" URLs with the OS
 
216
      self.setupSystemTray()
 
217
      self.setupUriRegistration()
 
218
 
 
219
 
 
220
      self.extraHeartbeatSpecial  = []
 
221
      self.extraHeartbeatAlways   = []
 
222
      self.extraHeartbeatOnline   = []
 
223
      self.extraNewTxFunctions    = []
 
224
      self.extraNewBlockFunctions = []
 
225
      self.extraShutdownFunctions = []
 
226
      self.extraGoOnlineFunctions = []
 
227
 
 
228
      """
 
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],
 
232
      where:
 
233
         func is the function
 
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
 
237
      """
 
238
 
 
239
 
 
240
      self.lblArmoryStatus = QRichLabel('<font color=%s>Offline</font> ' %
 
241
                                      htmlColor('TextWarn'), doWrap=False)
 
242
 
 
243
      self.statusBar().insertPermanentWidget(0, self.lblArmoryStatus)
 
244
 
 
245
      # Table for all the wallets
 
246
      self.walletModel = AllWalletsDispModel(self)
 
247
      self.walletsView  = QTableView()
 
248
 
 
249
      w,h = tightSizeNChar(self.walletsView, 55)
 
250
      viewWidth  = 1.2*w
 
251
      sectionSz  = 1.3*h
 
252
      viewHeight = 4.4*sectionSz
 
253
 
 
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)
 
261
 
 
262
 
 
263
 
 
264
      self.walletsView.hideColumn(0)
 
265
      if self.usermode == USERMODE.Standard:
 
266
         initialColResize(self.walletsView, [20, 0, 0.35, 0.2, 0.2])
 
267
      else:
 
268
         initialColResize(self.walletsView, [20, 0.15, 0.30, 0.2, 0.20])
 
269
 
 
270
 
 
271
      if self.settings.hasSetting('LastFilterState'):
 
272
         if self.settings.get('LastFilterState')==4:
 
273
            self.walletsView.showColumn(0)
 
274
 
 
275
 
 
276
      self.connect(self.walletsView, SIGNAL('doubleClicked(QModelIndex)'), 
 
277
                   self.execDlgWalletDetails)
 
278
      self.connect(self.walletsView, SIGNAL('clicked(QModelIndex)'), 
 
279
                   self.execClickRow)
 
280
 
 
281
      self.walletsView.setColumnWidth(WLTVIEWCOLS.Visible, 20)
 
282
      w,h = tightSizeNChar(GETFONT('var'), 100)
 
283
 
 
284
 
 
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
 
288
      self.sortLedgCol = 0
 
289
      self.currLedgMin = 1
 
290
      self.currLedgMax = 100
 
291
      self.currLedgWidth = 100
 
292
 
 
293
      # Table to display ledger/activity
 
294
      self.ledgerTable = []
 
295
      self.ledgerModel = LedgerDispModelSimple(self.ledgerTable, self, self)
 
296
 
 
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)
 
303
 
 
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)
 
308
 
 
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)
 
316
 
 
317
      # Another table and model, for lockboxes
 
318
      self.lockboxLedgTable = []
 
319
      self.lockboxLedgModel = LedgerDispModelSimple(self.lockboxLedgTable, 
 
320
                                                                   self, self, isLboxModel=True)
 
321
 
 
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])
 
327
 
 
328
      self.connect(self.ledgerView, SIGNAL('doubleClicked(QModelIndex)'), \
 
329
                   self.dblClickLedger)
 
330
 
 
331
      self.ledgerView.setContextMenuPolicy(Qt.CustomContextMenu)
 
332
      self.ledgerView.customContextMenuRequested.connect(self.showContextMenuLedger)
 
333
 
 
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)
 
338
 
 
339
      # Put the Wallet info into it's own little box
 
340
      lblAvail = QLabel("<b>Available Wallets:</b>")
 
341
      viewHeader = makeLayoutFrame(HORIZONTAL, [lblAvail, \
 
342
                                             'Stretch', \
 
343
                                             btnAddWallet, \
 
344
                                             btnImportWlt, ])
 
345
      wltFrame = QFrame()
 
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)
 
351
 
 
352
 
 
353
 
 
354
      # Make the bottom 2/3 a tabwidget
 
355
      self.mainDisplayTabs = QTabWidget()
 
356
 
 
357
      # Put the labels into scroll areas just in case window size is small.
 
358
      self.tabDashboard = QWidget()
 
359
      self.setupDashboard()
 
360
 
 
361
 
 
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)
 
368
 
 
369
 
 
370
      self.connect(self.comboWltSelect, SIGNAL('activated(int)'), 
 
371
                   self.changeWltFilter)
 
372
 
 
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);
 
376
 
 
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)
 
383
 
 
384
      self.lblTot.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
 
385
      self.lblSpd.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
 
386
      self.lblUcn.setAlignment(Qt.AlignRight | Qt.AlignVCenter)
 
387
 
 
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.')
 
398
 
 
399
      frmTotals = QFrame()
 
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)
 
405
 
 
406
      frmTotalsLayout.addWidget(self.lblTotalFunds,  0,1)
 
407
      frmTotalsLayout.addWidget(self.lblSpendFunds,  1,1)
 
408
      frmTotalsLayout.addWidget(self.lblUnconfFunds, 2,1)
 
409
 
 
410
      frmTotalsLayout.addWidget(self.lblBTC1, 0,2)
 
411
      frmTotalsLayout.addWidget(self.lblBTC2, 1,2)
 
412
      frmTotalsLayout.addWidget(self.lblBTC3, 2,2)
 
413
 
 
414
      frmTotalsLayout.addWidget(self.ttipTot, 0,3)
 
415
      frmTotalsLayout.addWidget(self.ttipSpd, 1,3)
 
416
      frmTotalsLayout.addWidget(self.ttipUcn, 2,3)
 
417
 
 
418
      frmTotals.setLayout(frmTotalsLayout)
 
419
 
 
420
 
 
421
 
 
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 )
 
431
 
 
432
 
 
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)
 
438
 
 
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)
 
443
 
 
444
 
 
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)
 
448
 
 
449
      frmFilter = makeVertFrame([QLabel('Filter:'), self.comboWltSelect, 'Stretch'])
 
450
 
 
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)
 
462
 
 
463
 
 
464
      frmLower = makeHorizFrame([ frmFilter, \
 
465
                                 'Stretch', \
 
466
                                 self.frmLedgUpDown, \
 
467
                                 'Stretch', \
 
468
                                 frmTotals])
 
469
 
 
470
      # Now add the ledger to the bottom of the window
 
471
      ledgFrame = QFrame()
 
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)
 
480
 
 
481
      self.tabActivity = QWidget()
 
482
      self.tabActivity.setLayout(ledgLayout)
 
483
 
 
484
      self.tabAnnounce = QWidget()
 
485
      self.setupAnnounceTab()
 
486
 
 
487
 
 
488
      # Add the available tabs to the main tab widget
 
489
      self.MAINTABS  = enum('Dash','Ledger','Announce')
 
490
 
 
491
      self.mainDisplayTabs.addTab(self.tabDashboard, 'Dashboard')
 
492
      self.mainDisplayTabs.addTab(self.tabActivity,  'Transactions')
 
493
      self.mainDisplayTabs.addTab(self.tabAnnounce,  'Announcements')
 
494
 
 
495
      ##########################################################################
 
496
      if USE_TESTNET and not CLI_OPTIONS.disableModules:
 
497
         self.loadArmoryModules()   
 
498
      ##########################################################################
 
499
 
 
500
 
 
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)"))
 
506
 
 
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)
 
512
 
 
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)
 
518
 
 
519
      logoBtnFrame = []
 
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')
 
530
 
 
531
      btnFrame = makeVertFrame(logoBtnFrame, STYLE_SUNKEN)
 
532
      logoWidth=220
 
533
      btnFrame.sizeHint = lambda: QSize(logoWidth*1.0, 10)
 
534
      btnFrame.setMaximumWidth(logoWidth*1.2)
 
535
      btnFrame.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
 
536
 
 
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)
 
543
 
 
544
      # Attach the layout to the frame that will become the central widget
 
545
      mainFrame = QFrame()
 
546
      mainFrame.setLayout(layout)
 
547
      self.setCentralWidget(mainFrame)
 
548
      self.setMinimumSize(750,500)
 
549
 
 
550
      # Start the user at the dashboard
 
551
      self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Dash)
 
552
 
 
553
 
 
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', \
 
559
                                                'MultiSig', 'Help')
 
560
      self.menu = self.menuBar()
 
561
      self.menusList = []
 
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') )
 
570
 
 
571
 
 
572
      def exportTx():
 
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. ',
 
577
               QMessageBox.Ok)
 
578
            return
 
579
         else:
 
580
            DlgExportTxHistory(self,self).exec_()
 
581
 
 
582
 
 
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)
 
593
 
 
594
 
 
595
      def chngStd(b):
 
596
         if b: self.setUserMode(USERMODE.Standard)
 
597
      def chngAdv(b):
 
598
         if b: self.setUserMode(USERMODE.Advanced)
 
599
      def chngDev(b):
 
600
         if b: self.setUserMode(USERMODE.Expert)
 
601
 
 
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)
 
606
 
 
607
      modeActGrp.addAction(actSetModeStd)
 
608
      modeActGrp.addAction(actSetModeAdv)
 
609
      modeActGrp.addAction(actSetModeDev)
 
610
 
 
611
      self.menusList[MENUS.User].addAction(actSetModeStd)
 
612
      self.menusList[MENUS.User].addAction(actSetModeAdv)
 
613
      self.menusList[MENUS.User].addAction(actSetModeDev)
 
614
 
 
615
 
 
616
 
 
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)
 
628
 
 
629
      def openMsgSigning():
 
630
         MessageSigningVerificationDialog(self,self).exec_()
 
631
 
 
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)
 
637
            return
 
638
         DlgBroadcastBlindTx(self,self).exec_()
 
639
 
 
640
 
 
641
 
 
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)
 
646
 
 
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)
 
651
 
 
652
      def mkprom():
 
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)
 
658
         else:
 
659
            DlgCreatePromNote(self, self).exec_()
 
660
 
 
661
 
 
662
      def msrevsign():
 
663
         title = tr('Import Multi-Spend Transaction')
 
664
         descr = tr("""
 
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, 
 
670
                                                         UnsignedTransaction)
 
671
         dlgImport.exec_()
 
672
         if dlgImport.returnObj:
 
673
            DlgMultiSpendReview(self, self, dlgImport.returnObj).exec_()
 
674
            
 
675
 
 
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)
 
680
 
 
681
      if not self.usermode==USERMODE.Expert:
 
682
         self.menusList[MENUS.MultiSig].menuAction().setVisible(False)
 
683
 
 
684
 
 
685
      # Addresses
 
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)
 
689
 
 
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)
 
694
 
 
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)
 
701
 
 
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)
 
708
 
 
709
      #self.menusList[MENUS.Wallets].addAction(actMigrateSatoshi)
 
710
      #self.menusList[MENUS.Wallets].addAction(actAddressBook)
 
711
 
 
712
      def execVersion():
 
713
         self.explicitCheckAnnouncements()
 
714
         self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Announce)
 
715
 
 
716
      execAbout   = lambda: DlgHelpAbout(self).exec_()
 
717
      execTrouble = lambda: webbrowser.open('https://bitcoinarmory.com/troubleshooting/')
 
718
      execBugReport = lambda: DlgBugReport(self, self).exec_()
 
719
 
 
720
 
 
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)
 
733
 
 
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)
 
747
 
 
748
 
 
749
 
 
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)
 
759
 
 
760
 
 
761
 
 
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')
 
766
      if len(hexgeom)>0:
 
767
         geom = QByteArray.fromHex(hexgeom)
 
768
         self.restoreGeometry(geom)
 
769
      if len(hexwltsz)>0:
 
770
         restoreTableView(self.walletsView, hexwltsz)
 
771
      if len(hexledgsz)>0:
 
772
         restoreTableView(self.ledgerView, hexledgsz)
 
773
         self.ledgerView.setColumnWidth(LEDGERCOLS.NumConf, 20)
 
774
         self.ledgerView.setColumnWidth(LEDGERCOLS.TxDir,   72)
 
775
 
 
776
      haveGUI[0] = True
 
777
      haveGUI[1] = self
 
778
      BDMcurrentBlock[1] = 1
 
779
 
 
780
      if DO_WALLET_CHECK: 
 
781
         self.checkWallets()
 
782
 
 
783
      self.setDashboardDetails()
 
784
 
 
785
      from twisted.internet import reactor
 
786
      reactor.callLater(0.1,  self.execIntroDialog)
 
787
      reactor.callLater(1, self.Heartbeat)
 
788
 
 
789
      if self.getSettingOrSetDefault('MinimizeOnOpen', False) and not CLI_ARGS:
 
790
         LOGINFO('MinimizeOnOpen is True')
 
791
         reactor.callLater(0, self.minimizeArmory)
 
792
 
 
793
 
 
794
      if CLI_ARGS:
 
795
         reactor.callLater(1, self.uriLinkClicked, CLI_ARGS[0])
 
796
 
 
797
 
 
798
   ####################################################
 
799
   def getWatchingOnlyWallets(self):
 
800
      result = []
 
801
      for wltID in self.walletIDList:
 
802
         if self.walletMap[wltID].watchingOnly:
 
803
            result.append(wltID)
 
804
      return result
 
805
 
 
806
 
 
807
   ####################################################
 
808
   def changeWltFilter(self):
 
809
 
 
810
      currIdx  = max(self.comboWltSelect.currentIndex(), 0)
 
811
      currText = str(self.comboWltSelect.currentText()).lower()
 
812
 
 
813
      if currText.lower().startswith('custom filter'):
 
814
         self.walletsView.showColumn(0)
 
815
      else:
 
816
         self.walletsView.hideColumn(0)
 
817
 
 
818
 
 
819
      # If "custom" is selected, do nothing...
 
820
      if currIdx==4:
 
821
         return
 
822
 
 
823
      for i in range(len(self.walletVisibleList)):
 
824
         self.walletVisibleList[i] = False
 
825
         self.setWltSetting(self.walletIDList[i], 'LedgerShow', False)
 
826
 
 
827
      # If a specific wallet is selected, just set that and you're done
 
828
      if currIdx >= 4:
 
829
         self.walletVisibleList[currIdx-7] = True
 
830
         self.setWltSetting(self.walletIDList[currIdx-7], 'LedgerShow', True)
 
831
      else:
 
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]
 
835
 
 
836
         for i,winfo in enumerate(typelist):
 
837
            wid,wtype = winfo[:]
 
838
            if currIdx==0:
 
839
               # My wallets
 
840
               doShow = wtype in [WLTTYPES.Offline,WLTTYPES.Crypt,WLTTYPES.Plain]
 
841
               self.walletVisibleList[i] = doShow
 
842
               self.setWltSetting(wid, 'LedgerShow', doShow)
 
843
            elif currIdx==1:
 
844
               # Offline wallets
 
845
               doShow = winfo[1] in [WLTTYPES.Offline]
 
846
               self.walletVisibleList[i] = doShow
 
847
               self.setWltSetting(wid, 'LedgerShow', doShow)
 
848
            elif currIdx==2:
 
849
               # Others' Wallets
 
850
               doShow = winfo[1] in [WLTTYPES.WatchOnly]
 
851
               self.walletVisibleList[i] = doShow
 
852
               self.setWltSetting(wid, 'LedgerShow', doShow)
 
853
            elif currIdx==3:
 
854
               # All Wallets
 
855
               self.walletVisibleList[i] = True
 
856
               self.setWltSetting(wid, 'LedgerShow', True)
 
857
 
 
858
      self.walletsView.reset()
 
859
      self.createCombinedLedger()
 
860
      if self.frmLedgUpDown.isVisible():
 
861
         self.changeNumShow()
 
862
 
 
863
 
 
864
   ############################################################################
 
865
   def loadArmoryModules(self):
 
866
      """
 
867
      This method checks for any .py files in the exec directory
 
868
      """ 
 
869
      moduleDir = os.path.join(GetExecDir(), 'modules')
 
870
      if not moduleDir or not os.path.exists(moduleDir):
 
871
         return
 
872
 
 
873
      LOGWARN('Attempting to load modules from: %s' % moduleDir)
 
874
 
 
875
      from dynamicImport import getModuleList, dynamicImport
 
876
 
 
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']))
 
884
 
 
885
         isSignedByATI = False
 
886
         if 'Signature' in infoMap:
 
887
            """
 
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.
 
893
            """
 
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])
 
898
 
 
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)))
 
904
            
 
905
 
 
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:
 
910
               <br><br>
 
911
                  <b>Module Name:</b>  %s<br>
 
912
                  <b>Module Path:</b>  %s<br>
 
913
                  <b>Module Hash:</b>  %s<br>
 
914
               <br><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)
 
919
 
 
920
            if not reply==QMessageBox.Yes:
 
921
               continue
 
922
 
 
923
 
 
924
         module = dynamicImport(moduleDir, name, globals())
 
925
         plugObj = module.PluginObject(self)
 
926
 
 
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)
 
934
            continue
 
935
               
 
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
 
943
               for a new version.
 
944
               <br><br>
 
945
               Do you want to continue loading the module?"""), 
 
946
               QMessageBox.Yes | QMessageBox.No)
 
947
 
 
948
            if not reply==QMessageBox.Yes:
 
949
               continue
 
950
 
 
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)
 
954
 
 
955
         # Also inject any extra methods that will be 
 
956
         injectFuncList = [ \
 
957
               ['injectHeartbeatAlwaysFunc', 'extraHeartbeatAlways'], 
 
958
               ['injectHeartbeatOnlineFunc', 'extraHeartbeatOnline'], 
 
959
               ['injectGoOnlineFunc',        'extraGoOnlineFunctions'], 
 
960
               ['injectNewTxFunc',           'extraNewTxFunctions'], 
 
961
               ['injectNewBlockFunc',        'extraNewBlockFunctions'], 
 
962
               ['injectShutdownFunc',        'extraShutdownFunctions'] ]
 
963
 
 
964
         # Add any methods
 
965
         for plugFuncName,funcListName in injectFuncList:
 
966
            if not hasattr(plugObj, plugFuncName):
 
967
               continue
 
968
      
 
969
            if not hasattr(self, funcListName):
 
970
               LOGERROR('Missing an ArmoryQt list variable: %s' % funcListName)
 
971
               continue
 
972
 
 
973
            LOGINFO('Found module function: %s' % plugFuncName)
 
974
            funcList = getattr(self, funcListName)
 
975
            plugFunc = getattr(plugObj, plugFuncName)
 
976
            funcList.append(plugFunc)
 
977
                                    
 
978
 
 
979
   ############################################################################
 
980
   def factoryReset(self):
 
981
      """
 
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.  '
 
985
         '<br><br>'
 
986
         'If you click "Yes," Armory will exit after settings are '
 
987
         'reverted.  You will have to manually start Armory again.'
 
988
         '<br><br>'
 
989
         'Do you want to continue? ', \
 
990
         QMessageBox.Yes | QMessageBox.No)
 
991
 
 
992
      if reply==QMessageBox.Yes:
 
993
         self.removeSettingsOnClose = True
 
994
         self.closeForReal()
 
995
      """
 
996
 
 
997
      if DlgFactoryReset(self,self).exec_():
 
998
         # The dialog already wrote all the flag files, just close now
 
999
         self.closeForReal()
 
1000
 
 
1001
 
 
1002
   ####################################################
 
1003
   def showPrivacyGeneric(self):
 
1004
      DlgPrivacyPolicy().exec_()
 
1005
 
 
1006
   ####################################################
 
1007
   def clearMemoryPool(self):
 
1008
      touchFile( os.path.join(ARMORY_HOME_DIR, 'clearmempool.flag') )
 
1009
      msg = tr("""
 
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:
 
1013
         msg += tr("""
 
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)
 
1018
 
 
1019
 
 
1020
 
 
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
 
1025
 
 
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.
 
1032
      mainWindow = self
 
1033
      
 
1034
      def newKPE(wself, event=None):
 
1035
         mainWindow.logEntropy()
 
1036
         super(wself.__class__, wself).keyPressEvent(event)
 
1037
 
 
1038
      def newKRE(wself, event=None):
 
1039
         mainWindow.logEntropy()
 
1040
         super(wself.__class__, wself).keyReleaseEvent(event)
 
1041
 
 
1042
      def newMPE(wself, event=None):
 
1043
         mainWindow.logEntropy()
 
1044
         super(wself.__class__, wself).mousePressEvent(event)
 
1045
 
 
1046
      def newMRE(wself, event=None):
 
1047
         mainWindow.logEntropy()
 
1048
         super(wself.__class__, wself).mouseReleaseEvent(event)
 
1049
 
 
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)
 
1055
 
 
1056
      
 
1057
   ####################################################
 
1058
   def logEntropy(self):
 
1059
      try:
 
1060
         self.entropyAccum.append(RightNow())
 
1061
         self.entropyAccum.append(QCursor.pos().x()) 
 
1062
         self.entropyAccum.append(QCursor.pos().y()) 
 
1063
      except:
 
1064
         LOGEXCEPT('Error logging keypress entropy')
 
1065
 
 
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,[]
 
1079
 
 
1080
      if len(source1)==0:
 
1081
         LOGERROR('Error getting extra entropy from mouse & key presses')
 
1082
 
 
1083
      source2 = []
 
1084
 
 
1085
      try:
 
1086
         if OS_WINDOWS:
 
1087
            tempDir = os.getenv('TEMP')
 
1088
            extraFiles = []
 
1089
         elif OS_LINUX:
 
1090
            tempDir = '/var/log'
 
1091
            extraFiles = ['/var/log/Xorg.0.log']
 
1092
         elif OS_MACOSX:
 
1093
            tempDir = '/var/log'
 
1094
            extraFiles = ['/var/log/system.log']
 
1095
 
 
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])
 
1103
 
 
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()))
 
1109
               
 
1110
         if len(source2)==0:
 
1111
            LOGWARN('Second source of supplemental entropy will be empty')
 
1112
 
 
1113
      except:
 
1114
         LOGEXCEPT('Error getting extra entropy from filesystem')
 
1115
 
 
1116
 
 
1117
      source3 = ''
 
1118
      try:
 
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()
 
1125
      except:
 
1126
         LOGEXCEPT('Third source of entropy (desktop screenshot) failed')
 
1127
         
 
1128
      if len(source3)==0:
 
1129
         LOGWARN('Error getting extra entropy from screenshot')
 
1130
 
 
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))
 
1136
      
 
1137
 
 
1138
      allEntropy = ''.join([str(a) for a in [source1, source1, source3]])
 
1139
      return SecureBinaryData(HMAC256('Armory Entropy', allEntropy))
 
1140
      
 
1141
 
 
1142
 
 
1143
 
 
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.
 
1150
         <br><br>
 
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') )
 
1155
 
 
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.
 
1162
         <br><br>
 
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') )
 
1167
 
 
1168
   ####################################################
 
1169
   def loadFailedManyTimesFunc(self, nFail):
 
1170
      """
 
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.
 
1175
      """
 
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)
 
1180
      else:
 
1181
         LOGERROR('File mempool.bin does not exist. Nothing deleted.')
 
1182
 
 
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.'
 
1189
         '<br><br>'
 
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.'
 
1193
         '<br><br>'
 
1194
         'Keys cannot be imported into watching-only wallets, only full '
 
1195
         'wallets.', QMessageBox.Ok)
 
1196
 
 
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.'
 
1204
         '<br><br>'
 
1205
         'Keys cannot be swept into watching-only wallets, only full '
 
1206
         'wallets.', QMessageBox.Ok)
 
1207
 
 
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;
 
1215
      else:
 
1216
         self.currLedgMax = self.currLedgMin + prefWidth - 1
 
1217
         self.currLedgWidth = prefWidth
 
1218
 
 
1219
      self.applyLedgerRange()
 
1220
 
 
1221
 
 
1222
   ####################################################
 
1223
   def clickLedgUp(self):
 
1224
      self.currLedgMin -= self.currLedgWidth
 
1225
      self.currLedgMax -= self.currLedgWidth
 
1226
      self.applyLedgerRange()
 
1227
 
 
1228
   ####################################################
 
1229
   def clickLedgDn(self):
 
1230
      self.currLedgMin += self.currLedgWidth
 
1231
      self.currLedgMax += self.currLedgWidth
 
1232
      self.applyLedgerRange()
 
1233
 
 
1234
 
 
1235
   ####################################################
 
1236
   def applyLedgerRange(self):
 
1237
      if self.currLedgMin < 1:
 
1238
         toAdd = 1 - self.currLedgMin
 
1239
         self.currLedgMin += toAdd
 
1240
         self.currLedgMax += toAdd
 
1241
 
 
1242
      if self.currLedgMax > self.ledgerSize:
 
1243
         toSub = self.currLedgMax - self.ledgerSize
 
1244
         self.currLedgMin -= toSub
 
1245
         self.currLedgMax -= toSub
 
1246
 
 
1247
      self.currLedgMin = max(self.currLedgMin, 1)
 
1248
 
 
1249
      self.btnLedgUp.setVisible(self.currLedgMin!=1)
 
1250
      self.btnLedgDn.setVisible(self.currLedgMax!=self.ledgerSize)
 
1251
 
 
1252
      self.createCombinedLedger()
 
1253
 
 
1254
 
 
1255
 
 
1256
   ####################################################
 
1257
   def openSettings(self):
 
1258
      LOGDEBUG('openSettings')
 
1259
      dlgSettings = DlgSettings(self, self)
 
1260
      dlgSettings.exec_()
 
1261
 
 
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)
 
1273
      menu = QMenu(self)
 
1274
 
 
1275
      def traySend():
 
1276
         self.bringArmoryToFront()
 
1277
         self.clickSendBitcoins()
 
1278
 
 
1279
      def trayRecv():
 
1280
         self.bringArmoryToFront()
 
1281
         self.clickReceiveCoins()
 
1282
 
 
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)
 
1291
      menu.addSeparator()
 
1292
      menu.addAction(actClose)
 
1293
      self.sysTray.setContextMenu(menu)
 
1294
      self.notifyQueue = []
 
1295
      self.notifyBlockedUntil = 0
 
1296
 
 
1297
   #############################################################################
 
1298
   @AllowAsync
 
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\"'
 
1304
 
 
1305
      #find mimeTypes.rdf file
 
1306
      home = os.getenv('HOME')
 
1307
      out,err = execAndWait('find %s -type f -name \"mimeTypes.rdf\"' % home)
 
1308
 
 
1309
      for rdfs in out.split('\n'):
 
1310
         if rdfs:
 
1311
            try:
 
1312
               FFrdf = open(rdfs, 'r+')
 
1313
            except:
 
1314
               continue
 
1315
 
 
1316
            ct = FFrdf.readlines()
 
1317
            rdfsch=-1
 
1318
            rdfsc=-1
 
1319
            rdfea=-1
 
1320
            i=0
 
1321
            #look for the nodes
 
1322
            for line in ct:
 
1323
               if rdfschemehandler in line:
 
1324
                  rdfsch=i
 
1325
               elif rdfscheme in line:
 
1326
                  rdfsc=i
 
1327
               elif rdfexternalApp in line:
 
1328
                  rdfea=i
 
1329
               i+=1
 
1330
 
 
1331
            #seek to end of file
 
1332
            FFrdf.seek(-11, 2)
 
1333
            i=0;
 
1334
 
 
1335
            #add the missing nodes
 
1336
            if rdfsch == -1:
 
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')
 
1342
               i+=1
 
1343
 
 
1344
            if rdfsc == -1:
 
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')
 
1349
               i+=1
 
1350
 
 
1351
            if rdfea == -1:
 
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')
 
1355
               i+=1
 
1356
 
 
1357
            if i != 0:
 
1358
               FFrdf.write('</RDF:RDF>\n')
 
1359
 
 
1360
            FFrdf.close()
 
1361
 
 
1362
   #############################################################################
 
1363
   def setupUriRegistration(self, justDoIt=False):
 
1364
      """
 
1365
      Setup Armory as the default application for handling bitcoin: links
 
1366
      """
 
1367
      LOGINFO('setupUriRegistration')
 
1368
 
 
1369
      if USE_TESTNET:
 
1370
         return
 
1371
 
 
1372
      if OS_LINUX:
 
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')
 
1375
 
 
1376
         #check FF protocol association
 
1377
         #checkFF_thread = threading.Thread(target=self.registerBitcoinWithFF)
 
1378
         #checkFF_thread.start()
 
1379
         self.registerBitcoinWithFF(async=True)
 
1380
 
 
1381
         def setAsDefault():
 
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')
 
1387
 
 
1388
 
 
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
 
1391
            setAsDefault()
 
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')
 
1401
               if reply[0]==True:
 
1402
                  setAsDefault()
 
1403
               if reply[1]==True:
 
1404
                  self.writeSetting('DNAA_DefaultApp', True)
 
1405
 
 
1406
      elif OS_WINDOWS:
 
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)
 
1413
         elif __file__:
 
1414
            return #running from a .py script, not gonna register URI on Windows
 
1415
 
 
1416
         #justDoIt = True
 
1417
         import ctypes
 
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)
 
1424
 
 
1425
         modulepathname += unicode(passstr[0:(rtlength*2)], encoding='utf16') + u'" "%1"'
 
1426
         modulepathname = modulepathname.encode('utf8')
 
1427
 
 
1428
         rootKey = 'bitcoin\\shell\\open\\command'
 
1429
         try:
 
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!')
 
1436
                  return
 
1437
               else:
 
1438
                  action = 'DoIt' #armory is registered, but to another path
 
1439
            else:
 
1440
               # Already set to something (at least created, which is enough)
 
1441
               action = 'AskUser'
 
1442
         except:
 
1443
            # No user-key set, check if root-key is set
 
1444
            try:
 
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!')
 
1449
                  return
 
1450
               else:
 
1451
                  # Root key is set (or at least created, which is enough)
 
1452
                  action = 'AskUser'
 
1453
            except:
 
1454
               action = 'DoIt'
 
1455
 
 
1456
         dontAsk = self.getSettingOrSetDefault('DNAA_DefaultApp', False)
 
1457
         dontAskDefault = self.getSettingOrSetDefault('AlwaysArmoryURI', False)
 
1458
         if justDoIt:
 
1459
            LOGINFO('URL-register: just doing it')
 
1460
            action = 'DoIt'
 
1461
         elif dontAsk and dontAskDefault:
 
1462
            LOGINFO('URL-register: user wants to do it by default')
 
1463
            action = 'DoIt'
 
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')
 
1472
 
 
1473
            if reply[1]==True:
 
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])
 
1477
 
 
1478
            if reply[0]==True:
 
1479
               action = 'DoIt'
 
1480
            else:
 
1481
               LOGINFO('User requested not to use Armory as URI handler')
 
1482
               return
 
1483
 
 
1484
         # Finally, do it if we're supposed to!
 
1485
         LOGINFO('URL-register action: %s', action)
 
1486
         if action=='DoIt':
 
1487
 
 
1488
            LOGINFO('Registering Armory  for current user')
 
1489
            baseDir = os.path.dirname(unicode(passstr[0:(rtlength*2)], encoding='utf16'))
 
1490
            regKeys = []
 
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])
 
1495
 
 
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)
 
1502
 
 
1503
            regKeysU = []
 
1504
            regKeysU.append(['Software\\Classes\\bitcoin\\shell\\open\\command',  '', \
 
1505
                           modulepathname])
 
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)
 
1516
 
 
1517
 
 
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.
 
1526
            <br><br>
 
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])
 
1532
 
 
1533
 
 
1534
   #############################################################################
 
1535
   def execOfflineTx(self):
 
1536
      self.warnNewUSTXFormat()
 
1537
 
 
1538
      dlgSelect = DlgOfflineSelect(self, self)
 
1539
      if dlgSelect.exec_():
 
1540
 
 
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_()
 
1547
 
 
1548
 
 
1549
   #############################################################################
 
1550
   def sizeHint(self):
 
1551
      return QSize(1000, 650)
 
1552
 
 
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)
 
1560
 
 
1561
 
 
1562
 
 
1563
   #############################################################################
 
1564
   def execIntroDialog(self):
 
1565
      if not self.getSettingOrSetDefault('DNAA_IntroDialog', False):
 
1566
         dlg = DlgIntroMessage(self, self)
 
1567
         result = dlg.exec_()
 
1568
 
 
1569
         if dlg.chkDnaaIntroDlg.isChecked():
 
1570
            self.writeSetting('DNAA_IntroDialog', True)
 
1571
 
 
1572
         if dlg.requestCreate:
 
1573
            self.startWalletWizard()
 
1574
 
 
1575
         if dlg.requestImport:
 
1576
            self.execImportWallet()
 
1577
 
 
1578
 
 
1579
 
 
1580
   #############################################################################
 
1581
   def makeWalletCopy(self, parent, wlt, copyType='Same', suffix='', changePass=False):
 
1582
      '''Create a digital backup of your wallet.'''
 
1583
      if changePass:
 
1584
         LOGERROR('Changing password is not implemented yet!')
 
1585
         raise NotImplementedError
 
1586
 
 
1587
      # Set the file name.
 
1588
      if copyType.lower()=='pkcc':
 
1589
         fn = 'armory_%s.%s' % (wlt.uniqueIDB58, suffix)
 
1590
      else:
 
1591
         fn = 'armory_%s_%s.wallet' % (wlt.uniqueIDB58, suffix)
 
1592
 
 
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:
 
1597
         return False
 
1598
 
 
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')
 
1605
            if not dlg.exec_():
 
1606
               return False
 
1607
         # Wallet should now be unlocked
 
1608
         wlt.makeUnencryptedWalletCopy(savePath)
 
1609
      elif copyType.lower()=='encrypt':
 
1610
         newPassphrase=None
 
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()))
 
1618
 
 
1619
         wlt.makeEncryptedWalletCopy(savePath, newPassphrase)
 
1620
      elif copyType.lower() == 'pkcc':
 
1621
         wlt.writePKCCFile(savePath)
 
1622
      else:
 
1623
         LOGERROR('Invalid "copyType" supplied to makeWalletCopy: %s', copyType)
 
1624
         return False
 
1625
 
 
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)
 
1629
      return True
 
1630
 
 
1631
 
 
1632
   #############################################################################
 
1633
   def createAction(self,  txt, slot, isCheckable=False, \
 
1634
                           ttip=None, iconpath=None, shortcut=None):
 
1635
      """
 
1636
      Modeled from the "Rapid GUI Programming with Python and Qt" book, page 174
 
1637
      """
 
1638
      icon = QIcon()
 
1639
      if iconpath:
 
1640
         icon = QIcon(iconpath)
 
1641
 
 
1642
      theAction = QAction(icon, txt, self)
 
1643
 
 
1644
      if isCheckable:
 
1645
         theAction.setCheckable(True)
 
1646
         self.connect(theAction, SIGNAL('toggled(bool)'), slot)
 
1647
      else:
 
1648
         self.connect(theAction, SIGNAL('triggered()'), slot)
 
1649
 
 
1650
      if ttip:
 
1651
         theAction.setToolTip(ttip)
 
1652
         theAction.setStatusTip(ttip)
 
1653
 
 
1654
      if shortcut:
 
1655
         theAction.setShortcut(shortcut)
 
1656
 
 
1657
      return theAction
 
1658
 
 
1659
 
 
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'))
 
1672
 
 
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)
 
1677
 
 
1678
      self.firstModeSwitch = False
 
1679
 
 
1680
 
 
1681
 
 
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()
 
1689
 
 
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
 
1694
      try:
 
1695
         unixTimeToFormatStr(1000000000, fmtStr)
 
1696
      except:
 
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.', \
 
1700
            QMessageBox.Ok)
 
1701
         return False
 
1702
 
 
1703
      self.writeSetting('DateFormat', binary_to_hex(fmtStr))
 
1704
      return True
 
1705
 
 
1706
 
 
1707
 
 
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
 
1714
 
 
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
 
1723
 
 
1724
 
 
1725
      url1 = ANNOUNCE_URL
 
1726
      url2 = ANNOUNCE_URL_BACKUP
 
1727
      fetchPath = os.path.join(ARMORY_HOME_DIR, 'atisignedannounce')
 
1728
      if self.announceFetcher is None:
 
1729
 
 
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))
 
1740
            
 
1741
         self.announceFetcher = AnnounceDataFetcher(url1, url2, fetchPath, currID)
 
1742
         self.announceFetcher.setStatsDisable(self.skipStatsReport)
 
1743
         self.announceFetcher.setFullyDisabled(self.skipAnnounceCheck)
 
1744
         self.announceFetcher.start()
 
1745
 
 
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
 
1749
 
 
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()
 
1758
 
 
1759
 
 
1760
 
 
1761
   #############################################################################
 
1762
   def processAnnounceData(self, forceCheck=False, forceWait=5):
 
1763
 
 
1764
      adf = self.announceFetcher
 
1765
 
 
1766
 
 
1767
 
 
1768
      # The ADF always fetches everything all the time.  If forced, do the
 
1769
      # regular fetch first, then examine the individual files without forcing
 
1770
      if forceCheck:
 
1771
         adf.fetchRightNow(forceWait)
 
1772
 
 
1773
      # Check each of the individual files for recent modifications
 
1774
      idFuncPairs = [
 
1775
                      ['announce',  self.updateAnnounceTab],
 
1776
                      ['changelog', self.processChangelog],
 
1777
                      ['downloads', self.processDownloads],
 
1778
                      ['notify',    self.processNotifications],
 
1779
                      ['bootstrap', self.processBootstrap] ]
 
1780
 
 
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)
 
1787
            func(fileText)
 
1788
 
 
1789
 
 
1790
 
 
1791
 
 
1792
   #############################################################################
 
1793
   def processChangelog(self, txt):
 
1794
      try:
 
1795
         clp = changelogParser()
 
1796
         self.changelog = clp.parseChangelogText(txt)
 
1797
      except:
 
1798
         # Don't crash on an error, but do log what happened
 
1799
         LOGEXCEPT('Failed to parse changelog data')
 
1800
 
 
1801
 
 
1802
 
 
1803
   #############################################################################
 
1804
   def processDownloads(self, txt):
 
1805
      try:
 
1806
         dlp = downloadLinkParser()
 
1807
         self.downloadLinks = dlp.parseDownloadList(txt)
 
1808
 
 
1809
         if self.downloadLinks is None:
 
1810
            return
 
1811
 
 
1812
         thisVer = getVersionInt(BTCARMORY_VERSION)
 
1813
 
 
1814
         # Check ARMORY versions
 
1815
         if not 'Armory' in self.downloadLinks:
 
1816
            LOGWARN('No Armory links in the downloads list')
 
1817
         else:
 
1818
            maxVer = 0
 
1819
            self.versionNotification = {}
 
1820
            for verStr,vermap in self.downloadLinks['Armory'].iteritems():
 
1821
               dlVer = getVersionInt(readVersionString(verStr))
 
1822
               if dlVer > maxVer:
 
1823
                  maxVer = dlVer
 
1824
                  self.armoryVersions[1] = verStr
 
1825
                  if thisVer >= maxVer:
 
1826
                     continue
 
1827
 
 
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>')
 
1844
                     
 
1845
            if 'ArmoryTesting' in self.downloadLinks:
 
1846
               for verStr,vermap in self.downloadLinks['ArmoryTesting'].iteritems():
 
1847
                  dlVer = getVersionInt(readVersionString(verStr))
 
1848
                  if dlVer > maxVer:
 
1849
                     maxVer = dlVer
 
1850
                     self.armoryVersions[1] = verStr
 
1851
                     if thisVer >= maxVer:
 
1852
                        continue
 
1853
 
 
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>')
 
1870
 
 
1871
 
 
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')
 
1877
         else:
 
1878
            try:
 
1879
               maxVer = 0
 
1880
               for verStr,vermap in self.downloadLinks['Satoshi'].iteritems():
 
1881
                  dlVer = getVersionInt(readVersionString(verStr))
 
1882
                  if dlVer > maxVer:
 
1883
                     maxVer = dlVer
 
1884
                     self.satoshiVersions[1] = verStr
 
1885
 
 
1886
               if not self.NetworkingFactory:
 
1887
                  return
 
1888
 
 
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]
 
1892
 
 
1893
               if sum([0 if c in '0123456789.' else 1 for c in thisVerStr]) > 0:
 
1894
                  return
 
1895
 
 
1896
               self.satoshiVersions[0] = thisVerStr
 
1897
 
 
1898
            except:
 
1899
               pass
 
1900
 
 
1901
 
 
1902
 
 
1903
 
 
1904
      except:
 
1905
         # Don't crash on an error, but do log what happened
 
1906
         LOGEXCEPT('Failed to parse download link data')
 
1907
 
 
1908
 
 
1909
   #############################################################################
 
1910
   def getVersionNotifyLongDescr(self, verStr, testing=False):
 
1911
      shortOS = None
 
1912
      if OS_WINDOWS:
 
1913
         shortOS = 'windows'
 
1914
      elif OS_LINUX:
 
1915
         shortOS = 'ubuntu'
 
1916
      elif OS_MACOSX:
 
1917
         shortOS = 'mac'
 
1918
 
 
1919
      webURL = 'https://bitcoinarmory.com/download/'
 
1920
      if shortOS is not None:
 
1921
         webURL += '#' + shortOS
 
1922
 
 
1923
      if testing:
 
1924
         return tr("""
 
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).
 
1928
            """) % (verStr)
 
1929
         
 
1930
      return tr("""
 
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:
 
1935
         <br><br>
 
1936
         <a href="%s">%s</a> """) % (verStr, webURL, webURL)
 
1937
 
 
1938
 
 
1939
 
 
1940
   #############################################################################
 
1941
   def processBootstrap(self, binFile):
 
1942
      # Nothing to process, actually.  We'll grab the bootstrap from its
 
1943
      # current location, if needed
 
1944
      pass
 
1945
 
 
1946
 
 
1947
 
 
1948
   #############################################################################
 
1949
   def notificationIsRelevant(self, notifyID, notifyMap):
 
1950
      currTime = RightNow()
 
1951
      thisVerInt = getVersionInt(BTCARMORY_VERSION)
 
1952
 
 
1953
      # Ignore transactions below the requested priority
 
1954
      minPriority = self.getSettingOrSetDefault('NotifyMinPriority', 2048)
 
1955
      if int(notifyMap['PRIORITY']) < minPriority:
 
1956
         return False
 
1957
 
 
1958
      # Ignore version upgrade notifications if disabled in the settings
 
1959
      if 'upgrade' in notifyMap['ALERTTYPE'].lower() and \
 
1960
         self.getSettingOrSetDefault('DisableUpgradeNotify', False):
 
1961
         return False
 
1962
 
 
1963
      if notifyID in self.notifyIgnoreShort:
 
1964
         return False
 
1965
 
 
1966
      if notifyMap['STARTTIME'].isdigit():
 
1967
         if currTime < long(notifyMap['STARTTIME']):
 
1968
            return False
 
1969
 
 
1970
      if notifyMap['EXPIRES'].isdigit():
 
1971
         if currTime > long(notifyMap['EXPIRES']):
 
1972
            return False
 
1973
 
 
1974
 
 
1975
      try:
 
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:
 
1982
            return False
 
1983
      except:
 
1984
         pass
 
1985
 
 
1986
 
 
1987
      try:
 
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:
 
1994
            return False
 
1995
      except:
 
1996
         pass
 
1997
 
 
1998
      return True
 
1999
 
 
2000
 
 
2001
   #############################################################################
 
2002
   def processNotifications(self, txt):
 
2003
 
 
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).
 
2008
      try:
 
2009
         np = notificationParser()
 
2010
         currNotificationList = np.parseNotificationText(txt)
 
2011
      except:
 
2012
         # Don't crash on an error, but do log what happened
 
2013
         LOGEXCEPT('Failed to parse notifications')
 
2014
 
 
2015
      if currNotificationList is None:
 
2016
         currNotificationList = {}
 
2017
 
 
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)
 
2023
 
 
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)
 
2034
 
 
2035
 
 
2036
      tabPriority = 0
 
2037
      self.maxPriorityID = None
 
2038
 
 
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)
 
2047
            continue
 
2048
 
 
2049
         if valmap['PRIORITY'].isdigit():
 
2050
            if int(valmap['PRIORITY']) > tabPriority:
 
2051
               tabPriority = int(valmap['PRIORITY'])
 
2052
               self.maxPriorityID = nid
 
2053
 
 
2054
         if not nid in self.almostFullNotificationList:
 
2055
            addedNotifyIDs.append(nid)
 
2056
 
 
2057
      # Now remove them from the set that we are working with
 
2058
      for nid in irrelevantIDs:
 
2059
         del currNotificationList[nid]
 
2060
 
 
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)
 
2066
 
 
2067
 
 
2068
      #for nid in removedNotifyIDs:
 
2069
         #self.notifyIgnoreShort.discard(nid)
 
2070
         #self.notifyIgnoreLong.discard(nid)
 
2071
 
 
2072
 
 
2073
 
 
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
 
2085
 
 
2086
      tabWidgetBar.setTabTextColor(self.MAINTABS.Announce, tabColor)
 
2087
      self.updateAnnounceTab()
 
2088
 
 
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()
 
2093
      elif vnotify:
 
2094
         if not vnotify['UNIQUEID'] in self.notifyIgnoreShort:
 
2095
            DlgNotificationWithDNAA(self,self,vnotify['UNIQUEID'],vnotify).show()
 
2096
 
 
2097
 
 
2098
 
 
2099
 
 
2100
 
 
2101
 
 
2102
 
 
2103
   #############################################################################
 
2104
   @TimeThisFunction
 
2105
   def setupNetworking(self):
 
2106
      LOGINFO('Setting up networking...')
 
2107
      self.internetAvail = False
 
2108
 
 
2109
      # Prevent Armory from being opened twice
 
2110
      from twisted.internet import reactor
 
2111
      import twisted
 
2112
      def uriClick_partial(a):
 
2113
         self.uriLinkClicked(a)
 
2114
 
 
2115
      if CLI_OPTIONS.interport > 1:
 
2116
         try:
 
2117
            self.InstanceListener = ArmoryListenerFactory(self.bringArmoryToFront, \
 
2118
                                                          uriClick_partial )
 
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)
 
2125
            os._exit(0)
 
2126
      else:
 
2127
         LOGWARN('*** Listening port is disabled.  URI-handling will not work')
 
2128
 
 
2129
 
 
2130
      settingSkipCheck = self.getSettingOrSetDefault('SkipOnlineCheck', False)
 
2131
      useTor = self.getSettingOrSetDefault('UseTorSettings', False)
 
2132
      self.forceOnline = CLI_OPTIONS.forceOnline or settingSkipCheck or useTor
 
2133
 
 
2134
      # Check general internet connection
 
2135
      self.internetAvail = False
 
2136
      if self.forceOnline:
 
2137
         LOGINFO('Skipping online check, forcing online mode')
 
2138
      else:
 
2139
         try:
 
2140
            import urllib2
 
2141
            response=urllib2.urlopen('http://google.com', timeout=CLI_OPTIONS.nettimeout)
 
2142
            self.internetAvail = True
 
2143
         except ImportError:
 
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...)
 
2147
            try:
 
2148
               response=urllib2.urlopen('http://microsoft.com', timeout=CLI_OPTIONS.nettimeout)
 
2149
            except:
 
2150
               LOGEXCEPT('Error checking for internet connection')
 
2151
               LOGERROR('Run --skip-online-check if you think this is an error')
 
2152
               self.internetAvail = False
 
2153
         except:
 
2154
            LOGEXCEPT('Error checking for internet connection')
 
2155
            LOGERROR('Run --skip-online-check if you think this is an error')
 
2156
            self.internetAvail = False
 
2157
 
 
2158
 
 
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())
 
2163
 
 
2164
 
 
2165
 
 
2166
 
 
2167
 
 
2168
   #############################################################################
 
2169
   def manageBitcoindAskTorrent(self):
 
2170
 
 
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.
 
2177
            <br><br>
 
2178
            <b>However</b>, Armory can shortcut most of this initial
 
2179
            synchronization
 
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.
 
2183
            <br><br>
 
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.
 
2187
            <br><br>
 
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')
 
2193
 
 
2194
         if not reply:
 
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)
 
2200
            return False
 
2201
 
 
2202
      else:
 
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
 
2207
            finished
 
2208
            <br><br>
 
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
 
2212
            faster (2x to 20x)
 
2213
            than letting the Bitcoin software download it via P2P.
 
2214
            <br><br>
 
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.
 
2219
            <br><br>
 
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)
 
2225
 
 
2226
         if reply:
 
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)
 
2233
         else:
 
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)
 
2240
            return False
 
2241
 
 
2242
         return True
 
2243
 
 
2244
 
 
2245
   ############################################################################
 
2246
   def findTorrentFileForSDM(self, forceWaitTime=0):
 
2247
      """
 
2248
      Hopefully the announcement fetcher has already gotten one for us,
 
2249
      or at least we have a default.
 
2250
      """
 
2251
 
 
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:
 
2255
         if forceWaitTime>0:
 
2256
            self.explicitCheckAnnouncements(forceWaitTime)
 
2257
 
 
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')
 
2262
      else:
 
2263
         srcTorrent = self.announceFetcher.getAnnounceFilePath('bootstrap')
 
2264
 
 
2265
      # Maybe we still don't have a torrent for some reason
 
2266
      if not srcTorrent or not os.path.exists(srcTorrent):
 
2267
         return ''
 
2268
 
 
2269
      torrentPath = os.path.join(ARMORY_HOME_DIR, 'bootstrap.dat.torrent')
 
2270
      LOGINFO('Using torrent file: ' + torrentPath)
 
2271
      shutil.copy(srcTorrent, torrentPath)
 
2272
 
 
2273
      return torrentPath
 
2274
 
 
2275
 
 
2276
 
 
2277
 
 
2278
 
 
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')
 
2284
         return False
 
2285
 
 
2286
      if not self.doAutoBitcoind:
 
2287
         LOGWARN('Tried to start bitcoind, but ManageSatoshi==False')
 
2288
         return False
 
2289
 
 
2290
      if satoshiIsAvailable():
 
2291
         LOGWARN('Tried to start bitcoind, but satoshi already running')
 
2292
         return False
 
2293
 
 
2294
      self.setSatoshiPaths()
 
2295
      TheSDM.setDisabled(False)
 
2296
 
 
2297
      torrentIsDisabled = self.getSettingOrSetDefault('DisableTorrent', False)
 
2298
 
 
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')
 
2304
         else:
 
2305
            TheSDM.tryToSetupTorrentDL(torrentFile)
 
2306
 
 
2307
 
 
2308
      try:
 
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')
 
2314
         return True
 
2315
      except:
 
2316
         LOGEXCEPT('Failed to setup SDM')
 
2317
         self.switchNetworkMode(NETWORKMODE.Offline)
 
2318
 
 
2319
 
 
2320
   ############################################################################
 
2321
   def setSatoshiPaths(self):
 
2322
      LOGINFO('setSatoshiPaths')
 
2323
 
 
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')]
 
2330
      else:
 
2331
         self.satoshiExeSearchPath = []
 
2332
 
 
2333
 
 
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)
 
2341
 
 
2342
      TheBDM.setSatoshiDir(self.satoshiHomePath)
 
2343
      TheSDM.setSatoshiDir(self.satoshiHomePath)
 
2344
      TheTDM.setSatoshiDir(self.satoshiHomePath)
 
2345
 
 
2346
 
 
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)
 
2363
 
 
2364
         self.switchNetworkMode(NETWORKMODE.Full)
 
2365
         #self.resetBdmBeforeScan()
 
2366
         TheBDM.setOnlineMode(True, wait=False)
 
2367
 
 
2368
      else:
 
2369
         self.switchNetworkMode(NETWORKMODE.Offline)
 
2370
         TheBDM.setOnlineMode(False, wait=False)
 
2371
 
 
2372
 
 
2373
   #############################################################################
 
2374
   def checkHaveBlockfiles(self):
 
2375
      return os.path.exists(os.path.join(TheBDM.btcdir, 'blocks'))
 
2376
 
 
2377
   #############################################################################
 
2378
   def onlineModeIsPossible(self):
 
2379
      return ((self.internetAvail or self.forceOnline) and \
 
2380
               satoshiIsAvailable() and \
 
2381
               self.checkHaveBlockfiles())
 
2382
 
 
2383
 
 
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()
 
2390
         return
 
2391
      elif newMode==NETWORKMODE.Full:
 
2392
 
 
2393
         # Actually setup the networking, now
 
2394
         from twisted.internet import reactor
 
2395
 
 
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):
 
2402
               return
 
2403
 
 
2404
            try:
 
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)
 
2409
            except:
 
2410
               LOGEXCEPT('Failed to show disconnect notification')
 
2411
 
 
2412
 
 
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):
 
2421
               return
 
2422
 
 
2423
            try:
 
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
 
2429
            except:
 
2430
               LOGEXCEPT('Failed to show reconnect notification')
 
2431
 
 
2432
 
 
2433
         self.NetworkingFactory = ArmoryClientFactory( \
 
2434
                                          TheBDM,
 
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)
 
2441
 
 
2442
 
 
2443
 
 
2444
 
 
2445
   #############################################################################
 
2446
   def newTxFunc(self, pytxObj):
 
2447
      if TheBDM.getBDMState() in ('Offline','Uninitialized') or self.doShutdown:
 
2448
         return
 
2449
 
 
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))
 
2453
 
 
2454
      # All extra tx functions take one arg:  the PyTx object of the new ZC tx
 
2455
      for txFunc in self.extraNewTxFunctions:
 
2456
         txFunc(pytxObj)   
 
2457
 
 
2458
 
 
2459
 
 
2460
   #############################################################################
 
2461
   def parseUriLink(self, uriStr, clickOrEnter='click'):
 
2462
      if len(uriStr) < 1:
 
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.', \
 
2466
               QMessageBox.Ok)
 
2467
         return {}
 
2468
      ClickOrEnter = clickOrEnter[0].upper() + clickOrEnter[1:]
 
2469
      LOGINFO('URI link clicked!')
 
2470
      LOGINFO('The following URI string was parsed:')
 
2471
      LOGINFO(uriStr.replace('%','%%'))
 
2472
 
 
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), \
 
2482
             QMessageBox.Ok)
 
2483
         return {}
 
2484
 
 
2485
      if len(uriDict)==0:
 
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.')
 
2491
         else:
 
2492
            warnMsg += 'The raw URI string is:<br><br>' + uriStr
 
2493
         QMessageBox.warning(self, 'Invalid URI', warnMsg, QMessageBox.Ok)
 
2494
         LOGERROR(warnMsg)
 
2495
         return {}
 
2496
 
 
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!')
 
2502
         return {}
 
2503
 
 
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), \
 
2515
            QMessageBox.Ok)
 
2516
         LOGERROR('URI link is for the wrong network!')
 
2517
         return {}
 
2518
 
 
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), \
 
2528
               QMessageBox.Ok)
 
2529
            LOGERROR('URI link contains unrecognized req- fields.')
 
2530
            return {}
 
2531
 
 
2532
      return uriDict
 
2533
 
 
2534
 
 
2535
 
 
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.', \
 
2544
            QMessageBox.Ok)
 
2545
         return
 
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']
 
2550
 
 
2551
         self.delayedURIData[qLen] = uriStr
 
2552
         qLen = qLen +1
 
2553
         self.delayedURIData['qLen'] = qLen
 
2554
         return
 
2555
 
 
2556
      uriDict = self.parseUriLink(uriStr, 'click')
 
2557
 
 
2558
      if len(uriDict)>0:
 
2559
         self.bringArmoryToFront()
 
2560
         return self.uriSendBitcoins(uriDict)
 
2561
 
 
2562
 
 
2563
   #############################################################################
 
2564
   @TimeThisFunction
 
2565
   def loadWalletsAndSettings(self):
 
2566
      LOGINFO('loadWalletsAndSettings')
 
2567
 
 
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)
 
2573
 
 
2574
 
 
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)
 
2583
      else:
 
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)
 
2587
 
 
2588
 
 
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
 
2595
 
 
2596
 
 
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))
 
2606
 
 
2607
 
 
2608
      # Load wallets found in the .armory directory
 
2609
      self.walletMap = {}
 
2610
      self.walletIndices = {}
 
2611
      self.walletIDSet = set()
 
2612
 
 
2613
      # I need some linear lists for accessing by index
 
2614
      self.walletIDList = []
 
2615
      self.walletVisibleList = []
 
2616
      self.combinedLedger = []
 
2617
      self.ledgerSize = 0
 
2618
      self.ledgerTable = []
 
2619
 
 
2620
      self.currBlockNum = 0
 
2621
 
 
2622
      LOGINFO('Loading wallets...')
 
2623
      wltPaths = readWalletFiles()
 
2624
 
 
2625
      wltExclude = self.settings.get('Excluded_Wallets', expectList=True)
 
2626
      wltOffline = self.settings.get('Offline_WalletIDs', expectList=True)
 
2627
      for fpath in wltPaths:
 
2628
         try:
 
2629
            wltLoad = PyBtcWallet().readWalletFile(fpath)
 
2630
            wltID = wltLoad.uniqueIDB58
 
2631
            if fpath in wltExclude or wltID in wltExclude:
 
2632
               continue
 
2633
 
 
2634
            if wltID in self.walletIDSet:
 
2635
               LOGWARN('***WARNING: Duplicate wallet detected, %s', wltID)
 
2636
               wo1 = self.walletMap[wltID].watchingOnly
 
2637
               wo2 = wltLoad.watchingOnly
 
2638
               if wo1 and not wo2:
 
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)
 
2644
               else:
 
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)
 
2648
            else:
 
2649
               # Update the maps/dictionaries
 
2650
               self.walletMap[wltID] = wltLoad
 
2651
               self.walletIndices[wltID] = len(self.walletMap)-1
 
2652
 
 
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
 
2661
         except:
 
2662
            LOGEXCEPT( '***WARNING: Wallet could not be loaded: %s (skipping)', 
 
2663
                                                                           fpath)
 
2664
            raise
 
2665
 
 
2666
 
 
2667
 
 
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)'
 
2673
         LOGINFO(dispStr)
 
2674
         # Register all wallets with TheBDM
 
2675
         TheBDM.registerWallet( wlt.cppWallet )
 
2676
         TheBDM.bdm.registerWallet(wlt.cppWallet)
 
2677
 
 
2678
 
 
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)
 
2684
 
 
2685
 
 
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)
 
2692
 
 
2693
 
 
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
 
2703
 
 
2704
      if not defaultFilename==None:
 
2705
         startPath = os.path.join(startPath, defaultFilename)
 
2706
 
 
2707
      types = ffilter
 
2708
      types.append('All files (*)')
 
2709
      typesStr = ';; '.join(types)
 
2710
 
 
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...
 
2714
      if not OS_MACOSX:
 
2715
         fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr))
 
2716
      else:
 
2717
         fullPath = unicode(QFileDialog.getSaveFileName(self, title, startPath, typesStr,
 
2718
                                             options=QFileDialog.DontUseNativeDialog))
 
2719
 
 
2720
 
 
2721
      fdir,fname = os.path.split(fullPath)
 
2722
      if fdir:
 
2723
         self.writeSetting('LastDirectory', fdir)
 
2724
      return fullPath
 
2725
 
 
2726
 
 
2727
   #############################################################################
 
2728
   def getFileLoad(self, title='Load Wallet File', \
 
2729
                         ffilter=['Wallet files (*.wallet)'], \
 
2730
                         defaultDir=None):
 
2731
 
 
2732
      LOGDEBUG('getFileLoad')
 
2733
 
 
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
 
2738
 
 
2739
 
 
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...
 
2746
      if not OS_MACOSX:
 
2747
         fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr))
 
2748
      else:
 
2749
         fullPath = unicode(QFileDialog.getOpenFileName(self, title, defaultDir, typesStr, \
 
2750
                                             options=QFileDialog.DontUseNativeDialog))
 
2751
 
 
2752
      self.writeSetting('LastDirectory', os.path.split(fullPath)[0])
 
2753
      return fullPath
 
2754
 
 
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)
 
2762
      else:
 
2763
         if not defaultValue=='':
 
2764
            self.setWltSetting(wltID, propName, defaultValue)
 
2765
         return defaultValue
 
2766
 
 
2767
   #############################################################################
 
2768
   def setWltSetting(self, wltID, propName, value):
 
2769
      wltPropName = 'Wallet_%s_%s' % (wltID, propName)
 
2770
      self.writeSetting(wltPropName, value)
 
2771
 
 
2772
 
 
2773
   #############################################################################
 
2774
   def toggleIsMine(self, wltID):
 
2775
      alreadyMine = self.getWltSetting(wltID, 'IsMine')
 
2776
      if alreadyMine:
 
2777
         self.setWltSetting(wltID, 'IsMine', False)
 
2778
      else:
 
2779
         self.setWltSetting(wltID, 'IsMine', True)
 
2780
 
 
2781
 
 
2782
   #############################################################################
 
2783
   def loadLockboxesFromFile(self, fn):
 
2784
      self.allLockboxes = []
 
2785
      self.cppLockboxWltMap = {}
 
2786
      if not os.path.exists(fn):
 
2787
         return
 
2788
 
 
2789
      lbList = readLockboxesFile(fn)
 
2790
      for lb in lbList:
 
2791
         self.updateOrAddLockbox(lb)
 
2792
 
 
2793
 
 
2794
   #############################################################################
 
2795
   def updateOrAddLockbox(self, lbObj, isFresh=False):
 
2796
      try:
 
2797
         lbID = lbObj.uniqueIDB58
 
2798
         index = self.lockboxIDMap.get(lbID)
 
2799
         if index is None:
 
2800
            # Add new lockbox to list
 
2801
            self.allLockboxes.append(lbObj)
 
2802
            self.lockboxIDMap[lbID] = len(self.allLockboxes)-1
 
2803
   
 
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)
 
2810
            if not isFresh:
 
2811
               self.cppLockboxWltMap[lbID].addScrAddress_1_(scraddrReg)
 
2812
               self.cppLockboxWltMap[lbID].addScrAddress_1_(scraddrP2SH)
 
2813
            else:
 
2814
               self.cppLockboxWltMap[lbID].addNewScrAddress(scraddrReg)
 
2815
               self.cppLockboxWltMap[lbID].addNewScrAddress(scraddrP2SH)
 
2816
 
 
2817
            # Save the scrAddr histories again to make sure no rescan nexttime
 
2818
            if TheBDM.getBDMState()=='BlockchainReady':
 
2819
               TheBDM.saveScrAddrHistories()
 
2820
         else:
 
2821
            # Replace the original
 
2822
            self.allLockboxes[index] = lbObj
 
2823
 
 
2824
         writeLockboxesFile(self.allLockboxes, MULTISIG_FILE)
 
2825
      except:
 
2826
         LOGEXCEPT('Failed to add/update lockbox')
 
2827
        
 
2828
   
 
2829
   #############################################################################
 
2830
   def removeLockbox(self, lbObj):
 
2831
      lbID = lbObj.uniqueIDB58
 
2832
      index = self.lockboxIDMap.get(lbID)
 
2833
      if index is None:
 
2834
         LOGERROR('Tried to remove lockbox that DNE: %s', lbID)
 
2835
      else:
 
2836
         del self.allLockboxes[index]
 
2837
         self.reconstructLockboxMaps()
 
2838
         writeLockboxesFile(self.allLockboxes, MULTISIG_FILE)
 
2839
 
 
2840
 
 
2841
   #############################################################################
 
2842
   def reconstructLockboxMaps(self):
 
2843
      self.lockboxIDMap.clear()
 
2844
      for i,box in enumerate(self.allLockboxes):
 
2845
         self.lockboxIDMap[box.uniqueIDB58] = i
 
2846
 
 
2847
   #############################################################################
 
2848
   def getLockboxByID(self, boxID):
 
2849
      index = self.lockboxIDMap.get(boxID)
 
2850
      return None if index is None else self.allLockboxes[index]
 
2851
   
 
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):
 
2859
            return lbox
 
2860
      return None
 
2861
 
 
2862
 
 
2863
   #############################################################################
 
2864
   def browseLockboxes(self):
 
2865
      DlgLockboxManager(self,self).exec_()
 
2866
 
 
2867
 
 
2868
 
 
2869
   #############################################################################
 
2870
   def getContribStr(self, binScript, contribID='', contribLabel=''):
 
2871
      """ 
 
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.
 
2876
 
 
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.
 
2880
      """
 
2881
 
 
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'])
 
2887
 
 
2888
      scriptType = getTxOutScriptType(binScript) 
 
2889
      scrAddr = script_to_scrAddr(binScript)
 
2890
 
 
2891
   
 
2892
      # At this point, we can use the contrib ID (and know we can't sign it)
 
2893
      if contribID or contribLabel:
 
2894
         if contribID:
 
2895
            if contribLabel:
 
2896
               outStr = 'Contributor "%s" (%s)' % (contribLabel, contribID)
 
2897
            else:
 
2898
               outStr = 'Contributor %s' % contribID
 
2899
         else:
 
2900
            if contribLabel:
 
2901
               outStr = 'Contributor "%s"' % contribLabel
 
2902
            else:
 
2903
               outStr = 'Unknown Contributor'
 
2904
               LOGERROR('How did we get to this impossible else-statement?')
 
2905
 
 
2906
         return outStr, ('CID:%s' % contribID)
 
2907
 
 
2908
      # If no contrib ID, then salvage anything
 
2909
      astr = displayInfo['AddrStr']
 
2910
      cid = None
 
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
 
2921
      else:
 
2922
         dispStr = 'Non-standard: P2SH=%s' % astr
 
2923
         cid     = 'NS:%s' % astr
 
2924
 
 
2925
      return dispStr, cid
 
2926
 
 
2927
 
 
2928
 
 
2929
   #############################################################################
 
2930
   def getWalletForAddr160(self, addr160):
 
2931
      for wltID, wlt in self.walletMap.iteritems():
 
2932
         if wlt.hasAddr(addr160):
 
2933
            return wltID
 
2934
      return ''
 
2935
 
 
2936
   #############################################################################
 
2937
   def getWalletForScrAddr(self, scrAddr):
 
2938
      for wltID, wlt in self.walletMap.iteritems():
 
2939
         if wlt.hasScrAddr(scrAddr):
 
2940
            return wltID
 
2941
      return ''
 
2942
 
 
2943
   #############################################################################
 
2944
   def getSettingOrSetDefault(self, settingName, defaultVal):
 
2945
      s = self.settings.getSettingOrSetDefault(settingName, defaultVal)
 
2946
      return s
 
2947
 
 
2948
   #############################################################################
 
2949
   def writeSetting(self, settingName, val):
 
2950
      self.settings.set(settingName, val)
 
2951
 
 
2952
   #############################################################################
 
2953
   def startRescanBlockchain(self, forceFullScan=False):
 
2954
      if TheBDM.getBDMState() in ('Offline','Uninitialized'):
 
2955
         LOGWARN('Rescan requested but Armory is in offline mode')
 
2956
         return
 
2957
 
 
2958
      if TheBDM.getBDMState()=='Scanning':
 
2959
         LOGINFO('Queueing rescan after current scan completes.')
 
2960
      else:
 
2961
         LOGINFO('Starting blockchain rescan...')
 
2962
 
 
2963
 
 
2964
      # Start it in the background
 
2965
      TheBDM.rescanBlockchain('AsNeeded', wait=False)
 
2966
      self.needUpdateAfterScan = True
 
2967
      self.setDashboardDetails()
 
2968
 
 
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()
 
2978
 
 
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()
 
2989
 
 
2990
 
 
2991
 
 
2992
 
 
2993
 
 
2994
   #############################################################################
 
2995
   @TimeThisFunction
 
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()
 
3005
 
 
3006
      for lbID,cppWallet in self.cppLockboxWltMap.iteritems():
 
3007
         TheBDM.scanRegisteredTxForWallet(cppWallet, wait=True)
 
3008
 
 
3009
 
 
3010
   @TimeThisFunction
 
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, \
 
3022
                                                        self.memPoolInit)
 
3023
         self.statusBar().showMessage('Blockchain loaded. Wallets synced!', 10000)
 
3024
 
 
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))
 
3033
 
 
3034
         self.blkReceived = TheBDM.getTopBlockHeader().getTimestamp()
 
3035
         self.writeSetting('LastBlkRecv',     self.currBlockNum)
 
3036
         self.writeSetting('LastBlkRecvTime', self.blkReceived)
 
3037
 
 
3038
         currSyncSuccess = self.getSettingOrSetDefault("SyncSuccessCount", 0)
 
3039
         self.writeSetting('SyncSuccessCount', min(currSyncSuccess+1, 10))
 
3040
 
 
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
 
3052
               instability.
 
3053
               <br><br>
 
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>"."""), \
 
3059
                QMessageBox.Ok)
 
3060
 
 
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')
 
3069
 
 
3070
            if remember==True:
 
3071
               self.writeSetting('NotifyBlkFinish',False)
 
3072
 
 
3073
         self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger)
 
3074
 
 
3075
         # Execute any extra functions we may have.
 
3076
         for fn in self.extraGoOnlineFunctions:
 
3077
            fn(self.currBlockNum)
 
3078
 
 
3079
         self.netMode = NETWORKMODE.Full
 
3080
         self.settings.set('FailedLoadCount', 0)
 
3081
      else:
 
3082
         self.statusBar().showMessage('! Blockchain loading failed !', 10000)
 
3083
 
 
3084
 
 
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()
 
3090
 
 
3091
      qLen = self.delayedURIData['qLen']
 
3092
      if qLen > 0:
 
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)
 
3098
 
 
3099
 
 
3100
   #############################################################################
 
3101
   def removeBootstrapDat(self):
 
3102
      bfile = os.path.join(BTC_HOME_DIR, 'bootstrap.dat.old')
 
3103
      if os.path.exists(bfile):
 
3104
         os.remove(bfile)
 
3105
 
 
3106
   #############################################################################
 
3107
   def changeLedgerSorting(self, col, order):
 
3108
      """
 
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.
 
3112
      """
 
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()
 
3118
 
 
3119
   #############################################################################
 
3120
   @TimeThisFunction
 
3121
   def createCombinedLedger(self, wltIDList=None, withZeroConf=True):
 
3122
      """
 
3123
      Create a ledger to display on the main screen, that consists of ledger
 
3124
      entries of any SUBSET of available wallets.
 
3125
      """
 
3126
      start = RightNow()
 
3127
      if wltIDList==None:
 
3128
         currIdx  = max(self.comboWltSelect.currentIndex(), 0)
 
3129
         wltIDList = []
 
3130
         for i,vis in enumerate(self.walletVisibleList):
 
3131
            if vis:
 
3132
               wltIDList.append(self.walletIDList[i])
 
3133
         self.writeSetting('LastFilterState', currIdx)
 
3134
 
 
3135
 
 
3136
      if wltIDList==None:
 
3137
         return
 
3138
 
 
3139
      self.combinedLedger = []
 
3140
      totalFunds  = 0
 
3141
      spendFunds  = 0
 
3142
      unconfFunds = 0
 
3143
      currBlk = 0xffffffff
 
3144
      if TheBDM.isInitialized():
 
3145
         currBlk = TheBDM.getTopBlockHeight()
 
3146
 
 
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')
 
3154
 
 
3155
 
 
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)
 
3162
 
 
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)
 
3169
 
 
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)
 
3182
 
 
3183
      self.ledgerSize = len(self.combinedLedger)
 
3184
 
 
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)
 
3189
 
 
3190
      # Many MainWindow objects haven't been created yet...
 
3191
      # let's try to update them and fail silently if they don't exist
 
3192
      try:
 
3193
         if TheBDM.getBDMState() in ('Offline', 'Scanning'):
 
3194
            self.lblTotalFunds.setText( '-'*12 )
 
3195
            self.lblSpendFunds.setText( '-'*12 )
 
3196
            self.lblUnconfFunds.setText('-'*12 )
 
3197
            return
 
3198
 
 
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)))
 
3209
 
 
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()
 
3215
 
 
3216
      except AttributeError:
 
3217
         raise
 
3218
 
 
3219
 
 
3220
      if not self.usermode==USERMODE.Expert:
 
3221
         return 
 
3222
 
 
3223
      # In expert mode, we're updating the lockbox info, too
 
3224
      try:
 
3225
         lockboxTable = []
 
3226
         for lbID,cppWlt in self.cppLockboxWltMap.iteritems():
 
3227
 
 
3228
            zcLedger = cppWlt.getZeroConfLedger()
 
3229
            for i in range(len(zcLedger)):
 
3230
               lockboxTable.append([lbID, zcLedger[i]])
 
3231
 
 
3232
            ledger = cppWlt.getTxLedger()
 
3233
            for i in range(len(ledger)):
 
3234
               lockboxTable.append([lbID, ledger[i]])
 
3235
 
 
3236
         self.lockboxLedgTable = self.convertLedgerToTable(lockboxTable)
 
3237
         self.lockboxLedgModel.ledger = self.lockboxLedgTable
 
3238
         self.lockboxLedgModel.reset()
 
3239
      except:
 
3240
         LOGEXCEPT('Failed to update lockbox ledger')
 
3241
 
 
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)
 
3248
         if wltID:
 
3249
            commentSet.add(self.walletMap[wltID].getCommentForLE(le))
 
3250
      return ' '.join(commentSet)
 
3251
 
 
3252
   #############################################################################
 
3253
   @TimeThisFunction
 
3254
   def convertLedgerToTable(self, ledger, showSentToSelfAmt=True):
 
3255
      table2D = []
 
3256
      datefmt = self.getPreferredDateFormat()
 
3257
      for wltID,le in ledger:
 
3258
         row = []
 
3259
 
 
3260
         wlt = self.walletMap.get(wltID)
 
3261
 
 
3262
         if wlt:
 
3263
            isWatch = (determineWalletType(wlt, self)[0] == WLTTYPES.WatchOnly)
 
3264
            wltName = wlt.labelName 
 
3265
            dispComment = self.getCommentForLE(wltID, le)
 
3266
         else:
 
3267
            lboxId = wltID
 
3268
            lbox = self.getLockboxByID(lboxId)
 
3269
            if not lbox:
 
3270
               continue
 
3271
            isWatch = True
 
3272
            wltName = '%s-of-%s: %s (%s)' % (lbox.M, lbox.N, lbox.shortName, lboxId)
 
3273
            dispComment = self.getCommentForLockboxTx(lboxId, le)
 
3274
 
 
3275
         nConf = self.currBlockNum - le.getBlockNum()+1
 
3276
         if le.getBlockNum()>=0xffffffff:
 
3277
            nConf=0
 
3278
 
 
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
 
3284
         # chain index
 
3285
         amt = le.getValue()
 
3286
         if le.isSentToSelf() and wlt and showSentToSelfAmt:
 
3287
            amt = determineSentToSelfAmt(le, wlt)[0]
 
3288
 
 
3289
         # NumConf
 
3290
         row.append(nConf)
 
3291
 
 
3292
         # UnixTime (needed for sorting)
 
3293
         row.append(le.getTxTime())
 
3294
 
 
3295
         # Date
 
3296
         row.append(unixTimeToFormatStr(le.getTxTime(), datefmt))
 
3297
 
 
3298
         # TxDir (actually just the amt... use the sign of the amt to determine dir)
 
3299
         row.append(coin2str(le.getValue(), maxZeros=2))
 
3300
 
 
3301
         # Wlt Name
 
3302
         row.append(wltName)
 
3303
 
 
3304
         # Comment
 
3305
         row.append(dispComment)
 
3306
 
 
3307
         # Amount
 
3308
         row.append(coin2str(amt, maxZeros=2))
 
3309
 
 
3310
         # Is this money mine?
 
3311
         row.append(isWatch)
 
3312
 
 
3313
         # ID to display (this might be the lockbox ID)
 
3314
         row.append( wltID )
 
3315
 
 
3316
         # TxHash
 
3317
         row.append( binary_to_hex(le.getTxHash() ))
 
3318
 
 
3319
         # Is this a coinbase/generation transaction
 
3320
         row.append( le.isCoinbase() )
 
3321
 
 
3322
         # Sent-to-self
 
3323
         row.append( le.isSentToSelf() )
 
3324
 
 
3325
         # Tx was invalidated!  (double=spend!)
 
3326
         row.append( not le.isValid())
 
3327
 
 
3328
         # Finally, attach the row to the table
 
3329
         table2D.append(row)
 
3330
 
 
3331
      return table2D
 
3332
 
 
3333
 
 
3334
   #############################################################################
 
3335
   @TimeThisFunction
 
3336
   def walletListChanged(self):
 
3337
      self.walletModel.reset()
 
3338
      self.populateLedgerComboBox()
 
3339
      self.createCombinedLedger()
 
3340
 
 
3341
 
 
3342
   #############################################################################
 
3343
   @TimeThisFunction
 
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)
 
3357
 
 
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()
 
3366
         return
 
3367
 
 
3368
      if index==None:
 
3369
         index = self.walletsView.selectedIndexes()
 
3370
         if len(self.walletMap)==1:
 
3371
            self.walletsView.selectRow(0)
 
3372
            index = self.walletsView.selectedIndexes()
 
3373
         elif len(index)==0:
 
3374
            QMessageBox.warning(self, 'Select a Wallet', \
 
3375
               'Please select a wallet on the right, to see its properties.', \
 
3376
               QMessageBox.Ok)
 
3377
            return
 
3378
         index = index[0]
 
3379
 
 
3380
      wlt = self.walletMap[self.walletIDList[index.row()]]
 
3381
      dialog = DlgWalletDetails(wlt, self.usermode, self, self)
 
3382
      dialog.exec_()
 
3383
      #self.walletListChanged()
 
3384
 
 
3385
   #############################################################################
 
3386
   def execClickRow(self, index=None):
 
3387
      row,col = index.row(), index.column()
 
3388
      if not col==WLTVIEWCOLS.Visible:
 
3389
         return
 
3390
 
 
3391
      wltID = self.walletIDList[row]
 
3392
      currEye = self.walletVisibleList[row]
 
3393
      self.walletVisibleList[row] = not currEye 
 
3394
      self.setWltSetting(wltID, 'LedgerShow', not currEye)
 
3395
      
 
3396
      # Set it to "Custom Filter"
 
3397
      self.comboWltSelect.setCurrentIndex(4)
 
3398
      
 
3399
      if TheBDM.getBDMState()=='BlockchainReady':
 
3400
         self.createCombinedLedger()
 
3401
         self.ledgerModel.reset()
 
3402
         self.walletModel.reset()
 
3403
 
 
3404
 
 
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())
 
3412
 
 
3413
      dialog = DlgSetComment(self, self, currComment, 'Transaction')
 
3414
      if dialog.exec_():
 
3415
         newComment = str(dialog.edtComment.text())
 
3416
         self.walletMap[wltID].setComment(hex_to_binary(txHash), newComment)
 
3417
         self.walletListChanged()
 
3418
 
 
3419
 
 
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())
 
3426
 
 
3427
      dialog = DlgSetComment(self, self, currComment, 'Address')
 
3428
      if dialog.exec_():
 
3429
         newComment = str(dialog.edtComment.text())
 
3430
         atype, addr160 = addrStr_to_hash160(addrStr)
 
3431
         if atype==P2SHBYTE:
 
3432
            LOGWARN('Setting comment for P2SH address: %s' % addrStr)
 
3433
         wlt.setComment(addr160, newComment)
 
3434
 
 
3435
 
 
3436
 
 
3437
   #############################################################################
 
3438
   @TimeThisFunction
 
3439
   def getAddrCommentIfAvailAll(self, txHash):
 
3440
      if not TheBDM.isInitialized():
 
3441
         return ''
 
3442
      else:
 
3443
 
 
3444
         appendedComments = []
 
3445
         for wltID,wlt in self.walletMap.iteritems():
 
3446
            cmt = wlt.getAddrCommentIfAvail(txHash)
 
3447
            if len(cmt)>0:
 
3448
               appendedComments.append(cmt)
 
3449
 
 
3450
         return '; '.join(appendedComments)
 
3451
 
 
3452
 
 
3453
 
 
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.
 
3458
 
 
3459
      return self.walletMap[wltID].getCommentForLE(le)
 
3460
      """
 
3461
      txHash = le.getTxHash()
 
3462
      if wlt.commentsMap.has_key(txHash):
 
3463
         comment = wlt.commentsMap[txHash]
 
3464
      else:
 
3465
         # [[ COMMENTS ]] are not meant to be displayed on main ledger
 
3466
         comment = self.getAddrCommentIfAvail(txHash)
 
3467
         if comment.startswith('[[') and comment.endswith(']]'):
 
3468
            comment = ''
 
3469
 
 
3470
      return comment
 
3471
      """
 
3472
 
 
3473
   #############################################################################
 
3474
   def addWalletToApplication(self, newWallet, walletIsNew=True):
 
3475
      LOGINFO('addWalletToApplication')
 
3476
      # Update the maps/dictionaries
 
3477
      newWltID = newWallet.uniqueIDB58
 
3478
 
 
3479
      if self.walletMap.has_key(newWltID):
 
3480
         return
 
3481
 
 
3482
      self.walletMap[newWltID] = newWallet
 
3483
      self.walletIndices[newWltID] = len(self.walletMap)-1
 
3484
 
 
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)
 
3491
 
 
3492
      ledger = []
 
3493
      self.walletListChanged()
 
3494
      self.mainWnd = self
 
3495
 
 
3496
 
 
3497
   #############################################################################
 
3498
   def removeWalletFromApplication(self, wltID):
 
3499
      LOGINFO('removeWalletFromApplication')
 
3500
      idx = -1
 
3501
      try:
 
3502
         idx = self.walletIndices[wltID]
 
3503
      except KeyError:
 
3504
         LOGERROR('Invalid wallet ID passed to "removeWalletFromApplication"')
 
3505
         raise WalletExistsError
 
3506
 
 
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]
 
3512
 
 
3513
      # Reconstruct walletIndices
 
3514
      for i,wltID in enumerate(self.walletIDList):
 
3515
         self.walletIndices[wltID] = i
 
3516
 
 
3517
      self.walletListChanged()
 
3518
 
 
3519
   #############################################################################
 
3520
   def RecoverWallet(self):
 
3521
      DlgWltRecoverWallet(self, self).promptWalletRecovery()
 
3522
 
 
3523
 
 
3524
   #############################################################################
 
3525
   def createSweepAddrTx(self, sweepFromAddrObjList, sweepToScript):
 
3526
      """
 
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.
 
3533
      """
 
3534
 
 
3535
 
 
3536
      LOGINFO('createSweepAddrTx')
 
3537
      if not isinstance(sweepFromAddrObjList, (list, tuple)):
 
3538
         sweepFromAddrObjList = [sweepFromAddrObjList]
 
3539
 
 
3540
      
 
3541
      addr160List = [a.getAddr160() for a in sweepFromAddrObjList]
 
3542
      utxoList = getUnspentTxOutsForAddr160List(addr160List, 'Sweep', 0)
 
3543
      if len(utxoList)==0:
 
3544
         return [None, 0, 0]
 
3545
 
 
3546
      outValue = sumTxOutList(utxoList)
 
3547
 
 
3548
      inputSide = []
 
3549
      outputSide = []
 
3550
 
 
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))
 
3561
               break
 
3562
 
 
3563
      minFee = calcMinSuggestedFees(utxoList, outValue, 0, 1)[1]
 
3564
 
 
3565
      if minFee > 0:
 
3566
         LOGDEBUG( 'Subtracting fee from Sweep-output')
 
3567
         outValue -= minFee
 
3568
 
 
3569
      if outValue<=0:
 
3570
         return [None, outValue, minFee]
 
3571
 
 
3572
      # Creating the output list is pretty easy...
 
3573
      outputSide = []
 
3574
      outputSide.append(DecoratedTxOut(sweepToScript, outValue))
 
3575
 
 
3576
      try:
 
3577
         # Make copies, destroy them in the finally clause
 
3578
         privKeyMap = {}
 
3579
         for addrObj in sweepFromAddrObjList:
 
3580
            scrAddr = SCRADDR_P2PKH_BYTE + addrObj.getAddr160()
 
3581
            privKeyMap[scrAddr] = addrObj.binPrivKey32_Plain.copy()
 
3582
   
 
3583
         pytx = PyCreateAndSignTx(inputSide, outputSide, privKeyMap)
 
3584
         return (pytx, outValue, minFee)
 
3585
 
 
3586
      finally:
 
3587
         for scraddr in privKeyMap:
 
3588
            privKeyMap[scraddr].destroy()
 
3589
 
 
3590
      """
 
3591
      # Try with zero fee and exactly one output
 
3592
      minFee = calcMinSuggestedFees(utxoList, outValue, 0, 1)[1]
 
3593
 
 
3594
      if minFee > 0:
 
3595
         LOGDEBUG( 'Subtracting fee from Sweep-output')
 
3596
         outValue -= minFee
 
3597
 
 
3598
      if outValue<=0:
 
3599
         return [None, outValue, minFee]
 
3600
 
 
3601
      outputSide = []
 
3602
      outputSide.append( [PyBtcAddress().createFromPublicKeyHash160(sweepTo160), \
 
3603
                          outValue] )
 
3604
 
 
3605
      pytx = PyCreateAndSignTx(inputSide, outputSide)
 
3606
      return (pytx, outValue, minFee)
 
3607
      """
 
3608
 
 
3609
 
 
3610
 
 
3611
 
 
3612
 
 
3613
   #############################################################################
 
3614
   def confirmSweepScan(self, pybtcaddrList, targAddr160):
 
3615
      LOGINFO('confirmSweepScan')
 
3616
      gt1 = len(self.sweepAfterScanList)>1
 
3617
 
 
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. '
 
3623
            '<br><br>'
 
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()
 
3630
         return False
 
3631
 
 
3632
 
 
3633
      confirmed=False
 
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)
 
3646
         confirmed=True
 
3647
 
 
3648
      else:
 
3649
         msgConfirm = ( \
 
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'))
 
3656
 
 
3657
         if TheBDM.getBDMState()=='Scanning':
 
3658
            msgConfirm += ( \
 
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':
 
3662
            msgConfirm += ( \
 
3663
               '<b>Would you like to start the scan operation right now?</b>')
 
3664
 
 
3665
         msgConfirm += ('<br><br>Clicking "No" will abort the sweep operation')
 
3666
 
 
3667
         confirmed = QMessageBox.question(self, 'Confirm Rescan', msgConfirm, \
 
3668
                                                QMessageBox.Yes | QMessageBox.No)
 
3669
 
 
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()
 
3678
         return True
 
3679
 
 
3680
 
 
3681
   #############################################################################
 
3682
   def finishSweepScan(self):
 
3683
      LOGINFO('finishSweepScan')
 
3684
      sweepList, self.sweepAfterScanList = self.sweepAfterScanList,[]
 
3685
 
 
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)
 
3691
 
 
3692
      gt1 = len(sweepList)>1
 
3693
 
 
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'), \
 
3699
               QMessageBox.Ok)
 
3700
            return
 
3701
         else:
 
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 '
 
3706
               'swept.'
 
3707
               '<br><br>'
 
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)), \
 
3712
               QMessageBox.Ok)
 
3713
            LOGERROR('Sweep amount (%s) is less than fee needed for sweeping (%s)', \
 
3714
                     coin2str(outVal+fee, maxZeros=0), coin2str(fee, maxZeros=0))
 
3715
            return
 
3716
 
 
3717
      wltID = self.getWalletForAddr160(self.sweepAfterScanTarg)
 
3718
      wlt = self.walletMap[wltID]
 
3719
 
 
3720
      # Finally, if we got here, we're ready to broadcast!
 
3721
      if gt1:
 
3722
         dispIn  = 'multiple addresses'
 
3723
      else:
 
3724
         dispIn  = 'address <b>%s</b>' % sweepList[0].getAddrStr()
 
3725
 
 
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)
 
3729
 
 
3730
      if TheBDM.getBDMState()=='BlockchainReady':
 
3731
         wlt.syncWithBlockchain(0)
 
3732
 
 
3733
      self.walletListChanged()
 
3734
 
 
3735
   #############################################################################
 
3736
   def broadcastTransaction(self, pytx, dryRun=False, withOldSigWarning=True):
 
3737
 
 
3738
      if dryRun:
 
3739
         #DlgDispTxInfo(pytx, None, self, self).exec_()
 
3740
         return
 
3741
      else:
 
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:
 
3753
               pytx = newTx
 
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...!')
 
3760
 
 
3761
 
 
3762
         def sendGetDataMsg():
 
3763
            msg = PyMessage('getdata')
 
3764
            msg.payload.invList.append( [MSG_INV_TX, newTxHash] )
 
3765
            self.NetworkingFactory.sendMessage(msg)
 
3766
 
 
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
 
3772
            # screen
 
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)
 
3780
 
 
3781
               supportURL       = 'https://bitcoinarmory.com/support' 
 
3782
               blkexplURL       = BLOCKEXPLORE_URL_TX % searchstr
 
3783
               blkexplURL_short = BLOCKEXPLORE_URL_TX % searchstr[:20]
 
3784
 
 
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 
 
3793
                  to %s:
 
3794
                  <br><br>
 
3795
                  <a href="%s">%s...</a>  
 
3796
                  <br><br>
 
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
 
3802
                  confirmation. 
 
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 
 
3808
                  ticket at 
 
3809
                  <a href="%s">%s</a>""") % (BLOCKEXPLORE_NAME, blkexplURL, 
 
3810
                  blkexplURL_short, supportURL, supportURL), QMessageBox.Ok)
 
3811
 
 
3812
         self.mainDisplayTabs.setCurrentIndex(self.MAINTABS.Ledger)
 
3813
 
 
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)
 
3819
 
 
3820
 
 
3821
   #############################################################################
 
3822
   def warnNoImportWhileScan(self):
 
3823
      extraMsg = ''
 
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)
 
3833
 
 
3834
 
 
3835
 
 
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)
 
3848
         return
 
3849
 
 
3850
      DlgUniversalRestoreSelect(self, self).exec_()
 
3851
 
 
3852
 
 
3853
   #############################################################################
 
3854
   def execGetImportWltName(self):
 
3855
      fn = self.getFileLoad('Import Wallet File')
 
3856
      if not os.path.exists(fn):
 
3857
         return
 
3858
 
 
3859
      wlt = PyBtcWallet().readWalletFile(fn, verifyIntegrity=False, \
 
3860
                                             doScanNow=False)
 
3861
      wltID = wlt.uniqueIDB58
 
3862
      wlt = None
 
3863
 
 
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, \
 
3869
            QMessageBox.Ok)
 
3870
         return
 
3871
 
 
3872
      fname = self.getUniqueWalletFilename(fn)
 
3873
      newpath = os.path.join(ARMORY_HOME_DIR, fname)
 
3874
 
 
3875
      LOGINFO('Copying imported wallet to: %s', newpath)
 
3876
      shutil.copy(fn, newpath)
 
3877
      newWlt = PyBtcWallet().readWalletFile(newpath)
 
3878
      newWlt.fillAddressPool()
 
3879
 
 
3880
      self.addWalletToAppAndAskAboutRescan(newWlt)
 
3881
 
 
3882
      """ I think the addWalletToAppAndAskAboutRescan replaces this...
 
3883
      if TheBDM.getBDMState() in ('Uninitialized', 'Offline'):
 
3884
         self.addWalletToApplication(newWlt, walletIsNew=False)
 
3885
         return
 
3886
 
 
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.'
 
3894
            '<br><br>'
 
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)
 
3899
      else:
 
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. '
 
3905
            '<br><br>'
 
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?'
 
3908
            '<br><br>'
 
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)
 
3913
 
 
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()
 
3920
      else:
 
3921
         LOGINFO('User aborted the wallet-import scan')
 
3922
         QMessageBox.warning(self, 'Import Failed', \
 
3923
            'The wallet was not imported.', QMessageBox.Ok)
 
3924
 
 
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')
 
3929
         os.remove(thepath)
 
3930
         os.remove(thepathBackup)
 
3931
         return
 
3932
 
 
3933
      self.addWalletToApplication(newWlt, walletIsNew=False)
 
3934
      self.newWalletList.append([newWlt, False])
 
3935
      LOGINFO('Import Complete!')
 
3936
      """
 
3937
 
 
3938
 
 
3939
 
 
3940
 
 
3941
   #############################################################################
 
3942
   def addWalletToAppAndAskAboutRescan(self, newWallet):
 
3943
      LOGINFO('Raw import successful.')
 
3944
 
 
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)
 
3950
         return
 
3951
 
 
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.'
 
3960
            '<br><br>'
 
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 '
 
3970
            'afterwards?'
 
3971
            '<br><br>'
 
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)
 
3976
      """
 
3977
 
 
3978
      doRescanNow = QMessageBox.Cancel
 
3979
 
 
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.
 
3985
            <br><br>
 
3986
            Would you like to do the scan now?  Clicking "No" will
 
3987
            abort the restore/import operation."""), \
 
3988
            QMessageBox.Yes | QMessageBox.No)
 
3989
      else:
 
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?
 
3996
            <br><br>
 
3997
            Clicking "No" will abort adding the wallet to Armory."""), \
 
3998
            QMessageBox.Yes | QMessageBox.No)
 
3999
 
 
4000
 
 
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()
 
4007
      else:
 
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')
 
4017
         os.remove(thepath)
 
4018
         os.remove(thepathBackup)
 
4019
         return
 
4020
 
 
4021
      self.addWalletToApplication(newWallet, walletIsNew=False)
 
4022
      LOGINFO('Import Complete!')
 
4023
 
 
4024
 
 
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)
 
4034
 
 
4035
 
 
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)
 
4048
      else:
 
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)
 
4052
            return
 
4053
         DlgAddressBook(self, self, None, None, None).exec_()
 
4054
 
 
4055
 
 
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)
 
4063
      newIndex = 2
 
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)
 
4068
         newIndex+=1
 
4069
         if newIndex==99:
 
4070
            raise WalletExistsError('Cannot find unique filename for wallet.'
 
4071
                                                       'Too many duplicates!')
 
4072
      return fname
 
4073
 
 
4074
 
 
4075
   #############################################################################
 
4076
   def addrViewDblClicked(self, index, wlt):
 
4077
      uacfv = lambda x: self.updateAddressCommentFromView(self.wltAddrView, self.wlt)
 
4078
 
 
4079
 
 
4080
   #############################################################################
 
4081
   def dblClickLedger(self, index):
 
4082
      if index.column()==LEDGERCOLS.Comment:
 
4083
         self.updateTxCommentFromView(self.ledgerView)
 
4084
      else:
 
4085
         self.showLedgerTx()
 
4086
 
 
4087
 
 
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())
 
4094
 
 
4095
      pytx = None
 
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())
 
4101
 
 
4102
      if pytx==None:
 
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)
 
4106
         return
 
4107
 
 
4108
      DlgDispTxInfo( pytx, self.walletMap[wltID], self, self, txtime=txtime).exec_()
 
4109
 
 
4110
 
 
4111
   #############################################################################
 
4112
   def showContextMenuLedger(self):
 
4113
      menu = QMenu(self.ledgerView)
 
4114
 
 
4115
      if len(self.ledgerView.selectedIndexes())==0:
 
4116
         return
 
4117
 
 
4118
      row = self.ledgerView.selectedIndexes()[0].row()
 
4119
 
 
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())
 
4123
 
 
4124
 
 
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())
 
4131
 
 
4132
      if action==actViewTx:
 
4133
         self.showLedgerTx()
 
4134
      elif action==actViewBlkChn:
 
4135
         try:
 
4136
            webbrowser.open(BLOCKEXPLORE_URL_TX % txHash)
 
4137
         except:
 
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()
 
4146
         clipb.clear()
 
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_()
 
4152
 
 
4153
   #############################################################################
 
4154
 
 
4155
   def getSelectedWallet(self):
 
4156
      wltID = None
 
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]
 
4165
 
 
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. '
 
4171
           '<br><br>'
 
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)
 
4176
         return
 
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.', \
 
4183
            QMessageBox.Ok)
 
4184
         return
 
4185
 
 
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()
 
4194
      else:
 
4195
         DlgSendBitcoins(self.getSelectedWallet(), self, self).exec_()
 
4196
 
 
4197
 
 
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)
 
4204
 
 
4205
      haveLbl = uri_has('label')
 
4206
      haveMsg = uri_has('message')
 
4207
 
 
4208
      newMsg = ''
 
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']
 
4215
 
 
4216
      descrStr = ''
 
4217
      descrStr = ('You just clicked on a "bitcoin:" link requesting bitcoins '
 
4218
                'to be sent to the following address:<br> ')
 
4219
 
 
4220
      descrStr += '<br>--<b>Address</b>:\t%s ' % uriDict['address']
 
4221
 
 
4222
      #if uri_has('label'):
 
4223
         #if len(uriDict['label'])>30:
 
4224
            #descrStr += '(%s...)' % uriDict['label'][:30]
 
4225
         #else:
 
4226
            #descrStr += '(%s)' % uriDict['label']
 
4227
 
 
4228
      amt = 0
 
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
 
4233
 
 
4234
 
 
4235
      if newMsg:
 
4236
         if len(newMsg)>60:
 
4237
            descrStr += '<br>--<b>Message</b>:\t%s...' % newMsg[:60]
 
4238
         else:
 
4239
            descrStr += '<br>--<b>Message</b>:\t%s' % newMsg
 
4240
 
 
4241
      uriDict['message'] = newMsg
 
4242
 
 
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. ')
 
4247
      else:
 
4248
          descrStr += ('<br><br><b>The specified amount <u>can</u> be changed</b> on the '
 
4249
            'next screen before hitting the "Send" button. ')
 
4250
 
 
4251
 
 
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()
 
4260
         return False
 
4261
      else:
 
4262
         DlgSendBitcoins(self.getSelectedWallet(), self, self, uriDict).exec_()
 
4263
      return True
 
4264
 
 
4265
 
 
4266
   #############################################################################
 
4267
   def clickReceiveCoins(self):
 
4268
      LOGDEBUG('Clicked "Receive Bitcoins Button"')
 
4269
      wltID = None
 
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()
 
4278
         return
 
4279
      elif len(self.walletMap)==1:
 
4280
         wltID = self.walletMap.keys()[0]
 
4281
      else:
 
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)
 
4288
         if dlg.exec_():
 
4289
            wltID = dlg.selectedID
 
4290
         else:
 
4291
            selectionMade = False
 
4292
 
 
4293
      if selectionMade:
 
4294
         wlt = self.walletMap[wltID]
 
4295
         wlttype = determineWalletType(wlt, self)[0]
 
4296
         if showRecvCoinsWarningIfNecessary(wlt, self):
 
4297
            DlgNewAddressDisp(wlt, self, self).exec_()
 
4298
 
 
4299
 
 
4300
 
 
4301
   #############################################################################
 
4302
   def sysTrayActivated(self, reason):
 
4303
      if reason==QSystemTrayIcon.DoubleClick:
 
4304
         self.bringArmoryToFront()
 
4305
 
 
4306
 
 
4307
 
 
4308
   #############################################################################
 
4309
   def bringArmoryToFront(self):
 
4310
      self.show()
 
4311
      self.setWindowState(Qt.WindowActive)
 
4312
      self.activateWindow()
 
4313
      self.raise_()
 
4314
 
 
4315
   #############################################################################
 
4316
   def minimizeArmory(self):
 
4317
      LOGDEBUG('Minimizing Armory')
 
4318
      self.hide()
 
4319
      self.sysTray.show()
 
4320
 
 
4321
   #############################################################################
 
4322
   def startWalletWizard(self):
 
4323
      walletWizard = WalletWizard(self, self)
 
4324
      walletWizard.exec_()
 
4325
 
 
4326
   #############################################################################
 
4327
   def startTxWizard(self, prefill=None, onlyOfflineWallets=False):
 
4328
      txWizard = TxWizard(self, self, self.getSelectedWallet(), prefill, onlyOfflineWallets=onlyOfflineWallets)
 
4329
      txWizard.exec_()
 
4330
 
 
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)
 
4340
 
 
4341
      if not reply==QMessageBox.Ok:
 
4342
         return
 
4343
 
 
4344
      if self.logFilePrivacyWarning(wCancel=True):
 
4345
         self.saveCombinedLogFile()
 
4346
 
 
4347
   #############################################################################
 
4348
   def getUserAgreeToPrivacy(self, getAgreement=False):
 
4349
      ptype = 'submitbug' if getAgreement else 'generic'
 
4350
      dlg = DlgPrivacyPolicy(self, self, ptype)
 
4351
      if not dlg.exec_():
 
4352
         return False
 
4353
 
 
4354
      return dlg.chkUserAgrees.isChecked()
 
4355
 
 
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>
 
4360
         <br><br>
 
4361
         You should review the <a href="%s">Armory Technologies, Inc. privacy 
 
4362
         policy</a> before sending any data to ATI servers.
 
4363
         <br><br>
 
4364
 
 
4365
         <b><u><font size=3>Wallet Analysis Log Files</font></u></b>
 
4366
         <br><br>
 
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 
 
4370
         keys are included.
 
4371
         <br><br>
 
4372
 
 
4373
         <b><u><font size=3>Regular Log Files</font></u></b>
 
4374
         <br><br>
 
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 
 
4381
         implications.
 
4382
         <br><br>
 
4383
 
 
4384
         <b><u><font size=3>Watching-only Wallet</font></u></b>
 
4385
         <br><br>
 
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.
 
4389
         <br><br>
 
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 
 
4393
         sending your
 
4394
         watching-only wallet unless it was specifically requested by an
 
4395
         Armory representative.""") % PRIVACY_URL, yesStr="&Ok")
 
4396
          
 
4397
 
 
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>
 
4402
         <br>
 
4403
         You should review the <a href="%s">Armory Technologies, Inc. privacy 
 
4404
         policy</a> before sending any data to ATI servers.
 
4405
         <br><br>
 
4406
 
 
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.
 
4411
         <br><br>
 
4412
 
 
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.
 
4417
         <br><br>
 
4418
 
 
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.
 
4423
 
 
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")
 
4428
 
 
4429
 
 
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)
 
4440
 
 
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))
 
4445
         fout.close()
 
4446
 
 
4447
         LOGINFO('Log saved to %s', saveFile)
 
4448
 
 
4449
   #############################################################################
 
4450
   def blinkTaskbar(self):
 
4451
      self.activateWindow()
 
4452
 
 
4453
 
 
4454
   #############################################################################
 
4455
   def lookForBitcoind(self):
 
4456
      LOGDEBUG('lookForBitcoind')
 
4457
      if satoshiIsAvailable():
 
4458
         return 'Running'
 
4459
 
 
4460
      self.setSatoshiPaths()
 
4461
 
 
4462
      try:
 
4463
         TheSDM.setupSDM(extraExeSearch=self.satoshiExeSearchPath)
 
4464
      except:
 
4465
         LOGEXCEPT('Error setting up SDM')
 
4466
         pass
 
4467
 
 
4468
      if TheSDM.failedFindExe:
 
4469
         return 'StillMissing'
 
4470
 
 
4471
      return 'AllGood'
 
4472
 
 
4473
   #############################################################################
 
4474
   def executeModeSwitch(self):
 
4475
      LOGDEBUG('executeModeSwitch')
 
4476
 
 
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()
 
4502
         else:
 
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)
 
4511
      else:
 
4512
         LOGERROR('ModeSwitch button pressed when it should be disabled')
 
4513
      time.sleep(0.3)
 
4514
      self.setDashboardDetails()
 
4515
 
 
4516
 
 
4517
 
 
4518
 
 
4519
   #############################################################################
 
4520
   @TimeThisFunction
 
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'))
 
4526
 
 
4527
      TheBDM.Reset(wait=False)
 
4528
      for wid,wlt in self.walletMap.iteritems():
 
4529
         TheBDM.registerWallet(wlt.cppWallet)
 
4530
 
 
4531
 
 
4532
 
 
4533
   #############################################################################
 
4534
   def setupDashboard(self):
 
4535
      LOGDEBUG('setupDashboard')
 
4536
      self.lblBusy = QLabel('')
 
4537
      if OS_WINDOWS:
 
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
 
4540
         # heartbeat
 
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)
 
4549
      else:
 
4550
         self.qmov = QMovie(':/busy.gif')
 
4551
         self.lblBusy.setMovie( self.qmov )
 
4552
         self.qmov.start()
 
4553
 
 
4554
 
 
4555
      self.btnModeSwitch = QPushButton('')
 
4556
      self.connect(self.btnModeSwitch, SIGNAL('clicked()'), \
 
4557
                                       self.executeModeSwitch)
 
4558
 
 
4559
 
 
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)
 
4565
 
 
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)
 
4570
 
 
4571
      self.barProgressTorrent = QProgressBar(self)
 
4572
      self.barProgressSync    = QProgressBar(self)
 
4573
      self.barProgressBuild   = QProgressBar(self)
 
4574
      self.barProgressScan    = QProgressBar(self)
 
4575
 
 
4576
      self.barProgressTorrent.setRange(0,100)
 
4577
      self.barProgressSync.setRange(0,100)
 
4578
      self.barProgressBuild.setRange(0,100)
 
4579
      self.barProgressScan.setRange(0,100)
 
4580
 
 
4581
 
 
4582
      self.lblTorrentStats       = QRichLabel('', hAlign=Qt.AlignHCenter)
 
4583
 
 
4584
      twid = relaxedSizeStr(self,'99 seconds')[0]
 
4585
      self.lblTimeLeftTorrent = QRichLabel('')
 
4586
      self.lblTimeLeftSync    = QRichLabel('')
 
4587
      self.lblTimeLeftBuild   = QRichLabel('')
 
4588
      self.lblTimeLeftScan    = QRichLabel('')
 
4589
 
 
4590
      self.lblTimeLeftSync.setMinimumWidth(twid)
 
4591
      self.lblTimeLeftScan.setMinimumWidth(twid)
 
4592
 
 
4593
      self.lblStatsTorrent = QRichLabel('')
 
4594
 
 
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)
 
4600
 
 
4601
      layoutDashMode.addWidget(self.lblDashModeSync,     2,0)
 
4602
      layoutDashMode.addWidget(self.barProgressSync,     2,1)
 
4603
      layoutDashMode.addWidget(self.lblTimeLeftSync,     2,2)
 
4604
 
 
4605
      layoutDashMode.addWidget(self.lblDashModeBuild,    3,0)
 
4606
      layoutDashMode.addWidget(self.barProgressBuild,    3,1)
 
4607
      layoutDashMode.addWidget(self.lblTimeLeftBuild,    3,2)
 
4608
 
 
4609
      layoutDashMode.addWidget(self.lblDashModeScan,     4,0)
 
4610
      layoutDashMode.addWidget(self.barProgressScan,     4,1)
 
4611
      layoutDashMode.addWidget(self.lblTimeLeftScan,     4,2)
 
4612
 
 
4613
      layoutDashMode.addWidget(self.lblBusy,             0,3, 5,1)
 
4614
      layoutDashMode.addWidget(self.btnModeSwitch,       0,3, 5,1)
 
4615
 
 
4616
      self.frmDashModeSub = QFrame()
 
4617
      self.frmDashModeSub.setFrameStyle(STYLE_SUNKEN)
 
4618
      self.frmDashModeSub.setLayout(layoutDashMode)
 
4619
      self.frmDashMode = makeHorizFrame(['Stretch', \
 
4620
                                         self.frmDashModeSub, \
 
4621
                                         'Stretch'])
 
4622
 
 
4623
 
 
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)
 
4633
 
 
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')
 
4645
 
 
4646
 
 
4647
      #####
 
4648
      def openBitcoinOrg():
 
4649
         webbrowser.open('http://www.bitcoin.org/en/download')
 
4650
 
 
4651
 
 
4652
      #####
 
4653
      def openInstruct():
 
4654
         if OS_WINDOWS:
 
4655
            webbrowser.open('https://www.bitcoinarmory.com/install-windows/')
 
4656
         elif OS_LINUX:
 
4657
            webbrowser.open('https://www.bitcoinarmory.com/install-linux/')
 
4658
         elif OS_MACOSX:
 
4659
            webbrowser.open('https://www.bitcoinarmory.com/install-macosx/')
 
4660
 
 
4661
 
 
4662
 
 
4663
 
 
4664
 
 
4665
 
 
4666
      self.connect(self.dashBtns[DASHBTNS.Close][BTN], SIGNAL('clicked()'), \
 
4667
                                                   self.closeExistingBitcoin)
 
4668
      self.connect(self.dashBtns[DASHBTNS.Install][BTN], SIGNAL('clicked()'), \
 
4669
                                                     self.openDLSatoshi)
 
4670
      self.connect(self.dashBtns[DASHBTNS.Browse][BTN], SIGNAL('clicked()'), \
 
4671
                                                             openBitcoinOrg)
 
4672
      self.connect(self.dashBtns[DASHBTNS.Settings][BTN], SIGNAL('clicked()'), \
 
4673
                                                           self.openSettings)
 
4674
      #self.connect(self.dashBtns[DASHBTNS.Instruct][BTN], SIGNAL('clicked()'), \
 
4675
                                                     #self.openInstructWindow)
 
4676
 
 
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')
 
4685
 
 
4686
 
 
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')
 
4700
 
 
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
 
4706
 
 
4707
      #if OS_LINUX:
 
4708
      if OS_WINDOWS:
 
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.')
 
4717
 
 
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>')
 
4724
      elif OS_LINUX:
 
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"""))
 
4733
      elif OS_MACOSX:
 
4734
         pass
 
4735
      else:
 
4736
         LOGERROR('Unrecognized OS!')
 
4737
 
 
4738
 
 
4739
      self.frmDashMgmtButtons = QFrame()
 
4740
      self.frmDashMgmtButtons.setFrameStyle(STYLE_SUNKEN)
 
4741
      layoutButtons = QGridLayout()
 
4742
      layoutButtons.addWidget(self.lblDashBtnDescr, 0,0, 1,3)
 
4743
      for r in range(5):
 
4744
         for c in range(3):
 
4745
            if c==LBL:
 
4746
               wMin = tightSizeNChar(self, 50)[0]
 
4747
               self.dashBtns[r][c].setMinimumWidth(wMin)
 
4748
            layoutButtons.addWidget(self.dashBtns[r][c],  r+1,c)
 
4749
 
 
4750
      self.frmDashMgmtButtons.setLayout(layoutButtons)
 
4751
      self.frmDashMidButtons  = makeHorizFrame(['Stretch', \
 
4752
                                              self.frmDashMgmtButtons,
 
4753
                                              'Stretch'])
 
4754
 
 
4755
      dashLayout = QVBoxLayout()
 
4756
      dashLayout.addWidget(self.frmDashMode)
 
4757
      dashLayout.addWidget(self.lblDashDescr1)
 
4758
      dashLayout.addWidget(self.frmDashMidButtons )
 
4759
      dashLayout.addWidget(self.lblDashDescr2)
 
4760
      frmInner = QFrame()
 
4761
      frmInner.setLayout(dashLayout)
 
4762
 
 
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)
 
4769
 
 
4770
 
 
4771
 
 
4772
   #############################################################################
 
4773
   def setupAnnounceTab(self):
 
4774
 
 
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)
 
4778
 
 
4779
      def checkUpd():
 
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)
 
4789
         else:
 
4790
            QMessageBox.warning(self, tr('Update'), tr("""
 
4791
               Announcements are now up to date!"""), QMessageBox.Ok)
 
4792
 
 
4793
 
 
4794
      self.lblLastUpdated = QRichLabel('', doWrap=False)
 
4795
      self.btnCheckForUpdates  = QPushButton(tr('Check for Updates'))
 
4796
      self.connect(self.btnCheckForUpdates, SIGNAL(CLICKED), checkUpd)
 
4797
 
 
4798
 
 
4799
      frmLastUpdate = makeHorizFrame(['Stretch', \
 
4800
                                      self.lblLastUpdated, \
 
4801
                                      self.btnCheckForUpdates, \
 
4802
                                      'Stretch'])
 
4803
 
 
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)
 
4809
 
 
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)
 
4816
 
 
4817
 
 
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)
 
4831
 
 
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)
 
4838
 
 
4839
 
 
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
 
4844
      class DlgGen():
 
4845
         def setParams(self, parent, nid, notifyMap):
 
4846
            self.parent = parent
 
4847
            self.nid = nid
 
4848
            self.notifyMap = notifyMap
 
4849
 
 
4850
         def __call__(self):
 
4851
            return DlgNotificationWithDNAA(self.parent, self.parent, \
 
4852
                                          self.nid, self.notifyMap, False).exec_()
 
4853
 
 
4854
      self.announceTableWidgets = \
 
4855
         [[QLabel(''), QRichLabel(''), QLabelButton('+'), DlgGen()] \
 
4856
                                                      for i in range(10)]
 
4857
 
 
4858
 
 
4859
 
 
4860
      layoutTable = QGridLayout()
 
4861
      for i in range(10):
 
4862
         for j in range(3):
 
4863
            layoutTable.addWidget(self.announceTableWidgets[i][j], i,j)
 
4864
         self.connect(self.announceTableWidgets[i][2], SIGNAL(CLICKED), \
 
4865
                      self.announceTableWidgets[i][3])
 
4866
 
 
4867
      layoutTable.setColumnStretch(0,0)
 
4868
      layoutTable.setColumnStretch(1,1)
 
4869
      layoutTable.setColumnStretch(2,0)
 
4870
 
 
4871
      frmTable = QFrame()
 
4872
      frmTable.setLayout(layoutTable)
 
4873
      frmTable.setFrameStyle(STYLE_SUNKEN)
 
4874
 
 
4875
      self.updateAnnounceTable()
 
4876
 
 
4877
 
 
4878
      frmEverything = makeVertFrame( [ self.lblAlertStr,
 
4879
                                       frmLastUpdate,
 
4880
                                       'Space(30)',
 
4881
                                       lblTableHeader,
 
4882
                                       frmTable,
 
4883
                                       'Space(30)',
 
4884
                                       lblVerHeader,
 
4885
                                       frmVersions,
 
4886
                                       'Stretch'])
 
4887
 
 
4888
      frmEverything.setMinimumWidth(300)
 
4889
      frmEverything.setMaximumWidth(800)
 
4890
 
 
4891
      frmFinal = makeHorizFrame(['Stretch', frmEverything, 'Stretch'])
 
4892
 
 
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)
 
4899
 
 
4900
      self.announceIsSetup = True
 
4901
 
 
4902
 
 
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_()
 
4908
 
 
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_()
 
4914
 
 
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_()
 
4920
 
 
4921
 
 
4922
   #############################################################################
 
4923
   def getDownloaderData(self):
 
4924
      dl = self.announceFetcher.getAnnounceFile('downloads')
 
4925
      cl = self.announceFetcher.getAnnounceFile('changelog')
 
4926
 
 
4927
      dlObj = downloadLinkParser().parseDownloadList(dl)
 
4928
      clObj = changelogParser().parseChangelogText(cl)
 
4929
 
 
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)
 
4937
         return None,None
 
4938
 
 
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)
 
4948
 
 
4949
      dl = self.announceFetcher.getAnnounceFile('downloads')
 
4950
      cl = self.announceFetcher.getAnnounceFile('changelog')
 
4951
 
 
4952
      return dl,cl
 
4953
 
 
4954
 
 
4955
 
 
4956
   #############################################################################
 
4957
   def updateAnnounceTab(self, *args):
 
4958
 
 
4959
      if not self.announceIsSetup:
 
4960
         return
 
4961
 
 
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'
 
4968
 
 
4969
      lastUpdate = self.announceFetcher.getLastSuccessfulFetchTime()
 
4970
      noAnnounce = (lastUpdate == 0)
 
4971
 
 
4972
      if noAnnounce:
 
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))
 
4978
      else:
 
4979
         updTimeStr = unixTimeToFormatStr(lastUpdate)
 
4980
         self.lblLastUpdated.setText(tr("<u>Last Updated</u>: %s") % updTimeStr)
 
4981
 
 
4982
 
 
4983
      verStrToInt = lambda s: getVersionInt(readVersionString(s))
 
4984
 
 
4985
      # Notify of Armory updates
 
4986
      self.icoArmorySWVersion.setPixmap(QPixmap(iconArmory).scaled(24,24))
 
4987
      self.icoSatoshiSWVersion.setPixmap(QPixmap(iconSatoshi).scaled(24,24))
 
4988
 
 
4989
      try:
 
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"""))
 
4998
         else:
 
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)
 
5006
      except:
 
5007
         self.btnSecureDLArmory.setVisible(False)
 
5008
         self.lblArmorySWVersion.setText(tr(""" You are running Armory
 
5009
            version %s""") % getVersionString(BTCARMORY_VERSION))
 
5010
 
 
5011
 
 
5012
      try:
 
5013
         satCurrStr,satLastStr = self.satoshiVersions
 
5014
         satCurrent = verStrToInt(satCurrStr) if satCurrStr else 0
 
5015
         satLatest  = verStrToInt(satLastStr) if satLastStr else 0
 
5016
 
 
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)
 
5025
            else:
 
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
 
5031
                  available!</b>"""))
 
5032
         elif satCurrent:
 
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)
 
5039
         elif satLatest:
 
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)
 
5046
         else:
 
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""") )
 
5053
 
 
5054
 
 
5055
 
 
5056
 
 
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])
 
5061
         #else:
 
5062
            #self.lblSatoshiSWVersion.setText(tr("""No information is
 
5063
            #available for the core Bitcoin software"""))
 
5064
      except:
 
5065
         LOGEXCEPT('Failed to process satoshi versions')
 
5066
 
 
5067
 
 
5068
      self.updateAnnounceTable()
 
5069
 
 
5070
 
 
5071
   #############################################################################
 
5072
   def updateAnnounceTable(self):
 
5073
 
 
5074
      # Default: Make everything non-visible except first row, middle column
 
5075
      for i in range(10):
 
5076
         for j in range(3):
 
5077
            self.announceTableWidgets[i][j].setVisible(i==0 and j==1)
 
5078
 
 
5079
      if len(self.almostFullNotificationList)==0:
 
5080
         self.announceTableWidgets[0][1].setText(tr("""
 
5081
            There are no announcements or alerts to display"""))
 
5082
         return
 
5083
 
 
5084
 
 
5085
      alertsForSorting = []
 
5086
      for nid,nmap in self.almostFullNotificationList.iteritems():
 
5087
         alertsForSorting.append([nid, int(nmap['PRIORITY'])])
 
5088
 
 
5089
      sortedAlerts = sorted(alertsForSorting, key=lambda a: -a[1])[:10]
 
5090
 
 
5091
      i = 0
 
5092
      for nid,priority in sortedAlerts:
 
5093
         if priority>=4096:
 
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')
 
5099
         else:
 
5100
            pixm = QPixmap(':/MsgBox_info48.png')
 
5101
 
 
5102
 
 
5103
         shortDescr = self.almostFullNotificationList[nid]['SHORTDESCR']
 
5104
         if priority>=4096:
 
5105
            shortDescr = '<font color="%s">' + shortDescr + '</font>'
 
5106
            shortDescr = shortDescr % htmlColor('TextWarn')
 
5107
 
 
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])
 
5113
 
 
5114
         for j in range(3):
 
5115
            self.announceTableWidgets[i][j].setVisible(True)
 
5116
 
 
5117
         i += 1
 
5118
 
 
5119
   #############################################################################
 
5120
   def explicitCheckAnnouncements(self, waitTime=3):
 
5121
      self.announceFetcher.fetchRightNow(waitTime)
 
5122
      self.processAnnounceData()
 
5123
      self.updateAnnounceTab()
 
5124
 
 
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)
 
5131
            time.sleep(2)
 
5132
            return
 
5133
 
 
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)
 
5138
 
 
5139
   #############################################################################
 
5140
   def getPercentageFinished(self, maxblk, lastblk):
 
5141
      curr = EstimateCumulativeBlockchainSize(lastblk)
 
5142
      maxb = EstimateCumulativeBlockchainSize(maxblk)
 
5143
      return float(curr)/float(maxb)
 
5144
 
 
5145
   #############################################################################
 
5146
   def updateSyncProgress(self):
 
5147
 
 
5148
      if TheTDM.getTDMState()=='Downloading':
 
5149
 
 
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')
 
5155
 
 
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%')
 
5161
 
 
5162
         self.lblDashModeSync.setVisible(True)
 
5163
         self.barProgressSync.setVisible(True)
 
5164
         self.barProgressSync.setValue(0)
 
5165
         self.lblTimeLeftSync.setVisible(True)
 
5166
         self.barProgressSync.setFormat('')
 
5167
 
 
5168
         self.lblDashModeBuild.setVisible(True)
 
5169
         self.barProgressBuild.setVisible(True)
 
5170
         self.barProgressBuild.setValue(0)
 
5171
         self.lblTimeLeftBuild.setVisible(True)
 
5172
         self.barProgressBuild.setFormat('')
 
5173
 
 
5174
         self.lblDashModeScan.setVisible(True)
 
5175
         self.barProgressScan.setVisible(True)
 
5176
         self.barProgressScan.setValue(0)
 
5177
         self.lblTimeLeftScan.setVisible(True)
 
5178
         self.barProgressScan.setFormat('')
 
5179
 
 
5180
         if not numSeeds:
 
5181
            self.barProgressTorrent.setValue(0)
 
5182
            self.lblTimeLeftTorrent.setText('')
 
5183
            self.lblTorrentStats.setText('')
 
5184
 
 
5185
            self.lblDashModeTorrent.setText(tr('Initializing Torrent Engine'), \
 
5186
                                          size=4, bold=True, color='Foreground')
 
5187
 
 
5188
            self.lblTorrentStats.setVisible(False)
 
5189
         else:
 
5190
            self.lblDashModeTorrent.setText(tr('Downloading via Armory CDN'), \
 
5191
                                          size=4, bold=True, color='Foreground')
 
5192
 
 
5193
            if fracDone:
 
5194
               self.barProgressTorrent.setValue(int(99.9*fracDone))
 
5195
 
 
5196
            if timeEst:
 
5197
               self.lblTimeLeftTorrent.setText(secondsToHumanTime(timeEst))
 
5198
 
 
5199
            self.lblTorrentStats.setText(tr("""
 
5200
               Bootstrap Torrent:  %s/sec from %d peers""") % \
 
5201
               (bytesToHumanSize(dlSpeed), numSeeds+numPeers))
 
5202
 
 
5203
            self.lblTorrentStats.setVisible(True)
 
5204
 
 
5205
 
 
5206
 
 
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('')
 
5214
 
 
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('')
 
5220
 
 
5221
         self.lblDashModeBuild.setVisible(True)
 
5222
         self.barProgressBuild.setVisible(True)
 
5223
         self.lblTimeLeftBuild.setVisible(True)
 
5224
 
 
5225
         self.lblDashModeScan.setVisible(True)
 
5226
         self.barProgressScan.setVisible(True)
 
5227
         self.lblTimeLeftScan.setVisible(True)
 
5228
 
 
5229
         # Scan time is super-simple to predict: it's pretty much linear
 
5230
         # with the number of bytes remaining.
 
5231
 
 
5232
         phase,pct,rate,tleft = TheBDM.predictLoadTime()
 
5233
         if phase==1:
 
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('')
 
5240
 
 
5241
         elif phase==3:
 
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%')
 
5250
         elif phase==4:
 
5251
            self.lblDashModeScan.setText( 'Global Blockchain Index', \
 
5252
                                        size=4, bold=True, color='Foreground')
 
5253
 
 
5254
         tleft15 = (int(tleft-1)/15 + 1)*15
 
5255
         if tleft < 2:
 
5256
            tstring = ''
 
5257
            pvalue  = 100
 
5258
         else:
 
5259
            tstring = secondsToHumanTime(tleft15)
 
5260
            pvalue = pct*100
 
5261
 
 
5262
         if phase==1:
 
5263
            self.lblTimeLeftBuild.setText(tstring)
 
5264
            self.barProgressBuild.setValue(pvalue)
 
5265
         elif phase==3:
 
5266
            self.lblTimeLeftScan.setText(tstring)
 
5267
            self.barProgressScan.setValue(pvalue)
 
5268
 
 
5269
      elif TheSDM.getSDMState() in ['BitcoindInitializing','BitcoindSynchronizing']:
 
5270
 
 
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('')
 
5277
 
 
5278
         self.lblDashModeSync.setVisible(True)
 
5279
         self.barProgressSync.setVisible(True)
 
5280
         self.lblTimeLeftSync.setVisible(True)
 
5281
         self.barProgressSync.setFormat('%p%')
 
5282
 
 
5283
         self.lblDashModeBuild.setVisible(True)
 
5284
         self.barProgressBuild.setVisible(True)
 
5285
         self.lblTimeLeftBuild.setVisible(False)
 
5286
         self.barProgressBuild.setValue(0)
 
5287
         self.barProgressBuild.setFormat('')
 
5288
 
 
5289
         self.lblDashModeScan.setVisible(True)
 
5290
         self.barProgressScan.setVisible(True)
 
5291
         self.lblTimeLeftScan.setVisible(False)
 
5292
         self.barProgressScan.setValue(0)
 
5293
         self.barProgressScan.setFormat('')
 
5294
 
 
5295
         ssdm = TheSDM.getSDMState()
 
5296
         lastBlkNum  = self.getSettingOrSetDefault('LastBlkRecv',     0)
 
5297
         lastBlkTime = self.getSettingOrSetDefault('LastBlkRecvTime', 0)
 
5298
 
 
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']
 
5304
 
 
5305
         # Use a reference point if we are starting from scratch
 
5306
         refBlock = max(290746,      lastBlkNum)
 
5307
         refTime  = max(1394922889,  lastBlkTime)
 
5308
 
 
5309
 
 
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, \
 
5315
                                                                  lastBlkNum)
 
5316
 
 
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
 
5323
            if dt>600:
 
5324
               self.initSyncCircBuff = self.initSyncCircBuff[1:]
 
5325
 
 
5326
            if dp>0 and dt>0:
 
5327
               dpPerSec = dp / dt
 
5328
               if lastBlkNum < 200000:
 
5329
                  dpPerSec = dpPerSec / 2
 
5330
               timeRemain = (1 - self.approxPctSoFar) / dpPerSec
 
5331
               #timeRemain = min(timeRemain, 8*HOUR)
 
5332
            else:
 
5333
               timeRemain = None
 
5334
         else:
 
5335
            timeRemain = None
 
5336
 
 
5337
 
 
5338
         intPct = int(100*self.approxPctSoFar)
 
5339
         strPct = '%d%%' % intPct
 
5340
 
 
5341
 
 
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)
 
5352
               else:
 
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))
 
5357
            else:
 
5358
               # If we're more than 10k blocks behind...
 
5359
               if timeRemain:
 
5360
                  timeRemain = min(24*HOUR, timeRemain)
 
5361
                  self.lblTimeLeftSync.setText(secondsToHumanTime(timeRemain))
 
5362
               else:
 
5363
                  self.lblTimeLeftSync.setText('')
 
5364
         elif ssdm == 'BitcoindInitializing':
 
5365
            sdmPercent = 0
 
5366
            self.barProgressSync.setFormat('')
 
5367
            self.barProgressBuild.setFormat('')
 
5368
            self.barProgressScan.setFormat('')
 
5369
         else:
 
5370
            LOGERROR('Should not predict sync info in non init/sync SDM state')
 
5371
            return ('UNKNOWN','UNKNOWN', 'UNKNOWN')
 
5372
 
 
5373
         self.barProgressSync.setValue(sdmPercent)
 
5374
      else:
 
5375
         LOGWARN('Called updateSyncProgress while not sync\'ing')
 
5376
 
 
5377
 
 
5378
   #############################################################################
 
5379
   def GetDashFunctionalityText(self, func):
 
5380
      """
 
5381
      Outsourcing all the verbose dashboard text to here, to de-clutter the
 
5382
      logic paths in the setDashboardDetails function
 
5383
      """
 
5384
      LOGINFO('Switching Armory functional mode to "%s"', func)
 
5385
      if func.lower() == 'scanning':
 
5386
         return ( \
 
5387
         'The following functionality is available while scanning in offline mode:'
 
5388
         '<ul>'
 
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>'
 
5395
         '</ul>'
 
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':
 
5402
         return ( \
 
5403
         'The following functionality is available in offline mode:'
 
5404
         '<ul>'
 
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>'
 
5412
         '</ul>'
 
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':
 
5419
         return ( \
 
5420
         '<ul>'
 
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>'
 
5433
         '</ul>')
 
5434
 
 
5435
 
 
5436
   #############################################################################
 
5437
   def GetDashStateText(self, mgmtMode, state):
 
5438
      """
 
5439
      Outsourcing all the verbose dashboard text to here, to de-clutter the
 
5440
      logic paths in the setDashboardDetails function
 
5441
      """
 
5442
      LOGINFO('Switching Armory state text to Mgmt:%s, State:%s', mgmtMode, state)
 
5443
 
 
5444
      # A few states don't care which mgmtMode you are in...
 
5445
      if state == 'NewUserInfo':
 
5446
         return tr("""
 
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.
 
5454
         <br><br>
 
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
 
5462
         Backups page</a>.
 
5463
         <br><br>
 
5464
         To learn about improving your security through the use of offline
 
5465
         wallets, visit the
 
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':
 
5471
         return ( \
 
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:'
 
5476
         '<br>')
 
5477
      elif state == 'OnlineFull2':
 
5478
         return ( \
 
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 '
 
5494
         'keys.</p>'))
 
5495
      elif state == 'OnlineNeedSweep':
 
5496
         return ( \
 
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 '
 
5500
         'swept. '
 
5501
         '<br><br>'
 
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':
 
5506
         return ( \
 
5507
         '<b>Wallet balances may '
 
5508
         'be incorrect until the rescan operation is performed!</b>'
 
5509
         '<br><br>'
 
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. '
 
5514
         '<br><br>'
 
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':
 
5518
         return ( \
 
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! '
 
5523
         '<br><br>'
 
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.'
 
5530
         '<br><br>'
 
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>.')
 
5534
 
 
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':
 
5541
            return ( \
 
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.'
 
5548
            '<br><br>'
 
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')
 
5555
            return ( \
 
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.'
 
5565
            '<br><br>'
 
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."'
 
5569
            '<br><br>'
 
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':
 
5581
            return ( \
 
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.'
 
5587
            '<br><br>'
 
5588
            'If this is intended to be an offline computer, note '
 
5589
            'that it is not necessary to have Bitcoin-Qt or bitcoind '
 
5590
            'running.' )
 
5591
         elif state == 'OfflineNoBlkFiles':
 
5592
            return ( \
 
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':
 
5602
            return ( \
 
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':
 
5612
            return ( \
 
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':
 
5617
            return ( \
 
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>')
 
5622
         else:
 
5623
            LOGERROR('Unrecognized dashboard state: Mgmt:%s, State:%s', \
 
5624
                                                          mgmtMode, state)
 
5625
            return ''
 
5626
      elif mgmtMode.lower()=='auto':
 
5627
         if state == 'OfflineBitcoindRunning':
 
5628
            return ( \
 
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':
 
5637
            return ( \
 
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 '
 
5643
            'restart Armory.')
 
5644
         if state == 'InitializingLongTime':
 
5645
            return tr("""
 
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
 
5651
            minutes.
 
5652
            <br><br>
 
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.
 
5656
            <br><br>
 
5657
            While you wait, you can manage your wallets.  Make new wallets,
 
5658
            make digital or paper backups, create Bitcoin addresses to receive
 
5659
            payments,
 
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':
 
5665
            return ( \
 
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.  '
 
5669
            '<br><br>'
 
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':
 
5678
            return ( \
 
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 '
 
5683
            'disappears.'
 
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':
 
5688
            return ( \
 
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.  '
 
5695
            '<br><br>'
 
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':
 
5699
            return ( \
 
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':
 
5709
            return ( \
 
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':
 
5716
            return ( \
 
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.'
 
5721
            '<br><br>'
 
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>'
 
5727
            '<br><br>'
 
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:
 
5740
               return  (tr("""
 
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 '') )
 
5748
            else:
 
5749
               return ( tr("""
 
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.
 
5754
                  <br><br>
 
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
 
5760
                  inconvenience!"""))
 
5761
 
 
5762
 
 
5763
   #############################################################################
 
5764
   @TimeThisFunction
 
5765
   def setDashboardDetails(self, INIT=False):
 
5766
      """
 
5767
      We've dumped all the dashboard text into the above 2 methods in order
 
5768
      to declutter this method.
 
5769
      """
 
5770
      onlineAvail = self.onlineModeIsPossible()
 
5771
 
 
5772
      sdmState = TheSDM.getSDMState()
 
5773
      bdmState = TheBDM.getBDMState()
 
5774
      tdmState = TheTDM.getTDMState()
 
5775
      descr  = ''
 
5776
      descr1 = ''
 
5777
      descr2 = ''
 
5778
 
 
5779
      # Methods for showing/hiding groups of widgets on the dashboard
 
5780
      def setBtnRowVisible(r, visBool):
 
5781
         for c in range(3):
 
5782
            self.dashBtns[r][c].setVisible(visBool)
 
5783
 
 
5784
      def setSyncRowVisible(b):
 
5785
         self.lblDashModeSync.setVisible(b)
 
5786
         self.barProgressSync.setVisible(b)
 
5787
         self.lblTimeLeftSync.setVisible(b)
 
5788
 
 
5789
 
 
5790
      def setTorrentRowVisible(b):
 
5791
         self.lblDashModeTorrent.setVisible(b)
 
5792
         self.barProgressTorrent.setVisible(b)
 
5793
         self.lblTimeLeftTorrent.setVisible(b)
 
5794
         self.lblTorrentStats.setVisible(b)
 
5795
 
 
5796
      def setBuildRowVisible(b):
 
5797
         self.lblDashModeBuild.setVisible(b)
 
5798
         self.barProgressBuild.setVisible(b)
 
5799
         self.lblTimeLeftBuild.setVisible(b)
 
5800
 
 
5801
      def setScanRowVisible(b):
 
5802
         self.lblDashModeScan.setVisible(b)
 
5803
         self.barProgressScan.setVisible(b)
 
5804
         self.lblTimeLeftScan.setVisible(b)
 
5805
 
 
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)
 
5814
 
 
5815
      def setBtnFrameVisible(b, descr=''):
 
5816
         self.frmDashMidButtons.setVisible(b)
 
5817
         self.lblDashBtnDescr.setVisible(len(descr)>0)
 
5818
         self.lblDashBtnDescr.setText(descr)
 
5819
 
 
5820
 
 
5821
      if INIT:
 
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)
 
5830
 
 
5831
      # This keeps popping up for some reason!
 
5832
      self.lblTorrentStats.setVisible(False)
 
5833
 
 
5834
      if self.doAutoBitcoind and not sdmState=='BitcoindReady':
 
5835
         # User is letting Armory manage the Satoshi client for them.
 
5836
 
 
5837
         if not sdmState==self.lastSDMState:
 
5838
 
 
5839
            self.lblBusy.setVisible(False)
 
5840
            self.btnModeSwitch.setVisible(False)
 
5841
 
 
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)
 
5850
 
 
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)
 
5866
                  else:
 
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)
 
5872
               else:
 
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 '
 
5877
                     'your settings.')
 
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', \
 
5896
                                                         size=4, bold=True)
 
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', \
 
5912
                                                         size=4, bold=True)
 
5913
                  else:
 
5914
                     self.lblDashModeSync.setText('Cannot find Bitcoin Home Directory', \
 
5915
                                                         size=4, bold=True)
 
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()
 
5969
                  descr1 += ''
 
5970
                  descr2 += self.GetDashFunctionalityText('Offline')
 
5971
                  self.lblDashDescr1.setText(descr1)
 
5972
                  self.lblDashDescr2.setText(descr2)
 
5973
               else:
 
5974
                  setBtnFrameVisible(False)
 
5975
                  descr1 += ''
 
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']:
 
5981
 
 
5982
                  extraTxt = ''
 
5983
                  if not self.wasSynchronizing:
 
5984
                     setOnlyDashModeVisible()
 
5985
                  else:
 
5986
                     extraTxt = tr("""
 
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('')
 
5994
 
 
5995
 
 
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()
 
6012
 
 
6013
 
 
6014
                  # If torrent ever ran, leave it visible
 
6015
                  setSyncRowVisible(True)
 
6016
                  setScanRowVisible(True)
 
6017
                  setTorrentRowVisible(TheTDM.isStarted())
 
6018
 
 
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)
 
6031
                  else:
 
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)
 
6037
 
 
6038
 
 
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')
 
6043
 
 
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')
 
6048
                  else:
 
6049
                     descr1 += self.GetDashStateText('Auto', 'InitializingDoneSoon')
 
6050
                     descr2 += self.GetDashStateText('Auto', 'NewUserInfo')
 
6051
 
 
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. ')
 
6057
 
 
6058
                  descr2 += self.GetDashFunctionalityText('Offline')
 
6059
                  self.lblDashDescr1.setText(descr1)
 
6060
                  self.lblDashDescr2.setText(descr2)
 
6061
      else:
 
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)
 
6085
 
 
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.')
 
6094
                  else:
 
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')
 
6100
 
 
6101
               descr += '<br><br>'
 
6102
               descr += self.GetDashFunctionalityText('Offline')
 
6103
               self.lblDashDescr1.setText(descr)
 
6104
 
 
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'))
 
6124
               else:
 
6125
                  self.lblDashDescr1.setText( self.GetDashStateText('User', 'OnlineDirty'))
 
6126
            else:
 
6127
               # Fully online mode
 
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)
 
6145
 
 
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')
 
6153
            else:
 
6154
               self.barProgressSync.setVisible(False)
 
6155
               self.lblTimeLeftSync.setVisible(False)
 
6156
               self.lblDashModeSync.setVisible(False)
 
6157
 
 
6158
            if len(str(self.lblDashModeBuild.text()).strip()) == 0:
 
6159
               self.lblDashModeBuild.setText( 'Preparing Databases', \
 
6160
                                          size=4, bold=True, color='Foreground')
 
6161
 
 
6162
            if len(str(self.lblDashModeScan.text()).strip()) == 0:
 
6163
               self.lblDashModeScan.setText( 'Scan Transaction History', \
 
6164
                                          size=4, bold=True, color='DisableFG')
 
6165
 
 
6166
            self.mainDisplayTabs.setTabEnabled(self.MAINTABS.Ledger, False)
 
6167
 
 
6168
            if len(self.walletMap)==0:
 
6169
               descr = self.GetDashStateText('User','ScanNoWallets')
 
6170
            else:
 
6171
               descr = self.GetDashStateText('User','ScanWithWallets')
 
6172
 
 
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)
 
6178
         else:
 
6179
            LOGERROR('What the heck blockchain mode are we in?  %s', bdmState)
 
6180
 
 
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()
 
6188
 
 
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
 
6191
      if not OS_MACOSX:
 
6192
         vbar.setValue(vbar.minimum())
 
6193
 
 
6194
   #############################################################################
 
6195
   def createToolTipWidget(self, tiptext, iconSz=2):
 
6196
      """
 
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
 
6200
      """
 
6201
      fgColor = htmlColor('ToolTipQ')
 
6202
      lbl = QLabel('<font size=%d color=%s>(?)</font>' % (iconSz, fgColor))
 
6203
      lbl.setMaximumWidth(relaxedSizeStr(lbl, '(?)')[0])
 
6204
 
 
6205
      def setAllText(wself, txt):
 
6206
         def pressEv(ev):
 
6207
            QWhatsThis.showText(ev.globalPos(), txt, self)
 
6208
         wself.mousePressEvent = pressEv
 
6209
         wself.setToolTip('<u></u>' + txt)
 
6210
         
 
6211
      # Calling setText on this widget will update both the tooltip and QWT
 
6212
      from types import MethodType
 
6213
      lbl.setText = MethodType(setAllText, lbl)
 
6214
 
 
6215
      lbl.setText(tiptext)
 
6216
      return lbl
 
6217
 
 
6218
   #############################################################################
 
6219
   def createAddressEntryWidgets(self, parent, initString='', maxDetectLen=128,
 
6220
                                           boldDetectParts=0, **cabbKWArgs):
 
6221
      """
 
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.
 
6225
 
 
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,
 
6232
                                  showLockboxes=True)
 
6233
 
 
6234
      Returns three widgets that can be put into layouts:
 
6235
         [[QLineEdit: addr/pubkey]]  [[Button: Addrbook]]
 
6236
         [[Label: Wallet/Lockbox/Addr autodetect]]
 
6237
      """
 
6238
 
 
6239
      addrEntryObjs = {}
 
6240
      addrEntryObjs['QLE_ADDR'] = QLineEdit()
 
6241
      addrEntryObjs['QLE_ADDR'].setText(initString)
 
6242
      addrEntryObjs['BTN_BOOK']  = createAddrBookButton(parent, 
 
6243
                                                        addrEntryObjs['QLE_ADDR'], 
 
6244
                                                        **cabbKWArgs)
 
6245
      addrEntryObjs['LBL_DETECT'] = QRichLabel('')
 
6246
      addrEntryObjs['CALLBACK_GETSCRIPT'] = None
 
6247
 
 
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
 
6252
      # 
 
6253
      # It's a little awkward to put this whole thing in here... this could
 
6254
      # probably use some refactoring
 
6255
      def updateAddrDetectLabels():
 
6256
         try:
 
6257
            enteredText = str(addrEntryObjs['QLE_ADDR'].text()).strip()
 
6258
 
 
6259
            scriptInfo = self.getScriptForUserString(enteredText)
 
6260
            displayInfo = self.getDisplayStringForScript(
 
6261
                           scriptInfo['Script'], maxDetectLen, boldDetectParts,
 
6262
                           prefIDOverAddr=scriptInfo['ShowID'])
 
6263
 
 
6264
            dispStr = displayInfo['String']
 
6265
            if displayInfo['WltID'] is None and displayInfo['LboxID'] is None:
 
6266
               addrEntryObjs['LBL_DETECT'].setText(dispStr)
 
6267
            else:
 
6268
               addrEntryObjs['LBL_DETECT'].setText(dispStr, color='TextBlue')
 
6269
 
 
6270
            # No point in repeating what the user just entered
 
6271
            addrEntryObjs['LBL_DETECT'].setVisible(enteredText != dispStr)
 
6272
            addrEntryObjs['QLE_ADDR'].setCursorPosition(0)
 
6273
 
 
6274
         except:
 
6275
            #LOGEXCEPT('Invalid recipient string')
 
6276
            addrEntryObjs['LBL_DETECT'].setVisible(False)
 
6277
            addrEntryObjs['LBL_DETECT'].setVisible(False)
 
6278
      # End function to be connected
 
6279
      ##########################################################################
 
6280
            
 
6281
      # Now actually connect the entry widgets
 
6282
      parent.connect(addrEntryObjs['QLE_ADDR'], SIGNAL('textChanged(QString)'), 
 
6283
                                                         updateAddrDetectLabels)
 
6284
 
 
6285
      updateAddrDetectLabels()
 
6286
 
 
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)
 
6292
      def getScript():
 
6293
         entered = str(addrEntryObjs['QLE_ADDR'].text()).strip()
 
6294
         return self.getScriptForUserString(entered)
 
6295
 
 
6296
      addrEntryObjs['CALLBACK_GETSCRIPT'] = getScript
 
6297
      return addrEntryObjs
 
6298
 
 
6299
 
 
6300
 
 
6301
   #############################################################################
 
6302
   def getScriptForUserString(self, userStr):
 
6303
      return getScriptForUserString(userStr, self.walletMap, self.allLockboxes)
 
6304
 
 
6305
 
 
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) 
 
6313
 
 
6314
 
 
6315
   #############################################################################
 
6316
   @TimeThisFunction
 
6317
   def checkNewZeroConf(self):
 
6318
      '''
 
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.
 
6322
      '''
 
6323
      while len(self.newZeroConfSinceLastUpdate)>0:
 
6324
         rawTx = self.newZeroConfSinceLastUpdate.pop()
 
6325
 
 
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.' \
 
6334
                        % wltID)
 
6335
               notifyIn = self.getSettingOrSetDefault('NotifyBtcIn', \
 
6336
                                                      not OS_MACOSX)
 
6337
               notifyOut = self.getSettingOrSetDefault('NotifyBtcOut', \
 
6338
                                                       not OS_MACOSX)
 
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()
 
6345
 
 
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
 
6349
         # table models.
 
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()
 
6359
 
 
6360
 
 
6361
   #############################################################################
 
6362
   #############################################################################
 
6363
   def Heartbeat(self, nextBeatSec=1):
 
6364
      """
 
6365
      This method is invoked when the app is initialized, and will
 
6366
      run every second, or whatever is specified in the nextBeatSec
 
6367
      argument.
 
6368
      """
 
6369
 
 
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:
 
6381
         try:
 
6382
            nextBeat = fn()
 
6383
            if nextBeat>0:
 
6384
               reactor.callLater(nextBeat, self.Heartbeat)
 
6385
            else:
 
6386
               self.extraHeartbeatSpecial = []
 
6387
               reactor.callLater(1, self.Heartbeat)
 
6388
         except:
 
6389
            LOGEXCEPT('Error in special heartbeat function')
 
6390
            self.extraHeartbeatSpecial = []
 
6391
            reactor.callLater(1, self.Heartbeat)
 
6392
         return
 
6393
 
 
6394
 
 
6395
      # TorrentDownloadManager
 
6396
      # SatoshiDaemonManager
 
6397
      # BlockDataManager
 
6398
      tdmState = TheTDM.getTDMState()
 
6399
      sdmState = TheSDM.getSDMState()
 
6400
      bdmState = TheBDM.getBDMState()
 
6401
      #print '(SDM, BDM) State = (%s, %s)' % (sdmState, bdmState)
 
6402
 
 
6403
      self.processAnnounceData()
 
6404
 
 
6405
      try:
 
6406
         for func in self.extraHeartbeatAlways:
 
6407
            if isinstance(func, list):
 
6408
               fnc = func[0]
 
6409
               kargs = func[1]
 
6410
               keep_running = func[2]
 
6411
               if keep_running == False:
 
6412
                  self.extraHeartbeatAlways.remove(func)
 
6413
               fnc(*kargs)
 
6414
            else:
 
6415
               func()
 
6416
 
 
6417
         for idx,wltID in enumerate(self.walletIDList):
 
6418
            self.walletMap[wltID].checkWalletLockTimeout()
 
6419
 
 
6420
 
 
6421
 
 
6422
 
 
6423
         if self.doAutoBitcoind:
 
6424
            if TheTDM.isRunning():
 
6425
               if tdmState=='Downloading':
 
6426
                  self.updateSyncProgress()
 
6427
 
 
6428
               downRate  = TheTDM.getLastStats('downRate')
 
6429
               self.torrentCircBuffer.append(downRate if downRate else 0)
 
6430
 
 
6431
               # Assumes 1 sec heartbeat
 
6432
               bufsz = len(self.torrentCircBuffer)
 
6433
               if bufsz > 5*MINUTE:
 
6434
                  self.torrentCircBuffer = self.torrentCircBuffer[1:]
 
6435
 
 
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.  
 
6446
                           <br><br>
 
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)
 
6453
 
 
6454
                        # For now, just show once then disable
 
6455
                        self.lastAskedUserStopTorrent = UINT64_MAX
 
6456
 
 
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()
 
6467
 
 
6468
            if not sdmState==self.lastSDMState or \
 
6469
               not bdmState==self.lastBDMState[0]:
 
6470
               self.setDashboardDetails()
 
6471
         else:
 
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()
 
6477
               return
 
6478
            elif bdmState=='Scanning':
 
6479
               self.updateSyncProgress()
 
6480
 
 
6481
 
 
6482
         if self.netMode==NETWORKMODE.Disconnected:
 
6483
            if self.onlineModeIsPossible():
 
6484
               self.switchNetworkMode(NETWORKMODE.Full)
 
6485
 
 
6486
         if not TheBDM.isDirty() == self.dirtyLastTime:
 
6487
            self.setDashboardDetails()
 
6488
         self.dirtyLastTime = TheBDM.isDirty()
 
6489
 
 
6490
 
 
6491
         if bdmState=='BlockchainReady':
 
6492
 
 
6493
            #####
 
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()
 
6500
 
 
6501
            #####
 
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()
 
6510
 
 
6511
            #####
 
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()
 
6522
 
 
6523
   
 
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()])
 
6527
 
 
6528
 
 
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
 
6533
 
 
6534
            if not newBlocks:
 
6535
               newBlocks = 0
 
6536
 
 
6537
 
 
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)
 
6544
 
 
6545
               for lbID,cppWlt in self.cppLockboxWltMap.iteritems():
 
6546
                  TheBDM.rescanWalletZeroConf(cppWlt, wait=True)
 
6547
                  
 
6548
 
 
6549
            self.checkNewZeroConf()
 
6550
 
 
6551
            # Trigger any notifications, if we have them...
 
6552
            self.doTheSystemTrayThing()
 
6553
 
 
6554
            if newBlocks>0 and not TheBDM.isDirty():
 
6555
 
 
6556
               # This says "after scan", but works when new blocks appear, too
 
6557
               TheBDM.updateWalletsAfterScan(wait=True)
 
6558
 
 
6559
               self.ledgerModel.reset()
 
6560
 
 
6561
               LOGINFO('New Block! : %d', self.currBlockNum)
 
6562
               didAffectUs = False
 
6563
 
 
6564
               # LITE sync means it won't rescan if addresses have been imported
 
6565
               didAffectUs = newBlockSyncRescanZC(TheBDM, self.walletMap, \
 
6566
                                                  prevLedgSize)
 
6567
 
 
6568
               if didAffectUs:
 
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)
 
6575
 
 
6576
               self.createCombinedLedger()
 
6577
               self.blkReceived  = RightNow()
 
6578
               self.writeSetting('LastBlkRecvTime', self.blkReceived)
 
6579
               self.writeSetting('LastBlkRecv',     self.currBlockNum)
 
6580
 
 
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))
 
6586
 
 
6587
 
 
6588
               # Update the wallet view to immediately reflect new balances
 
6589
               self.walletModel.reset()
 
6590
 
 
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:
 
6597
                     blockFunc(pyBlock)
 
6598
 
 
6599
 
 
6600
            blkRecvAgo  = RightNow() - self.blkReceived
 
6601
            #blkStampAgo = RightNow() - TheBDM.getTopBlockHeader().getTimestamp()
 
6602
            self.lblArmoryStatus.setToolTip('Last block received is %s ago' % \
 
6603
                                                secondsToHumanTime(blkRecvAgo))
 
6604
 
 
6605
 
 
6606
            for func in self.extraHeartbeatOnline:
 
6607
               func()
 
6608
 
 
6609
      except:
 
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
 
6612
         # has more info.
 
6613
         LOGEXCEPT('Error in heartbeat function')
 
6614
         (errType, errVal) = sys.exc_info()[:2]
 
6615
         errStr = 'Error Type: %s\nError Value: %s' % (errType, errVal)
 
6616
         LOGERROR(errStr)
 
6617
      finally:
 
6618
         reactor.callLater(nextBeatSec, self.Heartbeat)
 
6619
 
 
6620
 
 
6621
   #############################################################################
 
6622
   def printAlert(self, moneyID, ledgerAmt, txAmt):
 
6623
      '''
 
6624
      Function that prints a notification for a transaction that affects an
 
6625
      address we control.
 
6626
      '''
 
6627
      dispLines = []
 
6628
      title = ''
 
6629
      totalStr = coin2strNZS(txAmt)
 
6630
 
 
6631
 
 
6632
      if moneyID in self.walletMap:
 
6633
         wlt = self.walletMap[moneyID]
 
6634
         if len(wlt.labelName) <= 20:
 
6635
            dispName = '"%s"' % wlt.labelName
 
6636
         else:
 
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)
 
6643
         else:
 
6644
            dispName = '%d-of-%d "%s..."' % (lbox.M, lbox.N, lbox.shortName[:17])
 
6645
         dispName = 'Lockbox %s (%s)' % (dispName, lbox.uniqueIDB58)
 
6646
      else:
 
6647
         LOGERROR('Asked to show notification for wlt/lbox we do not have')
 
6648
         return
 
6649
 
 
6650
      # Collected everything we need to display, now construct it and do it.
 
6651
      if ledgerAmt > 0:
 
6652
         # Received!
 
6653
         title = 'Bitcoins Received!'
 
6654
         dispLines.append('Amount:  %s BTC' % totalStr)
 
6655
         dispLines.append('Recipient:  %s' % dispName)
 
6656
      elif ledgerAmt < 0:
 
6657
         # Sent!
 
6658
         title = 'Bitcoins Sent!'
 
6659
         dispLines.append('Amount:  %s BTC' % totalStr)
 
6660
         dispLines.append('Sender:  %s' % dispName)
 
6661
 
 
6662
      self.sysTray.showMessage(title, \
 
6663
                               '\n'.join(dispLines),  \
 
6664
                               QSystemTrayIcon.Information, \
 
6665
                               10000)
 
6666
      LOGINFO(title)
 
6667
 
 
6668
 
 
6669
   #############################################################################
 
6670
   @TimeThisFunction
 
6671
   def doTheSystemTrayThing(self):
 
6672
      """
 
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.
 
6677
      """
 
6678
      if not TheBDM.getBDMState()=='BlockchainReady' or \
 
6679
         RightNow()<self.notifyBlockedUntil:
 
6680
         return
 
6681
 
 
6682
      # Notify queue input is: [WltID/LBID, LedgerEntry, alreadyNotified]
 
6683
      for i in range(len(self.notifyQueue)):
 
6684
         moneyID, le, alreadyNotified = self.notifyQueue[i]
 
6685
 
 
6686
         # Skip the ones we've notified of already.
 
6687
         if alreadyNotified:
 
6688
            continue
 
6689
 
 
6690
         # Marke it alreadyNotified=True
 
6691
         self.notifyQueue[i][2] = True
 
6692
 
 
6693
         # Catch condition that somehow the tx isn't related to us
 
6694
         if le.getTxHash()=='\x00'*32:
 
6695
            continue
 
6696
 
 
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)
 
6701
 
 
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)
 
6705
            continue
 
6706
 
 
6707
         
 
6708
         if pywlt:
 
6709
            cppWlt  = self.walletMap[moneyID].cppWallet
 
6710
            wname = self.walletMap[moneyID].labelName
 
6711
            if len(wname)>20:
 
6712
               wname = wname[:17] + '...'
 
6713
            wltName = 'Wallet "%s" (%s)' % (wname, moneyID)
 
6714
         else:
 
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
 
6720
            if len(lname) > 20:
 
6721
               lname = lname[:17] + '...'
 
6722
            wltName = 'Lockbox %d-of-%d "%s" (%s)' % (M, N, lname, moneyID)
 
6723
 
 
6724
 
 
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)
 
6737
            return
 
6738
 
 
6739
 
 
6740
         # If coins were either received or sent from the loaded wlt/lbox         
 
6741
         dispLines = []
 
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()
 
6751
            recipStr = ''
 
6752
            for i in range(nOut):
 
6753
               script = txref.getTxOutCopy(i).getScript()
 
6754
               if cppWlt.hasScrAddress(script_to_scrAddr(script)):
 
6755
                  continue
 
6756
               if len(recipStr)==0:
 
6757
                  recipStr = self.getDisplayStringForScript(script, 45)['String']
 
6758
               else:
 
6759
                  recipStr = '<Multiple Recipients>'
 
6760
            
 
6761
            title = 'Bitcoins Sent!'
 
6762
            dispLines.append('Amount:  %s BTC' % totalStr)
 
6763
            dispLines.append('From:    %s' % wltName)
 
6764
            dispLines.append('To:      %s' % recipStr)
 
6765
   
 
6766
         self.sysTray.showMessage(title, '\n'.join(dispLines), 
 
6767
                                 QSystemTrayIcon.Information, 10000)
 
6768
         LOGINFO(title + '\n' + '\n'.join(dispLines))
 
6769
 
 
6770
         # Wait for 5 seconds before processing the next queue object.
 
6771
         self.notifyBlockedUntil = RightNow() + 5
 
6772
         return
 
6773
 
 
6774
 
 
6775
   #############################################################################
 
6776
   def closeEvent(self, event=None):
 
6777
      moc = self.getSettingOrSetDefault('MinimizeOrClose', 'DontKnow')
 
6778
      doClose, doMinimize = False, False
 
6779
      if moc=='DontKnow':
 
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')
 
6784
         if reply==True:
 
6785
            doMinimize = True
 
6786
            if remember:
 
6787
               self.writeSetting('MinimizeOrClose', 'Minimize')
 
6788
         else:
 
6789
            doClose = True;
 
6790
            if remember:
 
6791
               self.writeSetting('MinimizeOrClose', 'Close')
 
6792
 
 
6793
      if doMinimize or moc=='Minimize':
 
6794
         self.minimizeArmory()
 
6795
         if event:
 
6796
            event.ignore()
 
6797
      elif doClose or moc=='Close':
 
6798
         self.doShutdown = True
 
6799
         self.sysTray.hide()
 
6800
         self.closeForReal(event)
 
6801
      else:
 
6802
         return  # how would we get here?
 
6803
 
 
6804
 
 
6805
 
 
6806
   #############################################################################
 
6807
   def unpackLinuxTarGz(self, targzFile, changeSettings=True):
 
6808
      if targzFile is None:
 
6809
         return None
 
6810
 
 
6811
      if not os.path.exists(targzFile):
 
6812
         return None
 
6813
 
 
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)
 
6820
 
 
6821
      os.mkdir(unpackDir)
 
6822
 
 
6823
      out,err = execAndWait('tar -zxf %s -C %s' % (targzFile, unpackDir), \
 
6824
                                                                  timeout=5)
 
6825
 
 
6826
      LOGINFO('UNPACK STDOUT: "' + out + '"')
 
6827
      LOGINFO('UNPACK STDERR: "' + err + '"')
 
6828
 
 
6829
      
 
6830
      # There should only be one subdir
 
6831
      unpackDirChild = None
 
6832
      for fn in os.listdir(unpackDir):
 
6833
         unpackDirChild = os.path.join(unpackDir, fn)
 
6834
 
 
6835
      if unpackDirChild is None:
 
6836
         LOGERROR('There was apparently an error unpacking the file')
 
6837
         return None
 
6838
 
 
6839
      finalDir = os.path.abspath(unpackDirChild)
 
6840
      LOGWARN('Bitcoin Core unpacked into: %s', finalDir)
 
6841
 
 
6842
      if changeSettings:
 
6843
         self.settings.set('SatoshiExe', finalDir)
 
6844
 
 
6845
      return finalDir
 
6846
      
 
6847
 
 
6848
 
 
6849
   #############################################################################
 
6850
   def closeForReal(self, event=None):
 
6851
      '''
 
6852
      Unlike File->Quit or clicking the X on the window, which may actually
 
6853
      minimize Armory, this method is for *really* closing Armory
 
6854
      '''
 
6855
      try:
 
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))
 
6860
 
 
6861
         if TheBDM.getBDMState()=='Scanning':
 
6862
            LOGINFO('BDM state is scanning -- force shutdown BDM')
 
6863
            TheBDM.execCleanShutdown(wait=False)
 
6864
         else:
 
6865
            LOGINFO('BDM is safe for clean shutdown')
 
6866
            TheBDM.execCleanShutdown(wait=True)
 
6867
 
 
6868
         # This will do nothing if bitcoind isn't running.
 
6869
         TheSDM.stopBitcoind()
 
6870
      except:
 
6871
         # Don't want a strange error here interrupt shutdown
 
6872
         LOGEXCEPT('Strange error during shutdown')
 
6873
 
 
6874
 
 
6875
      # Any extra shutdown activities, perhaps added by modules
 
6876
      for fn in self.extraShutdownFunctions:
 
6877
         try:
 
6878
            fn()
 
6879
         except:
 
6880
            LOGEXCEPT('Shutdown function failed.  Skipping.')
 
6881
 
 
6882
 
 
6883
      from twisted.internet import reactor
 
6884
      LOGINFO('Attempting to close the main window!')
 
6885
      reactor.stop()
 
6886
      if event:
 
6887
         event.accept()
 
6888
 
 
6889
 
 
6890
 
 
6891
   #############################################################################
 
6892
   def execTrigger(self, toSpawn):
 
6893
      super(ArmoryDialog, toSpawn).exec_()
 
6894
 
 
6895
 
 
6896
   #############################################################################
 
6897
   def initTrigger(self, toInit):
 
6898
      if isinstance(toInit, DlgProgress):
 
6899
         toInit.setup(self)
 
6900
         toInit.status = 1
 
6901
 
 
6902
 
 
6903
   #############################################################################
 
6904
   def checkForNegImports(self):
 
6905
      
 
6906
      negativeImports = []
 
6907
      
 
6908
      for wlt in self.walletMap:
 
6909
         if self.walletMap[wlt].hasNegativeImports:
 
6910
            negativeImports.append(self.walletMap[wlt].uniqueIDB58)
 
6911
            
 
6912
      # If we detect any negative import
 
6913
      if len(negativeImports) > 0:
 
6914
         logDirs = []
 
6915
         for wltID in negativeImports:
 
6916
            if not wltID in self.walletMap:
 
6917
               continue
 
6918
 
 
6919
            homedir = os.path.dirname(self.walletMap[wltID].walletPath)
 
6920
            wltlogdir  = os.path.join(homedir, wltID)
 
6921
            if not os.path.exists(wltlogdir):
 
6922
               continue
 
6923
   
 
6924
            for subdirname in os.listdir(wltlogdir):
 
6925
               subdirpath = os.path.join(wltlogdir, subdirname)
 
6926
               logDirs.append([wltID, subdirpath])
 
6927
 
 
6928
         
 
6929
         DlgInconsistentWltReport(self, self, logDirs).exec_()
 
6930
 
 
6931
 
 
6932
   #############################################################################
 
6933
   def getAllRecoveryLogDirs(self, wltIDList):
 
6934
      self.logDirs = []
 
6935
      for wltID in wltIDList:
 
6936
         if not wltID in self.walletMap:
 
6937
            continue
 
6938
 
 
6939
         homedir = os.path.dirname(self.walletMap[wltID].walletPath)
 
6940
         logdir  = os.path.join(homedir, wltID)
 
6941
         if not os.path.exists(logdir):
 
6942
            continue
 
6943
 
 
6944
         self.logDirs.append([wltID, logdir])
 
6945
 
 
6946
      return self.logDirs 
 
6947
 
 
6948
      
 
6949
   #############################################################################
 
6950
   @AllowAsync
 
6951
   def CheckWalletConsistency(self, wallets, prgAt=None):
 
6952
 
 
6953
      if prgAt:
 
6954
         totalSize = 0
 
6955
         walletSize = {}
 
6956
         for wlt in wallets:
 
6957
            statinfo = os.stat(wallets[wlt].walletPath)
 
6958
            walletSize[wlt] = statinfo.st_size
 
6959
            totalSize = totalSize + statinfo.st_size
 
6960
 
 
6961
      i=0
 
6962
      dlgrdy = [0]
 
6963
      nerrors = 0
 
6964
 
 
6965
      for wlt in wallets:
 
6966
         if prgAt:
 
6967
            prgAt[0] = i
 
6968
            f = 10000*walletSize[wlt]/totalSize
 
6969
            prgAt[1] = f
 
6970
            i = f +i
 
6971
 
 
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]:
 
6976
               time.sleep(0.01)
 
6977
            nerrors = nerrors +1
 
6978
 
 
6979
      prgAt[2] = 1
 
6980
 
 
6981
      dlgrdy[0] = 0
 
6982
      while prgAt[2] != 2:
 
6983
         time.sleep(0.1)
 
6984
      if nerrors == 0:
 
6985
         self.emit(SIGNAL('UWCS'), [1, 'All wallets are consistent', 10000, dlgrdy])
 
6986
         self.emit(SIGNAL('checkForNegImports'))
 
6987
      else:
 
6988
         while not dlgrdy:
 
6989
            self.emit(SIGNAL('UWCS'), [1, 'Consistency Check Failed!', 0, dlgrdy])
 
6990
            time.sleep(1)
 
6991
 
 
6992
         self.checkRdyForFix()
 
6993
 
 
6994
 
 
6995
   def checkRdyForFix(self):
 
6996
      #check BDM first
 
6997
      time.sleep(1)
 
6998
      self.dlgCptWlt.emit(SIGNAL('Show'))
 
6999
      while 1:
 
7000
         if TheBDM.getBDMState() == 'Scanning':
 
7001
            canFix = tr("""
 
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])
 
7006
            time.sleep(1)
 
7007
         elif TheBDM.getBDMState() == 'Offline' or \
 
7008
              TheBDM.getBDMState() == 'Uninitialized':
 
7009
            TheSDM.setDisabled(True)
 
7010
            CLI_OPTIONS.offline = True
 
7011
            break
 
7012
         else:
 
7013
            break
 
7014
 
 
7015
      #check running dialogs
 
7016
      self.dlgCptWlt.emit(SIGNAL('Show'))
 
7017
      runningList = []
 
7018
      while 1:
 
7019
         listchanged = 0
 
7020
         canFix = []
 
7021
         for dlg in runningList:
 
7022
            if dlg not in runningDialogsList:
 
7023
               runningList.remove(dlg)
 
7024
               listchanged = 1
 
7025
 
 
7026
         for dlg in runningDialogsList:
 
7027
            if not isinstance(dlg, DlgCorruptWallet):
 
7028
               if dlg not in runningList:
 
7029
                  runningList.append(dlg)
 
7030
                  listchanged = 1
 
7031
 
 
7032
         if len(runningList):
 
7033
            if listchanged:
 
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)
 
7039
            time.sleep(0.2)
 
7040
         else:
 
7041
            break
 
7042
 
 
7043
 
 
7044
      canFix.append('Ready to analyze inconsistent wallets!')
 
7045
      self.dlgCptWlt.UpdateCanFix(canFix, True)
 
7046
      self.dlgCptWlt.exec_()
 
7047
 
 
7048
   def checkWallets(self):
 
7049
      nwallets = len(self.walletMap)
 
7050
 
 
7051
      if nwallets > 0:
 
7052
         self.prgAt = [0, 0, 0]
 
7053
 
 
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)
 
7061
 
 
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)
 
7067
 
 
7068
   @AllowAsync
 
7069
   def UpdateConsistencyCheckMessage(self):
 
7070
      while self.prgAt[2] == 0:
 
7071
         self.emit(SIGNAL('UWCS'), [0, self.prgAt[0]])
 
7072
         time.sleep(0.5)
 
7073
 
 
7074
      self.emit(SIGNAL('UWCS'), [2])
 
7075
      self.prgAt[2] = 2
 
7076
 
 
7077
   def UpdateWalletConsistencyStatus(self, msg):
 
7078
      if msg[0] == 0:
 
7079
         self.pbarWalletProgress.setValue(msg[1])
 
7080
      elif msg[0] == 1:
 
7081
         self.statusBar().showMessage(msg[1], msg[2])
 
7082
         msg[3][0] = 1
 
7083
      else:
 
7084
         self.pbarWalletProgress.hide()
 
7085
 
 
7086
   def WltCstError(self, wlt, status, dlgrdy):
 
7087
      self.emit(SIGNAL('PWCE'), dlgrdy, wlt, status)
 
7088
      LOGERROR('Wallet consistency check failed! (%s)', wlt.uniqueIDB58)
 
7089
 
 
7090
   def PromptWltCstError(self, dlgrdy, wallet=None, status='', mode=None):
 
7091
      if not self.dlgCptWlt:
 
7092
         self.dlgCptWlt = DlgCorruptWallet(wallet, status, self, self)
 
7093
         dlgrdy[0] = 1
 
7094
      else:
 
7095
         self.dlgCptWlt.addStatus(wallet, status)
 
7096
 
 
7097
      if not mode:
 
7098
         self.dlgCptWlt.show()
 
7099
      else:
 
7100
         self.dlgCptWlt.exec_()
 
7101
 
 
7102
 
 
7103
############################################
 
7104
class ArmoryInstanceListener(Protocol):
 
7105
   def connectionMade(self):
 
7106
      LOGINFO('Another Armory instance just tried to open.')
 
7107
      self.factory.func_conn_made()
 
7108
 
 
7109
   def dataReceived(self, data):
 
7110
      LOGINFO('Received data from alternate Armory instance')
 
7111
      self.factory.func_recv_data(data)
 
7112
      self.transport.loseConnection()
 
7113
 
 
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
 
7120
 
 
7121
 
 
7122
 
 
7123
############################################
 
7124
def checkForAlreadyOpen():
 
7125
   import socket
 
7126
   LOGDEBUG('Checking for already open socket...')
 
7127
   try:
 
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
 
7130
 
 
7131
      if OS_WINDOWS:
 
7132
         # Windows can be tricky, sometimes holds sockets even after closing
 
7133
         checkForAlreadyOpenError()
 
7134
 
 
7135
      LOGERROR('Socket already in use.  Sending CLI args to existing proc.')
 
7136
      if CLI_ARGS:
 
7137
         sock.send(CLI_ARGS[0])
 
7138
      sock.close()
 
7139
      LOGERROR('Exiting...')
 
7140
      os._exit(0)
 
7141
   except:
 
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
 
7144
      pass
 
7145
 
 
7146
 
 
7147
 
 
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.
 
7153
   armoryExists = []
 
7154
   bitcoindExists = []
 
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)
 
7165
 
 
7166
   if len(armoryExists)>0:
 
7167
      LOGINFO('Not an error!  Armory really is open')
 
7168
      return
 
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:
 
7174
         killProcess(pid)
 
7175
      time.sleep(0.5)
 
7176
      raise
 
7177
 
 
7178
 
 
7179
############################################
 
7180
if 1:
 
7181
 
 
7182
   import qt4reactor
 
7183
   qt4reactor.install()
 
7184
 
 
7185
   if CLI_OPTIONS.interport > 1:
 
7186
      checkForAlreadyOpen()
 
7187
 
 
7188
   pixLogo = QPixmap(':/splashlogo.png')
 
7189
   if USE_TESTNET:
 
7190
      pixLogo = QPixmap(':/splashlogo_testnet.png')
 
7191
   SPLASH = QSplashScreen(pixLogo)
 
7192
   SPLASH.setMask(pixLogo.mask())
 
7193
   SPLASH.show()
 
7194
   QAPP.processEvents()
 
7195
 
 
7196
   # Will make this customizable
 
7197
   QAPP.setFont(GETFONT('var'))
 
7198
 
 
7199
   form = ArmoryMainWindow()
 
7200
   form.show()
 
7201
 
 
7202
   SPLASH.finish(form)
 
7203
 
 
7204
   from twisted.internet import reactor
 
7205
   def endProgram():
 
7206
      LOGINFO('Resetting BlockDataMgr, freeing memory')
 
7207
      TheBDM.Reset()
 
7208
      TheBDM.execCleanShutdown(wait=False)
 
7209
      if reactor.threadpool is not None:
 
7210
         reactor.threadpool.stop()
 
7211
      QAPP.quit()
 
7212
      os._exit(0)
 
7213
 
 
7214
   QAPP.connect(form, SIGNAL("lastWindowClosed()"), endProgram)
 
7215
   reactor.addSystemEventTrigger('before', 'shutdown', endProgram)
 
7216
   QAPP.setQuitOnLastWindowClosed(True)
 
7217
   reactor.runReturn()
 
7218
   os._exit(QAPP.exec_())