From ca826c4b576c968d76103ab345ec46dd78dc856e Mon Sep 17 00:00:00 2001 From: etotheipi Date: Fri, 9 Dec 2011 22:05:35 -0500 Subject: [PATCH] Changed program name, switch to testnet, GUI sucks right now --- BitcoinArmoryQt.py => ArmoryQt.py | 182 +++++++++++++++--- LICENSE | 2 +- README | 6 +- ..._PyBtcEngine.README => Using_Armory.README | 6 +- btcarmoryengine.py => armoryengine.py | 97 +++++++--- armorymodels.py | 175 ++++++++++++++++- cppForSwig/BlockObjRef.h | 3 + cppForSwig/BlockUtils.cpp | 83 ++++++-- cppForSwig/BlockUtils.h | 3 + cppForSwig/BlockUtilsTest.cpp | 24 +++ cppForSwig/BtcUtils.h | 15 ++ cppForSwig/Makefile | 3 +- unittest.py | 32 +-- 13 files changed, 508 insertions(+), 123 deletions(-) rename BitcoinArmoryQt.py => ArmoryQt.py (60%) rename Using_PyBtcEngine.README => Using_Armory.README (99%) rename btcarmoryengine.py => armoryengine.py (99%) diff --git a/BitcoinArmoryQt.py b/ArmoryQt.py similarity index 60% rename from BitcoinArmoryQt.py rename to ArmoryQt.py index adc4e5acd..7f2a8c8a0 100644 --- a/BitcoinArmoryQt.py +++ b/ArmoryQt.py @@ -1,4 +1,3 @@ -#! /usr/bin/python ################################################################################ # # Copyright (C) 2011, Alan C. Reiner @@ -7,7 +6,7 @@ # ################################################################################ # -# Project: BitcoinArmory (https://github.com/etotheipi/BitcoinArmory) +# Project: Armory (https://github.com/etotheipi/BitcoinArmory) # Author: Alan Reiner # Orig Date: 20 November, 2011 # Descr: This file serves as an engine for python-based Bitcoin software. @@ -35,21 +34,13 @@ from PyQt4.QtGui import * # 8000 lines of python to help us out... -from btcarmoryengine import * +from armoryengine import * from armorymodels import * # All the twisted/networking functionality from twisted.internet.protocol import Protocol, ClientFactory from twisted.internet.defer import Deferred -# This is an amazing trick for create enum-like dictionaries. -# Either automatically numbers (*args), or name-val pairs (**kwargs) -#http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python -def enum(*sequential, **named): - enums = dict(zip(sequential, range(len(sequential))), **named) - return type('Enum', (), enums) - -TXTBL = enum("Status", "Date", "Direction", "Address", "Amount") SETTINGS_PATH = os.path.join(ARMORY_HOME_DIR, 'ArmorySettings.txt') @@ -71,6 +62,7 @@ def __init__(self, parent=None, settingsPath=None): self.loadWalletsAndSettings() self.setupNetworking() + self.loadBlockchain() self.lblAvailWlt = QLabel('Available Wallets:') self.lblAvailWlt.setAlignment(Qt.AlignBottom) @@ -82,6 +74,7 @@ def __init__(self, parent=None, settingsPath=None): self.setWindowTitle('Armory - Bitcoin Wallet Management') self.setWindowIcon(QIcon('icons/armory_logo_32x32.png')) + # Table for all the wallets self.walletModel = WalletDispModel(self) self.walletView = QTableView() self.walletView.setModel(self.walletModel) @@ -96,17 +89,29 @@ def __init__(self, parent=None, settingsPath=None): self.walletView.hideColumn(0) self.walletView.horizontalHeader().resizeSection(1, 200) + # Table to display ledger/activity + self.ledgerModel = ActivityDispModel(self) + self.ledgerView = QTableView() + self.ledgerView.setModel(self.ledgerModel) + self.ledgerView.setSelectionBehavior(QTableView.SelectRows) + self.ledgerView.setSelectionMode(QTableView.SingleSelection) + self.ledgerView.horizontalHeader().setStretchLastSection(True) + self.ledgerView.verticalHeader().setDefaultSectionSize(25) + self.ledgerView.horizontalHeader().resizeSection(1, 150) + layout = QGridLayout() - layout.addWidget(QLabel("Available Wallets:"), 0, 0, 1, 1) - layout.addWidget(self.walletView, 2, 0, 1, 2) - layout.addWidget(self.lblLogoIcon, 0, 1, 1, 1) + layout.addWidget(self.lblLogoIcon, 0, 2, 1, 1) + layout.addWidget(QLabel("Available Wallets:"), 1, 0, 1, 1) + layout.addWidget(self.walletView, 2, 0, 3, 2) + layout.addWidget(QLabel("Transactions:"), 5, 0, 1, 1) + layout.addWidget(self.ledgerView, 6, 0, 3, 3) # Attach the layout to the frame that will become the central widget mainFrame = QFrame() mainFrame.setLayout(layout) self.setCentralWidget(mainFrame) - self.setMinimumSize(500,300) + self.setMinimumSize(700,700) self.statusBar().showMessage('Blockchain loading, please wait...') @@ -122,11 +127,13 @@ def restartConnection(protoObj, failReason): print '! Trying to restart connection !' reactor.connectTCP(protoObj.peer[0], protoObj.peer[1], self.NetworkingFactory) - self.NetworkingFactory = BitcoinArmoryClientFactory( \ + self.NetworkingFactory = ArmoryClientFactory( \ func_loseConnect=restartConnection) #reactor.connectTCP('127.0.0.1', BITCOIN_PORT, self.NetworkingFactory) + + ############################################################################# def loadWalletsAndSettings(self): self.settings = SettingsFile(self.settingsPath) @@ -146,29 +153,39 @@ def loadWalletsAndSettings(self): else: self.usermode = UserMode.Standard - # Load wallets found in the .bitcoinarmory directory + # Load wallets found in the .armory directory wltPaths = self.settings.get('Other_Wallets', expectList=True) self.walletMap = {} - self.walletIDSet = set() - self.walletIDList = [] # Also need an easily, deterministically-iterable list - self.walletBalances = [] # Also need an easily, deterministically-iterable list self.walletIndices = {} + self.walletIDSet = set() + # I need some linear lists for accessing by index + self.walletIDList = [] + self.walletBalances = [] + self.walletSubLedgers = [] + self.walletLedgers = [] + self.combinedLedger = [] + self.ledgerSize = 0 + + self.latestBlockNum = 0 + + # Use this store IDs of wallets that are watching-only, + self.walletOfflines = set() print 'Loading wallets...' - for root,subs,files in os.walk(ARMORY_HOME_DIR): - for f in files: - if f.startswith('armory_') and f.endswith('.wallet') and \ - not f.endswith('backup.wallet') and not ('unsuccessful' in f): - wltPaths.append(os.path.join(root, f)) + for f in os.listdir(ARMORY_HOME_DIR): + if f.startswith('armory_') and f.endswith('.wallet') and \ + not f.endswith('backup.wallet') and not ('unsuccessful' in f): + wltPaths.append(os.path.join(root, f)) wltExclude = self.settings.get('Excluded_Wallets', expectList=True) - for index,fpath in enumerate(wltPaths): + wltOffline = self.settings.get('Offline_WalletIDs', expectList=True) + for fpath in wltPaths: try: wltLoad = PyBtcWallet().readWalletFile(fpath) wltID = wltLoad.wltUniqueIDB58 - if wltID in wltExclude: + if fpath in wltExclude: continue if wltID in self.walletIDSet: @@ -176,11 +193,17 @@ def loadWalletsAndSettings(self): print ' '*10, 'Wallet 1 (loaded): ', self.walletMap[wltID].walletPath print ' '*10, 'Wallet 2 (skipped):', fpath else: + # Update the maps/dictionaries self.walletMap[wltID] = wltLoad + self.walletIndices[wltID] = len(self.walletMap)-1 + + # Maintain some linear lists of wallet info self.walletIDSet.add(wltID) self.walletIDList.append(wltID) self.walletBalances.append(-1) - self.walletIndices[wltID] = index + + if wltID in wltOffline or fpath in wltOffline: + self.walletOfflines.add(wltID) except: print '***WARNING: Wallet could not be loaded:', fpath print ' skipping... ' @@ -207,7 +230,10 @@ def getWalletForAddr160(self, addr160): ############################################################################# def loadBlockchain(self): print 'Loading blockchain' + BDM_LoadBlockchainFile() + + # Now that theb blockchain is loaded, let's populate the wallet info if TheBDM.isInitialized(): self.statusBar().showMessage('Syncing wallets with blockchain...') print 'Syncing wallets with blockchain...' @@ -215,15 +241,101 @@ def loadBlockchain(self): print 'Syncing', wltID self.walletMap[wltID].setBlockchainSyncFlag(BLOCKCHAIN_READONLY) self.walletMap[wltID].syncWithBlockchain() - index = self.walletIndices[wltID] - self.walletBalances[index] = self.walletMap[wltID].getBalance() + + # We need to mirror all blockchain & wallet data in linear lists + wltIndex = self.walletIndices[wltID] + + self.walletBalances[wltIndex] = wlt.getBalance() + self.walletSubLedgers.append([]) + for addrIndex,addr in enumerate(wlt.getAddrList()): + addr20 = addr.getAddr160() + self.walletSubLedgers[-1].append(wlt.getTxLedger(addr20)) + + t = wlt.getTxLedger() + print wltID, len(t) + self.walletLedgers.append(wlt.getTxLedger()) + + self.createCombinedLedger() + self.ledgerSize = len(self.combinedLedger) + self.latestBlockNum = TheBDM.getTopBlockHeader().getBlockHeight() + print len(self.combinedLedger), self.latestBlockNum self.statusBar().showMessage('Blockchain loaded, wallets sync\'d!', 10000) else: self.statusBar().showMessage('! Blockchain loading failed !', 10000) # This will force the table to refresh with new data - self.walletView.selectRow(-1) + #for row in range(len(self.walletMap)): + #self.walletView.selectRow(row) + #self.walletView.selectRow(0) + def createZeroConfLedger(self, wlt): + """ + This is kind of hacky, but I don't want to disrupt the C++ code + too much to implement a *proper* solution... which is that I need + to find a way to process zero-confirmation transactions and produce + ledger entries for them, the same as all the other [past] txs. + + So, I added TxRef::getLedgerEntriesForZeroConfTxList to the C++ code + (name was created to be annoying so maybe I remove/replace later). + Then we carefully create TxRef objects to pass into it and copy out + the resulting list. But since these are TxREF objects, they need + to point to persistent memory, which is why the following loops are + weird: they are guaranteed to create data once, and not move it + around in memory, so that my TxRef objects don't get mangled. We + only need them long enough to get the vector result. + + (to be more specific, I'm pretty sure this should work no matter + how wacky python's memory mgmt is, unless it moves list data around + in memory between calls) + """ + # We are starting with a map of PyTx objects + zcMap = self.NetworkingFactory.zeroConfTx + timeMap = self.NetworkingFactory.zeroConfTxTime + #print 'ZeroConfListSize:', len(zcMap) + zcTxBinList = [] + zcTxRefList = [] + zcTxRefPtrList = vector_TxRefPtr(0) + zcTxTimeList = [] + # Create persistent list of serialized Tx objects (language-agnostic) + for zchash in zcMap.keys(): + zcTxBinList.append( zcMap[zchash].serialize() ) + zcTxTimeList.append(timeMap[zchash]) + # Create list of TxRef objects + for zc in zcTxBinList: + zcTxRefList.append( TxRef(zc) ) + # Python will cast to pointers when we try to add to a vector + for zc in zcTxRefList: + zcTxRefPtrList.push_back(zc) + + # At this point, we will get a vector list and TxRefs + # can safely go out of scope + return wlt.cppWallet.getLedgerEntriesForZeroConfTxList(zcTxRefPtrList) + + + def createCombinedLedger(self, wltIDList=None, withZeroConf=True): + """ + Create a ledger to display on the main screen, that consists of ledger + entries of any SUBSET of available wallets. + """ + start = RightNow() + if wltIDList==None: + wltIDList = self.walletIDList + + self.combinedLedger = [] + #for wltID,wlt in self.walletMap.iteritems(): + for wltID in wltIDList: + wlt = self.walletMap[wltID] + index = self.walletIndices[wltID] + # Make sure the ledgers are up to date and then combine and sort + #self.walletLedgers[index] = self.walletMap[wltID].getTxLedger() + id_le_pairs = [ [wltID, le] for le in self.walletLedgers[index] ] + id_le_zcpairs = [ [wltID, le] for le in self.createZeroConfLedger(wlt)] + self.combinedLedger.extend(id_le_pairs) + self.combinedLedger.extend(id_le_zcpairs) + + self.combinedLedger.sort(key=lambda x:x[1], reverse=True) + print 'Combined ledger:', (RightNow()-start), 'sec', len(self.combinedLedger) + def Heartbeat(self, nextBeatSec=3): """ @@ -236,8 +348,15 @@ def Heartbeat(self, nextBeatSec=3): newBlks = TheBDM.readBlkFileUpdate() if newBlks>0: pass # do something eventually + else: + self.latestBlockNum = TheBDM.getTopBlockHeader().getBlockHeight() + + # Check for new tx in the zeroConf pool + self.createCombinedLedger() + + """ self.txNotInBlkchainYet = [] if TheBDM.isInitialized(): for hsh,tx in self.NetworkingFactory.zeroConfTx.iteritems(): @@ -252,6 +371,7 @@ def Heartbeat(self, nextBeatSec=3): for tx in self.txNotInBlkchainYet: print ' ',binary_to_hex(tx) + """ for wltID, wlt in self.walletMap.iteritems(): diff --git a/LICENSE b/LICENSE index 20cb6920b..049c65e35 100755 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ ******************************************************************************** - PyBtcEngine -- C++/Python/SWIG backend for developing Bitcoin software + Armory -- Bitcoin Client Software Copyright (C) 2011, Alan C. Reiner diff --git a/README b/README index 4481705de..7a1cc2fe7 100755 --- a/README +++ b/README @@ -8,7 +8,7 @@ ******************************************************************************** * -* Project: BitcoinArmory +* Project: Armory * Author: Alan Reiner * Orig Date: 20 November, 2011 * Descr: This file serves as an engine for python-based Bitcoin software. @@ -201,7 +201,7 @@ -- TxDPs: Based on BIP 0010: https://github.com/genjix/bips/blob/master/bip-0010.md - All transaction operations in BitcoinArmory go through + All transaction operations in Armory go through "Tx Distribution Proposals", which is an ASCII-able serialization for unsigned transactions, which can be easily transmitted through email, and used to collect @@ -259,7 +259,7 @@ it ... unless you have double-broadcast detection! A master list of TxOuts waiting to enter the blockchain - is maintained in the BitcoinArmoryClientFactory, and a + is maintained in the ArmoryClientFactory, and a flag is raised if a tx is received which tries to spend any outputs already in the queue. The client will usually just drop the tx because it conflicts with another one in diff --git a/Using_PyBtcEngine.README b/Using_Armory.README similarity index 99% rename from Using_PyBtcEngine.README rename to Using_Armory.README index a203f6206..3636e308a 100755 --- a/Using_PyBtcEngine.README +++ b/Using_Armory.README @@ -6,7 +6,7 @@ ## ## ################################################################################ -PyBtcEngine contains over 7,000 lines of code, between the C++ and python +Armory contains over 15,000 lines of code, between the C++ and python libraries. This can be very confusing for someone unfamiliar with the code (you). Below I have attempted to illustrate the CONOPS (concept of operations) that the library was designed for, so you know how to use it @@ -16,7 +16,7 @@ the following three files: [**SWIG**] unittest.py (this has almost everything!) [ C++ ] cppForSwig/BlockUtilsTest.cpp - [ Python ] btcarmoryengine.py + [ Python ] armoryengine.py But of course, sample code alone does not make great documentation. I will attempt to provide reference info for everything else you need to know, here. @@ -114,7 +114,7 @@ that all features with an X in either column are accessible in SWIG. # available functionality in the libraries. I will update this as soon # as I get the first client release out of the way. For now, you can # can see most of the C++ methods below (accessed via Python), and then -# do an "import btcarmoryengine; help(btcarmoryengine)" to see a MASSIVE +# do an "import armoryengine; help(armoryengine)" to see a MASSIVE # list of the available classes and methods. Seriously... it's huge. # ################################################################################ diff --git a/btcarmoryengine.py b/armoryengine.py similarity index 99% rename from btcarmoryengine.py rename to armoryengine.py index f8efeca09..a74063151 100644 --- a/btcarmoryengine.py +++ b/armoryengine.py @@ -6,7 +6,7 @@ # ################################################################################ # -# Project: BitcoinArmory +# Project: Armory # Author: Alan Reiner # Orig Date: 20 November, 2011 # Descr: This file serves as an engine for python-based Bitcoin software. @@ -53,7 +53,7 @@ # These are overriden for testnet -USE_TESTNET = False +USE_TESTNET = True # Version Numbers -- numDigits [var, 2, 2, 3] BTCARMORY_VERSION = (0,50,0,0) # (Major, Minor, Minor++, even-more-minor) @@ -93,10 +93,10 @@ def readVersionInt(verInt): return tuple(verList[::-1]) print '********************************************************************************' -print 'Loading BitcoinArmory Engine:' -print ' BitcoinArmory Version:', getVersionString(BTCARMORY_VERSION) -print ' PyBtcAddress Version:', getVersionString(PYBTCADDRESS_VERSION) -print ' PyBtcWallet Version:', getVersionString(PYBTCWALLET_VERSION) +print 'Loading Armory Engine:' +print ' Armory Version: ', getVersionString(BTCARMORY_VERSION) +print ' PyBtcAddress Version:', getVersionString(PYBTCADDRESS_VERSION) +print ' PyBtcWallet Version:', getVersionString(PYBTCWALLET_VERSION) # Get the host operating system import platform @@ -115,17 +115,17 @@ def readVersionInt(verInt): OS_NAME = 'Windows' USER_HOME_DIR = os.getenv('APPDATA') BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'BitcoinArmory', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) elif OS_LINUX: OS_NAME = 'Linux' USER_HOME_DIR = os.getenv('HOME') BTC_HOME_DIR = os.path.join(USER_HOME_DIR, '.bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, '.bitcoinarmory', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, '.armory', SUBDIR) elif OS_MACOSX: OS_NAME = 'Mac/OSX' USER_HOME_DIR = os.path.expanduser('~/Library/Application Support') BTC_HOME_DIR = os.path.join(USER_HOME_DIR, 'Bitcoin', SUBDIR) - ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'BitcoinArmory', SUBDIR) + ARMORY_HOME_DIR = os.path.join(USER_HOME_DIR, 'Armory', SUBDIR) else: print '***Unknown operating system!' print '***Cannot determine default directory locations' @@ -137,7 +137,7 @@ def readVersionInt(verInt): print ' User home-directory :', USER_HOME_DIR print ' Satoshi BTC directory :', BTC_HOME_DIR print ' Satoshi blk0001.dat :', BLK0001_PATH -print ' BitcoinArmory home dir:', ARMORY_HOME_DIR +print ' Armory home dir :', ARMORY_HOME_DIR if ARMORY_HOME_DIR and not os.path.exists(ARMORY_HOME_DIR): os.mkdir(ARMORY_HOME_DIR) @@ -210,6 +210,14 @@ def coin2str(nSatoshi, ndec=8, rJust=False): s = s.strip(' ') return s +# This is a sweet trick for create enum-like dictionaries. +# Either automatically numbers (*args), or name-val pairs (**kwargs) +#http://stackoverflow.com/questions/36932/whats-the-best-way-to-implement-an-enum-in-python +def enum(*sequential, **named): + enums = dict(zip(sequential, range(len(sequential))), **named) + return type('Enum', (), enums) + + # Some useful constants to be used throughout everything BASE58DIGITS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' @@ -5190,6 +5198,19 @@ def syncWithBlockchain(self): def getBalance(self): return sumTxOutList(self.getUnspentTxOutList()) + ############################################################################# + def getTxLedger(self, addr160=None): + """ + Gets the complete ledger for a specific address, or the wallet as a whole. + """ + if addr160==None: + return self.cppWallet.getTxLedger() + else: + if not self.hasAddr(addr160): + return [] + else: + return self.cppWallet.getAddrByHash160(addr160).getTxLedger() + ############################################################################# def getUnspentTxOutList(self): if not self.doBlockchainSync==BLOCKCHAIN_DONOTUSE: @@ -5216,6 +5237,8 @@ def hasAddr(self, addrData): return self.addrMap.has_key(addrData.getAddr160()) else: return False + + ############################################################################# def setDefaultKeyLifetime(self, newlifetime): """ Set a new default lifetime for holding the unlock key. Min 2 sec """ @@ -5335,8 +5358,9 @@ def createNewWallet(self, newWalletFilePath=None, \ self.walletPath = newWalletFilePath if not newWalletFilePath: shortName = self.labelName .replace(' ','_') - for c in ',?;:\'"?/\\=+-|[]{}<>': - shortName = shortName.replace(c,'_') + # This was really only needed when we were putting name in filename + #for c in ',?;:\'"?/\\=+-|[]{}<>': + #shortName = shortName.replace(c,'_') newName = 'armory_%s_.wallet' % self.wltUniqueIDB58 self.walletPath = os.path.join(ARMORY_HOME_DIR, newName) @@ -5414,7 +5438,9 @@ def forkOnlineWallet(self, newWalletFile=None, \ if not newWalletFile: - newWalletFile = os.path.join(BITCOIN_HOME_DIR, wltname) + wltpieces = os.path.splitext(self.walletPath) + wltname = wltpieces[0] + 'Online' + wltpieces[1] + newWalletFile = os.path.join(ARMORY_HOME_DIR, wltname) onlineWallet = PyBtcWallet() onlineWallet.fileTypeStr = self.fileTypeStr @@ -5423,7 +5449,7 @@ def forkOnlineWallet(self, newWalletFile=None, \ onlineWallet.wltCreateDate = self.wltCreateDate onlineWallet.useEncryption = False onlineWallet.watchingOnly = True - onlineWallet.labelName = (self.labelName + '_Online')[:32] + onlineWallet.labelName = (self.labelName + ' (Watch-Only)')[:32] onlineWallet.labelDescr = longLabel newAddrMap = {} @@ -5860,7 +5886,7 @@ def setComment(self, hashVal, newComment): ############################################################################# - def setWalletLabels(self, lshort, llong): + def setWalletLabels(self, lshort, llong=''): toWriteS = lshort.ljust( 32, '\x00') toWriteL = lshort.ljust(256, '\x00') @@ -5977,12 +6003,12 @@ def unpackHeader(self, binUnpacker): if not self.magicBytes == MAGIC_BYTES: print '***ERROR: Requested wallet is for a different blockchain!' print ' Wallet is for: ', BLOCKCHAINS[self.magicBytes] - print ' BtcArmoryEngine: ', BLOCKCHAINS[MAGIC_BYTES] + print ' ArmoryEngine: ', BLOCKCHAINS[MAGIC_BYTES] return if not self.wltUniqueIDBin[-1] == ADDRBYTE: print '***ERROR: Requested wallet is for a different network!' print ' Wallet is for: ', NETWORKS[netByte] - print ' BtcArmoryEngine: ', NETWORKS[ADDRBYTE] + print ' ArmoryEngine: ', NETWORKS[ADDRBYTE] return # User-supplied description/name for wallet @@ -6753,14 +6779,30 @@ def lock(self): raise WalletLockError, 'Unlock with passphrase before locking again' ############################################################################# - def getAddrListSortedByChainIndex(self): + def getAddrListSortedByChainIndex(self, withRoot=False): """ Returns Addr160 list """ addrList = [] for addr160,addrObj in self.addrMap.iteritems(): + if not withRoot and addr160=='ROOT': + continue addrList.append( [addrObj.chainIndex, addr160, addrObj] ) + addrList.sort(key=lambda x: x[0]) return addrList + ############################################################################# + def getAddrList(self, withRoot=False): + """ Returns list of PyBtcAddress objects """ + addrList = [] + for addr160,addrObj in self.addrMap.iteritems(): + if not withRoot and addr160=='ROOT': + continue + # I assume these will be references, not copies + addrList.append( addrObj ) + return addrList + + + ############################################################################# def pprint(self, indent='', allAddrInfo=True): print indent + 'PyBtcWallet :', self.wltUniqueIDB58 @@ -6775,7 +6817,7 @@ def pprint(self, indent='', allAddrInfo=True): if allAddrInfo: self.addrMap['ROOT'].pprint(indent=indent) print indent + 'All usable keys:' - sortedAddrList = self.getAddrListSortedByChainIndex() + sortedAddrList = self.getAddrListSortedByChainIndex(withRoot=True) for i,addr160,addrObj in sortedAddrList: if not addr160=='ROOT': print '\n' + indent + 'Address:', addrObj.getAddrStr() @@ -7404,7 +7446,7 @@ def serialize(self): from twisted.internet.defer import Deferred except ImportError: print '***Python-Twisted is not installed -- cannot enable' - print ' networking-related methods for BtcArmoryEngine' + print ' networking-related methods for ArmoryEngine' ################################################################################ @@ -7419,10 +7461,10 @@ def forceDeferred(callbk): ################################################################################ # -# Bitcoin Armory Networking: +# Armory Networking: # # This is where I will define all the network operations needed for -# BitcoinArmory to operate, using python-twisted. There are "better" +# Armory to operate, using python-twisted. There are "better" # ways to do this with "reusable" code structures (i.e. using huge # deferred callback chains), but this is not the central "creative" # part of the Bitcoin protocol. I need just enough to broadcast tx @@ -7430,7 +7472,7 @@ def forceDeferred(callbk): # I'll just be ignoring everything else. # ################################################################################ -class BitcoinArmoryClient(Protocol): +class ArmoryClient(Protocol): """ This is where all the Bitcoin-specific networking stuff goes. In the Twisted way, you need to inject your own chains of @@ -7566,6 +7608,7 @@ def processMessage(self, msg): print '***!!!*** before considering this transaction valid!' else: self.factory.zeroConfTx[pytx.getHash()] = pytx.copy() + self.factory.zeroConfTxTime[pytx.getHash()] = RightNow() if msg.cmd=='block': # We don't care much about blocks right now -- We will find # out about them when the Satoshi client updates blk0001.dat @@ -7616,10 +7659,10 @@ def sendTx(self, txObj): ################################################################################ ################################################################################ -class BitcoinArmoryClientFactory(ClientFactory): +class ArmoryClientFactory(ClientFactory): """ Spawns Protocol objects used for communicating over the socket. All such - objects (BitcoinArmoryClients) can share information through this factory. + objects (ArmoryClients) can share information through this factory. However, at the moment, this class is designed to only create a single connection -- to localhost. @@ -7628,8 +7671,9 @@ class BitcoinArmoryClientFactory(ClientFactory): which are due to two transactions being send at the same time with different recipients but the same inputs. """ - protocol = BitcoinArmoryClient + protocol = ArmoryClient zeroConfTx = {} + zeroConfTxTime = {} zeroConfTxOutMap = {} # map[OutPoint] = txHash doubleBroadcastAlerts = {} # map[Addr160] = txHash lastAlert = 0 @@ -7675,6 +7719,7 @@ def purgeMemoryPool(self): for hsh,tx in self.zeroConfTx.iteritems(): if TheBDM.getTxByHash(hsh): del self.zeroConfTx[hsh] + del self.zeroConfTxTime[hsh] # We also need to clean up the double-spend detector for key,val in self.zeroConfTxOutMap.iteritems(): if hsh==val: diff --git a/armorymodels.py b/armorymodels.py index dbdaccbfc..7d56a131c 100755 --- a/armorymodels.py +++ b/armorymodels.py @@ -6,11 +6,24 @@ from PyQt4.QtGui import * sys.path.append('..') sys.path.append('../cppForSwig') -from btcarmoryengine import * +from armoryengine import * from CppBlockUtils import * +Colors = enum(LightBlue= QColor(215,215,255), \ + LightGray= QColor(235,235,235), \ + DarkGray= QColor( 64, 64, 64), \ + Green= QColor( 0,100, 0), \ + Red= QColor(100, 0, 0), \ + Black= QColor( 0, 0, 0) \ + ) + + class WalletDispModel(QAbstractTableModel): + + # The columns enumeration + COL = enum('ID', 'Name', 'Secure', 'Bal') + def __init__(self, mainWindow): super(WalletDispModel, self).__init__() self.main = mainWindow @@ -22,43 +35,59 @@ def columnCount(self, index=QModelIndex()): return 4 def data(self, index, role=Qt.DisplayRole): + COL = self.COL row,col = index.row(), index.column() wlt = self.main.walletMap[self.main.walletIDList[row]] + wltID = wlt.wltUniqueIDB58 if role==Qt.DisplayRole: - if col==0: return QVariant(wlt.wltUniqueIDB58) - if col==1: return QVariant(wlt.labelName.ljust(32)) - if col==2: + if col==COL.ID: + return QVariant(wltID) + if col==COL.Name: + return QVariant(wlt.labelName.ljust(32)) + if col==COL.Secure: if wlt.watchingOnly: - return QVariant('Watching-only') + if wltID in self.main.walletOfflines: + return QVariant('Offline Keys') + else: + return QVariant('Watching-only') elif wlt.useEncryption: return QVariant('Encrypted') else: return QVariant('Not Encrypted') - if col==3: + if col==COL.Bal: bal = self.main.walletBalances[row] if bal==-1: return QVariant('(...)') else: if bal==0: - ndigit=0 + ndigit=1 elif bal<1000: ndigit = 8 elif bal<100000: ndigit = 6 else: ndigit = 4 - return QVariant(coin2str(bal, ndec=ndigit)) + dispStr = [c for c in coin2str(bal, ndec=8)] + dispStr[-8+ndigit:] = ' '*(8-ndigit) + return QVariant(''.join(dispStr)) elif role==Qt.TextAlignmentRole: if col in (0,1): return QVariant(int(Qt.AlignLeft | Qt.AlignVCenter)) elif col in (2,): return QVariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) elif col in (3,): - return QVariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) + return QVariant(int(Qt.AlignRight | Qt.AlignVCenter)) elif role==Qt.BackgroundColorRole: - return QVariant( QColor(235,235,255) ) + if wlt.watchingOnly and not wltID in self.main.walletOfflines: + return QVariant( Colors.LightGray ) + else: + return QVariant( Colors.LightBlue ) + elif role==Qt.FontRole: + if col==3: + return QFont("DejaVu Sans Mono", 10) return QVariant() + def headerData(self, section, orientation, role=Qt.DisplayRole): colLabels = ['ID', 'Name', 'Security', 'Balance'] if role==Qt.DisplayRole: @@ -69,6 +98,132 @@ def headerData(self, section, orientation, role=Qt.DisplayRole): + # This might work for checkbox-in-tableview + #QStandardItemModel* tableModel = new QStandardItemModel(); + #// create text item + #tableModel->setItem(0, 0, new QStandardItem("text item")); + #// create check box item + #QStandardItem* item0 = new QStandardItem(true); + #item0->setCheckable(true); + #item0->setCheckState(Qt::Checked); + #item0->setText("some text"); + #tableModel->setItem(0, 1, item0); + #// set model + #ui->tableView->setModel(tableModel); + + # Perhaps delegate for rich text in QTableViews + #void SpinBoxDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const + #{ + #QTextDocument document; + #QVariant value = index.data(Qt::DisplayRole); + #if (value.isValid() && !value.isNull()) { + #QString text("This is highlighted."); + #text.append(" ("); + #text.append(value.toString()); + #text.append(")"); + #document.setHtml(text); + #painter->translate(option.rect.topLeft()); + #document.drawContents(painter); + #painter->translate(-option.rect.topLeft()); + #} + +################################################################################ +class ActivityDispModel(QAbstractTableModel): + + COL = enum('Status', 'Date', 'WltID', 'WltName', 'Comment', 'TxDir', 'Amount') + + def __init__(self, mainWindow): + super(ActivityDispModel, self).__init__() + self.main = mainWindow + + def rowCount(self, index=QModelIndex()): + return int(self.main.ledgerSize) + + def columnCount(self, index=QModelIndex()): + return 7 + + def data(self, index, role=Qt.DisplayRole): + COL = self.COL + row,col = index.row(), index.column() + wltID,le = self.main.combinedLedger[row] + nConf = self.main.latestBlockNum - le.getBlockNum() + if le.getBlockNum() >= 0xffffffff: + nConf = 0 + + if role==Qt.DisplayRole: + # A little more pre-processing before display + blkheader = TheBDM.getHeaderByHeight(le.getBlockNum()) + txtime = blkheader.getTimestamp() + txHash = le.getTxHash() + if nConf == 0: + txtime = self.main.NetworkingFactory.zeroConfTxTime[txHash] + + if col==COL.Status: + return QVariant(nConf) + if col==COL.Date: + return QVariant(unixTimeToFormatStr(txtime)) + if col==COL.WltID: + return QVariant(wltID) + if col==COL.WltName: + wlt = self.main.walletMap[wltID] + return QVariant(wlt.labelName) + if col==COL.Comment: + wlt = self.main.walletMap[wltID] + if wlt.commentsMap.has_key(txHash): + return QVariant(wlt.commentsMap[txHash]) + if col==COL.TxDir: + if le.getValue()>0: + return QVariant('Recv') + else: + return QVariant('Sent') + if col==COL.Amount: + return QVariant( coin2str(le.getValue()) ) + elif role==Qt.TextAlignmentRole: + if col in (COL.Status, COL.Date, COL.TxDir): + return QVariant(int(Qt.AlignHCenter | Qt.AlignVCenter)) + elif col in (COL.WltID, COL.Comment): + return QVariant(int(Qt.AlignLeft | Qt.AlignVCenter)) + elif col in (COL.Amount,): + return QVariant(int(Qt.AlignRight | Qt.AlignVCenter)) + elif role==Qt.BackgroundColorRole: + if self.main.walletMap[wltID].watchingOnly and \ + not wltID in self.main.walletOfflines: + return QVariant( Colors.LightGray ) + else: + return QVariant( Colors.LightBlue ) + elif role==Qt.ForegroundRole: + if col==COL.Amount: + if le.getValue()>0: return QVariant(Colors.Green) + elif le.getValue()<0: return QVariant(Colors.Red) + else: return QVariant(Colors.DarkGray) + + + return QVariant() + + + + + def headerData(self, section, orientation, role=Qt.DisplayRole): + COL = self.COL + if role==Qt.DisplayRole: + if orientation==Qt.Horizontal: + if section==COL.Status: + return QVariant('#') + if section==COL.Date: + return QVariant('Date') + if section==COL.WltID: + return QVariant('(ID)') + if section==COL.WltName: + return QVariant('Wallet') + if section==COL.Comment: + return QVariant('Comments') + if section==COL.TxDir: + return QVariant('Tx/Rx') + if section==COL.Amount: + return QVariant('Amount') + elif role==Qt.TextAlignmentRole: + return QVariant( int(Qt.AlignHCenter | Qt.AlignVCenter) ) + """ class HeaderDataModel(QAbstractTableModel): diff --git a/cppForSwig/BlockObjRef.h b/cppForSwig/BlockObjRef.h index 05af910e2..16f0e2be8 100644 --- a/cppForSwig/BlockObjRef.h +++ b/cppForSwig/BlockObjRef.h @@ -350,6 +350,9 @@ class TxRef uint64_t getTxStartByte(void) { return fileByteLoc_; } void setTxStartByte(uint64_t b) { fileByteLoc_ = b; } + uint32_t getTxInOffset(uint32_t i) const { return offsetsTxIn_[i]; } + uint32_t getTxOutOffset(uint32_t i) const { return offsetsTxOut_[i]; } + Tx getCopy(void) const; BlockHeaderRef* getHeaderPtr(void) const { return headerPtr_; } void setHeaderPtr(BlockHeaderRef* bhr) { headerPtr_ = bhr; } diff --git a/cppForSwig/BlockUtils.cpp b/cppForSwig/BlockUtils.cpp index 9da52b0dd..af1dd0eb4 100644 --- a/cppForSwig/BlockUtils.cpp +++ b/cppForSwig/BlockUtils.cpp @@ -274,42 +274,59 @@ void BtcWallet::scanTx(TxRef & tx, vector thisTxInIsOurs (tx.getNumTxIn(), false); vector thisTxOutIsOurs(tx.getNumTxOut(), false); + uint8_t const * txStartPtr = tx.getPtr(); + // Since 99.99%+ of all transactions are not ours, let's do the // fastest bulk filter possible, even though it will add - // redundant computation to the tx that are ours. - // - // TODO: We may even consider doing lower-level ops to pull - // out the exact information we need from the binary - // stream, and avoid constructing TxInRef/TxOutRef objs + // redundant computation to the tx that are ours. In fact, + // we will skip the TxInRef/TxOutRef computations and take the + // pointers directly the data we want for(uint32_t iin=0; iin -1) - scanNonStdTx(blknum, txIndex, tx, iout, thisAddr); - continue; + if(txout.getScriptRef().find(thisAddr.getAddrStr20()) > -1) + scanNonStdTx(blknum, txIndex, tx, iout, thisAddr); + continue; } + break; } - - if( hasAddr(txout.getRecipientAddr()) ) - anyTxOutIsOurs = true; } @@ -474,6 +491,34 @@ void BtcWallet::scanTx(TxRef & tx, } +//////////////////////////////////////////////////////////////////////////////// +// This is kind of a hack: I forgot the possibility that we might need +// to do temporary scan of some Tx data without actually adding it to +// the wallet yet (because we need to process zero-conf transactions, +// but can't update the wallets until we have a verified blk number). +// Perhaps this was an oversight to not design the wallets and Ledger +// Entry objects to handle this situation, but for now we will leave +// what's working alone, and outsource a solution to this method: +// +// (1) Create a new wallet +// (2) Copy in all the addresses of this wallet +// (3) Run scanTx on all the zero-conf tx, using 0xffffffff for blknum +// (4) Return the combined ledger for the zero-conf addresses +// (5) Maintain this ledger separately in the calling calling code +// (6) Throw away this ledger whenever a new block is received, rescan +vector BtcWallet::getLedgerEntriesForZeroConfTxList( + vector zcList) +{ + // Prepare fresh, temporary wallet with same addresses + BtcWallet tempWlt; + for(uint32_t i=0; igetAddrStr20() ); + + for(uint32_t i=0; i getLedgerEntriesForZeroConfTxList( + vector zcList); + void scanNonStdTx(uint32_t blknum, uint32_t txidx, TxRef& txref, diff --git a/cppForSwig/BlockUtilsTest.cpp b/cppForSwig/BlockUtilsTest.cpp index 176f3307a..23cb9942f 100644 --- a/cppForSwig/BlockUtilsTest.cpp +++ b/cppForSwig/BlockUtilsTest.cpp @@ -143,6 +143,7 @@ void TestScanForWalletTx(string blkfile) myAddress.createFromHex("8996182392d6f05e732410de4fc3fa273bac7ee6"); wlt.addAddress(myAddress); myAddress.createFromHex("b5e2331304bc6c541ffe81a66ab664159979125b"); wlt.addAddress(myAddress); myAddress.createFromHex("ebbfaaeedd97bc30df0d6887fd62021d768f5cb8"); wlt.addAddress(myAddress); + myAddress.createFromHex("11b366edfc0a8b66feebae5c2e25a7b6a5d1cf31"); wlt.addAddress(myAddress); #else // Test-network addresses myAddress.createFromHex("abda0c878dd7b4197daa9622d96704a606d2cd14"); wlt.addAddress(myAddress); @@ -189,6 +190,29 @@ void TestScanForWalletTx(string blkfile) } + ///////////////////////////////////////////////////////////////////////////// + // Need to test the const-scan of a random tx-list without updating the wlt + cout << "\nTesting tx-list wallet scan for zero-conf tx..." << endl; + vector txlist; + for(uint32_t blk=0; blk<300; blk++) + { + BlockHeaderRef* bhr = bdm.getHeaderByHeight(blk); + vector blkTxList = bhr->getTxRefPtrList(); + txlist.insert(txlist.end(), blkTxList.begin(), blkTxList.end()); + } + vector testLedger; + testLedger = wlt.getLedgerEntriesForZeroConfTxList(txlist); + for(uint32_t j=0; j