From e80551192b9f299d552dab7b0348668a8198a81d Mon Sep 17 00:00:00 2001 From: f321x Date: Mon, 5 May 2025 18:16:29 +0200 Subject: [PATCH] plugins: structure plugin storage in wallet store all plugin data by plugin name in a root dictionary `plugin_data` inside the wallet db so that plugin data can get deleted again. Prunes the data of plugins from the wallet db on wallet stop if the plugin is not installed anymore. --- electrum/json_db.py | 5 +++++ electrum/plugin.py | 4 ++++ electrum/plugins/nwc/nwcserver.py | 26 ++++++++++------------- electrum/plugins/psbt_nostr/psbt_nostr.py | 4 ++-- electrum/plugins/psbt_nostr/qml.py | 3 ++- electrum/plugins/psbt_nostr/qt.py | 7 +++--- electrum/simple_config.py | 6 +++++- electrum/wallet.py | 1 + electrum/wallet_db.py | 14 +++++++++++- 9 files changed, 47 insertions(+), 23 deletions(-) diff --git a/electrum/json_db.py b/electrum/json_db.py index 4b87e6f59..91e93ba1d 100644 --- a/electrum/json_db.py +++ b/electrum/json_db.py @@ -189,6 +189,11 @@ class StoredDict(dict): self.db.add_patch({'op': 'remove', 'path': key_path(self.path, key)}) return r + def setdefault(self, key, default = None, /): + if key not in self: + self.__setitem__(key, default) + return self[key] + class StoredList(list): diff --git a/electrum/plugin.py b/electrum/plugin.py index 377b3fe93..48888c9df 100644 --- a/electrum/plugin.py +++ b/electrum/plugin.py @@ -654,6 +654,10 @@ class BasePlugin(Logger): def read_file(self, filename: str) -> bytes: return self.parent.read_file(self.name, filename) + def get_storage(self, wallet: 'Abstract_Wallet') -> dict: + """Returns a dict which is persisted in the per-wallet database.""" + plugin_storage = wallet.db.get_plugin_storage() + return plugin_storage.setdefault(self.name, {}) class DeviceUnpairableError(UserFacingException): pass class HardwarePluginLibraryUnavailable(Exception): pass diff --git a/electrum/plugins/nwc/nwcserver.py b/electrum/plugins/nwc/nwcserver.py index 1ff8759c0..be77a661f 100644 --- a/electrum/plugins/nwc/nwcserver.py +++ b/electrum/plugins/nwc/nwcserver.py @@ -25,8 +25,6 @@ if TYPE_CHECKING: from aiohttp_socks import ProxyConnector -STORAGE_NAME = 'nwc_plugin' - class NWCServerPlugin(BasePlugin): URI_SCHEME = 'nostr+walletconnect://' @@ -47,10 +45,10 @@ class NWCServerPlugin(BasePlugin): if self.initialized: # this might be called for several wallets. only use one. return - storage = self.get_plugin_storage(wallet) - self.connections = storage['connections'] + storage = self.get_storage(wallet) + self.connections = storage.setdefault('connections', {}) self.delete_expired_connections() - self.nwc_server = NWCServer(self.config, wallet, self.taskgroup) + self.nwc_server = NWCServer(self.config, wallet, self.taskgroup, self.connections) asyncio.run_coroutine_threadsafe(self.taskgroup.spawn(self.nwc_server.run()), get_asyncio_loop()) self.initialized = True @@ -67,13 +65,6 @@ class NWCServerPlugin(BasePlugin): ) self.logger.debug(f"NWCServerPlugin closed, stopping taskgroup") - @staticmethod - def get_plugin_storage(wallet: 'Abstract_Wallet') -> dict: - storage = wallet.db.get_dict(STORAGE_NAME) - if 'connections' not in storage: - storage['connections'] = {} - return storage - def delete_expired_connections(self): if self.connections is None: return @@ -167,12 +158,17 @@ class NWCServer(Logger, EventListener): 'notifications'] SUPPORTED_NOTIFICATIONS: list[str] = ["payment_sent", "payment_received"] - def __init__(self, config: 'SimpleConfig', wallet: 'Abstract_Wallet', taskgroup: 'OldTaskGroup'): + def __init__( + self, + config: 'SimpleConfig', + wallet: 'Abstract_Wallet', + taskgroup: 'OldTaskGroup', + connection_storage: dict, + ): Logger.__init__(self) self.config = config # type: 'SimpleConfig' self.wallet = wallet # type: 'Abstract_Wallet' - storage = wallet.db.get_dict(STORAGE_NAME) # type: dict - self.connections = storage['connections'] # type: dict[str, dict] # client hex pubkey -> connection data + self.connections = connection_storage # type: dict[str, dict] # client hex pubkey -> connection data self.relays = config.NOSTR_RELAYS.split(",") or [] # type: List[str] self.do_stop = False self.taskgroup = taskgroup # type: 'OldTaskGroup' diff --git a/electrum/plugins/psbt_nostr/psbt_nostr.py b/electrum/plugins/psbt_nostr/psbt_nostr.py index a1ac9d836..0be6ab995 100644 --- a/electrum/plugins/psbt_nostr/psbt_nostr.py +++ b/electrum/plugins/psbt_nostr/psbt_nostr.py @@ -78,7 +78,7 @@ class CosignerWallet(Logger): KEEP_DELAY = 24*60*60 - def __init__(self, wallet: 'Multisig_Wallet'): + def __init__(self, wallet: 'Multisig_Wallet', db_storage: dict): assert isinstance(wallet, Multisig_Wallet) self.wallet = wallet @@ -90,7 +90,7 @@ class CosignerWallet(Logger): self.pending = asyncio.Event() self.wallet_uptodate = asyncio.Event() - self.known_events = wallet.db.get_dict('cosigner_events') + self.known_events = db_storage.setdefault('cosigner_events', {}) for k, v in list(self.known_events.items()): if v < now() - self.KEEP_DELAY: diff --git a/electrum/plugins/psbt_nostr/qml.py b/electrum/plugins/psbt_nostr/qml.py index 8dc9d65b4..caf48328e 100644 --- a/electrum/plugins/psbt_nostr/qml.py +++ b/electrum/plugins/psbt_nostr/qml.py @@ -117,7 +117,8 @@ class Plugin(PsbtNostrPlugin): class QmlCosignerWallet(EventListener, CosignerWallet): def __init__(self, wallet: 'Multisig_Wallet', plugin: 'Plugin'): - CosignerWallet.__init__(self, wallet) + db_storage = plugin.get_storage(wallet) + CosignerWallet.__init__(self, wallet, db_storage) self.plugin = plugin self.register_callbacks() diff --git a/electrum/plugins/psbt_nostr/qt.py b/electrum/plugins/psbt_nostr/qt.py index 160bb2541..e18f31af5 100644 --- a/electrum/plugins/psbt_nostr/qt.py +++ b/electrum/plugins/psbt_nostr/qt.py @@ -57,7 +57,7 @@ class Plugin(PsbtNostrPlugin): return if wallet.wallet_type == '2fa': return - self.add_cosigner_wallet(wallet, QtCosignerWallet(wallet, window)) + self.add_cosigner_wallet(wallet, QtCosignerWallet(wallet, window, self)) @hook def on_close_window(self, window): @@ -83,8 +83,9 @@ class Plugin(PsbtNostrPlugin): class QtCosignerWallet(EventListener, CosignerWallet): - def __init__(self, wallet: 'Multisig_Wallet', window: 'ElectrumWindow'): - CosignerWallet.__init__(self, wallet) + def __init__(self, wallet: 'Multisig_Wallet', window: 'ElectrumWindow', plugin: 'Plugin'): + db_storage = plugin.get_storage(wallet) + CosignerWallet.__init__(self, wallet, db_storage) self.window = window self.obj = QReceiveSignalObject() self.obj.cosignerReceivedPsbt.connect(self.on_receive) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 4f16f1db1..767e4027d 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -4,7 +4,7 @@ import time import os import stat from decimal import Decimal -from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set, Callable +from typing import Union, Optional, Dict, Sequence, Tuple, Any, Set, Callable, AbstractSet from numbers import Real from functools import cached_property @@ -349,6 +349,10 @@ class SimpleConfig(Logger): def is_plugin_enabled(self, name: str) -> bool: return bool(self.get(f'plugins.{name}.enabled')) + def get_installed_plugins(self) -> AbstractSet[str]: + """Returns all plugin names registered in the config.""" + return self.get('plugins', {}).keys() + def enable_plugin(self, name: str): self.set_key(f'plugins.{name}.enabled', True, save=True) diff --git a/electrum/wallet.py b/electrum/wallet.py index 12e2df1b5..55421b273 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -556,6 +556,7 @@ class Abstract_Wallet(ABC, Logger, EventListener): finally: # even if we get cancelled if any([ks.is_requesting_to_be_rewritten_to_wallet_file for ks in self.get_keystores()]): self.save_keystore() + self.db.prune_uninstalled_plugin_data(self.config.get_installed_plugins()) self.save_db() def is_up_to_date(self) -> bool: diff --git a/electrum/wallet_db.py b/electrum/wallet_db.py index 1c7715b6f..6aedd4741 100644 --- a/electrum/wallet_db.py +++ b/electrum/wallet_db.py @@ -29,7 +29,8 @@ import json import copy import threading from collections import defaultdict -from typing import Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence, TYPE_CHECKING, Union +from typing import (Dict, Optional, List, Tuple, Set, Iterable, NamedTuple, Sequence, TYPE_CHECKING, + Union, AbstractSet) import binascii import time from functools import partial @@ -1725,5 +1726,16 @@ class WalletDB(JsonDB): if wallet_type in plugin_loaders: plugin_loaders[wallet_type]() + def get_plugin_storage(self) -> dict: + return self.get_dict('plugin_data') + + def prune_uninstalled_plugin_data(self, installed_plugins: AbstractSet[str]) -> None: + """Remove plugin data for plugins that are not installed anymore.""" + plugin_storage = self.get_plugin_storage() + for name in list(plugin_storage.keys()): + if name not in installed_plugins: + plugin_storage.pop(name) + self.logger.info(f"deleting plugin data: {name=}") + def set_keystore_encryption(self, enable): self.put('use_encryption', enable)