From 0761a8337d37638fbdc11ffd165e5bad9aa96617 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Thu, 12 Mar 2026 16:45:47 +0000 Subject: [PATCH] account: mnemonic recovery Related to https://github.com/status-im/status-bot/issues/7 - Recover account with mnemonic phrase in `login` --- bot/account.py | 69 ++++++++++++++++++++++++++++++--------------- bot/docs/account.md | 25 ++++++++++++++-- 2 files changed, 69 insertions(+), 25 deletions(-) diff --git a/bot/account.py b/bot/account.py index 7036d08..41c9787 100644 --- a/bot/account.py +++ b/bot/account.py @@ -41,6 +41,7 @@ class Account: "initialize": f"{self.http_base_url}InitializeApplication", "login": f"{self.http_base_url}LoginAccount", "create": f"{self.http_base_url}CreateAccountAndLogin", + "restore": f"{self.http_base_url}RestoreAccountAndLogin", "logout": f"{self.http_base_url}Logout", "rpc": f"{self.http_base_url}CallRPC" }, @@ -49,26 +50,12 @@ class Account: } } self.__signal = Signal(self.urls["socket"]["signals"]) - response = requests.post(self.urls["http"]["initialize"], json={ - "dataDir": self.__unix_folder - }) - data: dict = response.json() - accounts: list[dict] = data.get("accounts", []) - if not isinstance(accounts, list): - accounts = [] - - self.__available_accounts = [ - { - "display_name": account["name"], - "key_uid": account["key-uid"], - "created_at": datetime.datetime.fromtimestamp(account["timestamp"]) - } - for account in accounts - ] + # Initialize profile + self.available_accounts # In case if there is a hanging logged in session self.logout() - def login(self, password: str, key_uid: Optional[str] = None, display_name: Optional[str] = None): + def login(self, password: str, key_uid: Optional[str] = None, display_name: Optional[str] = None, mnemonic: Optional[str] = None): """ Login to the given account. If it does not exist, it will be created and automatically logged in. @@ -77,13 +64,15 @@ class Account: - `password` - your Status password - `key_uid` - your key unique identifier. If not provided `display_name` will be used to fetch it. This means that each `display_name` can be linked to one `key_uid` - `display_name` - your Status display name. Use `display_name` and `password` parameter combination if you have a 1 to 1 mapping (each display name has a unique `key_uid`) + - `mnemonic` - the mnemonic when creating an account. Use this field with `password` and `display_name` to recover an account """ if not key_uid and not display_name: raise ValueError("Please provide either a Key Unique Identifier (key_uid) or a Display Name (display_name)...") + available_accounts = self.available_accounts # Login combination: display_name + password if not key_uid: - for account in self.__available_accounts: + for account in available_accounts: if account["display_name"] != display_name: continue @@ -91,19 +80,34 @@ class Account: break # Login combination: key_uid + password else: - available_key_uids = [current["key_uid"] for current in self.__available_accounts] + available_key_uids = [current["key_uid"] for current in available_accounts] if key_uid not in available_key_uids: info = "\n".join([f"{current['key_uid']} - {current['display_name']}" for current in self.__available_accounts]) raise ValueError(f"Given Key Unique Identifier is invalid...\nAvailable Key Unique Identifiers:\n{info}") is_new_account = isinstance(key_uid, type(None)) + is_recovery = not isinstance(mnemonic, type(None)) + url_key = "login" params = { "keyUid": key_uid, "password": password, 'kdfIterations': self.__kd_iterations } - if is_new_account: + if is_recovery: + self.__validate_display_name(display_name) + params = { + "mnemonic": mnemonic, + "rootDataDir": self.__unix_folder, + "kdfIterations": self.__kd_iterations, + "displayName": display_name, + "password": password, + "customizationColor": "primary", + "wakuV2LightClient": False, + "thirdpartyServicesEnabled": True + } + url_key = "restore" + elif is_new_account: self.__validate_display_name(display_name) params = { "rootDataDir": self.__unix_folder, @@ -114,8 +118,10 @@ class Account: "wakuV2LightClient": False, "thirdpartyServicesEnabled": True, } + url_key = "create" + self.logout() - url = self.urls["http"]["login" if not is_new_account else "create"] + url = self.urls["http"][url_key] response = requests.post(url, json=params) signal_event = self.__signal.get("node.login") if signal_event["is_error"]: @@ -127,7 +133,7 @@ class Account: "emojis": event["emojiHash"], "key_uid": event["key-uid"], "compressed_key": event["compressedKey"], - "mnemonic": event["mnemonic"], + "mnemonic": event.get("mnemonic", mnemonic), "display_name": event["display-name"], "bio": event.get("bio", ""), "password": password, @@ -152,7 +158,23 @@ class Account: """ All locally available accounts """ - return self.__available_accounts + response = requests.post(self.urls["http"]["initialize"], json={ + "dataDir": self.__unix_folder + }) + data: dict = response.json() + accounts: list[dict] = data.get("accounts", []) + if not isinstance(accounts, list): + accounts = [] + + current_available_accounts = [ + { + "display_name": account["name"], + "key_uid": account["key-uid"], + "created_at": datetime.datetime.fromtimestamp(account["timestamp"]) + } + for account in accounts + ] + return current_available_accounts @property def info(self) -> dict: @@ -484,6 +506,7 @@ class Account: if self.__is_messenger_launched: return self.__call_rpc("messaging", "startMessenger") + self.__signal.get("wakuv2.peerstats") self.__is_messenger_launched = True def __del__(self): diff --git a/bot/docs/account.md b/bot/docs/account.md index 217b153..12e6b45 100644 --- a/bot/docs/account.md +++ b/bot/docs/account.md @@ -42,18 +42,22 @@ If a display name does not follow these rules, a **`ValueError`** will be raised ## Methods -### `login(password, key_uid=None, display_name=None)` +### `login(password, key_uid=None, display_name=None, mnemonic=None)` Login to an existing Status account. If the account does not exist in the initialized data directory, a new account will be created and automatically logged in. After a successful login, the decentralized messenger service is automatically started so the account can send and receive messages. +An account can also be recovered if the `mnemonic` is passed. + | Name | Type | Required | Description | |-----|-----|-----|-------------| | `password` | `str` | Yes | Password used to encrypt the account | | `key_uid` | `str` | Yes* | Unique key identifier of the account. If provided, the account will be logged in directly using this identifier. If not provided, then you must use `display_name` and `password` to login. | -| `display_name` | `str` | Yes* | Display name of the account. Used to resolve the `key_uid` if it is not provided, or to create a new account if one does not already exist. | +| `display_name` | `str` | Yes* | Display name of the account. Used to resolve the `key_uid` if it is not provided, or to create a new account if one does not already exist. This field is required if an account needs to be recovered with `mnemonic`. | +| `mnemonic` | `str` | No | The mnemonic from [`info`](./account.md#info). Use this field with `password` and `display_name` to recover the account.
**Note**: You can pass a different `display_name` but that will be internal only. When an account is recovered setting [`display_name`](./account.md#display_name) can be buggy. Ideally when recovering the account, use the original `display_name` of the account. | Returns the current `Account` instance, allowing method chaining. +Login with `display_name`: ```python from bot import Account @@ -65,6 +69,9 @@ params = { account.login(**params) ``` +**Note**: This assumes that `display_name` and is unique for every `key_uid`. If there are duplicated `display_names` then the first found match will be used. You can log in with `key_uid` if you have `display_name` duplicates. + +Login with `key_uid`: ```python from bot import Account @@ -76,6 +83,20 @@ params = { account.login(**params) ``` +Recover account: + +```python +from bot import Account + +account = Account() +params = { + "display_name": "status-app-bot", + "password": "SNTPUMP", + "mnemonic" : "phrase_1 phrase_2 phrase_3 phrase_4 phrase_5 phrase_6 phrase_7 phrase_8 phrase_9 phrase_10 phrase_11 phrase_12" +} +account.login(**params) +``` + ### `logout()` Logout from the currently logged-in Status account. This method also clears the internal account state and stops the active messenger session. This function is also supported in `del` and when the script automatically finishes.