Browse Source

Implement address labeling

master
Kristaps Kaupe 4 years ago
parent
commit
21c0e3e758
No known key found for this signature in database
GPG Key ID: 33E472FE870C7E5D
  1. 1
      README.md
  2. 3
      jmclient/jmclient/__init__.py
  3. 65
      jmclient/jmclient/wallet.py
  4. 49
      jmclient/jmclient/wallet_utils.py
  5. 29
      jmclient/test/test_wallet.py
  6. 48
      jmclient/test/test_walletutils.py
  7. 22
      scripts/joinmarket-qt.py

1
README.md

@ -26,6 +26,7 @@ For a quick introduction to Joinmarket you can watch [this demonstration](https:
* GUI to support Taker role, including tumbler/automated coinjoin sequence. * GUI to support Taker role, including tumbler/automated coinjoin sequence.
* PayJoin - [BIP78](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) to pay users of other wallets (e.g. merchants), as well as between two compatible wallet users (Joinmarket, Wasabi, others). This is a way to boost fungibility/privacy while paying. * PayJoin - [BIP78](https://github.com/bitcoin/bips/blob/master/bip-0078.mediawiki) to pay users of other wallets (e.g. merchants), as well as between two compatible wallet users (Joinmarket, Wasabi, others). This is a way to boost fungibility/privacy while paying.
* Protection from [forced address reuse](https://en.bitcoin.it/wiki/Privacy#Forced_address_reuse) attacks. * Protection from [forced address reuse](https://en.bitcoin.it/wiki/Privacy#Forced_address_reuse) attacks.
* Address labeling
### Quickstart - RECOMMENDED INSTALLATION METHOD (Linux and macOS only) ### Quickstart - RECOMMENDED INSTALLATION METHOD (Linux and macOS only)

3
jmclient/jmclient/__init__.py

@ -15,7 +15,8 @@ from .wallet import (Mnemonic, estimate_tx_fee, WalletError, BaseWallet, ImportW
BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet, BIP39WalletMixin, BIP32Wallet, BIP49Wallet, LegacyWallet,
SegwitWallet, SegwitLegacyWallet, FidelityBondMixin, SegwitWallet, SegwitLegacyWallet, FidelityBondMixin,
FidelityBondWatchonlyWallet, SegwitWalletFidelityBonds, FidelityBondWatchonlyWallet, SegwitWalletFidelityBonds,
UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime) UTXOManager, WALLET_IMPLEMENTATIONS, compute_tx_locktime,
UnknownAddressForLabel)
from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError, from .storage import (Argon2Hash, Storage, StorageError, RetryableStorageError,
StoragePasswordError, VolatileStorage) StoragePasswordError, VolatileStorage)
from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH, EngineError, from .cryptoengine import (BTCEngine, BTC_P2PKH, BTC_P2SH_P2WPKH, BTC_P2WPKH, EngineError,

65
jmclient/jmclient/wallet.py

@ -42,6 +42,12 @@ class WalletError(Exception):
pass pass
class UnknownAddressForLabel(Exception):
def __init__(self, addr):
super().__init__('Unknown address for this wallet: ' + addr)
class Mnemonic(MnemonicParent): class Mnemonic(MnemonicParent):
@classmethod @classmethod
def detect_language(cls, code): def detect_language(cls, code):
@ -276,6 +282,43 @@ class UTXOManager(object):
self.selector is o.selector self.selector is o.selector
class AddressLabelsManager(object):
STORAGE_KEY = b'addr_labels'
def __init__(self, storage):
self.storage = storage
self._addr_labels = None
self._load_storage()
assert self._addr_labels is not None
@classmethod
def initialize(cls, storage):
storage.data[cls.STORAGE_KEY] = {}
def _load_storage(self):
if self.STORAGE_KEY in self.storage.data:
self._addr_labels = self.storage.data[self.STORAGE_KEY]
else:
self._addr_labels = {}
def save(self):
self.storage.data[self.STORAGE_KEY] = self._addr_labels
def get_label(self, address):
address = bytes(address, 'ascii')
if address in self._addr_labels:
return self._addr_labels[address].decode()
else:
return None
def set_label(self, address, label):
address = bytes(address, 'ascii')
if label:
self._addr_labels[address] = bytes(label, 'utf-8')
elif address in self._addr_labels:
del self._addr_labels[address]
class BaseWallet(object): class BaseWallet(object):
TYPE = None TYPE = None
@ -303,6 +346,7 @@ class BaseWallet(object):
self.gap_limit = gap_limit self.gap_limit = gap_limit
self._storage = storage self._storage = storage
self._utxos = None self._utxos = None
self._addr_labels = None
# highest mixdepth ever used in wallet, important for synching # highest mixdepth ever used in wallet, important for synching
self.max_mixdepth = None self.max_mixdepth = None
# effective maximum mixdepth to be used by joinmarket # effective maximum mixdepth to be used by joinmarket
@ -350,6 +394,7 @@ class BaseWallet(object):
.format(self.TYPE)) .format(self.TYPE))
self.network = self._storage.data[b'network'].decode('ascii') self.network = self._storage.data[b'network'].decode('ascii')
self._utxos = UTXOManager(self._storage, self.merge_algorithm) self._utxos = UTXOManager(self._storage, self.merge_algorithm)
self._addr_labels = AddressLabelsManager(self._storage)
def get_storage_location(self): def get_storage_location(self):
""" Return the location of the """ Return the location of the
@ -364,6 +409,7 @@ class BaseWallet(object):
Write data to associated storage object and trigger persistent update. Write data to associated storage object and trigger persistent update.
""" """
self._utxos.save() self._utxos.save()
self._addr_labels.save()
@classmethod @classmethod
def initialize(cls, storage, network, max_mixdepth=2, timestamp=None, def initialize(cls, storage, network, max_mixdepth=2, timestamp=None,
@ -395,6 +441,7 @@ class BaseWallet(object):
storage.data[b'wallet_type'] = cls.TYPE storage.data[b'wallet_type'] = cls.TYPE
UTXOManager.initialize(storage) UTXOManager.initialize(storage)
AddressLabelsManager.initialize(storage)
if write: if write:
storage.save() storage.save()
@ -784,10 +831,12 @@ class BaseWallet(object):
continue continue
script = self.get_script_from_path(path) script = self.get_script_from_path(path)
addr = self.get_address_from_path(path) addr = self.get_address_from_path(path)
label = self.get_address_label(addr)
script_utxos[md][utxo] = {'script': script, script_utxos[md][utxo] = {'script': script,
'path': path, 'path': path,
'value': value, 'value': value,
'address': addr} 'address': addr,
'label': label}
if includeheight: if includeheight:
script_utxos[md][utxo]['height'] = height script_utxos[md][utxo]['height'] = height
return script_utxos return script_utxos
@ -1048,12 +1097,26 @@ class BaseWallet(object):
return False return False
return True return True
def set_address_label(self, addr, label):
if self.is_known_addr(addr):
self._addr_labels.set_label(addr, label)
self.save()
else:
raise UnknownAddressForLabel(addr)
def get_address_label(self, addr):
if self.is_known_addr(addr):
return self._addr_labels.get_label(addr)
else:
raise UnknownAddressForLabel(addr)
def close(self): def close(self):
self._storage.close() self._storage.close()
def __del__(self): def __del__(self):
self.close() self.close()
class PSBTWalletMixin(object): class PSBTWalletMixin(object):
""" """
Mixin for BaseWallet to provide BIP174 Mixin for BaseWallet to provide BIP174

49
jmclient/jmclient/wallet_utils.py

@ -52,6 +52,7 @@ The method is one of the following:
(addtxoutproof) Add a tx out proof as metadata to a burner transaction. Specify path with (addtxoutproof) Add a tx out proof as metadata to a burner transaction. Specify path with
-H and proof which is output of Bitcoin Core\'s RPC call gettxoutproof. -H and proof which is output of Bitcoin Core\'s RPC call gettxoutproof.
(createwatchonly) Create a watch-only fidelity bond wallet. (createwatchonly) Create a watch-only fidelity bond wallet.
(setlabel) Set the label associated with the given address.
""" """
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]', parser = OptionParser(usage='usage: %prog [options] [wallet file] [method] [args..]',
description=description, formatter=IndentedHelpFormatterWithNL()) description=description, formatter=IndentedHelpFormatterWithNL())
@ -146,7 +147,8 @@ class WalletViewBase(object):
class WalletViewEntry(WalletViewBase): class WalletViewEntry(WalletViewBase):
def __init__(self, wallet_path_repr, account, address_type, aindex, addr, amounts, def __init__(self, wallet_path_repr, account, address_type, aindex, addr, amounts,
used = 'new', serclass=str, priv=None, custom_separator=None): used = 'new', serclass=str, priv=None, custom_separator=None,
label=None):
super().__init__(wallet_path_repr, serclass=serclass, super().__init__(wallet_path_repr, serclass=serclass,
custom_separator=custom_separator) custom_separator=custom_separator)
self.account = account self.account = account
@ -162,6 +164,7 @@ class WalletViewEntry(WalletViewBase):
#note no validation here #note no validation here
self.private_key = priv self.private_key = priv
self.used = used self.used = used
self.label = label
def get_balance(self, include_unconf=True): def get_balance(self, include_unconf=True):
"""Overwrites base class since no children """Overwrites base class since no children
@ -174,8 +177,11 @@ class WalletViewEntry(WalletViewBase):
left = self.serialize_wallet_position() left = self.serialize_wallet_position()
addr = self.serialize_address() addr = self.serialize_address()
amounts = self.serialize_amounts() amounts = self.serialize_amounts()
used = self.serialize_used()
label = self.serialize_label()
extradata = self.serialize_extra_data() extradata = self.serialize_extra_data()
return self.serclass(self.separator.join([left, addr, amounts, extradata])) return self.serclass(self.separator.join([
left, addr, amounts, used, label, extradata]))
def serialize_wallet_position(self): def serialize_wallet_position(self):
return self.wallet_path_repr.ljust(20) return self.wallet_path_repr.ljust(20)
@ -191,11 +197,20 @@ class WalletViewEntry(WalletViewBase):
"not yet implemented.") "not yet implemented.")
return self.serclass("{0:.08f}".format(self.unconfirmed_amount/1e8)) return self.serclass("{0:.08f}".format(self.unconfirmed_amount/1e8))
def serialize_used(self):
return self.serclass(self.used)
def serialize_label(self):
if self.label:
return self.serclass(self.label)
else:
return self.serclass("")
def serialize_extra_data(self): def serialize_extra_data(self):
ed = self.used
if self.private_key: if self.private_key:
ed += self.separator + self.serclass(self.private_key) return self.serclass(self.private_key)
return self.serclass(ed) else:
return self.serclass("")
class WalletViewEntryBurnOutput(WalletViewEntry): class WalletViewEntryBurnOutput(WalletViewEntry):
# balance in burn outputs shouldnt be counted # balance in burn outputs shouldnt be counted
@ -368,7 +383,9 @@ def wallet_showutxos(wallet_service, showprivkey):
tries = podle.get_podle_tries(u, key, max_tries) tries = podle.get_podle_tries(u, key, max_tries)
tries_remaining = max(0, max_tries - tries) tries_remaining = max(0, max_tries - tries)
mixdepth = wallet_service.wallet.get_details(av['path'])[0] mixdepth = wallet_service.wallet.get_details(av['path'])[0]
unsp[us] = {'address': av['address'], 'value': av['value'], unsp[us] = {'address': av['address'],
'label': av['label'] if av['label'] else "",
'value': av['value'],
'tries': tries, 'tries_remaining': tries_remaining, 'tries': tries, 'tries_remaining': tries_remaining,
'external': False, 'external': False,
'mixdepth': mixdepth, 'mixdepth': mixdepth,
@ -389,7 +406,7 @@ def wallet_showutxos(wallet_service, showprivkey):
unsp[us] = {'tries': tries, 'tries_remaining': tries_remaining, unsp[us] = {'tries': tries, 'tries_remaining': tries_remaining,
'external': True} 'external': True}
return json.dumps(unsp, indent=4) return json.dumps(unsp, indent=4, ensure_ascii=False)
def wallet_display(wallet_service, showprivkey, displayall=False, def wallet_display(wallet_service, showprivkey, displayall=False,
@ -453,6 +470,7 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
for k in range(unused_index + wallet_service.gap_limit): for k in range(unused_index + wallet_service.gap_limit):
path = wallet_service.get_path(m, address_type, k) path = wallet_service.get_path(m, address_type, k)
addr = wallet_service.get_address_from_path(path) addr = wallet_service.get_address_from_path(path)
label = wallet_service.get_address_label(addr)
balance, used = get_addr_status( balance, used = get_addr_status(
path, utxos[m], k >= unused_index, address_type) path, utxos[m], k >= unused_index, address_type)
if showprivkey: if showprivkey:
@ -463,7 +481,7 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
(used == 'new' and address_type == 0)): (used == 'new' and address_type == 0)):
entrylist.append(WalletViewEntry( entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k, addr, wallet_service.get_path_repr(path), m, address_type, k, addr,
[balance, balance], priv=privkey, used=used)) [balance, balance], priv=privkey, used=used, label=label))
wallet_service.set_next_index(m, address_type, unused_index) wallet_service.set_next_index(m, address_type, unused_index)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type)) path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist, branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
@ -476,6 +494,7 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
for timenumber in range(FidelityBondMixin.TIMENUMBER_COUNT): for timenumber in range(FidelityBondMixin.TIMENUMBER_COUNT):
path = wallet_service.get_path(m, address_type, timenumber) path = wallet_service.get_path(m, address_type, timenumber)
addr = wallet_service.get_address_from_path(path) addr = wallet_service.get_address_from_path(path)
label = wallet_service.get_address_label(addr)
timelock = datetime.utcfromtimestamp(0) + timedelta(seconds=path[-1]) timelock = datetime.utcfromtimestamp(0) + timedelta(seconds=path[-1])
balance = sum([utxodata["value"] for utxo, utxodata in balance = sum([utxodata["value"] for utxo, utxodata in
@ -488,7 +507,8 @@ def wallet_display(wallet_service, showprivkey, displayall=False,
if displayall or balance > 0: if displayall or balance > 0:
entrylist.append(WalletViewEntry( entrylist.append(WalletViewEntry(
wallet_service.get_path_repr(path), m, address_type, k, wallet_service.get_path_repr(path), m, address_type, k,
addr, [balance, balance], priv=privkey, used=status)) addr, [balance, balance], priv=privkey, used=status,
label=label))
xpub_key = wallet_service.get_bip32_pub_export(m, address_type) xpub_key = wallet_service.get_bip32_pub_export(m, address_type)
path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type)) path = wallet_service.get_path_repr(wallet_service.get_path(m, address_type))
branchlist.append(WalletViewBranch(path, m, address_type, entrylist, branchlist.append(WalletViewBranch(path, m, address_type, entrylist,
@ -1459,7 +1479,7 @@ def wallet_tool_main(wallet_root_path):
noseed_methods = ['generate', 'recover', 'createwatchonly'] noseed_methods = ['generate', 'recover', 'createwatchonly']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey', methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos', 'freeze', 'gettimelockaddress', 'history', 'showutxos', 'freeze', 'gettimelockaddress',
'addtxoutproof', 'changepass'] 'addtxoutproof', 'changepass', 'setlabel']
methods.extend(noseed_methods) methods.extend(noseed_methods)
noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage', noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage',
'changepass'] 'changepass']
@ -1584,6 +1604,15 @@ def wallet_tool_main(wallet_root_path):
jmprint("args: [master public key]", "error") jmprint("args: [master public key]", "error")
sys.exit(EXIT_ARGERROR) sys.exit(EXIT_ARGERROR)
return wallet_createwatchonly(wallet_root_path, args[1]) return wallet_createwatchonly(wallet_root_path, args[1])
elif method == "setlabel":
if len(args) < 4:
jmprint("args: address label", "error")
sys.exit(EXIT_ARGERROR)
wallet.set_address_label(args[2], args[3])
if args[3]:
return "Address label set"
else:
return "Address label removed"
else: else:
parser.error("Unknown wallet-tool method: " + method) parser.error("Unknown wallet-tool method: " + method)
sys.exit(EXIT_ARGERROR) sys.exit(EXIT_ARGERROR)

29
jmclient/test/test_wallet.py

@ -13,9 +13,12 @@ from jmclient import load_test_config, jm_single, BaseWallet, \
VolatileStorage, get_network, cryptoengine, WalletError,\ VolatileStorage, get_network, cryptoengine, WalletError,\
SegwitWallet, WalletService, SegwitWalletFidelityBonds,\ SegwitWallet, WalletService, SegwitWalletFidelityBonds,\
create_wallet, open_test_wallet_maybe, \ create_wallet, open_test_wallet_maybe, \
FidelityBondMixin, FidelityBondWatchonlyWallet, wallet_gettimelockaddress FidelityBondMixin, FidelityBondWatchonlyWallet,\
wallet_gettimelockaddress, UnknownAddressForLabel
from test_blockchaininterface import sync_test_wallet from test_blockchaininterface import sync_test_wallet
from freezegun import freeze_time from freezegun import freeze_time
from bitcointx.wallet import CCoinAddressError
testdir = os.path.dirname(os.path.realpath(__file__)) testdir = os.path.dirname(os.path.realpath(__file__))
@ -557,6 +560,30 @@ def test_remove_old_utxos(setup_wallet):
assert len(balances) == wallet.max_mixdepth + 1 assert len(balances) == wallet.max_mixdepth + 1
def test_address_labels(setup_wallet):
wallet = get_populated_wallet(num=2)
addr1 = wallet.get_internal_addr(0)
addr2 = wallet.get_internal_addr(1)
assert wallet.get_address_label(addr2) is None
assert wallet.get_address_label(addr2) is None
wallet.set_address_label(addr1, "test")
# utf-8 characters here are on purpose, to test utf-8 encoding / decoding
wallet.set_address_label(addr2, "glāžšķūņu rūķīši")
assert wallet.get_address_label(addr1) == "test"
assert wallet.get_address_label(addr2) == "glāžšķūņu rūķīši"
wallet.set_address_label(addr1, "")
wallet.set_address_label(addr2, None)
assert wallet.get_address_label(addr2) is None
assert wallet.get_address_label(addr2) is None
with pytest.raises(UnknownAddressForLabel):
wallet.get_address_label("2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4")
wallet.set_address_label("2MzY5yyonUY7zpHspg7jB7WQs1uJxKafQe4",
"test")
with pytest.raises(CCoinAddressError):
wallet.get_address_label("badaddress")
wallet.set_address_label("badaddress", "test")
def test_initialize_twice(setup_wallet): def test_initialize_twice(setup_wallet):
wallet = get_populated_wallet(num=0) wallet = get_populated_wallet(num=0)
storage = wallet._storage storage = wallet._storage

48
jmclient/test/test_walletutils.py

@ -64,44 +64,44 @@ def test_walletview():
'JM wallet\n' 'JM wallet\n'
'mixdepth\t0\n' 'mixdepth\t0\n'
'external addresses\tm/0\txpubDUMMYXPUB0\n' 'external addresses\tm/0\txpubDUMMYXPUB0\n'
'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB1\n' 'internal addresses\tm/0\txpubDUMMYXPUB1\n'
'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS0\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS1\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 0:\t1.20000000\n' 'Balance for mixdepth 0:\t1.20000000\n'
'mixdepth\t1\n' 'mixdepth\t1\n'
'external addresses\tm/0\txpubDUMMYXPUB1\n' 'external addresses\tm/0\txpubDUMMYXPUB1\n'
'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB2\n' 'internal addresses\tm/0\txpubDUMMYXPUB2\n'
'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS1\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS4\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 1:\t1.20000000\n' 'Balance for mixdepth 1:\t1.20000000\n'
'mixdepth\t2\n' 'mixdepth\t2\n'
'external addresses\tm/0\txpubDUMMYXPUB2\n' 'external addresses\tm/0\txpubDUMMYXPUB2\n'
'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'internal addresses\tm/0\txpubDUMMYXPUB3\n' 'internal addresses\tm/0\txpubDUMMYXPUB3\n'
'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\n' 'm/0 \tDUMMYADDRESS2\t0.00000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\n' 'm/0 \tDUMMYADDRESS3\t0.10000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\n' 'm/0 \tDUMMYADDRESS4\t0.20000000\tnew\t\t\n'
'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\n' 'm/0 \tDUMMYADDRESS5\t0.30000000\tnew\t\t\n'
'Balance:\t0.60000000\n' 'Balance:\t0.60000000\n'
'Balance for mixdepth 2:\t1.20000000\n' 'Balance for mixdepth 2:\t1.20000000\n'
'Total balance:\t3.60000000')) 'Total balance:\t3.60000000'))

22
scripts/joinmarket-qt.py

@ -1305,7 +1305,7 @@ class CoinsTab(QWidget):
def getHeaders(self): def getHeaders(self):
'''Function included in case dynamic in future''' '''Function included in case dynamic in future'''
return ['Txid:n', 'Amount in BTC', 'Address'] return ['Txid:n', 'Amount in BTC', 'Address', 'Label']
def updateUtxos(self): def updateUtxos(self):
""" Note that this refresh of the display only accesses in-process """ Note that this refresh of the display only accesses in-process
@ -1313,7 +1313,7 @@ class CoinsTab(QWidget):
""" """
self.cTW.clear() self.cTW.clear()
def show_blank(): def show_blank():
m_item = QTreeWidgetItem(["No coins", "", ""]) m_item = QTreeWidgetItem(["No coins", "", "", ""])
self.cTW.addChild(m_item) self.cTW.addChild(m_item)
self.cTW.show() self.cTW.show()
@ -1335,15 +1335,15 @@ class CoinsTab(QWidget):
for i in range(jm_single().config.getint("GUI", "max_mix_depth")): for i in range(jm_single().config.getint("GUI", "max_mix_depth")):
uem = utxos_enabled.get(i) uem = utxos_enabled.get(i)
udm = utxos_disabled.get(i) udm = utxos_disabled.get(i)
m_item = QTreeWidgetItem(["Mixdepth " + str(i), '', '']) m_item = QTreeWidgetItem(["Mixdepth " + str(i), '', '', ''])
self.cTW.addChild(m_item) self.cTW.addChild(m_item)
for heading in ["NOT FROZEN", "FROZEN"]: for heading in ["NOT FROZEN", "FROZEN"]:
um = uem if heading == "NOT FROZEN" else udm um = uem if heading == "NOT FROZEN" else udm
seq_item = QTreeWidgetItem([heading, '', '']) seq_item = QTreeWidgetItem([heading, '', '', ''])
m_item.addChild(seq_item) m_item.addChild(seq_item)
seq_item.setExpanded(True) seq_item.setExpanded(True)
if um is None: if um is None:
item = QTreeWidgetItem(['None', '', '']) item = QTreeWidgetItem(['None', '', '', ''])
seq_item.addChild(item) seq_item.addChild(item)
else: else:
for k, v in um.items(): for k, v in um.items():
@ -1353,7 +1353,7 @@ class CoinsTab(QWidget):
assert success assert success
s = "{0:.08f}".format(v['value']/1e8) s = "{0:.08f}".format(v['value']/1e8)
a = mainWindow.wallet_service.script_to_addr(v["script"]) a = mainWindow.wallet_service.script_to_addr(v["script"])
item = QTreeWidgetItem([t, s, a]) item = QTreeWidgetItem([t, s, a, v["label"]])
item.setFont(0, QFont(MONOSPACE_FONT)) item.setFont(0, QFont(MONOSPACE_FONT))
#if rows[i][forchange][j][3] != 'new': #if rows[i][forchange][j][3] != 'new':
# item.setForeground(3, QBrush(QColor('red'))) # item.setForeground(3, QBrush(QColor('red')))
@ -1430,7 +1430,7 @@ class JMWalletTab(QWidget):
def getHeaders(self): def getHeaders(self):
'''Function included in case dynamic in future''' '''Function included in case dynamic in future'''
return ['Address', 'Index', 'Balance', 'Used/New'] return ['Address', 'Index', 'Balance', 'Used/New', 'Label']
def create_menu(self, position): def create_menu(self, position):
item = self.walletTree.currentItem() item = self.walletTree.currentItem()
@ -1451,6 +1451,9 @@ class JMWalletTab(QWidget):
menu.addAction("Copy address to clipboard", menu.addAction("Copy address to clipboard",
lambda: app.clipboard().setText(txt), lambda: app.clipboard().setText(txt),
shortcut=QKeySequence(QKeySequence.Copy)) shortcut=QKeySequence(QKeySequence.Copy))
if item.text(4):
menu.addAction("Copy label to clipboard",
lambda: app.clipboard().setText(item.text(4)))
# Show QR code option only for new addresses to avoid address reuse # Show QR code option only for new addresses to avoid address reuse
if item.text(3) == "new": if item.text(3) == "new":
menu.addAction("Show QR code", menu.addAction("Show QR code",
@ -2242,7 +2245,7 @@ def get_wallet_printout(wallet_service):
We retrieve a WalletView abstraction, and iterate over We retrieve a WalletView abstraction, and iterate over
sub-objects to arrange the per-mixdepth and per-address lists. sub-objects to arrange the per-mixdepth and per-address lists.
The format of the returned 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,label],[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
xpubs: [[xpubext, xpubint], ...] xpubs: [[xpubext, xpubint], ...]
@ -2263,7 +2266,8 @@ def get_wallet_printout(wallet_service):
rows[-1][i].append([entry.serialize_address(), rows[-1][i].append([entry.serialize_address(),
entry.serialize_wallet_position(), entry.serialize_wallet_position(),
entry.serialize_amounts(), entry.serialize_amounts(),
entry.serialize_extra_data()]) entry.serialize_used(),
entry.serialize_label()])
# in case the wallet is not yet synced, don't return an incorrect # in case the wallet is not yet synced, don't return an incorrect
# 0 balance, but signal incompleteness: # 0 balance, but signal incompleteness:
total_bal = walletview.get_fmt_balance() if wallet_service.synced else None total_bal = walletview.get_fmt_balance() if wallet_service.synced else None

Loading…
Cancel
Save