From 75051e2c9863e7f5b79381b2ebbaca061881d645 Mon Sep 17 00:00:00 2001 From: Nick Ninov Date: Tue, 23 Jun 2026 08:01:37 +0300 Subject: [PATCH] wallet: Add Infura Token - Wallet usage can be broken down into 3 parts - transactions (Alchemy), prices (Coingecko) and Ethereum RPC calls (Infura). - Related to https://github.com/status-im/status-python-sdk/issues/3 --- README.md | 6 +++-- bot/account.py | 67 ++++++++++++++++++++++++++++++----------------- bot/exceptions.py | 8 +++--- docs/account.md | 39 ++++++++++++++++++--------- 4 files changed, 79 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index e183f49..3350653 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,8 @@ graph TB subgraph external[External Services] COINGECKO[CoinGecko] - EVM + ALCHEMY[Alchemy] + INFURA[Infura] end SDK --> SIGNAL @@ -43,7 +44,8 @@ graph TB SDK --> Vol1 SDK --> Vol2 RPC --> |coingecko_api_key| COINGECKO - RPC --> |alchemy_token| EVM + RPC --> |infura_token| INFURA + HTTP --> |alchemy_token| ALCHEMY ``` ## Setup diff --git a/bot/account.py b/bot/account.py index 807a811..da2641c 100644 --- a/bot/account.py +++ b/bot/account.py @@ -101,7 +101,7 @@ class Account: # 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, mnemonic: Optional[str] = None, alchemy_token: Optional[str] = None, coingecko_api_key: Optional[str] = None): + def login(self, password: str, key_uid: Optional[str] = None, display_name: Optional[str] = None, mnemonic: Optional[str] = None, infura_token: Optional[str] = None, alchemy_token: Optional[str] = None, coingecko_api_key: Optional[str] = None): """ Login to the given account. If it does not exist, it will be created and automatically logged in. @@ -111,6 +111,7 @@ class Account: - `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 + - `infura_token` - https://www.infura.io/ RPC token to allow Status Backend to use a wallet - `alchemy_token` - https://alchemy.com/ RPC token to allow Status Backend to use a wallet - `coingecko_api_key` - https://www.coingecko.com/ API key to allow Status Backend to use a wallet """ @@ -168,15 +169,24 @@ class Account: self.logout() - # Wallet usage + # Wallet usage is broken down into 3 components: + # - transactions + # - prices + # - Ethereum RPC + + # Necessary for user transactions if alchemy_token: - params["infuraToken"] = alchemy_token self.__alchemy_token = alchemy_token + # Necessary for prices if coingecko_api_key: - params["coingeckoApiKey"] = coingecko_api_key + params["coingeckoDemoAPIKey"] = coingecko_api_key - if alchemy_token and coingecko_api_key: + # Necessary for Ethereum RPC + if infura_token: + params["infuraToken"] = infura_token + + if alchemy_token and coingecko_api_key and infura_token: self.__is_wallet_set = True url = self.__urls["http"][url_key] @@ -624,22 +634,27 @@ class Account: balance = self.balance tokens = (balance["chain_id"].astype(str) + "-" + balance["address"]).to_list() - result = self.__call_rpc("wallets", "fetchPrices", [tokens, [ccy]]).get("result", {}) - if not result: - return pd.DataFrame() - rates = pd.DataFrame([ - { - "chain_id": int(address.split("-")[0]), - "address": address.split("-")[1], - "rate": price, - "ccy": ccy, - } - for address, prices in result.items() - for ccy, price in prices.items() - ]) - balance = balance.merge(rates, "left", ["chain_id", "address"]) - balance["fiat_value"] = balance["amount"] * balance["rate"] + if result: + rates = pd.DataFrame([ + { + "chain_id": int(address.split("-")[0]), + "address": address.split("-")[1], + "rate": price, + "ccy": ccy, + } + for address, prices in result.items() + for ccy, price in prices.items() + ]) + balance = balance.merge(rates, "left", ["chain_id", "address"]) + balance["fiat_value"] = balance["amount"] * balance["rate"] + else: + balance = balance.assign( + rate = None, + ccy = ccy, + fiat_value = None, + ) + return balance.copy() def send_message(self, chat_id: str, message: str): @@ -1074,8 +1089,7 @@ class Account: def get_transactions(self, refresh: bool = False) -> pd.DataFrame: """ - Get wallet transactions from all Alchemy chains. To fetch wallet transactions, make sure you pass - a `etherscan_api_key` when calling `def login`. + Get wallet transactions from all Alchemy chains. Parameters: - `refresh` - if `True` then the data will be refetched from scratch. If `False` then the data will be cached after the first call. @@ -1083,7 +1097,7 @@ class Account: Output: - Wallet's transactions """ - if not self.__is_wallet_set: + if not self.__alchemy_token: raise exceptions.WalletNotConfiguredError() if not refresh and isinstance(self.__transactions, pd.DataFrame): @@ -1116,9 +1130,14 @@ class Account: transfers += current_transfers page_key = result.get("pageKey") + if len(transfers) == 0: + continue transfers = pd.DataFrame(transfers).assign(chain_id = chain_id) final.append(transfers) + if len(final) == 0: + return pd.DataFrame() + final: pd.DataFrame = pd.concat(final, ignore_index=True) columns = { "blockNum": "block_number", @@ -1128,6 +1147,7 @@ class Account: "value": "amount", "asset": "symbol", "category": "trx_type", + "chain_id": "chain_id" } final = final[list(columns.keys())].rename(columns=columns) @@ -1214,7 +1234,6 @@ class Account: os.remove(sdk_file_path) if len(error) == 0: - self.__signal.get("messages.new") self.logger.info(f"Successfully loaded file!") else: self.logger.warning(error) diff --git a/bot/exceptions.py b/bot/exceptions.py index ca9c0cb..9962f45 100644 --- a/bot/exceptions.py +++ b/bot/exceptions.py @@ -1,3 +1,5 @@ +from typing import Optional + class BackendError(Exception): pass @@ -6,14 +8,14 @@ class NotLoggedInError(Exception): super().__init__("Make sure you are logged in to your Status account with login() first...") class WalletNotConfiguredError(Exception): - def __init__(self): - super().__init__("Cannot use this method without setting an `alchemy_token` and `coingecko_api_key` when calling `login`.") + def __init__(self, msg: Optional[str] = None): + super().__init__(msg or "Cannot use this method without setting an `infura_token` and `coingecko_api_key` when calling `login`.") class InvalidDisplayNameError(ValueError): pass class InvalidContactError(ValueError): - def __init__(self, msg=None): + def __init__(self, msg: Optional[str] = None): super().__init__(msg or "Please provide either a Key Unique Identifier (key_uid) or a Display Name (display_name)...") class InvalidCurrencyError(Exception): diff --git a/docs/account.md b/docs/account.md index 4980c95..0abecf7 100644 --- a/docs/account.md +++ b/docs/account.md @@ -6,7 +6,7 @@ The account class allows you to easily work with a Status account. ## Display name -The **display name** is the human‑readable identifier for a Status account. It is used when creating an account, resolving an existing account during [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone), and when updating the account name through the [`display_name`](./account.md#display_name) property. +The **display name** is the human‑readable identifier for a Status account. It is used when creating an account, resolving an existing account during [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone), and when updating the account name through the [`display_name`](./account.md#display_name) property. Display names must follow strict validation rules enforced by the library and expected by the Status application. A valid display name must satisfy all of the following conditions: @@ -50,7 +50,7 @@ Backup files (`.bkp`) can be both created in [Status App](https://our.status.im/ [Status Backend](https://github.com/status-im/status-go) backup folder is exposed in a Docker volume so users can: -- **Upload backup** - by dropping `.bkp` files in the `backups` folder locally (linked to Status Backend Docker container). Backups are automatically uploaded if a [`mnemonic` is provided during `login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone). +- **Upload backup** - by dropping `.bkp` files in the `backups` folder locally (linked to Status Backend Docker container). Backups are automatically uploaded if a [`mnemonic` is provided during `login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone). - **Create backup** - by using [`backup()`](./account.md#backup) or creating one in [Status App](https://our.status.im/status-desktop-v2-35-local-backups-new-home-page-performance-boosts-and-more/). **Note**: Status App will not automatically backup messages. This has to be manually overridden on the app (above screenshot). When using the Python SDK, the messages are automatically stored in the `.bkp` files. @@ -95,9 +95,7 @@ Create a new `Account` instance ready to be logged in. The constructor wires the | `is_secure` | `bool` | No | When `True`, the SDK communicates over `https`; otherwise `http` is used. Defaults to `False`. | | `backup_folder` | `str` | No | Absolute path on the host machine where `.bkp` files will be stored and loaded from. If not provided, the SDK's own `backups/` folder is used. See [Backups](./account.md#backups). | -The constructor does not log into any account on its own - call [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone) afterwards. To discover what accounts already exist in the configured data directory, use the [`available_accounts`](./account.md#available_accounts) property, which is also populated automatically during initialization. - -Default setup (localhost, port 8080, http): +The constructor does not log into any account on its own - call [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone) afterwards. To discover what accounts already exist in the configured data directory, use the [`available_accounts`](./account.md#available_accounts) property, which is also populated automatically during initialization. ```python from bot import Account @@ -125,13 +123,13 @@ account = Account( ) ``` -**Note**: Status Backend must be running before initializing `Account`. You can launch the backend container with [`launch_docker_container`](./utils.md#launch_docker_container). If the backend is not reachable on `domain:port`, calls to [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone) will fail. +**Note**: Status Backend must be running before initializing `Account`. You can launch the backend container with [`launch_docker_container`](./utils.md#launch_docker_container). If the backend is not reachable on `domain:port`, calls to [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone) will fail. **Note**: When `backup_folder` is set, [`backup`](./account.md#backup) moves the generated `.bkp` file out of the SDK's internal `backups/` folder into the provided path, and recovery via `mnemonic` will look in this same folder for `.bkp` files to auto-load. Make sure the folder exists and is writable. ## Methods -### `login(password, key_uid=None, display_name=None, mnemonic=None, alchemy_token=None, coingecko_api_key=None)` +### `login(password, key_uid=None, display_name=None, mnemonic=None, infura_token=None, alchemy_token=None, coingecko_api_key=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. @@ -147,8 +145,11 @@ An account can also be recovered if the [`mnemonic`](https://status.app/help/pro | `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. This field is required if an account needs to be recovered with `mnemonic`. | | `mnemonic` | `str` | No | The [mnemonic](https://status.app/help/profile/understand-your-status-keys-and-recovery-phrase#about-your-recovery-phrase) from [`info`](./account.md#info). Use this field with `password` and `display_name` to recover the account. If you have [`.bkp`](./account.md#backup) files, in the backup Docker volume they will be automatically picked up and loaded.

**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. | -| `alchemy_token` | `str` | No | [API token](https://www.alchemy.com/) to allow Status Backend to use a wallet. Also used by [`get_transactions`](./account.md#get_transactionsrefreshfalse) to fetch wallet transaction history via the Alchemy REST API, so no separate key is needed for transactions. | -| `coingecko_api_key` | `str` | No | [API token](https://www.coingecko.com/) to allow Status Backend to use a wallet. | +| `infura_token` | `str` | No | [RPC token](https://www.infura.io/) used by Status Backend for the Ethereum RPC component of the wallet. | +| `alchemy_token` | `str` | No | Used to fetch [wallet transactions](./account.md#get_transactionsrefreshfalse) to fetch wallet transaction history via the Alchemy REST API, so no separate key is needed for transactions. | +| `coingecko_api_key` | `str` | No | [API key](https://www.coingecko.com/) used by Status Backend to fetch token prices. | + +Wallet functionality is split into three components, each backed by a token: Ethereum RPC (`infura_token`), transactions (`alchemy_token`) and prices (`coingecko_api_key`). All three must be provided for wallet RPC methods to work — if any is missing, wallet calls raise a `WalletNotConfiguredError`. Returns the current `Account` instance, allowing method chaining. @@ -213,13 +214,14 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } account.login(**params) ``` -**Note**: `alchemy_token` and `coingecko_api_key` can be used when creating, recovering and logging in to an account. `alchemy_token` covers both the wallet RPC and transaction history via [`get_transactions`](./account.md#get_transactionsrefreshfalse), so no additional key is needed. +**Note**: `infura_token`, `alchemy_token` and `coingecko_api_key` can be used when creating, recovering and logging in to an account. All three are required to enable wallet functionality — if any is missing, those calls raise a `WalletNotConfiguredError`. ### `logout()` @@ -491,6 +493,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -523,6 +526,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -549,6 +553,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -576,6 +581,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -607,6 +613,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -653,7 +660,7 @@ Returns `pd.DataFrame`. #### `get_transactions(refresh=False)` -Retrieve the historical transactions for the **logged-in account wallet** across all chains in [`chains`](./account.md#chains). Data is fetched from the [Alchemy REST API](https://www.alchemy.com/) using the `alchemy_token` provided during [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone) and combines three transaction types into a single `DataFrame`: regular transactions (`transaction`), internal transactions (`internal`) and ERC-20 token transfers (`ERC-20`). +Retrieve the historical transactions for the **logged-in account wallet** across all chains in [`chains`](./account.md#chains). Data is fetched from the [Alchemy REST API](https://www.alchemy.com/) using the `alchemy_token` provided during [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone) and combines three transaction types into a single `DataFrame`: regular transactions (`transaction`), internal transactions (`internal`) and ERC-20 token transfers (`ERC-20`). | Name | Type | Required | Description | |-----|-----|-----|-------------| @@ -685,6 +692,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -723,6 +731,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -748,6 +757,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -772,6 +782,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -797,6 +808,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -812,7 +824,7 @@ tx_hash = account.send_transaction( ) ``` -**Note**: This is a wallet method, so it requires both `alchemy_token` and `coingecko_api_key` to be provided in [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-alchemy_tokennone-coingecko_api_keynone). If either is missing, an exception will be raised when this method is called. +**Note**: This is a wallet method, so it requires `infura_token`, `alchemy_token` and `coingecko_api_key` to all be provided in [`login`](./account.md#loginpassword-key_uidnone-display_namenone-mnemonicnone-infura_tokennone-alchemy_tokennone-coingecko_api_keynone). If any is missing, a `WalletNotConfiguredError` is raised when this method is called. **Note**: The sender and receiver must be on the **same chain**. Cross-chain transfers are not supported by this method - set `chain_id` to the chain where the funds currently exist. @@ -1259,6 +1271,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -1288,6 +1301,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" } @@ -1305,6 +1319,7 @@ account = Account() params = { "display_name": "status-app-bot", "password": "SNTPUMP", + "infura_token": "token from https://www.infura.io/", "alchemy_token": "token from https://www.alchemy.com/", "coingecko_api_key": "API key from https://www.coingecko.com/" }