Skip to content

Commit 79fd500

Browse files
committed
make StoredDict database agnostic
- WalletDB no longer inherits from JsonDB, it uses a StoredDict - JsonDB inherits from BaseDB - FileStorage is only seen by JsonDB - calling JSonDB constructor creates the storage
1 parent b00296e commit 79fd500

35 files changed

Lines changed: 862 additions & 443 deletions

electrum/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class GuiImportError(ImportError):
1717
from .version import ELECTRUM_VERSION
1818
from .util import format_satoshis
1919
from .wallet import Wallet
20-
from .storage import WalletStorage
20+
from .stored_dict import WalletStorage
2121
from .coinchooser import COIN_CHOOSERS
2222
from .network import Network, pick_random_server
2323
from .interface import Interface

electrum/daemon.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
log_exceptions, randrange, OldTaskGroup, UserFacingException, JsonRPCError, os_chmod
4848
)
4949
from .wallet import Wallet, Abstract_Wallet
50-
from .storage import WalletStorage
50+
from .stored_dict import WalletStorage
5151
from .wallet_db import WalletDB, WalletUnfinished
5252
from .commands import known_commands, Commands
5353
from .simple_config import SimpleConfig
@@ -551,8 +551,7 @@ def _load_wallet(
551551
if not password:
552552
raise InvalidPassword('No password given')
553553
storage.decrypt(password)
554-
# read data, pass it to db
555-
db = WalletDB(storage.read(), storage=storage, upgrade=upgrade)
554+
db = WalletDB(storage, upgrade=upgrade)
556555
if db.get_action():
557556
raise WalletUnfinished(db)
558557
wallet = Wallet(db, config=config)

electrum/gui/qml/qedaemon.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from electrum.lnchannel import ChannelState
1414
from electrum.bitcoin import is_address
1515
from electrum.bitcoin import verify_usermessage_with_address
16-
from electrum.storage import StorageReadWriteError, WalletStorage
16+
from electrum.stored_dict import StorageReadWriteError, WalletStorage
1717

1818
from .auth import AuthMixin, auth_protect
1919
from .qefx import QEFX

electrum/gui/qml/qewallet.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,8 @@ def setPassword(self, password):
751751

752752
try:
753753
self._logger.info('setting new password')
754-
self.wallet.update_password(current_password, password, encrypt_storage=True)
754+
encrypt_storage = self.wallet.storage.supports_file_encryption()
755+
self.wallet.update_password(current_password, password, encrypt_storage=encrypt_storage)
755756
# restore the invariant that all loaded wallets in qml must be unlocked:
756757
self.wallet.unlock(password)
757758
return True

electrum/gui/qt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,7 @@ def _start_wizard_to_select_or_create_wallet(self, path) -> Optional[Abstract_Wa
507507
self.logger.info('wizard dialog cancelled by user')
508508
return
509509
db.put('x3', wizard.get_wizard_data()['x3'])
510-
db.write_and_force_consolidation() # TODO API for db is a bit weird: there should be a close method
510+
db.storage.write() # TODO API for db is a bit weird: there should be a close method
511511

512512
wallet = self.daemon.load_wallet(wallet_file, password, upgrade=True)
513513
return wallet

electrum/gui/qt/main_window.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1748,7 +1748,7 @@ def save_notes_text(self):
17481748

17491749
def update_console(self):
17501750
console = self.console
1751-
console.history = self.wallet.db.get_stored_item("qt-console-history", [])
1751+
console.history = self.wallet.db.get_list("qt-console-history")
17521752
console.history_index = len(console.history)
17531753

17541754
console.updateNamespace({
@@ -1950,8 +1950,12 @@ def on_password(hw_dev_pw):
19501950
self.update_lock_menu()
19511951

19521952
def _update_wallet_password(self, *, old_password, new_password, xpub_encrypt=False):
1953+
encrypt_storage = self.wallet.storage.supports_file_encryption()
19531954
try:
1954-
self.wallet.update_password(old_password, new_password, encrypt_storage=True, xpub_encrypt=xpub_encrypt)
1955+
self.wallet.update_password(
1956+
old_password, new_password,
1957+
encrypt_storage=encrypt_storage,
1958+
xpub_encrypt=xpub_encrypt)
19551959
except InvalidPassword as e:
19561960
self.show_error(str(e))
19571961
return

electrum/gui/qt/wizard/wallet.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ def on_filename(filename_or_path):
327327
msg = ""
328328
self.valid = temp_storage is not None
329329
user_needs_to_enter_password = False
330-
if temp_storage:
330+
if temp_storage is not None:
331331
if not temp_storage.file_exists():
332332
msg = _("This file does not exist.") + '\n' \
333333
+ _("Press 'Next' to create this wallet, or choose another file.")

electrum/gui/text.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from electrum.transaction import PartialTxOutput
2222
from electrum.wallet import Wallet, Abstract_Wallet
2323
from electrum.wallet_db import WalletDB
24-
from electrum.storage import WalletStorage
24+
from electrum.stored_dict import WalletStorage
2525
from electrum.network import NetworkParameters, TxBroadcastError, BestEffortRequestFailed, ProxySettings
2626
from electrum.interface import ServerAddr
2727
from electrum.invoices import Invoice

electrum/json_db.py

Lines changed: 156 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,12 @@
3030
import jsonpatch
3131
import jsonpointer
3232

33-
from . import util
34-
from .util import WalletFileException, profiler, sticky_property
33+
from .util import WalletFileException, profiler, sticky_property, MyEncoder
3534
from .logging import Logger
36-
from .stored_dict import StoredDict, _FLEX_KEY, registered_names, registered_keys, _convert_dict_key, _convert_dict_value
35+
from .stored_dict import _FLEX_KEY, BaseDB, _convert_dict_key, _convert_dict_value
36+
from .storage import FileStorage
3737

3838

39-
if TYPE_CHECKING:
40-
from .storage import WalletStorage
41-
4239

4340
# We monkeypatch exceptions in the jsonpatch package to ensure they do not contain secrets from the DB.
4441
# We often log exceptions and offer to send them to the crash reporter, so they must not contain secrets.
@@ -84,36 +81,155 @@ def wrapper(self, *args, **kwargs):
8481

8582

8683

87-
88-
class JsonDB(Logger):
84+
class JsonDB(BaseDB):
8985

9086
def __init__(
91-
self,
92-
s: str,
93-
*,
94-
storage: Optional['WalletStorage'] = None,
95-
encoder=None,
96-
upgrader=None,
87+
self,
88+
path: Optional[str],
89+
*,
90+
allow_partial_writes = True,
91+
init_db = True,
92+
encoder = MyEncoder,
93+
upgrader = None,
9794
):
98-
Logger.__init__(self)
95+
BaseDB.__init__(self, path)
96+
self._is_closed = True
9997
self.lock = threading.RLock()
100-
self.storage = storage
10198
self.encoder = encoder
99+
self.upgrader = upgrader
102100
self.pending_changes = [] # type: List[str]
103101
self._modified = False
104-
# load data
105-
data = self.load_data(s)
106-
if upgrader:
107-
data, was_upgraded = upgrader(data)
102+
if self.path:
103+
self.storage = FileStorage(path, allow_partial_writes=allow_partial_writes)
104+
if init_db and not self.is_encrypted():
105+
# open DB if file is not encrypted
106+
# otherwise, this will be called in self.decrypt
107+
self.init_db()
108+
else:
109+
self.storage = None
110+
self.set_data('{}')
111+
self._is_closed = False
112+
113+
def set_data(self, json_str):
114+
data = self.load_data(json_str)
115+
if self.upgrader:
116+
data, was_upgraded = self.upgrader(data)
108117
self._modified |= was_upgraded
109-
# convert json to python objects
110-
data = self._convert_dict([], data)
111-
# convert dict to StoredDict
112-
self.data = StoredDict(data, self)
113-
self.data.set_parent(key='', parent=None)
118+
self.json_data = self._convert_dict([], data)
119+
120+
def init_db(self):
121+
if self.storage.is_encrypted():
122+
assert self.storage.is_past_initial_decryption()
123+
json_str = self.storage.read()
124+
self.set_data(json_str)
114125
# write file in case there was a db upgrade
115-
if self.storage and self.storage.file_exists():
116-
self.write_and_force_consolidation()
126+
self.write_and_force_consolidation()
127+
self._is_closed = False
128+
129+
def decrypt(self, password: str):
130+
self.storage.decrypt(password)
131+
json_str = self.storage.read()
132+
self.json_data = self.load_data(json_str)
133+
self._is_closed = False
134+
135+
def check_password(self, password):
136+
self.storage.check_password(password)
137+
138+
def supports_file_encryption(self):
139+
return bool(self.storage)
140+
141+
def get_encryption_version(self):
142+
return self.storage.get_encryption_version()
143+
144+
def is_encrypted(self):
145+
return self.storage and self.storage.is_encrypted()
146+
147+
def is_encrypted_with_user_pw(self) -> bool:
148+
return self.storage and self.storage.is_encrypted_with_user_pw()
149+
150+
def is_encrypted_with_hw_device(self) -> bool:
151+
return self.storage and self.storage.is_encrypted_with_hw_device()
152+
153+
def set_password(self, password: str, enc_version=None):
154+
self.storage.set_password(password, enc_version=enc_version)
155+
156+
def file_exists(self):
157+
return self.storage and self.storage.file_exists()
158+
159+
def _subdict(self, path):
160+
d = self.json_data
161+
for k in path[1:]:
162+
d = d[k]
163+
return d
164+
165+
def iter_keys(self, path):
166+
d = self._subdict(path)
167+
return d.__iter__()
168+
169+
def dict_len(self, path):
170+
d = self._subdict(path)
171+
return len(d)
172+
173+
def contains(self, path, key):
174+
d = self._subdict(path)
175+
return key in d
176+
177+
def replace(self, path, key, value):
178+
# called by setattr
179+
self.db_replace(path, key, value)
180+
181+
@modifier
182+
def put(self, path, key, value):
183+
d = self._subdict(path)
184+
is_new = key not in d
185+
d[key] = value
186+
self.db_add(path, key, value) if is_new else self.db_replace(path, key, value)
187+
188+
@modifier
189+
def clear(self, path):
190+
d = self._subdict(path)
191+
d.clear()
192+
193+
def get(self, hint, key):
194+
return hint[key]
195+
196+
def get_hint(self, path):
197+
return self._subdict(path)
198+
199+
@modifier
200+
def remove(self, path, key):
201+
d = self._subdict(path)
202+
d.pop(key)
203+
self.db_remove(path, key)
204+
205+
@modifier
206+
def list_append(self, path, item):
207+
_list = self._subdict(path)
208+
n = len(_list)
209+
_list.append(item)
210+
self.db_add(path, str(n), item)
211+
212+
def list_index(self, path, item):
213+
_list = self._subdict(path)
214+
return _list.index(item)
215+
216+
def list_len(self, path):
217+
_list = self._subdict(path)
218+
return len(_list)
219+
220+
@modifier
221+
def list_clear(self, path):
222+
_list = self._subdict(path)
223+
_list.clear()
224+
self.db_remove(path[:-1], path[-1])
225+
self.db_add(path[:-1], path[-1], [])
226+
227+
@modifier
228+
def list_remove(self, path, item):
229+
_list = self._subdict(path)
230+
n = _list.index(item)
231+
_list.remove(item)
232+
self.db_remove(path, str(n)) # fixme: keys
117233

118234
def load_data(self, s: str) -> Dict[str, Any]:
119235
if s == '':
@@ -185,71 +301,30 @@ def add_patch(self, patch):
185301
self.pending_changes.append(json.dumps(patch, cls=self.encoder))
186302
self.set_modified(True)
187303

188-
def add(self, path, key: _FLEX_KEY, value) -> None:
304+
def db_add(self, path, key: _FLEX_KEY, value) -> None:
189305
assert isinstance(key, _FLEX_KEY), repr(key)
190306
self.add_patch({'op': 'add', 'path': key_path(path, key), 'value': value})
191307

192-
def replace(self, path, key: _FLEX_KEY, value) -> None:
308+
def db_replace(self, path, key: _FLEX_KEY, value) -> None:
193309
assert isinstance(key, _FLEX_KEY), repr(key)
194310
self.add_patch({'op': 'replace', 'path': key_path(path, key), 'value': value})
195311

196-
def remove(self, path, key: _FLEX_KEY) -> None:
312+
def db_remove(self, path, key: _FLEX_KEY) -> None:
197313
assert isinstance(key, _FLEX_KEY), repr(key)
198314
self.add_patch({'op': 'remove', 'path': key_path(path, key)})
199315

200-
@locked
201-
def get(self, key, default=None):
202-
v = self.data.get(key)
203-
if v is None:
204-
v = default
205-
return v
206-
207-
@modifier
208-
def put(self, key, value):
209-
try:
210-
json.dumps(key, cls=self.encoder)
211-
json.dumps(value, cls=self.encoder)
212-
except Exception:
213-
self.logger.info(f"json error: cannot save {repr(key)} ({repr(value)})")
214-
return False
215-
if value is not None:
216-
if self.data.get(key) != value:
217-
self.data[key] = copy.deepcopy(value)
218-
return True
219-
elif key in self.data:
220-
self.data.pop(key)
221-
return True
222-
return False
223-
224-
@locked
225-
def get_dict(self, name) -> dict:
226-
# Warning: interacts un-intuitively with 'put': certain parts
227-
# of 'data' will have pointers saved as separate variables.
228-
if name not in self.data:
229-
self.data[name] = {}
230-
return self.data[name]
231-
232-
@locked
233-
def get_stored_item(self, key, default) -> dict:
234-
if key not in self.data:
235-
self.data[key] = default
236-
return self.data[key]
237-
238316
@locked
239317
def dump(self, *, human_readable: bool = True) -> str:
240318
"""Serializes the DB as a string.
241319
'human_readable': makes the json indented and sorted, but this is ~2x slower
242320
"""
243321
return json.dumps(
244-
self.data,
322+
self.json_data,
245323
indent=4 if human_readable else None,
246324
sort_keys=bool(human_readable),
247325
cls=self.encoder,
248326
)
249327

250-
def _should_convert_to_stored_dict(self, key) -> bool:
251-
return True
252-
253328
def _convert_dict_key(self, path: List[str], key: str) -> _FLEX_KEY:
254329
return _convert_dict_key(path, key)
255330

@@ -272,11 +347,20 @@ def _convert_dict(self, path: List[str], data: dict):
272347

273348
@locked
274349
def write(self):
350+
if not self.storage:
351+
return
275352
if self.storage.should_do_full_write_next():
276353
self.write_and_force_consolidation()
277354
else:
278355
self._append_pending_changes()
279356

357+
def close(self):
358+
# do not call write
359+
self._is_closed = True
360+
361+
def is_closed(self):
362+
return self._is_closed
363+
280364
@locked
281365
def _append_pending_changes(self):
282366
if threading.current_thread().daemon:

0 commit comments

Comments
 (0)