Skip to content

Commit b71e5de

Browse files
committed
stored_dict: perform type conversions at request time
this restores DB upgrades. (DB upgrades require not converting stored classes)
1 parent a19f666 commit b71e5de

12 files changed

Lines changed: 183 additions & 131 deletions

electrum/invoices.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def get_id(self) -> str:
236236
else: # on-chain
237237
return get_id_from_onchain_outputs(outputs=self.get_outputs(), timestamp=self.time)
238238

239-
def as_dict(self, status):
239+
def export(self, status):
240240
d = {
241241
'is_lightning': self.is_lightning(),
242242
'amount_BTC': format_satoshis(self.get_amount_sat()),
@@ -298,7 +298,7 @@ def can_be_paid_onchain(self) -> bool:
298298
return True
299299

300300
def to_debug_json(self) -> Dict[str, Any]:
301-
d = self.to_json()
301+
d = self.as_dict()
302302
d["lnaddr"] = self._lnaddr.to_debug_json()
303303
return d
304304

electrum/json_db.py

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@
3030
import jsonpatch
3131
import jsonpointer
3232

33-
from .util import WalletFileException, profiler, sticky_property, MyEncoder
33+
from .util import WalletFileException, profiler, sticky_property
3434
from .logging import Logger
35-
from .stored_dict import _FLEX_KEY, BaseDB, _convert_dict_key, _convert_dict_value
35+
from .stored_dict import _FLEX_KEY, BaseDB
3636
from .storage import FileStorage
3737

3838

@@ -89,12 +89,10 @@ def __init__(
8989
*,
9090
allow_partial_writes = True,
9191
init_db = True,
92-
encoder = MyEncoder,
9392
):
9493
BaseDB.__init__(self, path)
9594
self._is_closed = True
9695
self.lock = threading.RLock()
97-
self.encoder = encoder
9896
self.pending_changes = [] # type: List[str]
9997
self._modified = False
10098
if self.path:
@@ -105,18 +103,17 @@ def __init__(
105103
self.init_db()
106104
else:
107105
self.storage = None
108-
self.set_data('{}')
106+
self.json_data = {}
109107
self._is_closed = False
110108

111109
def set_data(self, json_str):
112-
data = self.load_data(json_str)
113-
self.json_data = self._convert_dict([], data)
110+
self.json_data = self.load_data(json_str)
114111

115112
def init_db(self):
116113
if self.storage.is_encrypted():
117114
assert self.storage.is_past_initial_decryption()
118115
json_str = self.storage.read()
119-
self.set_data(json_str)
116+
self.json_data = self.load_data(json_str)
120117
# write file in case there was a db upgrade
121118
self.write_and_force_consolidation()
122119
self._is_closed = False
@@ -286,7 +283,7 @@ def modified(self):
286283

287284
@locked
288285
def add_patch(self, patch):
289-
self.pending_changes.append(json.dumps(patch, cls=self.encoder))
286+
self.pending_changes.append(json.dumps(patch))
290287
self.set_modified(True)
291288

292289
def db_add(self, path, key: _FLEX_KEY, value) -> None:
@@ -310,29 +307,8 @@ def dump(self, *, human_readable: bool = True) -> str:
310307
self.json_data,
311308
indent=4 if human_readable else None,
312309
sort_keys=bool(human_readable),
313-
cls=self.encoder,
314310
)
315311

316-
def _convert_dict_key(self, path: List[str], key: str) -> _FLEX_KEY:
317-
return _convert_dict_key(path, key)
318-
319-
def _convert_dict_value(self, path: List[str], v) -> Any:
320-
v = _convert_dict_value(path, v)
321-
if isinstance(v, dict):
322-
v = self._convert_dict(path, v)
323-
return v
324-
325-
def _convert_dict(self, path: List[str], data: dict):
326-
# recursively convert json dict to StoredDict
327-
assert all(isinstance(x, str) for x in path), repr(path)
328-
d = {}
329-
for k, v in list(data.items()):
330-
child_path = path + [k]
331-
k = self._convert_dict_key(path, k)
332-
v = self._convert_dict_value(child_path, v)
333-
d[k] = v
334-
return d
335-
336312
@locked
337313
def write(self):
338314
if not self.storage:

electrum/lnchannel.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
from .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage
6363
from .lnutil import format_short_channel_id
6464
from .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
65+
from .stored_dict import stored_at
6566

6667
if TYPE_CHECKING:
6768
from .lnworker import LNWallet

electrum/lnpeer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1191,7 +1191,7 @@ async def channel_establishment_flow(
11911191
lnworker=self.lnworker,
11921192
initial_feerate=feerate
11931193
)
1194-
temp_chan.storage['funding_inputs'] = [txin.prevout.to_json() for txin in funding_tx.inputs()]
1194+
temp_chan.storage['funding_inputs'] = [txin.prevout for txin in funding_tx.inputs()]
11951195
temp_chan.storage['has_onchain_backup'] = has_onchain_backup
11961196
temp_chan.storage['init_height'] = self.lnworker.network.get_local_height()
11971197
temp_chan.storage['init_timestamp'] = int(time.time())

electrum/lnutil.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1942,7 +1942,7 @@ def from_tuple(amount_msat, rhash, cltv_abs, htlc_id, timestamp) -> 'UpdateAddHt
19421942
htlc_id=htlc_id,
19431943
timestamp=timestamp)
19441944

1945-
def to_json(self):
1945+
def as_tuple(self):
19461946
self._validate()
19471947
return dataclasses.astuple(self)
19481948

electrum/stored_dict.py

Lines changed: 71 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -89,39 +89,29 @@ def decorator(func):
8989
return decorator
9090

9191

92-
def _walk_path(d, path):
93-
for k in path:
94-
if k in d:
95-
d = d[k]
96-
elif '*' in d:
97-
d = d['*']
98-
else:
99-
return None
100-
return d
101-
102-
def _convert_dict_key(path: List[str], key: str) -> _FLEX_KEY:
103-
"""Maybe convert key from str to python type (typically int or IntEnum)"""
104-
assert all(isinstance(x, str) for x in path), repr(path)
105-
r = _walk_path(registered_keys, path)
106-
if r:
107-
if func := r.get('self'):
108-
key = func(key)
109-
assert isinstance(key, _FLEX_KEY), f"unexpected type for {key=!r} at {path=}"
110-
return key
111-
112-
def _convert_dict_value(path: List[str], v) -> Any:
113-
assert all(isinstance(x, str) for x in path), repr(path)
114-
r = _walk_path(registered_names, path)
115-
if r and type(r) is tuple:
116-
_type, constructor = r
117-
if _type == dict:
118-
v = constructor(**v)
119-
elif _type == tuple:
120-
v = constructor(*v)
121-
else:
122-
v = constructor(v)
123-
return v
124-
92+
def to_default(obj):
93+
"""Convert user-defined classes to python built-in types.
94+
Also convert bytes to hex, so that the result is json serializable.
95+
"""
96+
if obj is None or isinstance(obj, (str, int, float)):
97+
return obj
98+
if isinstance(obj, bytes):
99+
return obj.hex()
100+
if hasattr(obj, 'as_str') and callable(obj.as_str):
101+
return obj.as_str()
102+
if hasattr(obj, 'as_dict') and callable(obj.as_dict):
103+
obj = obj.as_dict()
104+
if hasattr(obj, 'as_tuple') and callable(obj.as_tuple):
105+
obj = obj.as_tuple()
106+
if isinstance(obj, (set, frozenset)):
107+
return [to_default(x) for x in list(obj)]
108+
if isinstance(obj, dict):
109+
return dict([(key_to_str(k), to_default(v)) for k, v in obj.items()])
110+
if isinstance(obj, list):
111+
return [to_default(x) for x in obj]
112+
if isinstance(obj, tuple):
113+
return tuple([to_default(x) for x in list(obj)])
114+
raise Exception('unsupported type', type(obj))
125115

126116

127117

@@ -131,6 +121,7 @@ def __init__(self, path):
131121
Logger.__init__(self)
132122
self._write_batch = None
133123
self.path = path
124+
self._should_convert = True
134125

135126
def file_exists(self):
136127
raise NotImplementedError()
@@ -178,9 +169,9 @@ def _to_stored_dict_or_list(self, key, value):
178169
value = StoredList(self._db, key=key, parent=self)
179170
elif isinstance(value, dict):
180171
value = StoredDict(self._db, key=key, parent=self)
181-
#elif isinstance(value, tuple):
182-
# value = StoredList(self._db, key=key, parent=self)
183-
# value = tuple(value[:]) # do not expose StoredTuple to callers
172+
elif isinstance(value, tuple):
173+
value = StoredList(self._db, key=key, parent=self)
174+
value = tuple(value[:]) # do not expose StoredTuple to callers
184175
return value
185176

186177
@property
@@ -193,12 +184,37 @@ def hint(self):
193184
def db_get(self, key):
194185
value = self._db.get(self.hint, key)
195186
value = self._to_stored_dict_or_list(key, value)
187+
if not self.should_convert():
188+
return value
189+
value = self._convert_value(key, value)
196190
# set db for StoredObject, because it is not set in the constructor
197191
if isinstance(value, StoredObject):
198192
value.set_db(self._db)
199193
value.set_parent(key=key, parent=self)
200194
return value
201195

196+
def _convert_key(self, key: str) -> _FLEX_KEY:
197+
"""Maybe convert key from str to python type (typically int or IntEnum)"""
198+
if self._key_converters:
199+
if func := self._key_converters.get('self'):
200+
key = func(key)
201+
assert isinstance(key, _FLEX_KEY), f"unexpected type for {key=!r} at {self._path}"
202+
return key
203+
204+
def _convert_value(self, key, v) -> Any:
205+
reg = self.get_constructor(key)
206+
if reg:
207+
if isinstance(v, (StoredDict, StoredList)):
208+
v = v.dump()
209+
_type, constructor = reg
210+
if _type == dict:
211+
v = constructor(**v)
212+
elif _type == tuple:
213+
v = constructor(*v)
214+
else:
215+
v = constructor(v)
216+
return v
217+
202218
def get_constructor(self, key):
203219
if self._constructor:
204220
r = self._constructor.get(key, self._constructor.get('*', None))
@@ -245,10 +261,10 @@ def __setattr__(self, key: str, value):
245261
assert isinstance(key, str), repr(key)
246262
if not key.startswith('_') and self._path:
247263
if value != getattr(self, key):
248-
self._db.replace(self.hint, self._path, key, value)
264+
self._db.replace(self.hint, self._path, key, to_default(value))
249265
object.__setattr__(self, key, value)
250266

251-
def to_json(self):
267+
def as_dict(self):
252268
d = dict(vars(self))
253269
# don't expose/store private stuff
254270
d = {k: v for k, v in d.items()
@@ -277,6 +293,9 @@ def __init__(self, db: BaseDB, key: _FLEX_KEY, parent):
277293
self.init_constructor()
278294
self.init_key_converters()
279295

296+
def should_convert(self):
297+
return self._db._should_convert
298+
280299
def dump(self) -> dict:
281300
data = {}
282301
for k, v in self.items():
@@ -286,18 +305,23 @@ def dump(self) -> dict:
286305
return data
287306

288307
def __getitem__(self, key: _FLEX_KEY) -> Any:
308+
key = key_to_str(key)
289309
return self.db_get(key)
290310

291311
def __setitem__(self, key: _FLEX_KEY, value: Any) -> None:
312+
key = key_to_str(key)
292313
if isinstance(value, StoredObject):
293314
# side effect
294315
value.set_db(self._db)
295316
value.set_parent(key=key, parent=self)
296317
if isinstance(value, (StoredList, StoredDict)):
297318
value = value.dump()
319+
# convert to python
320+
value = to_default(value)
298321
self._db.put(self.hint, self._path, key, value)
299322

300323
def __delitem__(self, key: _FLEX_KEY) -> None:
324+
key = key_to_str(key)
301325
self._db.remove(self.hint, self._path, key)
302326

303327
def __iter__(self) -> Iterator[str]:
@@ -309,19 +333,20 @@ def __len__(self) -> int:
309333
# ---- Dict-like extras ----
310334

311335
def __contains__(self, key: object) -> bool:
336+
key = key_to_str(key)
312337
return self._db.dict_contains(self.hint, self._path, key)
313338

314339
def keys(self) -> Iterable[str]:
315340
for k in self._db.iter_keys(self.hint, self._path):
316-
yield k
341+
yield self._convert_key(k)
317342

318343
def values(self) -> Iterator[Any]:
319344
for k in self._db.iter_keys(self.hint, self._path):
320345
yield self[k]
321346

322347
def items(self) -> Iterator[Tuple[str, Any]]:
323348
for k in self._db.iter_keys(self.hint, self._path):
324-
yield (k, self[k])
349+
yield (self._convert_key(k), self[k])
325350

326351
def get(self, key: _FLEX_KEY, default: Any = None, add_if_missing=False) -> Any:
327352
# If add_if_missing is True, create DB entry if it does not exist.
@@ -383,6 +408,9 @@ def __init__(self, db: BaseDB, key: _FLEX_KEY, parent):
383408
self.init_constructor()
384409
self.init_key_converters()
385410

411+
def should_convert(self):
412+
return self._db._should_convert
413+
386414
def _get_list_item(self, key: int):
387415
key = int(key)
388416
return self.db_get(key)
@@ -408,16 +436,19 @@ def __iter__(self) -> Iterator[str]:
408436
yield self._get_list_item(i)
409437

410438
def append(self, value):
439+
value = to_default(value)
411440
self._db.list_append(self.hint, self._path, value)
412441

413442
def clear(self):
414443
self._db.list_clear(self.hint, self._path)
415444
assert len(self) == 0
416445

417446
def index(self, item) -> int:
447+
item = to_default(item)
418448
return self._db.list_index(self.hint, self._path, item)
419449

420450
def remove(self, item):
451+
item = to_default(item)
421452
self._db.list_remove(self.hint, self._path, item)
422453

423454
def dump(self) -> list:

electrum/transaction.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ def to_legacy_tuple(self) -> Tuple[int, str, Union[int, str]]:
164164
return TYPE_ADDRESS, self.address, self.value
165165
return TYPE_SCRIPT, self.scriptpubkey.hex(), self.value
166166

167+
def as_tuple(self):
168+
return self.to_legacy_tuple()
169+
167170
@classmethod
168171
def from_legacy_tuple(cls, _type: int, addr: str, val: Union[int, str]) -> Union['TxOutput', 'PartialTxOutput']:
169172
if _type == TYPE_ADDRESS:
@@ -305,8 +308,8 @@ def __repr__(self):
305308
def to_str(self) -> str:
306309
return f"{self.txid.hex()}:{self.out_idx}"
307310

308-
def to_json(self):
309-
return [self.txid.hex(), self.out_idx]
311+
def as_tuple(self):
312+
return (self.txid.hex(), self.out_idx)
310313

311314
def serialize_to_network(self) -> bytes:
312315
return self.txid[::-1] + int.to_bytes(self.out_idx, length=4, byteorder="little", signed=False)
@@ -907,6 +910,9 @@ class Transaction:
907910
def __str__(self):
908911
return self.serialize()
909912

913+
def as_str(self):
914+
return str(self)
915+
910916
def __init__(self, raw):
911917
if raw is None:
912918
self._cached_network_ser = None

0 commit comments

Comments
 (0)