Browse Source

Refactored wallet printout, reusing WalletView

Add extpub keys for branches to wallet view
Make extpub keys copyable via context menu
Fix bug in seedwords display
Make all wallet view columns resizable
hexseed only for regtest
master
Adam Gibson 8 years ago
parent
commit
5018e28e61
No known key found for this signature in database
GPG Key ID: B3AE09F1E9A3197A
  1. 3
      jmclient/jmclient/__init__.py
  2. 55
      jmclient/jmclient/wallet_utils.py
  3. 182
      scripts/joinmarket-qt.py
  4. 10
      scripts/qtsupport.py

3
jmclient/jmclient/__init__.py

@ -37,7 +37,8 @@ from .commitment_utils import get_utxo_info, validate_utxo_data, quit
from .taker_utils import (tumbler_taker_finished_update, restart_waiter, from .taker_utils import (tumbler_taker_finished_update, restart_waiter,
restart_wait, get_tumble_log, direct_send, restart_wait, get_tumble_log, direct_send,
tumbler_filter_orders_callback) tumbler_filter_orders_callback)
from .wallet_utils import wallet_tool_main from .wallet_utils import (wallet_tool_main, wallet_generate_recover_bip39,
wallet_display)
from .maker import Maker from .maker import Maker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain
# Set default logging handler to avoid "No handler found" warnings. # Set default logging handler to avoid "No handler found" warnings.

55
jmclient/jmclient/wallet_utils.py

@ -299,9 +299,11 @@ def wallet_showutxos(wallet, showprivkey):
return json.dumps(unsp, indent=4) return json.dumps(unsp, indent=4)
def wallet_display(wallet, gaplimit, showprivkey, displayall=False): def wallet_display(wallet, gaplimit, showprivkey, displayall=False,
serialized=True):
"""build the walletview object, """build the walletview object,
then return its serialization directly then return its serialization directly if serialized,
else return the WalletView object.
""" """
acctlist = [] acctlist = []
rootpath = wallet.get_root_path() rootpath = wallet.get_root_path()
@ -342,9 +344,12 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False):
acctlist.append(WalletViewAccount(rootpath, m, branchlist, acctlist.append(WalletViewAccount(rootpath, m, branchlist,
xpub=xpub_account)) xpub=xpub_account))
walletview = WalletView(rootpath, acctlist) walletview = WalletView(rootpath, acctlist)
return walletview.serialize() if serialized:
return walletview.serialize()
else:
return walletview
def get_password_check(): def cli_password_check():
password = get_password('Enter wallet encryption passphrase: ') password = get_password('Enter wallet encryption passphrase: ')
password2 = get_password('Reenter wallet encryption passphrase: ') password2 = get_password('Reenter wallet encryption passphrase: ')
if password != password2: if password != password2:
@ -353,13 +358,23 @@ def get_password_check():
password_key = btc.bin_dbl_sha256(password) password_key = btc.bin_dbl_sha256(password)
return password, password_key return password, password_key
def persist_walletfile(walletspath, default_wallet_name, encrypted_seed): def cli_get_walletname():
return raw_input('Input wallet file name (default: wallet.json): ')
def cli_user_words(words):
print('Write down this wallet recovery seed\n\n' + words +'\n')
def cli_user_words_entry():
return raw_input("Input 12 word recovery seed: ")
def persist_walletfile(walletspath, default_wallet_name, encrypted_seed,
callbacks=(cli_get_walletname,)):
timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S") timestamp = datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
walletfile = json.dumps({'creator': 'joinmarket project', walletfile = json.dumps({'creator': 'joinmarket project',
'creation_time': timestamp, 'creation_time': timestamp,
'encrypted_seed': encrypted_seed.encode('hex'), 'encrypted_seed': encrypted_seed.encode('hex'),
'network': get_network()}) 'network': get_network()})
walletname = raw_input('Input wallet file name (default: wallet.json): ') walletname = callbacks[0]()
if len(walletname) == 0: if len(walletname) == 0:
walletname = default_wallet_name walletname = default_wallet_name
walletpath = os.path.join(walletspath, walletname) walletpath = os.path.join(walletspath, walletname)
@ -374,25 +389,39 @@ def persist_walletfile(walletspath, default_wallet_name, encrypted_seed):
print('saved to ' + walletname) print('saved to ' + walletname)
return True return True
def wallet_generate_recover_bip39(method, walletspath, default_wallet_name): def wallet_generate_recover_bip39(method, walletspath, default_wallet_name,
callbacks=(cli_user_words,
cli_user_words_entry,
cli_password_check,
cli_get_walletname)):
"""Optionally provide callbacks:
0 - display seed
1 - enter seed (for recovery)
2 - enter password
3 - enter wallet name
The defaults are for terminal entry.
"""
#using 128 bit entropy, 12 words, mnemonic module #using 128 bit entropy, 12 words, mnemonic module
m = Mnemonic("english") m = Mnemonic("english")
if method == "generate": if method == "generate":
words = m.generate() words = m.generate()
print('Write down this wallet recovery seed\n\n' + words +'\n') callbacks[0](words)
elif method == 'recover': elif method == 'recover':
words = raw_input('Input 12 word recovery seed: ') words = callbacks[1]()
entropy = str(m.to_entropy(words)) entropy = str(m.to_entropy(words))
password, password_key = get_password_check() password, password_key = callbacks[2]()
if not password: if not password:
return False return False
encrypted_entropy = encryptData(password_key, entropy) encrypted_entropy = encryptData(password_key, entropy)
return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy) return persist_walletfile(walletspath, default_wallet_name, encrypted_entropy,
callbacks=(callbacks[3],))
def wallet_generate_recover(method, walletspath, def wallet_generate_recover(method, walletspath,
default_wallet_name='wallet.json'): default_wallet_name='wallet.json'):
if jm_single().config.get("POLICY", "segwit") == "true": if jm_single().config.get("POLICY", "segwit") == "true":
return wallet_generate_recover_bip39(method, walletspath, default_wallet_name) #Here using default callbacks for scripts (not used in Qt)
return wallet_generate_recover_bip39(method, walletspath,
default_wallet_name)
if method == 'generate': if method == 'generate':
seed = btc.sha256(os.urandom(64))[:32] seed = btc.sha256(os.urandom(64))[:32]
words = mn_encode(seed) words = mn_encode(seed)
@ -406,7 +435,7 @@ def wallet_generate_recover(method, walletspath,
return False return False
seed = mn_decode(words) seed = mn_decode(words)
print(seed) print(seed)
password, password_key = get_password_check() password, password_key = cli_password_check()
if not password: if not password:
return False return False
encrypted_seed = encryptData(password_key, seed.decode('hex')) encrypted_seed = encryptData(password_key, seed.decode('hex'))

182
scripts/joinmarket-qt.py

@ -44,16 +44,17 @@ donation_address = "1AZgQZWYRteh6UyF87hwuvyWj73NvWKpL"
JM_CORE_VERSION = '0.2.2' JM_CORE_VERSION = '0.2.2'
JM_GUI_VERSION = '5' JM_GUI_VERSION = '5'
from jmclient import (load_program_config, get_network, Wallet, from jmclient import (load_program_config, get_network, SegwitWallet,
get_p2pk_vbyte, jm_single, validate_address, get_p2sh_vbyte, jm_single, validate_address,
get_log, weighted_order_choose, Taker, get_log, weighted_order_choose, Taker,
JMTakerClientProtocolFactory, WalletError, JMClientProtocolFactory, WalletError,
start_reactor, get_schedule, get_tumble_schedule, start_reactor, get_schedule, get_tumble_schedule,
schedule_to_text, mn_decode, mn_encode, create_wallet_file, schedule_to_text, create_wallet_file,
get_blockchain_interface_instance, sync_wallet, direct_send, get_blockchain_interface_instance, sync_wallet, direct_send,
RegtestBitcoinCoreInterface, tweak_tumble_schedule, RegtestBitcoinCoreInterface, tweak_tumble_schedule,
human_readable_schedule_entry, tumbler_taker_finished_update, human_readable_schedule_entry, tumbler_taker_finished_update,
get_tumble_log, restart_wait, tumbler_filter_orders_callback) get_tumble_log, restart_wait, tumbler_filter_orders_callback,
wallet_generate_recover_bip39, wallet_display)
from qtsupport import (ScheduleWizard, TumbleRestartWizard, warnings, config_tips, from qtsupport import (ScheduleWizard, TumbleRestartWizard, warnings, config_tips,
config_types, TaskThread, QtHandler, XStream, Buttons, config_types, TaskThread, QtHandler, XStream, Buttons,
@ -678,7 +679,7 @@ class SpendTab(QWidget):
if not self.clientfactory: if not self.clientfactory:
#First run means we need to start: create clientfactory #First run means we need to start: create clientfactory
#and start reactor Thread #and start reactor Thread
self.clientfactory = JMTakerClientProtocolFactory(self.taker) self.clientfactory = JMClientProtocolFactory(self.taker)
thread = TaskThread(self) thread = TaskThread(self)
daemon = jm_single().config.getint("DAEMON", "no_daemon") daemon = jm_single().config.getint("DAEMON", "no_daemon")
daemon = True if daemon == 1 else False daemon = True if daemon == 1 else False
@ -1128,18 +1129,24 @@ class JMWalletTab(QWidget):
def create_menu(self, position): def create_menu(self, position):
item = self.history.currentItem() item = self.history.currentItem()
address_valid = False address_valid = False
xpub_exists = False
if item: if item:
address = str(item.text(0)) txt = str(item.text(0))
try: if validate_address(txt)[0]:
btc.b58check_to_hex(address)
address_valid = True address_valid = True
except AssertionError: if "EXTERNAL" in txt:
log.debug('no btc address found, not creating menu item') parsed = txt.split()
if len(parsed) > 1:
xpub = parsed[1]
xpub_exists = True
menu = QMenu() menu = QMenu()
if address_valid: if address_valid:
menu.addAction("Copy address to clipboard", menu.addAction("Copy address to clipboard",
lambda: app.clipboard().setText(address)) lambda: app.clipboard().setText(txt))
if xpub_exists:
menu.addAction("Copy extended pubkey to clipboard",
lambda: app.clipboard().setText(xpub))
menu.addAction("Resync wallet from blockchain", menu.addAction("Resync wallet from blockchain",
lambda: w.resyncWallet()) lambda: w.resyncWallet())
#TODO add more items to context menu #TODO add more items to context menu
@ -1150,7 +1157,7 @@ class JMWalletTab(QWidget):
l.clear() l.clear()
if walletinfo: if walletinfo:
self.mainwindow = self.parent().parent().parent() self.mainwindow = self.parent().parent().parent()
rows, mbalances, total_bal = walletinfo rows, mbalances, xpubs, total_bal = walletinfo
if get_network() == 'testnet': if get_network() == 'testnet':
self.wallet_name = self.mainwindow.wallet.seed self.wallet_name = self.mainwindow.wallet.seed
else: else:
@ -1167,9 +1174,10 @@ class JMWalletTab(QWidget):
mdbalance, '', '', '', '']) mdbalance, '', '', '', ''])
l.addChild(m_item) l.addChild(m_item)
for forchange in [0, 1]: for forchange in [0, 1]:
heading = 'EXTERNAL' if forchange == 0 else 'INTERNAL' heading = "EXTERNAL" if forchange == 0 else "INTERNAL"
heading_end = ' addresses m/0/%d/%d/' % (i, forchange) if walletinfo and heading == "EXTERNAL":
heading += heading_end heading_end = ' ' + xpubs[i][forchange]
heading += heading_end
seq_item = QTreeWidgetItem([heading, '', '', '', '']) seq_item = QTreeWidgetItem([heading, '', '', '', ''])
m_item.addChild(seq_item) m_item.addChild(seq_item)
if not forchange: if not forchange:
@ -1313,7 +1321,7 @@ class JMMainWindow(QMainWindow):
priv = self.wallet.get_key_from_addr(addr) priv = self.wallet.get_key_from_addr(addr)
private_keys[addr] = btc.wif_compressed_privkey( private_keys[addr] = btc.wif_compressed_privkey(
priv, priv,
vbyte=get_p2pk_vbyte()) vbyte=111)
d.emit(QtCore.SIGNAL('computing_privkeys')) d.emit(QtCore.SIGNAL('computing_privkeys'))
d.emit(QtCore.SIGNAL('show_privkeys')) d.emit(QtCore.SIGNAL('show_privkeys'))
@ -1411,7 +1419,7 @@ class JMMainWindow(QMainWindow):
title="Error") title="Error")
def selectWallet(self, testnet_seed=None): def selectWallet(self, testnet_seed=None):
if get_network() != 'testnet': if jm_single().config.get("BLOCKCHAIN", "blockchain_source") != "regtest":
current_path = os.path.dirname(os.path.realpath(__file__)) current_path = os.path.dirname(os.path.realpath(__file__))
if os.path.isdir(os.path.join(current_path, 'wallets')): if os.path.isdir(os.path.join(current_path, 'wallets')):
current_path = os.path.join(current_path, 'wallets') current_path = os.path.join(current_path, 'wallets')
@ -1448,7 +1456,7 @@ class JMMainWindow(QMainWindow):
def loadWalletFromBlockchain(self, firstarg=None, pwd=None): def loadWalletFromBlockchain(self, firstarg=None, pwd=None):
if (firstarg and pwd) or (firstarg and get_network() == 'testnet'): if (firstarg and pwd) or (firstarg and get_network() == 'testnet'):
try: try:
self.wallet = Wallet( self.wallet = SegwitWallet(
str(firstarg), str(firstarg),
pwd, pwd,
max_mix_depth=jm_single().config.getint( max_mix_depth=jm_single().config.getint(
@ -1489,7 +1497,7 @@ class JMMainWindow(QMainWindow):
def generateWallet(self): def generateWallet(self):
log.debug('generating wallet') log.debug('generating wallet')
if get_network() == 'testnet': if jm_single().config.get("BLOCKCHAIN", "blockchain_source") == "regtest":
seed = self.getTestnetSeed() seed = self.getTestnetSeed()
self.selectWallet(testnet_seed=seed) self.selectWallet(testnet_seed=seed)
else: else:
@ -1506,24 +1514,7 @@ class JMMainWindow(QMainWindow):
return return
return str(text).strip() return str(text).strip()
def initWallet(self, seed=None): def getPassword(self):
'''Creates a new mainnet
wallet
'''
if not seed:
seed = btc.sha256(os.urandom(64))[:32]
words = mn_encode(seed)
mb = QMessageBox()
seed_recovery_warning = [
"WRITE DOWN THIS WALLET RECOVERY SEED.",
"If you fail to do this, your funds are",
"at risk. Do NOT ignore this step!!!"
]
mb.setText("\n".join(seed_recovery_warning))
mb.setInformativeText(' '.join(words))
mb.setStandardButtons(QMessageBox.Ok)
ret = mb.exec_()
pd = PasswordDialog() pd = PasswordDialog()
while True: while True:
pd.exec_() pd.exec_()
@ -1534,79 +1525,92 @@ class JMMainWindow(QMainWindow):
title="Error") title="Error")
continue continue
break break
self.textpassword = str(pd.new_pw.text())
def getPasswordKey(self):
self.getPassword()
password_key = btc.bin_dbl_sha256(self.textpassword)
return (self.textpassword, password_key)
walletfile = create_wallet_file(str(pd.new_pw.text()), seed) def getWalletName(self):
walletname, ok = QInputDialog.getText(self, 'Choose wallet name', walletname, ok = QInputDialog.getText(self, 'Choose wallet name',
'Enter wallet file name:', 'Enter wallet file name:',
QLineEdit.Normal, "wallet.json") QLineEdit.Normal, "wallet.json")
if not ok: if not ok:
JMQtMessageBox(self, "Create wallet aborted", mbtype='warn') JMQtMessageBox(self, "Create wallet aborted", mbtype='warn')
return return None
#create wallets subdir if it doesn't exist self.walletname = str(walletname)
if not os.path.exists('wallets'): return self.walletname
os.makedirs('wallets')
walletpath = os.path.join('wallets', str(walletname)) def displayWords(self, words):
# Does a wallet with the same name exist? mb = QMessageBox()
if os.path.isfile(walletpath): seed_recovery_warning = [
JMQtMessageBox(self, "WRITE DOWN THIS WALLET RECOVERY SEED.",
walletpath + ' already exists. Aborting.', "If you fail to do this, your funds are",
mbtype='warn', "at risk. Do NOT ignore this step!!!"
title="Error") ]
return mb.setText("\n".join(seed_recovery_warning))
else: mb.setInformativeText(words)
fd = open(walletpath, 'w') mb.setStandardButtons(QMessageBox.Ok)
fd.write(walletfile) ret = mb.exec_()
fd.close()
JMQtMessageBox(self, def initWallet(self, seed=None):
'Wallet saved to ' + str(walletname), '''Creates a new mainnet
wallet
'''
if not seed:
success = wallet_generate_recover_bip39("generate",
"wallets",
"wallet.json",
callbacks=(self.displayWords,
None,
self.getPasswordKey,
self.getWalletName))
if not success:
JMQtMessageBox(self, "Failed to create new wallet file.",
title="Error", mbtype="warn")
return
JMQtMessageBox(self, 'Wallet saved to ' + self.walletname,
title="Wallet created") title="Wallet created")
self.loadWalletFromBlockchain( self.loadWalletFromBlockchain(self.walletname, pwd=self.textpassword)
str(walletname), str(pd.new_pw.text())) else:
print('no seed to do')
def get_wallet_printout(wallet): def get_wallet_printout(wallet):
"""Given a joinmarket wallet, retrieve the list of """Given a joinmarket wallet, retrieve the list of
addresses and corresponding balances to be displayed; addresses and corresponding balances to be displayed.
this could/should be a re-used function for both We retrieve a WalletView abstraction, and iterate over
command line and GUI. sub-objects to arrange the per-mixdepth and per-address lists.
The format of the retrieved data is: The format of the returned data is:
rows: is of format [[[addr,index,bal,used],[addr,...]]*5, rows: is of format [[[addr,index,bal,used],[addr,...]]*5,
[[addr, index,..], [addr, index..]]*5] [[addr, index,..], [addr, index..]]*5]
mbalances: is a simple array of 5 mixdepth balances mbalances: is a simple array of 5 mixdepth balances
total_balance: whole wallet xpubs: [[xpubext, xpubint], ...]
Bitcoin amounts returned are in btc, not satoshis Bitcoin amounts returned are in btc, not satoshis
TODO add metadata such as xpubs
""" """
walletview = wallet_display(wallet, jm_single().config.getint("GUI",
"gaplimit"), False, serialized=False)
rows = [] rows = []
mbalances = [] mbalances = []
total_balance = 0 xpubs = []
for m in range(wallet.max_mix_depth): for j, acct in enumerate(walletview.children):
mbalances.append(acct.get_fmt_balance())
rows.append([]) rows.append([])
balance_depth = 0 xpubs.append([])
for forchange in [0, 1]: for i, branch in enumerate(acct.children):
rows[m].append([]) xpubs[j].append(branch.xpub)
for k in range(wallet.index[m][forchange] + jm_single( rows[j].append([])
).config.getint("GUI", "gaplimit")): for entry in branch.children:
addr = wallet.get_addr(m, forchange, k) rows[-1][i].append([entry.serialize_address(),
balance = 0.0 entry.serialize_wallet_position(),
for addrvalue in wallet.unspent.values(): entry.serialize_amounts(),
if addr == addrvalue['address']: entry.serialize_extra_data()])
balance += addrvalue['value'] return (rows, mbalances, xpubs, walletview.get_fmt_balance())
balance_depth += balance
used = ('used' if k < wallet.index[m][forchange] else 'new')
if balance > 0.0 or (k >= wallet.index[m][forchange] and
forchange == 0):
rows[m][forchange].append([addr, str(k), "{0:.8f}".format(
balance / 1e8), used])
mbalances.append(balance_depth)
total_balance += balance_depth
return (rows, ["{0:.8f}".format(x / 1e8) for x in mbalances],
"{0:.8f}".format(total_balance / 1e8))
################################ ################################
config_load_error = False config_load_error = False
print('Temporarily disabled in version 0.3.0 waiting for update, please use scripts.')
sys.exit(0)
app = QApplication(sys.argv) app = QApplication(sys.argv)
try: try:
load_program_config() load_program_config()

10
scripts/qtsupport.py

@ -25,11 +25,7 @@ from decimal import Decimal
from PyQt4 import QtCore from PyQt4 import QtCore
from PyQt4.QtGui import * from PyQt4.QtGui import *
from jmclient import (load_program_config, get_network, Wallet, from jmclient import (jm_single, validate_address, get_tumble_schedule)
get_p2pk_vbyte, jm_single, validate_address,
get_log, weighted_order_choose, Taker,
JMTakerClientProtocolFactory, WalletError,
start_reactor, get_schedule, get_tumble_schedule)
GREEN_BG = "QWidget {background-color:#80ff80;}" GREEN_BG = "QWidget {background-color:#80ff80;}"
@ -413,8 +409,8 @@ class MyTreeWidget(QTreeWidget):
self.setHeaderLabels(headers) self.setHeaderLabels(headers)
self.header().setStretchLastSection(False) self.header().setStretchLastSection(False)
for col in range(len(headers)): for col in range(len(headers)):
sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents #note, a single stretch column is currently not used.
self.header().setResizeMode(col, sm) self.header().setResizeMode(col, QHeaderView.Interactive)
def editItem(self, item, column): def editItem(self, item, column):
if column in self.editable_columns: if column in self.editable_columns:

Loading…
Cancel
Save