diff --git a/src/cryptoadvance/specter/managers/service_manager.py b/src/cryptoadvance/specter/managers/service_manager.py index d4cfba7bb6..b981cffd46 100644 --- a/src/cryptoadvance/specter/managers/service_manager.py +++ b/src/cryptoadvance/specter/managers/service_manager.py @@ -65,10 +65,13 @@ def __init__(self, specter, devstatus_threshold): logger.info(f"Loading Service {clazz.__name__} from {clazz.__module__}") # First configure the service self.configure_service_for_module(clazz) - # Now activate it + # Now activate it / creates the only instance self._services[clazz.id] = clazz( active=clazz.id in self.specter.config.get("services", []), specter=self.specter, + storage_manager=self.specter.service_encrypted_storage_manager + if clazz.encrypt_data + else self.specter.service_unencrypted_storage_manager, ) self.specter.ext[clazz.id] = self._services[clazz.id] # maybe register the blueprint diff --git a/src/cryptoadvance/specter/services/service.py b/src/cryptoadvance/specter/services/service.py index 4b66c0b08b..c0fe354cc0 100644 --- a/src/cryptoadvance/specter/services/service.py +++ b/src/cryptoadvance/specter/services/service.py @@ -40,7 +40,7 @@ class Service: devstatus = devstatus_alpha encrypt_data = False - def __init__(self, active, specter): + def __init__(self, active, specter, storage_manager): if not hasattr(self, "id"): raise Exception(f"Service {self.__class__} needs ID") if not hasattr(self, "name"): @@ -48,56 +48,46 @@ def __init__(self, active, specter): self.active = active self.specter = specter - @classmethod - def _storage_manager(cls): - return ( - app.specter.service_encrypted_storage_manager - if cls.encrypt_data - else app.specter.service_unencrypted_storage_manager - ) + # ensure that an encrypted storage manager is passed + if self.encrypt_data: + assert isinstance(storage_manager, ServiceEncryptedStorageManager) + self.storage_manager = storage_manager def callback(self, callback_id, *argv, **kwargv): if callback_id == callbacks.after_serverpy_init_app: if hasattr(self, "callback_after_serverpy_init_app"): self.callback_after_serverpy_init_app(kwargv["scheduler"]) - @classmethod - def set_current_user_service_data(cls, service_data: dict): - cls._storage_manager().set_current_user_service_data( - service_id=cls.id, service_data=service_data + def set_current_user_service_data(self, service_data: dict): + self.storage_manager.set_current_user_service_data( + service_id=self.id, service_data=service_data ) - @classmethod - def update_current_user_service_data(cls, service_data: dict): - cls._storage_manager().update_current_user_service_data( - service_id=cls.id, service_data=service_data + def update_current_user_service_data(self, service_data: dict): + self._storage_manager().update_current_user_service_data( + service_id=self.id, service_data=service_data ) - @classmethod - def get_current_user_service_data(cls) -> dict: - return cls._storage_manager().get_current_user_service_data(service_id=cls.id) + def get_current_user_service_data(self) -> dict: + return self._storage_manager().get_current_user_service_data(service_id=self.id) - @classmethod - def get_blueprint_name(cls): - return f"{cls.id}_endpoint" + def get_blueprint_name(self): + return f"{self.id}_endpoint" - @classmethod def default_address_label(cls): # Have to str() it; can't pass a LazyString to json serializer - return str(_("Reserved for {}").format(cls.name)) + return str(_("Reserved for {}").format(self.name)) - @classmethod - def reserve_address(cls, wallet: Wallet, address: str, label: str = None): + def reserve_address(self, wallet: Wallet, address: str, label: str = None): # Mark an Address in a persistent way as being reserved by a Service if not label: - label = cls.default_address_label() + label = self.default_address_label() wallet.associate_address_with_service( - address=address, service_id=cls.id, label=label + address=address, service_id=self.id, label=label ) - @classmethod def reserve_addresses( - cls, + self, wallet: Wallet, label: str = None, num_addresses: int = 10, @@ -116,14 +106,14 @@ def reserve_addresses( # Track Service-related addresses in ServiceAnnotationsStorage if annotations: annotations_storage = ServiceAnnotationsStorage( - service_id=cls.id, wallet=wallet + service_id=self.id, wallet=wallet ) # Start with the addresses that are already reserved but still unused addresses: List[str] = wallet.get_associated_addresses( - service_id=cls.id, unused_only=True + service_id=self.id, unused_only=True ) - logger.debug(f"Already have {len(addresses)} addresses reserved for {cls.id}") + logger.debug(f"Already have {len(addresses)} addresses reserved for {self.id}") if len(addresses) < num_addresses: if addresses: @@ -145,8 +135,8 @@ def reserve_addresses( continue # Mark an Address in a persistent way as being reserved by a Service - cls.reserve_address(wallet=wallet, address=address) - logger.debug(f"Reserved {address} for {cls.id}") + self.reserve_address(wallet=wallet, address=address) + logger.debug(f"Reserved {address} for {self.id}") addresses.append(address) @@ -159,16 +149,15 @@ def reserve_addresses( return addresses - @classmethod - def unreserve_addresses(cls, wallet: Wallet): + def unreserve_addresses(self, wallet: Wallet): """ Clear out Services-related data from any unused Addresses, but leave already-used Addresses as-is. """ annotations_storage = ServiceAnnotationsStorage( - service_id=cls.id, wallet=wallet + service_id=self.id, wallet=wallet ) - addrs = wallet.get_associated_addresses(service_id=cls.id, unused_only=True) + addrs = wallet.get_associated_addresses(service_id=self.id, unused_only=True) for addr_obj in addrs: wallet.deassociate_address(addr_obj["address"]) annotations_storage.remove_addr_annotations( @@ -182,7 +171,6 @@ def unreserve_addresses(cls, wallet: Wallet): # def set_active(self, value): # self.active = value - @classmethod def update(self): """ Called by backend periodic process to keep Service in sync with any remote @@ -194,11 +182,9 @@ def update(self): Update hooks *********************************************************************** """ - @classmethod def on_user_login(cls): pass - @classmethod def inject_in_basejinja_head(cls): """overwrite this method to inject a snippet of code in specter's base.jinja the snippet will be placed at the end of the head-section @@ -207,7 +193,6 @@ def inject_in_basejinja_head(cls): """ pass - @classmethod def inject_in_basejinja_body_top(cls): """overwrite this method to inject a snippet of code in specter's base.jinja the snippet will be placed at the top of the body-section @@ -216,7 +201,6 @@ def inject_in_basejinja_body_top(cls): """ pass - @classmethod def inject_in_basejinja_body_bottom(cls): """overwrite this method to inject a snippet of code in specter's base.jinja the snippet will be placed at the top of the body-section diff --git a/src/cryptoadvance/specter/services/swan/service.py b/src/cryptoadvance/specter/services/swan/service.py index 498ddbf6b7..0fc3730aa2 100644 --- a/src/cryptoadvance/specter/services/swan/service.py +++ b/src/cryptoadvance/specter/services/swan/service.py @@ -45,56 +45,51 @@ class SwanService(Service): AUTOWITHDRAWAL_ID = "autowithdrawal_id" AUTOWITHDRAWAL_THRESHOLD = "withdrawal_threshold" - @classmethod - def client(cls) -> SwanClient: - if hasattr(cls, "_client"): - return cls._client + def client(self) -> SwanClient: + if hasattr(self, "_client"): + return self._client try: - cls._client = SwanClient( + self._client = SwanClient( urlparse(request.url).netloc, - cls.get_current_user_service_data().get(cls.ACCESS_TOKEN), - cls.get_current_user_service_data().get(cls.ACCESS_TOKEN_EXPIRES), - cls.get_current_user_service_data().get(cls.REFRESH_TOKEN), + self.get_current_user_service_data().get(self.ACCESS_TOKEN), + self.get_current_user_service_data().get(self.ACCESS_TOKEN_EXPIRES), + self.get_current_user_service_data().get(self.REFRESH_TOKEN), ) except Exception as e: raise e - return cls._client + return self._client - @classmethod - def is_access_token_valid(cls): - service_data = cls.get_current_user_service_data() - if not service_data or not service_data.get(cls.ACCESS_TOKEN_EXPIRES): + def is_access_token_valid(self): + service_data = self.get_current_user_service_data() + if not service_data or not service_data.get(self.ACCESS_TOKEN_EXPIRES): return False return ( - service_data[cls.ACCESS_TOKEN_EXPIRES] + service_data[self.ACCESS_TOKEN_EXPIRES] > datetime.datetime.now(tz=pytz.utc).timestamp() ) - @classmethod - def store_new_api_access_data(cls): + def store_new_api_access_data(self): new_api_data = { - cls.ACCESS_TOKEN: cls.client().access_token, - cls.ACCESS_TOKEN_EXPIRES: cls.client().access_token_expires, + self.ACCESS_TOKEN: self.client().access_token, + self.ACCESS_TOKEN_EXPIRES: self.client().access_token_expires, } - if cls.client().refresh_token: - new_api_data[cls.REFRESH_TOKEN] = cls.client().refresh_token + if self.client().refresh_token: + new_api_data[self.REFRESH_TOKEN] = self.client().refresh_token logger.debug(f"Storing: {new_api_data}") - cls.update_current_user_service_data(new_api_data) + self.update_current_user_service_data(new_api_data) - @classmethod - def has_refresh_token(cls): - return cls.REFRESH_TOKEN in cls.get_current_user_service_data() + def has_refresh_token(self): + return self.REFRESH_TOKEN in self.get_current_user_service_data() - @classmethod - def get_associated_wallet(cls) -> Wallet: + def get_associated_wallet(self) -> Wallet: """Get the Specter `Wallet` that is currently associated with Swan auto-withdrawals""" - service_data = cls.get_current_user_service_data() - if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data: + service_data = self.get_current_user_service_data() + if not service_data or self.SPECTER_WALLET_ALIAS not in service_data: # Service is not initialized; nothing to do return try: return app.specter.wallet_manager.get_by_alias( - service_data[cls.SPECTER_WALLET_ALIAS] + service_data[self.SPECTER_WALLET_ALIAS] ) except SpecterError as e: logger.debug(e) @@ -102,14 +97,12 @@ def get_associated_wallet(cls) -> Wallet: # TODO: keep ignoring or remove the unknown wallet from service_data? return - @classmethod - def set_associated_wallet(cls, wallet: Wallet): + def set_associated_wallet(self, wallet: Wallet): """Set the Specter `Wallet` that is currently associated with Swan auto-withdrawals""" - cls.update_current_user_service_data({cls.SPECTER_WALLET_ALIAS: wallet.alias}) + self.update_current_user_service_data({self.SPECTER_WALLET_ALIAS: wallet.alias}) - @classmethod def reserve_addresses( - cls, wallet: Wallet, label: str = None, num_addresses: int = 10 + self, wallet: Wallet, label: str = None, num_addresses: int = 10 ) -> List[str]: """ * Reserves addresses for Swan auto-withdrawals @@ -117,7 +110,7 @@ def reserve_addresses( * Removes any existing unused reserved addresses in the previously associated `Wallet` * Performs matching cleanup and update on the Swan side - Overrides base classmethod to add Swan-specific functionality & data management. + Overrides base method to add Swan-specific functionality & data management. """ from . import client as swan_client @@ -127,28 +120,27 @@ def reserve_addresses( ) # Clear out any prior unused reserved addresses if this is a different Wallet - cur_wallet = cls.get_associated_wallet() + cur_wallet = self.get_associated_wallet() if cur_wallet and cur_wallet != wallet: super().unreserve_addresses(cur_wallet) # Store our `Wallet` as the current one for Swan auto-withdrawals - cls.set_associated_wallet(wallet) + self.set_associated_wallet(wallet) # Send the new list to Swan (DELETES any unused ones; creates a new SWAN_WALLET_ID if needed) - swan_wallet_id = cls.client().update_autowithdrawal_addresses( - cls.get_current_user_service_data().get(cls.SWAN_WALLET_ID), + swan_wallet_id = self.client().update_autowithdrawal_addresses( + self.get_current_user_service_data().get(self.SWAN_WALLET_ID), specter_wallet_name=wallet.name, specter_wallet_alias=wallet.alias, addresses=addresses, ) logger.debug(f"Updating the Swan wallet id to {swan_wallet_id}") if swan_wallet_id: - cls.update_current_user_service_data({cls.SWAN_WALLET_ID: swan_wallet_id}) + self.update_current_user_service_data({self.SWAN_WALLET_ID: swan_wallet_id}) return addresses - @classmethod - def set_autowithdrawal_settings(cls, wallet: Wallet, btc_threshold: str): + def set_autowithdrawal_settings(self, wallet: Wallet, btc_threshold: str): """ btc_threshold: "0", "0.01", "0.025", or "0.05" @@ -159,32 +151,31 @@ def set_autowithdrawal_settings(cls, wallet: Wallet, btc_threshold: str): from . import client as swan_client # Reserve auto-withdrawal addresses for this Wallet; clear out an unused ones in a prior wallet - cls.reserve_addresses( - wallet=wallet, num_addresses=cls.MIN_PENDING_AUTOWITHDRAWAL_ADDRS + self.reserve_addresses( + wallet=wallet, num_addresses=self.MIN_PENDING_AUTOWITHDRAWAL_ADDRS ) - swan_wallet_id = cls.get_current_user_service_data().get(cls.SWAN_WALLET_ID) + swan_wallet_id = self.get_current_user_service_data().get(self.SWAN_WALLET_ID) # Send the autowithdrawal threshold - resp = cls.client().set_autowithdrawal( + resp = self.client().set_autowithdrawal( swan_wallet_id, btc_threshold=btc_threshold ) autowithdrawal_id = resp["item"]["id"] - if autowithdrawal_id != cls.get_current_user_service_data().get( + if autowithdrawal_id != self.get_current_user_service_data().get( SwanService.AUTOWITHDRAWAL_ID ): - cls.update_current_user_service_data( + self.update_current_user_service_data( { SwanService.AUTOWITHDRAWAL_ID: autowithdrawal_id, } ) # Store the threshold setting in the User's service data - cls.update_current_user_service_data( + self.update_current_user_service_data( {SwanService.AUTOWITHDRAWAL_THRESHOLD: btc_threshold} ) - @classmethod - def sync_swan_data(cls): + def sync_swan_data(self): """ Called when the user completes the OAuth2 link with Swan. @@ -204,7 +195,7 @@ def sync_autowithdrawal_settings(service_data): Otherwise clear any local autowithdrawal data. """ - autowithdrawal_info = cls.client().get_autowithdrawal_info() + autowithdrawal_info = self.client().get_autowithdrawal_info() """ { "entity": "automaticWithdrawal", @@ -256,14 +247,14 @@ def sync_autowithdrawal_settings(service_data): SwanService.set_current_user_service_data(service_data) return - service_data = cls.get_current_user_service_data() + service_data = self.get_current_user_service_data() if SwanService.SWAN_WALLET_ID in service_data: # This user has previously/currently linked to Swan on this instance swan_wallet_id = service_data[SwanService.SWAN_WALLET_ID] logger.debug(f"swan_wallet_id: {swan_wallet_id}") # Confirm that the Swan walletId exists on the Swan side - details = cls.client().get_wallet_details(swan_wallet_id) + details = self.client().get_wallet_details(swan_wallet_id) """ { "entity": "wallet", @@ -285,7 +276,7 @@ def sync_autowithdrawal_settings(service_data): # Clear the local SWAN_WALLET_ID and continue below to try to find one. logger.debug(f"swan_wallet_id {swan_wallet_id} not found on Swan") del service_data[SwanService.SWAN_WALLET_ID] - cls.set_current_user_service_data(service_data) + self.set_current_user_service_data(service_data) elif ( "item" in details @@ -307,7 +298,7 @@ def sync_autowithdrawal_settings(service_data): logger.debug( f"Updating service_data to use wallet_alias {wallet_alias}" ) - cls.update_current_user_service_data( + self.update_current_user_service_data( {SwanService.SPECTER_WALLET_ALIAS: wallet_alias} ) @@ -323,11 +314,11 @@ def sync_autowithdrawal_settings(service_data): if SwanService.SPECTER_WALLET_ALIAS in service_data: # Clear the local reference to that unknown SPECTER_WALLET_ALIAS del service_data[SwanService.SPECTER_WALLET_ALIAS] - cls.set_current_user_service_data(service_data) + self.set_current_user_service_data(service_data) # This Specter instance has no idea if there might already be wallet data on the Swan side. # Fetch all Swan wallets, if any exist. - wallet_entries = cls.client().get_wallets().get("list") + wallet_entries = self.client().get_wallets().get("list") if not wallet_entries: # No Swan data at all yet. Nothing to do. logger.debug("No wallets on the Swan side yet") @@ -342,7 +333,7 @@ def sync_autowithdrawal_settings(service_data): ]: # All is good; we've matched Swan's wallet data with a Specter `Wallet` that we recognize. # Use this Swan walletId going forward. - cls.update_current_user_service_data( + self.update_current_user_service_data( { SwanService.SWAN_WALLET_ID: swan_wallet_id, SwanService.SPECTER_WALLET_ALIAS: specter_wallet_alias, @@ -366,15 +357,14 @@ def sync_autowithdrawal_settings(service_data): # Did we at least get a Swan walletId that we can update later? if swan_wallet_id: - cls.update_current_user_service_data( + self.update_current_user_service_data( { SwanService.SWAN_WALLET_ID: swan_wallet_id, } ) logger.debug(f"Setting swan_wallet_id to {swan_wallet_id}") - @classmethod - def remove_swan_integration(cls, user: User): + def remove_swan_integration(self, user: User): # Unreserve unused addresses in all wallets for wallet_name, wallet in user.wallet_manager.wallets.items(): SwanService.unreserve_addresses(wallet=wallet) @@ -382,14 +372,14 @@ def remove_swan_integration(cls, user: User): # If an autowithdrawal setup is active, remove pending addrs from Swan try: service_data = SwanService.get_current_user_service_data() - if service_data.get(cls.SPECTER_WALLET_ALIAS) and service_data.get( - cls.SWAN_WALLET_ID + if service_data.get(self.SPECTER_WALLET_ALIAS) and service_data.get( + self.SWAN_WALLET_ID ): # Import here to prevent circular dependency from . import client as swan_client - cls.client().delete_autowithdrawal_addresses( - service_data[cls.SWAN_WALLET_ID] + self.client().delete_autowithdrawal_addresses( + service_data[self.SWAN_WALLET_ID] ) except Exception as e: # Note the exception but proceed with clearing local data @@ -405,8 +395,7 @@ def remove_swan_integration(cls, user: User): Update hooks overrides *********************************************************************** """ - @classmethod - def update(cls): + def update(self): """ Periodic or at-login call to check our Swan address status and send more when needed. @@ -414,7 +403,7 @@ def update(cls): * Add more pending autowithdrawal addrs if we're under the threshold. """ # Which Specter `Wallet` has been configured to receive Swan autowithdrawals? - wallet = cls.get_associated_wallet() + wallet = self.get_associated_wallet() if not wallet: # Swan autowithdrawals to Specter aren't set up yet; nothing to do. logger.debug( @@ -424,10 +413,10 @@ def update(cls): # Scan the Wallet for any new Swan autowithdrawals reserved_addresses: List[Address] = wallet.get_associated_addresses( - service_id=cls.id, unused_only=False + service_id=self.id, unused_only=False ) for addr_obj in reserved_addresses: - if addr_obj.used and addr_obj.label == cls.default_address_label(): + if addr_obj.used and addr_obj.label == self.default_address_label(): # This addr has received an autowithdrawal since we last checked logger.debug( f"Updating address label for {json.dumps(addr_obj, indent=4)}" @@ -437,12 +426,11 @@ def update(cls): num_pending_autowithdrawal_addrs = len( [addr_obj for addr_obj in reserved_addresses if not addr_obj["used"]] ) - if num_pending_autowithdrawal_addrs < cls.MIN_PENDING_AUTOWITHDRAWAL_ADDRS: + if num_pending_autowithdrawal_addrs < self.MIN_PENDING_AUTOWITHDRAWAL_ADDRS: logger.debug("Need to send more addrs to Swan") - cls.reserve_addresses( - wallet=wallet, num_addresses=cls.MIN_PENDING_AUTOWITHDRAWAL_ADDRS + self.reserve_addresses( + wallet=wallet, num_addresses=self.MIN_PENDING_AUTOWITHDRAWAL_ADDRS ) - @classmethod - def on_user_login(cls): - cls.update() + def on_user_login(self): + self.update() diff --git a/src/cryptoadvance/specterext/devhelp/service.py b/src/cryptoadvance/specterext/devhelp/service.py index bf5393ae76..78ef9d982b 100644 --- a/src/cryptoadvance/specterext/devhelp/service.py +++ b/src/cryptoadvance/specterext/devhelp/service.py @@ -33,16 +33,15 @@ class DevhelpService(Service): # ServiceEncryptedStorage field names for Swan SPECTER_WALLET_ALIAS = "wallet" - @classmethod - def get_associated_wallet(cls) -> Wallet: + def get_associated_wallet(self) -> Wallet: """Get the Specter `Wallet` that is currently associated with this service""" - service_data = cls.get_current_user_service_data() - if not service_data or cls.SPECTER_WALLET_ALIAS not in service_data: + service_data = self.get_current_user_service_data() + if not service_data or self.SPECTER_WALLET_ALIAS not in service_data: # Service is not initialized; nothing to do return try: return app.specter.wallet_manager.get_by_alias( - service_data[cls.SPECTER_WALLET_ALIAS] + service_data[self.SPECTER_WALLET_ALIAS] ) except SpecterError as e: logger.debug(e) @@ -50,11 +49,9 @@ def get_associated_wallet(cls) -> Wallet: # TODO: keep ignoring or remove the unknown wallet from service_data? return - @classmethod - def set_associated_wallet(cls, wallet: Wallet): + def set_associated_wallet(self, wallet: Wallet): """Set the Specter `Wallet` that is currently associated with Swan auto-withdrawals""" - cls.update_current_user_service_data({cls.SPECTER_WALLET_ALIAS: wallet.alias}) + self.update_current_user_service_data({self.SPECTER_WALLET_ALIAS: wallet.alias}) - @classmethod - def inject_in_basejinja_body_top(cls): + def inject_in_basejinja_body_top(self): return render_template("devhelp/html_inject_in_basejinja.jinja")