Move StoredDict class into new 'stored_dict' module
What changed, and why it matters
This commit simply moves the StoredDict, StoredObject, StoredList, and related helper code from one file (json_db.py) into a brand-new file (stored_dict.py). It then updates other files to import from the new location. The commit message and the diff itself show no changes to how the code works—only where it lives. There is no security fix or vulnerability introduced here.
No security action needed. Treat as routine code hygiene; standard review/merge procedures apply.
Security signals we found
No strong security signals were identified.
Evidence from the diff
Pure refactor: StoredDict, BaseStoredObject, StoredObject, StoredList, registration helpers, and decorators are relocated from electrum/json_db.py to a new electrum/stored_dict.py module. json_db.py now imports these symbols from stored_dict.py. Other modules (invoices.py, lnutil.py, lnworker.py, submarine_swaps.py, wallet_db.py) update their imports accordingly. wallet_db.py switches from json_db.register_ to stored_dict.register_ calls. No logic, behavior, or data-handling changes are visible in the diff.
Changed components
electrum/json_db.pyelectrum/stored_dict.py (new)electrum/invoices.pyelectrum/lnutil.pyelectrum/lnworker.pyelectrum/submarine_swaps.pyelectrum/wallet_db.pyInspect captured patch +265 / −214
diff --git a/electrum/invoices.py b/electrum/invoices.py
index c15f329..deff7a1 100644
--- a/electrum/invoices.py
+++ b/electrum/invoices.py
@@ -4,7 +4,7 @@ from decimal import Decimal
import attr
-from .json_db import StoredObject, stored_in
+from .stored_dict import StoredObject, stored_in
from .i18n import _
from .util import age, InvoiceError, format_satoshis
from .bip21 import create_bip21_uri
diff --git a/electrum/json_db.py b/electrum/json_db.py
index cfad514..856e4fd 100644
--- a/electrum/json_db.py
+++ b/electrum/json_db.py
@@ -33,6 +33,8 @@ import jsonpointer
from . import util
from .util import WalletFileException, profiler, sticky_property
from .logging import Logger
+from .stored_dict import StoredDict, _FLEX_KEY, registered_names, registered_dicts, registered_dict_keys, registered_parent_keys
+
if TYPE_CHECKING:
from .storage import WalletStorage
@@ -52,53 +54,6 @@ setattr(jsonpatch.JsonPatchException, '__context__', sticky_property(None))
setattr(jsonpatch.JsonPatchException, '__suppress_context__', sticky_property(True))
-def modifier(func):
- def wrapper(self, *args, **kwargs):
- with self.lock:
- self._modified = True
- return func(self, *args, **kwargs)
- return wrapper
-
-def locked(func):
- def wrapper(self, *args, **kwargs):
- with self.lock:
- return func(self, *args, **kwargs)
- return wrapper
-
-
-registered_names = {}
-registered_dicts = {}
-registered_dict_keys = {}
-registered_parent_keys = {}
-
-def register_dict(name, method, _type):
- registered_dicts[name] = method, _type
-
-def register_name(name, method, _type):
- registered_names[name] = method, _type
-
-def register_dict_key(name, method):
- registered_dict_keys[name] = method
-
-def register_parent_key(name, method):
- registered_parent_keys[name] = method
-
-def stored_as(name, _type=dict):
- """ decorator that indicates the storage key of a stored object"""
- def decorator(func):
- registered_names[name] = func, _type
- return func
- return decorator
-
-def stored_in(name, _type=dict):
- """ decorator that indicates the storage key of an element in a StoredDict"""
- def decorator(func):
- registered_dicts[name] = func, _type
- return func
- return decorator
-
-_FLEX_KEY = str | int | None
-
def key_path(path: Sequence[_FLEX_KEY], key: _FLEX_KEY) -> str:
def to_str(x: _FLEX_KEY) -> str:
assert isinstance(x, _FLEX_KEY), repr(x)
@@ -113,164 +68,20 @@ def key_path(path: Sequence[_FLEX_KEY], key: _FLEX_KEY) -> str:
items.append(to_str(key))
return '/'.join(items)
-class BaseStoredObject:
-
- _db: 'JsonDB' = None
- _key: _FLEX_KEY = None
- _parent: Optional['BaseStoredObject'] = None
- _lock: threading.RLock = None
-
- def set_db(self, db):
- self._db = db
- self._lock = self._db.lock if self._db else threading.RLock()
-
- def set_parent(self, *, key: _FLEX_KEY, parent: Optional['BaseStoredObject']) -> None:
- assert (key == "") == (parent is None), f"{key=!r}, {parent=!r}"
- assert isinstance(key, _FLEX_KEY), repr(key)
- self._key = key
- self._parent = parent
-
- @property
- def lock(self):
- return self._lock
-
- @property
- def path(self) -> Sequence[_FLEX_KEY] | None:
- # return None iff we are pruned from root
- x = self
- s = [x._key]
- while x._parent is not None:
- x = x._parent
- s = [x._key] + s
- if x._key != '':
- return None
- assert self._db is not None
- return s
-
- def db_add(self, key: _FLEX_KEY, value) -> None:
- assert isinstance(key, _FLEX_KEY), repr(key)
- if self.path:
- self._db.add(self.path, key, value)
-
- def db_replace(self, key: _FLEX_KEY, value) -> None:
- assert isinstance(key, _FLEX_KEY), repr(key)
- if self.path:
- self._db.replace(self.path, key, value)
-
- def db_remove(self, key: _FLEX_KEY) -> None:
- assert isinstance(key, _FLEX_KEY), repr(key)
- if self.path:
- self._db.remove(self.path, key)
-
-
-class StoredObject(BaseStoredObject):
- """for attr.s objects """
-
- def __setattr__(self, key: str, value):
- assert isinstance(key, str), repr(key)
- if self.path and not key.startswith('_'):
- if value != getattr(self, key):
- self.db_replace(key, value)
- object.__setattr__(self, key, value)
-
- def to_json(self):
- d = dict(vars(self))
- # don't expose/store private stuff
- d = {k: v for k, v in d.items()
- if not k.startswith('_')}
- return d
+def modifier(func):
+ def wrapper(self, *args, **kwargs):
+ with self.lock:
+ self._modified = True
+ return func(self, *args, **kwargs)
+ return wrapper
+def locked(func):
+ def wrapper(self, *args, **kwargs):
+ with self.lock:
+ return func(self, *args, **kwargs)
+ return wrapper
-_RaiseKeyError = object() # singleton for no-default behavior
-
-
-class StoredDict(dict, BaseStoredObject):
-
- def __init__(self, data: dict, db: 'JsonDB'):
- self.set_db(db)
- # recursively convert dicts to StoredDict
- for k, v in list(data.items()):
- self.__setitem__(k, v)
-
- @locked
- def __setitem__(self, key: _FLEX_KEY, v) -> None:
- assert isinstance(key, _FLEX_KEY), repr(key)
- is_new = key not in self
- # early return to prevent unnecessary disk writes
- if not is_new and self._db and json.dumps(v, cls=self._db.encoder) == json.dumps(self[key], cls=self._db.encoder):
- return
- # convert dict to StoredDict.
- if type(v) == dict and (self._db is None or self._db._should_convert_to_stored_dict(key)):
- v = StoredDict(v, self._db)
- # convert list to StoredList
- elif type(v) == list:
- v = StoredList(v, self._db)
- # reject sets. they do not work well with jsonpatch
- elif isinstance(v, set):
- raise Exception(f"Do not store sets inside jsondb. path={self.path!r}")
- # set db for StoredObject, because it is not set in the constructor
- if isinstance(v, StoredObject):
- v.set_db(self._db)
- # set parent
- if isinstance(v, BaseStoredObject):
- v.set_parent(key=key, parent=self)
- # set item
- dict.__setitem__(self, key, v)
- self.db_add(key, v) if is_new else self.db_replace(key, v)
-
- @locked
- def __delitem__(self, key: _FLEX_KEY) -> None:
- assert isinstance(key, _FLEX_KEY), repr(key)
- r = self.get(key, None)
- dict.__delitem__(self, key)
- self.db_remove(key)
- if isinstance(r, BaseStoredObject):
- r._parent = None
-
- @locked
- def pop(self, key: _FLEX_KEY, v=_RaiseKeyError) -> Any:
- assert isinstance(key, _FLEX_KEY), repr(key)
- if key not in self:
- if v is _RaiseKeyError:
- raise KeyError(key)
- else:
- return v
- r = dict.pop(self, key)
- self.db_remove(key)
- if isinstance(r, BaseStoredObject):
- r._parent = None
- return r
-
- def setdefault(self, key: _FLEX_KEY, default = None, /):
- assert isinstance(key, _FLEX_KEY), repr(key)
- if key not in self:
- self.__setitem__(key, default)
- return self[key]
-
-
-class StoredList(list, BaseStoredObject):
-
- def __init__(self, data, db: 'JsonDB'):
- list.__init__(self, data)
- self.set_db(db)
-
- @locked
- def append(self, item):
- n = len(self)
- list.append(self, item)
- self.db_add('%d'%n, item)
-
- @locked
- def remove(self, item):
- n = self.index(item)
- list.remove(self, item)
- self.db_remove('%d'%n)
-
- @locked
- def clear(self):
- list.clear(self)
- self.db_replace(None, [])
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
index bb992db..e30a189 100644
--- a/electrum/lnutil.py
+++ b/electrum/lnutil.py
@@ -30,7 +30,7 @@ from .bip32 import BIP32Node, BIP32_PRIME
from .transaction import BCDataStream, OPPushDataGeneric
from .logging import get_logger
from .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
-from .json_db import StoredObject, stored_in, stored_as
+from .stored_dict import StoredObject, stored_in, stored_as
if TYPE_CHECKING:
diff --git a/electrum/lnworker.py b/electrum/lnworker.py
index e71802f..5152600 100644
--- a/electrum/lnworker.py
+++ b/electrum/lnworker.py
@@ -31,7 +31,7 @@ from aiorpcx import run_in_thread, NetAddress, ignore_after
from .logging import Logger
from .i18n import _
-from .json_db import stored_in
+from .stored_dict import stored_in
from .channel_db import UpdateStatus, ChannelDBNotLoaded, get_mychannel_info, get_mychannel_policy
from . import constants, util, lnutil
diff --git a/electrum/stored_dict.py b/electrum/stored_dict.py
new file mode 100644
index 0000000..6b377c3
--- /dev/null
+++ b/electrum/stored_dict.py
@@ -0,0 +1,239 @@
+#!/usr/bin/env python
+#
+# Electrum - lightweight Bitcoin client
+# Copyright (C) 2019 The Electrum Developers
+#
+# Permission is hereby granted, free of charge, to any person
+# obtaining a copy of this software and associated documentation files
+# (the "Software"), to deal in the Software without restriction,
+# including without limitation the rights to use, copy, modify, merge,
+# publish, distribute, sublicense, and/or sell copies of the Software,
+# and to permit persons to whom the Software is furnished to do so,
+# subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import threading
+import json
+from typing import TYPE_CHECKING, Optional, Sequence, List, Union, Any
+
+
+if TYPE_CHECKING:
+ from .json_db import JsonDB
+ from .storage import WalletStorage
+
+
+
+
+def locked(func):
+ def wrapper(self, *args, **kwargs):
+ with self.lock:
+ return func(self, *args, **kwargs)
+ return wrapper
+
+
+registered_names = {}
+registered_dicts = {}
+registered_dict_keys = {}
+registered_parent_keys = {}
+
+def register_dict(name, method, _type):
+ registered_dicts[name] = method, _type
+
+def register_name(name, method, _type):
+ registered_names[name] = method, _type
+
+def register_dict_key(name, method):
+ registered_dict_keys[name] = method
+
+def register_parent_key(name, method):
+ registered_parent_keys[name] = method
+
+def stored_as(name, _type=dict):
+ """ decorator that indicates the storage key of a stored object"""
+ def decorator(func):
+ registered_names[name] = func, _type
+ return func
+ return decorator
+
+def stored_in(name, _type=dict):
+ """ decorator that indicates the storage key of an element in a StoredDict"""
+ def decorator(func):
+ registered_dicts[name] = func, _type
+ return func
+ return decorator
+
+_FLEX_KEY = str | int | None
+
+
+class BaseStoredObject:
+
+ _db: 'JsonDB' = None
+ _key: _FLEX_KEY = None
+ _parent: Optional['BaseStoredObject'] = None
+ _lock: threading.RLock = None
+
+ def set_db(self, db):
+ self._db = db
+ self._lock = self._db.lock if self._db else threading.RLock()
+
+ def set_parent(self, *, key: _FLEX_KEY, parent: Optional['BaseStoredObject']) -> None:
+ assert (key == "") == (parent is None), f"{key=!r}, {parent=!r}"
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ self._key = key
+ self._parent = parent
+
+ @property
+ def lock(self):
+ return self._lock
+
+ @property
+ def path(self) -> Sequence[_FLEX_KEY] | None:
+ # return None iff we are pruned from root
+ x = self
+ s = [x._key]
+ while x._parent is not None:
+ x = x._parent
+ s = [x._key] + s
+ if x._key != '':
+ return None
+ assert self._db is not None
+ return s
+
+ def db_add(self, key: _FLEX_KEY, value) -> None:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ if self.path:
+ self._db.add(self.path, key, value)
+
+ def db_replace(self, key: _FLEX_KEY, value) -> None:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ if self.path:
+ self._db.replace(self.path, key, value)
+
+ def db_remove(self, key: _FLEX_KEY) -> None:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ if self.path:
+ self._db.remove(self.path, key)
+
+
+class StoredObject(BaseStoredObject):
+ """for attr.s objects """
+
+ def __setattr__(self, key: str, value):
+ assert isinstance(key, str), repr(key)
+ if self.path and not key.startswith('_'):
+ if value != getattr(self, key):
+ self.db_replace(key, value)
+ object.__setattr__(self, key, value)
+
+ def to_json(self):
+ d = dict(vars(self))
+ # don't expose/store private stuff
+ d = {k: v for k, v in d.items()
+ if not k.startswith('_')}
+ return d
+
+
+
+_RaiseKeyError = object() # singleton for no-default behavior
+
+
+class StoredDict(dict, BaseStoredObject):
+
+ def __init__(self, data: dict, db: 'JsonDB'):
+ self.set_db(db)
+ # recursively convert dicts to StoredDict
+ for k, v in list(data.items()):
+ self.__setitem__(k, v)
+
+ @locked
+ def __setitem__(self, key: _FLEX_KEY, v) -> None:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ is_new = key not in self
+ # early return to prevent unnecessary disk writes
+ if not is_new and self._db and json.dumps(v, cls=self._db.encoder) == json.dumps(self[key], cls=self._db.encoder):
+ return
+ # convert dict to StoredDict.
+ if type(v) == dict and (self._db is None or self._db._should_convert_to_stored_dict(key)):
+ v = StoredDict(v, self._db)
+ # convert list to StoredList
+ elif type(v) == list:
+ v = StoredList(v, self._db)
+ # reject sets. they do not work well with jsonpatch
+ elif isinstance(v, set):
+ raise Exception(f"Do not store sets inside jsondb. path={self.path!r}")
+ # set db for StoredObject, because it is not set in the constructor
+ if isinstance(v, StoredObject):
+ v.set_db(self._db)
+ # set parent
+ if isinstance(v, BaseStoredObject):
+ v.set_parent(key=key, parent=self)
+ # set item
+ dict.__setitem__(self, key, v)
+ self.db_add(key, v) if is_new else self.db_replace(key, v)
+
+ @locked
+ def __delitem__(self, key: _FLEX_KEY) -> None:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ r = self.get(key, None)
+ dict.__delitem__(self, key)
+ self.db_remove(key)
+ if isinstance(r, BaseStoredObject):
+ r._parent = None
+
+ @locked
+ def pop(self, key: _FLEX_KEY, v=_RaiseKeyError) -> Any:
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ if key not in self:
+ if v is _RaiseKeyError:
+ raise KeyError(key)
+ else:
+ return v
+ r = dict.pop(self, key)
+ self.db_remove(key)
+ if isinstance(r, BaseStoredObject):
+ r._parent = None
+ return r
+
+ def setdefault(self, key: _FLEX_KEY, default = None, /):
+ assert isinstance(key, _FLEX_KEY), repr(key)
+ if key not in self:
+ self.__setitem__(key, default)
+ return self[key]
+
+
+class StoredList(list, BaseStoredObject):
+
+ def __init__(self, data, db: 'JsonDB'):
+ list.__init__(self, data)
+ self.set_db(db)
+
+ @locked
+ def append(self, item):
+ n = len(self)
+ list.append(self, item)
+ self.db_add('%d'%n, item)
+
+ @locked
+ def remove(self, item):
+ n = self.index(item)
+ list.remove(self, item)
+ self.db_remove('%d'%n)
+
+ @locked
+ def clear(self):
+ list.clear(self)
+ self.db_replace(None, [])
+
+
+
diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py
index d5e69fb..9b84932 100644
--- a/electrum/submarine_swaps.py
+++ b/electrum/submarine_swaps.py
@@ -39,7 +39,7 @@ from .util import (
from . import lnutil
from .lnutil import hex_to_bytes, REDEEM_AFTER_DOUBLE_SPENT_DELAY, Keypair
from .bolt11 import decode_bolt11_invoice
-from .json_db import StoredObject, stored_in
+from .stored_dict import StoredObject, stored_in
from . import constants
from .address_synchronizer import (TX_HEIGHT_LOCAL, TX_HEIGHT_FUTURE, TX_HEIGHT_UNCONFIRMED,
TX_HEIGHT_UNCONF_PARENT)
diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py
index d00883b..4a0fc07 100644
--- a/electrum/wallet_db.py
+++ b/electrum/wallet_db.py
@@ -41,8 +41,9 @@ from .transaction import Transaction, TxOutpoint, tx_from_any, PartialTransactio
from .logging import Logger
from .lnutil import HTLCOwner, ChannelType, RecvMPPResolution
-from . import json_db
-from .json_db import JsonDB, locked, modifier, StoredObject, stored_in, stored_as
+from .json_db import JsonDB, locked, modifier
+from . import stored_dict
+from .stored_dict import StoredObject, stored_in, stored_as
from .plugin import run_hook, plugin_loaders
from .version import ELECTRUM_VERSION
from .i18n import _
@@ -103,18 +104,18 @@ class WalletFileExceptionVersion51(WalletFileException): pass
# register dicts that require value conversions not handled by constructor
-json_db.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None)
-json_db.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None)
-json_db.register_dict('contacts', tuple, None)
+stored_dict.register_dict('transactions', lambda x: tx_from_any(x, deserialize=False), None)
+stored_dict.register_dict('data_loss_protect_remote_pcp', lambda x: bytes.fromhex(x), None)
+stored_dict.register_dict('contacts', tuple, None)
# register dicts that require key conversion
for key in [
'adds', 'locked_in', 'settles', 'fails', 'fee_updates', 'buckets',
'unacked_updates', 'unfulfilled_htlcs', 'onion_keys']:
- json_db.register_dict_key(key, int)
+ stored_dict.register_dict_key(key, int)
for key in ['log']:
- json_db.register_dict_key(key, lambda x: HTLCOwner(int(x)))
+ stored_dict.register_dict_key(key, lambda x: HTLCOwner(int(x)))
for key in ['locked_in', 'fails', 'settles']:
- json_db.register_parent_key(key, lambda x: HTLCOwner(int(x)))
+ stored_dict.register_parent_key(key, lambda x: HTLCOwner(int(x)))
class WalletDBUpgrader(Logger):
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.