Browse Source

Basic coin control.

Wallet persists utxo metadata; currently only
contains a field 'disabled' indexed by utxo.
User can switch this on or off (enabled) via
wallet-tool 'freeze' method.
Disabled utxos will not be used in coin
selection in any transaction.
Wallet still displays all utxo balances in
display method (and in GUI).

Add tests of disabling to test_utxomanager

Add Coins tab to Qt, with freeze/unfreeze feature.

Coins tab shows updated utxo info txid:n, amt, address and
enabled/disabled, which can be toggled from right click menu.
master
AdamISZ 7 years ago
parent
commit
9295673821
No known key found for this signature in database
GPG Key ID: 141001A1AF77F20B
  1. 2
      jmclient/jmclient/__init__.py
  2. 91
      jmclient/jmclient/wallet.py
  3. 120
      jmclient/jmclient/wallet_utils.py
  4. 38
      jmclient/test/test_utxomanager.py
  5. 16
      jmclient/test/test_wallet.py
  6. 3
      scripts/add-utxo.py
  7. 114
      scripts/joinmarket-qt.py

2
jmclient/jmclient/__init__.py

@ -45,7 +45,7 @@ from .taker_utils import (tumbler_taker_finished_update, restart_waiter,
from .wallet_utils import (
wallet_tool_main, wallet_generate_recover_bip39, open_wallet,
open_test_wallet_maybe, create_wallet, get_wallet_cls, get_wallet_path,
wallet_display)
wallet_display, get_utxos_enabled_disabled)
from .maker import Maker, P2EPMaker
from .yieldgenerator import YieldGenerator, YieldGeneratorBasic, ygmain
# Set default logging handler to avoid "No handler found" warnings.

91
jmclient/jmclient/wallet.py

@ -117,6 +117,7 @@ def deprecated(func):
class UTXOManager(object):
STORAGE_KEY = b'utxo'
METADATA_KEY = b'meta'
TXID_LEN = 32
def __init__(self, storage, merge_func):
@ -124,6 +125,11 @@ class UTXOManager(object):
self.selector = merge_func
# {mixdexpth: {(txid, index): (path, value)}}
self._utxo = None
# metadata kept as a separate key in the database
# for backwards compat; value as dict for forward-compat.
# format is {(txid, index): value-dict} with "disabled"
# as the only currently used key in the dict.
self._utxo_meta = None
self._load_storage()
assert self._utxo is not None
@ -135,6 +141,7 @@ class UTXOManager(object):
assert isinstance(self.storage.data[self.STORAGE_KEY], dict)
self._utxo = collections.defaultdict(dict)
self._utxo_meta = collections.defaultdict(dict)
for md, data in self.storage.data[self.STORAGE_KEY].items():
md = int(md)
md_data = self._utxo[md]
@ -143,28 +150,46 @@ class UTXOManager(object):
index = int(utxo[self.TXID_LEN:])
md_data[(txid, index)] = value
# Wallets may not have any metadata
if self.METADATA_KEY in self.storage.data:
for utxo, value in self.storage.data[self.METADATA_KEY].items():
txid = utxo[:self.TXID_LEN]
index = int(utxo[self.TXID_LEN:])
self._utxo_meta[(txid, index)] = value
def save(self, write=True):
new_data = {}
self.storage.data[self.STORAGE_KEY] = new_data
for md, data in self._utxo.items():
md = _int_to_bytestr(md)
new_data[md] = {}
# storage keys must be bytes()
for (txid, index), value in data.items():
new_data[md][txid + _int_to_bytestr(index)] = value
new_meta_data = {}
self.storage.data[self.METADATA_KEY] = new_meta_data
for (txid, index), value in self._utxo_meta.items():
new_meta_data[txid + _int_to_bytestr(index)] = value
if write:
self.storage.save()
def reset(self):
self._utxo = collections.defaultdict(dict)
def have_utxo(self, txid, index):
def have_utxo(self, txid, index, include_disabled=True):
if not include_disabled and self.is_disabled(txid, index):
return False
for md in self._utxo:
if (txid, index) in self._utxo[md]:
return md
return False
def remove_utxo(self, txid, index, mixdepth):
# currently does not remove metadata associated
# with this utxo
assert isinstance(txid, bytes)
assert len(txid) == self.TXID_LEN
assert isinstance(index, numbers.Integral)
@ -173,6 +198,8 @@ class UTXOManager(object):
return self._utxo[mixdepth].pop((txid, index))
def add_utxo(self, txid, index, path, value, mixdepth):
# Assumed: that we add a utxo only if we want it enabled,
# so metadata is not currently added.
assert isinstance(txid, bytes)
assert len(txid) == self.TXID_LEN
assert isinstance(index, numbers.Integral)
@ -181,22 +208,59 @@ class UTXOManager(object):
self._utxo[mixdepth][(txid, index)] = (path, value)
def is_disabled(self, txid, index):
if not self._utxo_meta:
return False
if (txid, index) not in self._utxo_meta:
return False
if b'disabled' not in self._utxo_meta[(txid, index)]:
return False
if not self._utxo_meta[(txid, index)][b'disabled']:
return False
return True
def disable_utxo(self, txid, index, disable=True):
assert isinstance(txid, bytes)
assert len(txid) == self.TXID_LEN
assert isinstance(index, numbers.Integral)
if b'disabled' not in self._utxo_meta[(txid, index)]:
self._utxo_meta[(txid, index)] = {}
self._utxo_meta[(txid, index)][b'disabled'] = disable
def enable_utxo(self, txid, index):
self.disable_utxo(txid, index, disable=False)
def select_utxos(self, mixdepth, amount, utxo_filter=(), select_fn=None):
assert isinstance(mixdepth, numbers.Integral)
utxos = self._utxo[mixdepth]
# do not select anything in the filter
available = [{'utxo': utxo, 'value': val}
for utxo, (addr, val) in utxos.items() if utxo not in utxo_filter]
# do not select anything disabled
available = [u for u in available if not self.is_disabled(*u['utxo'])]
selector = select_fn or self.selector
selected = selector(available, amount)
return {s['utxo']: {'path': utxos[s['utxo']][0],
'value': utxos[s['utxo']][1]}
for s in selected}
def get_balance_by_mixdepth(self, max_mixdepth=float('Inf')):
def get_balance_by_mixdepth(self, max_mixdepth=float('Inf'),
include_disabled=True):
""" By default this returns a dict of aggregated bitcoin
balance per mixdepth: {0: N sats, 1: M sats, ...} for all
currently available mixdepths.
If max_mixdepth is set it will return balances only up
to that mixdepth.
To get only enabled balance, set include_disabled=False.
"""
balance_dict = collections.defaultdict(int)
for mixdepth, utxomap in self._utxo.items():
if mixdepth > max_mixdepth:
continue
if not include_disabled:
utxomap = {k: v for k, v in utxomap.items(
) if not self.is_disabled(*k)}
value = sum(x[1] for x in utxomap.values())
balance_dict[mixdepth] = value
return balance_dict
@ -285,7 +349,7 @@ class BaseWallet(object):
"""
Write data to associated storage object and trigger persistent update.
"""
self._storage.save()
self._utxos.save()
@classmethod
def initialize(cls, storage, network, max_mixdepth=2, timestamp=None,
@ -610,17 +674,28 @@ class BaseWallet(object):
return ret
def disable_utxo(self, txid, index, disable=True):
self._utxos.disable_utxo(txid, index, disable)
# make sure the utxo database is persisted
self.save()
def toggle_disable_utxo(self, txid, index):
is_disabled = self._utxos.is_disabled(txid, index)
self.disable_utxo(txid, index, disable= not is_disabled)
def reset_utxos(self):
self._utxos.reset()
def get_balance_by_mixdepth(self, verbose=True):
def get_balance_by_mixdepth(self, verbose=True,
include_disabled=False):
"""
Get available funds in each active mixdepth.
By default ignores disabled utxos in calculation.
returns: {mixdepth: value}
"""
# TODO: verbose
return self._utxos.get_balance_by_mixdepth(max_mixdepth=self.mixdepth)
return self._utxos.get_balance_by_mixdepth(max_mixdepth=self.mixdepth,
include_disabled=include_disabled)
@deprecated
def get_utxos_by_mixdepth(self, verbose=True):
@ -636,7 +711,7 @@ class BaseWallet(object):
utxos_conv[md][utxo_str] = data
return utxos_conv
def get_utxos_by_mixdepth_(self):
def get_utxos_by_mixdepth_(self, include_disabled=False):
"""
Get all UTXOs for active mixdepths.
@ -651,6 +726,8 @@ class BaseWallet(object):
if md > self.mixdepth:
continue
for utxo, (path, value) in data.items():
if not include_disabled and self._utxos.is_disabled(*utxo):
continue
script = self.get_script_path(path)
script_utxos[md][utxo] = {'script': script,
'path': path,

120
jmclient/jmclient/wallet_utils.py

@ -18,6 +18,7 @@ from jmclient import (get_network, WALLET_IMPLEMENTATIONS, Storage, podle,
LegacyWallet, SegwitWallet, is_native_segwit_mode)
from jmbase.support import get_password, jmprint
from .cryptoengine import TYPE_P2PKH, TYPE_P2SH_P2WPKH, TYPE_P2WPKH
from .output import fmt_utxo
import jmbitcoin as btc
@ -40,7 +41,8 @@ def get_wallettool_parser():
'(importprivkey) Adds privkeys to this wallet, privkeys are spaces or commas separated.\n'
'(dumpprivkey) Export a single private key, specify an hd wallet path\n'
'(signmessage) Sign a message with the private key from an address in \n'
'the wallet. Use with -H and specify an HD wallet path for the address.')
'the wallet. Use with -H and specify an HD wallet path for the address.\n'
'(freeze) Freeze or un-freeze a specific utxo. Specify mixdepth with -m.')
parser = OptionParser(usage='usage: %prog [options] [wallet file] [method]',
description=description)
parser.add_option('-p',
@ -332,7 +334,8 @@ def get_imported_privkey_branch(wallet, m, showprivkey):
addr = wallet.get_addr_path(path)
script = wallet.get_script_path(path)
balance = 0.0
for data in wallet.get_utxos_by_mixdepth_()[m].values():
for data in wallet.get_utxos_by_mixdepth_(
include_disabled=True)[m].values():
if script == data['script']:
balance += data['value']
used = ('used' if balance > 0.0 else 'empty')
@ -407,7 +410,9 @@ def wallet_display(wallet, gaplimit, showprivkey, displayall=False,
return addr_balance, out_status
acctlist = []
utxos = wallet.get_utxos_by_mixdepth_()
# TODO - either optionally not show disabled utxos, or
# mark them differently in display (labels; colors)
utxos = wallet.get_utxos_by_mixdepth_(include_disabled=True)
for m in range(wallet.mixdepth + 1):
branchlist = []
for forchange in [0, 1]:
@ -816,12 +821,15 @@ def wallet_fetch_history(wallet, options):
jmprint('scipy not installed, unable to predict accumulation rate')
jmprint('to add it to this virtualenv, use `pip install scipy`')
total_wallet_balance = sum(wallet.get_balance_by_mixdepth().values())
# includes disabled utxos in accounting:
total_wallet_balance = sum(wallet.get_balance_by_mixdepth(
include_disabled=True).values())
if balance != total_wallet_balance:
jmprint(('BUG ERROR: wallet balance (%s) does not match balance from ' +
'history (%s)') % (sat_to_str(total_wallet_balance),
sat_to_str(balance)))
wallet_utxo_count = sum(map(len, wallet.get_utxos_by_mixdepth_().values()))
wallet_utxo_count = sum(map(len, wallet.get_utxos_by_mixdepth_(
include_disabled=True).values()))
if utxo_count != wallet_utxo_count:
jmprint(('BUG ERROR: wallet utxo count (%d) does not match utxo count from ' +
'history (%s)') % (wallet_utxo_count, utxo_count))
@ -894,6 +902,104 @@ def wallet_signmessage(wallet, hdpath, message):
retval = "Signature: {}\nTo verify this in Bitcoin Core".format(sig)
return retval + " use the RPC command 'verifymessage'"
def display_utxos_for_disable_choice_default(utxos_enabled, utxos_disabled):
""" CLI implementation of the callback required as described in
wallet_disableutxo
"""
def default_user_choice(umax):
jmprint("Choose an index 0 .. {} to freeze/unfreeze or "
"-1 to just quit.".format(umax))
while True:
try:
ret = int(input())
except ValueError:
jmprint("Invalid choice, must be an integer.", "error")
continue
if not isinstance(ret, int) or ret < -1 or ret > umax:
jmprint("Invalid choice, must be between: -1 and {}, "
"try again.".format(umax), "error")
continue
break
return ret
def output_utxos(utxos, status, start=0):
for (txid, idx), v in utxos.items():
value = v['value']
jmprint("{:4}: {:68}: {} sats, -- {}".format(
start, fmt_utxo((txid, idx)), value, status))
start += 1
yield txid, idx
ulist = list(output_utxos(utxos_disabled, 'FROZEN'))
disabled_max = len(ulist) - 1
ulist.extend(output_utxos(utxos_enabled, 'NOT FROZEN', start=len(ulist)))
max_id = len(ulist) - 1
chosen_idx = default_user_choice(max_id)
if chosen_idx == -1:
return None
# the return value 'disable' is the action we are going to take;
# so it should be true if the utxos is currently unfrozen/enabled.
disable = False if chosen_idx <= disabled_max else True
return ulist[chosen_idx], disable
def get_utxos_enabled_disabled(wallet, md):
""" Returns dicts for enabled and disabled separately
"""
utxos_enabled = wallet.get_utxos_by_mixdepth_()[md]
utxos_all = wallet.get_utxos_by_mixdepth_(include_disabled=True)[md]
utxos_disabled_keyset = set(utxos_all).difference(set(utxos_enabled))
utxos_disabled = {}
for u in utxos_disabled_keyset:
utxos_disabled[u] = utxos_all[u]
return utxos_enabled, utxos_disabled
def wallet_freezeutxo(wallet, md, display_callback=None, info_callback=None):
""" Given a wallet and a mixdepth, display to the user
the set of available utxos, indexed by integer, and accept a choice
of index to "freeze", then commit this disabling to the wallet storage,
so that this disable-ment is persisted. Also allow unfreezing of a
chosen utxo which is currently frozen.
Callbacks for display and reporting can be specified in the keyword
arguments as explained below, otherwise default CLI is used.
** display_callback signature:
args:
1. utxos_enabled ; dict of utxos as format in wallet.py.
2. utxos_disabled ; as above, for disabled
returns:
1.((txid(str), index(int)), disabled(bool)) of chosen utxo
for freezing/unfreezing, or None for no action/cancel.
** info_callback signature:
args:
1. message (str)
2. type (str) ("info", "error" etc as per jmprint)
returns: None
"""
if display_callback is None:
display_callback = display_utxos_for_disable_choice_default
if info_callback is None:
info_callback = jmprint
if md is None:
info_callback("Specify the mixdepth with the -m flag", "error")
return "Failed"
utxos_enabled, utxos_disabled = get_utxos_enabled_disabled(wallet, md)
if utxos_disabled == {} and utxos_enabled == {}:
info_callback("The mixdepth: " + str(md) + \
" contains no utxos to freeze/unfreeze.", "error")
return "Failed"
display_ret = display_callback(utxos_enabled, utxos_disabled)
if display_ret is None:
return "OK"
(txid, index), disable = display_ret
wallet.disable_utxo(txid, index, disable)
if disable:
info_callback("Utxo: {} is now frozen and unavailable for spending."
.format(fmt_utxo((txid, index))))
else:
info_callback("Utxo: {} is now unfrozen and available for spending."
.format(fmt_utxo((txid, index))))
return "Done"
def get_wallet_type():
if is_segwit_mode():
@ -1047,7 +1153,7 @@ def wallet_tool_main(wallet_root_path):
noseed_methods = ['generate', 'recover']
methods = ['display', 'displayall', 'summary', 'showseed', 'importprivkey',
'history', 'showutxos']
'history', 'showutxos', 'freeze']
methods.extend(noseed_methods)
noscan_methods = ['showseed', 'importprivkey', 'dumpprivkey', 'signmessage']
readonly_methods = ['display', 'displayall', 'summary', 'showseed',
@ -1120,6 +1226,8 @@ def wallet_tool_main(wallet_root_path):
return "Key import completed."
elif method == "signmessage":
return wallet_signmessage(wallet, options.hd_path, args[2])
elif method == "freeze":
return wallet_freezeutxo(wallet, options.mixdepth)
#Testing (can port to test modules, TODO)

38
jmclient/test/test_utxomanager.py

@ -16,6 +16,12 @@ def select(unspent, value):
def test_utxomanager_persist(setup_env_nodeps):
""" Tests that the utxo manager's data is correctly
persisted and can be recreated from storage.
This persistence is currently only used for metadata
(specifically, disabling coins for coin control).
"""
storage = MockStorage(None, 'wallet.jmdat', None, create=True)
UTXOManager.initialize(storage)
um = UTXOManager(storage, select)
@ -28,27 +34,45 @@ def test_utxomanager_persist(setup_env_nodeps):
um.add_utxo(txid, index, path, value, mixdepth)
um.add_utxo(txid, index+1, path, value, mixdepth+1)
# the third utxo will be disabled and we'll check if
# the disablement persists in the storage across UM instances
um.add_utxo(txid, index+2, path, value, mixdepth+1)
um.disable_utxo(txid, index+2)
um.save()
# Remove and recreate the UM from the same storage.
del um
um = UTXOManager(storage, select)
assert um.have_utxo(txid, index) == mixdepth
assert um.have_utxo(txid, index+1) == mixdepth + 1
assert um.have_utxo(txid, index+2) == False
# The third should not be registered as present given flag:
assert um.have_utxo(txid, index+2, include_disabled=False) == False
# check is_disabled works:
assert not um.is_disabled(txid, index)
assert not um.is_disabled(txid, index+1)
assert um.is_disabled(txid, index+2)
# check re-enabling works
um.enable_utxo(txid, index+2)
assert not um.is_disabled(txid, index+2)
um.disable_utxo(txid, index+2)
utxos = um.get_utxos_by_mixdepth()
assert len(utxos[mixdepth]) == 1
assert len(utxos[mixdepth+1]) == 1
assert len(utxos[mixdepth+1]) == 2
assert len(utxos[mixdepth+2]) == 0
balances = um.get_balance_by_mixdepth()
assert balances[mixdepth] == value
assert balances[mixdepth+1] == value
assert balances[mixdepth+1] == value * 2
um.remove_utxo(txid, index, mixdepth)
assert um.have_utxo(txid, index) == False
# check that removing a utxo does not remove the metadata
um.remove_utxo(txid, index+2, mixdepth+1)
assert um.is_disabled(txid, index+2)
um.save()
del um
@ -87,6 +111,12 @@ def test_utxomanager_select(setup_env_nodeps):
um.add_utxo(txid, index+1, path, value, mixdepth)
assert len(um.select_utxos(mixdepth, value)) == 2
# ensure that added utxos that are disabled do not
# get used by the selector
um.add_utxo(txid, index+2, path, value, mixdepth)
um.disable_utxo(txid, index+2)
assert len(um.select_utxos(mixdepth, value)) == 2
@pytest.fixture
def setup_env_nodeps(monkeypatch):

16
jmclient/test/test_wallet.py

@ -333,6 +333,22 @@ def test_signing_simple(setup_wallet, wallet_cls, type_check):
txout = jm_single().bc_interface.pushtx(btc.serialize(tx))
assert txout
def test_get_bbm(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')
amount = 10**8
num_tx = 3
wallet = get_populated_wallet(amount, num_tx)
# disable a utxo and check we can correctly report
# balance with the disabled flag off:
utxo_1 = list(wallet._utxos.get_utxos_by_mixdepth()[0].keys())[0]
wallet.disable_utxo(*utxo_1)
balances = wallet.get_balance_by_mixdepth(include_disabled=True)
assert balances[0] == num_tx * amount
balances = wallet.get_balance_by_mixdepth()
assert balances[0] == (num_tx - 1) * amount
wallet.toggle_disable_utxo(*utxo_1)
balances = wallet.get_balance_by_mixdepth()
assert balances[0] == num_tx * amount
def test_add_utxos(setup_wallet):
jm_single().config.set('BLOCKCHAIN', 'network', 'testnet')

3
scripts/add-utxo.py

@ -182,6 +182,9 @@ def main():
while not jm_single().bc_interface.wallet_synced:
sync_wallet(wallet, fast=options.fastsync)
# minor note: adding a utxo from an external wallet for commitments, we
# default to not allowing disabled utxos to avoid a privacy leak, so the
# user would have to explicitly enable.
for md, utxos in wallet.get_utxos_by_mixdepth_().items():
for (txid, index), utxo in utxos.items():
txhex = binascii.hexlify(txid).decode('ascii') + ':' + str(index)

114
scripts/joinmarket-qt.py

@ -72,7 +72,7 @@ from jmclient import load_program_config, get_network,\
get_blockchain_interface_instance, direct_send,\
RegtestBitcoinCoreInterface, tumbler_taker_finished_update,\
get_tumble_log, restart_wait, tumbler_filter_orders_callback,\
wallet_generate_recover_bip39, wallet_display
wallet_generate_recover_bip39, wallet_display, get_utxos_enabled_disabled
from qtsupport import ScheduleWizard, TumbleRestartWizard, config_tips,\
config_types, QtHandler, XStream, Buttons, OkButton, CancelButton,\
PasswordDialog, MyTreeWidget, JMQtMessageBox, BLUE_FG,\
@ -999,6 +999,106 @@ class TxHistoryTab(QWidget):
','.join([str(item.text(_)) for _ in range(4)])))
menu.exec_(self.tHTW.viewport().mapToGlobal(position))
class CoinsTab(QWidget):
def __init__(self):
super(CoinsTab, self).__init__()
self.initUI()
def initUI(self):
self.cTW = MyTreeWidget(self, self.create_menu, self.getHeaders())
self.cTW.setSelectionMode(QAbstractItemView.ExtendedSelection)
self.cTW.header().setSectionResizeMode(QHeaderView.Interactive)
self.cTW.header().setStretchLastSection(False)
self.cTW.on_update = self.updateUtxos
vbox = QVBoxLayout()
self.setLayout(vbox)
vbox.setContentsMargins(0,0,0,0)
vbox.setSpacing(0)
vbox.addWidget(self.cTW)
self.updateUtxos()
self.show()
def getHeaders(self):
'''Function included in case dynamic in future'''
return ['Txid:n', 'Amount in BTC', 'Address']
def updateUtxos(self):
""" Note that this refresh of the display only accesses in-process
utxo database (no sync e.g.) so can be immediate.
"""
self.cTW.clear()
def show_blank():
m_item = QTreeWidgetItem(["No coins", "", ""])
self.cTW.addChild(m_item)
self.show()
if not w.wallet:
show_blank()
return
utxos_enabled = {}
utxos_disabled = {}
for i in range(jm_single().config.getint("GUI", "max_mix_depth")):
utxos_e, utxos_d = get_utxos_enabled_disabled(w.wallet, i)
if utxos_e != {}:
utxos_enabled[i] = utxos_e
if utxos_d != {}:
utxos_disabled[i] = utxos_d
if utxos_enabled == {} and utxos_disabled == {}:
show_blank()
return
for i in range(jm_single().config.getint("GUI", "max_mix_depth")):
uem = utxos_enabled.get(i)
udm = utxos_disabled.get(i)
m_item = QTreeWidgetItem(["Mixdepth " + str(i), '', ''])
self.cTW.addChild(m_item)
for heading in ["NOT FROZEN", "FROZEN"]:
um = uem if heading == "NOT FROZEN" else udm
seq_item = QTreeWidgetItem([heading, '', ''])
m_item.addChild(seq_item)
seq_item.setExpanded(True)
if um is None:
item = QTreeWidgetItem(['None', '', ''])
seq_item.addChild(item)
else:
for k, v in um.items():
# txid:index, btc, address
t = btc.safe_hexlify(k[0])+":"+str(k[1])
s = "{0:.08f}".format(v['value']/1e8)
a = w.wallet.script_to_addr(v["script"])
item = QTreeWidgetItem([t, s, a])
item.setFont(0, QFont(MONOSPACE_FONT))
#if rows[i][forchange][j][3] != 'new':
# item.setForeground(3, QBrush(QColor('red')))
seq_item.addChild(item)
self.show()
def toggle_utxo_disable(self, txid, idx):
txid_bytes = btc.safe_from_hex(txid)
w.wallet.toggle_disable_utxo(txid_bytes, idx)
self.updateUtxos()
def create_menu(self, position):
item = self.cTW.currentItem()
if not item:
return
try:
txidn = item.text(0)
txid, idx = txidn.split(":")
assert len(txid) == 64
idx = int(idx)
assert idx >= 0
except:
return
menu = QMenu()
menu.addAction("Freeze/un-freeze utxo (toggle)",
lambda: self.toggle_utxo_disable(txid, idx))
menu.addAction("Copy transaction id to clipboard",
lambda: app.clipboard().setText(txid))
menu.exec_(self.cTW.viewport().mapToGlobal(position))
class JMWalletTab(QWidget):
@ -1613,6 +1713,15 @@ if not jm_single().config.get("POLICY", "segwit") == "true":
update_config_for_gui()
def onTabChange(i):
""" Respond to change of tab.
"""
# TODO: hardcoded literal;
# note that this is needed for an auto-update
# of utxos on the Coins tab only atm.
if i == 4:
tabWidget.widget(4).updateUtxos()
#to allow testing of confirm/unconfirm callback for multiple txs
if isinstance(jm_single().bc_interface, RegtestBitcoinCoreInterface):
jm_single().bc_interface.tick_forward_chain_interval = 10
@ -1639,11 +1748,14 @@ settingsTab = SettingsTab()
tabWidget.addTab(settingsTab, "Settings")
tabWidget.addTab(SpendTab(), "Coinjoins")
tabWidget.addTab(TxHistoryTab(), "Tx History")
tabWidget.addTab(CoinsTab(), "Coins")
w.resize(600, 500)
suffix = ' - Testnet' if get_network() == 'testnet' else ''
w.setWindowTitle(appWindowTitle + suffix)
tabWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
w.setCentralWidget(tabWidget)
tabWidget.currentChanged.connect(onTabChange)
w.show()
reactor.runReturn()
sys.exit(app.exec_())

Loading…
Cancel
Save