account: mnemonic recovery

Related to https://github.com/status-im/status-bot/issues/7

- Recover account with mnemonic phrase in `login`
This commit is contained in:
Nick Ninov
2026-03-12 16:47:53 +00:00
parent 5209e0b54e
commit 0761a8337d
2 changed files with 69 additions and 25 deletions
+46 -23
View File
@@ -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):
+23 -2
View File
@@ -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.<br>**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.