|
|
|
|
@ -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, |
|
|
|
|
|