PyPI: PIP setup

- Rename repository from `bot` to `status_sdk`
- Add `pyproject.toml` file
- Update code documentation
- Move `docker-compose.yaml` in folder `status_sdk`
- Update `Account` docker volume paths
- Update Agent example
- Add `volume_folder` to `Account`
This commit is contained in:
Nick Ninov
2026-07-15 15:04:22 +03:00
parent 314feac2ff
commit 9626c9ed1b
20 changed files with 420 additions and 150 deletions
+55 -20
View File
@@ -2,14 +2,14 @@
![Status Python SDK header image](./docs/images/overview-header.png)
[Status](http://status.app/) is a decentralized, open-source super app combining a crypto wallet, messenger, and community spaces. It uses peer-to-peer technology so no central server can censor your messages or access your data.
The initial Python Status Backend was built with testing in mind, instead of easy developer access. The objective of this repository is to make a SDK that is:
- **light** - as less external packages when it comes to working with Status App
- **fast** - quick to get started with Status Python
- **documented** - clear explanations of what was done and **why it was done in a specific way**.
Currently this repository is not on [PyPi](https://pypi.org/) but will be added when core functionality has been devleoped and tested.
## How it works
```mermaid
@@ -25,7 +25,7 @@ graph TB
end
subgraph bot[Python SDK]
subgraph bot[status-im/status-python-sdk]
REQUIREMENTS[requirements.txt]
SDK[class Account]
SIGNAL[class Signal]
@@ -56,7 +56,7 @@ To access Python funcitonality you will have to set up [Status Backend](https://
sequenceDiagram
actor User
participant Docker
participant Python@{"alias": "status-im/status-bot"}
participant Python@{"alias": "status-im/status-python-sdk"}
participant Github@{"alias": "status-im/status-go" }
User ->> Docker: docker-compose up
@@ -64,37 +64,72 @@ sequenceDiagram
Docker ->> Docker: Build
User ->> Docker: Run container
User ->> Python: initialize module
Note over User,Python: from bot import Account<br>account = Account()
Note over User,Python: from status_sdk import Account<br>account = Account()
```
### Python
1. Setup environment. [Conda](https://www.anaconda.com/) example:
```bash
conda create -n status-sdk python=3.12
#### Install
Clone the repository and move into it:
```
git clone https://github.com/status-im/status-python-sdk.git
cd status-python-sdk
```
**Note**: Code has been tested with **Python 3.12**.
Install the base library:
2. Install requirements
```bash
pip install -r ./requirements.txt
```
pip install .
```
If you want to modify the library itself without having to reinstall:
```
pip install -e .
```
#### Uninstall
```
pip uninstall status-sdk
```
### Docker
Setup [`status-im/status-go`](https://github.com/status-im/status-go/) with the provided `docker-compose.yaml` file.
[`status-im/status-go`](https://github.com/status-im/status-go/) runs from the provided [`docker-compose.yaml`](./status_sdk/docker-compose.yaml) file. It does not use a pre-built image - it builds the backend from source, pulling directly from GitHub.
```
docker compose up -d
```
If you would like to initialize and start the container with Python:
To run on Windows, please make sure you have set up [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). It is **required** for the `build: context` above. The SDK invokes Docker through WSL so it can build the Linux image and clone the repository during the build. If the `build` is changed to point to a local repository, then WSL is not required.
You can set it up in **two** ways.
#### With Python
Use [`launch_docker_container`](./docs/utils.md#launch_docker_containercommitnone-wait_seconds5-platformlinuxamd64), which builds and starts the container for you. This is the recommended option, as it handles platform selection and (on Windows) recovers from stale Docker mounts:
```python
from bot import launch_docker_container
from status_sdk import launch_docker_container
launch_docker_container()
```
**Note**: To run on Windows, please make sure you have set up [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
#### Manually
Run the compose file yourself. It lives inside the installed package, so point Docker at it:
```
docker compose -f status_sdk/docker-compose.yaml up -d
```
The compose file reads two variables from the environment. Both have a default, so the command above works as-is, but they can be overridden:
| Variable | Default | Description |
|-----|-----|-------------|
| `STATUS_GO_REF` | `develop` | The [`status-im/status-go`](https://github.com/status-im/status-go/) git ref (commit SHA, branch or tag) to build from. |
| `STATUS_GO_PLATFORM` | `linux/amd64` | The platform the image is built for. |
```
STATUS_GO_REF=2bee8b6a38cdc8f92d74e2dbb8c4e77fbbeea149 STATUS_GO_PLATFORM=linux/amd64 docker compose -f status_sdk/docker-compose.yaml up -d
```
-49
View File
@@ -1,49 +0,0 @@
import shutil, os, subprocess, sys, time
from pathlib import Path
from typing import Optional
from .logger import Logger
from . import exceptions
def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5):
"""
Launch the Status Backend Docker container using `docker-compose.yaml`
Parameters:
- `commit` - the commit SHA. If no commit is provided, the latest version is pulled
- `wait_seconds` - nunmber of seconds to wait before the code resumes. Sleep prevents calling `class Account` faster than launching the docker container. This only happens when the container already exists and it is must be turned on.
"""
logger = Logger()
platform = sys.platform
is_windows = platform == "win32"
if not shutil.which("docker"):
raise exceptions.DockerError("Please install Docker.")
logger.info(f"Running Docker on {platform}")
ref = commit if commit else "develop"
DOCKER_COMPOSE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "docker-compose.yaml")
docker_path = DOCKER_COMPOSE_PATH
if is_windows:
p = Path(DOCKER_COMPOSE_PATH)
drive = p.drive.rstrip(":").lower()
docker_path = f"/mnt/{drive}/" + "/".join(p.parts[1:])
cmd = ["env", f"STATUS_GO_REF={ref}", "docker", "compose", "-f", docker_path, "up", "-d", "--build"]
if is_windows:
if not shutil.which("wsl"):
raise exceptions.DockerError("Please install wsl - https://learn.microsoft.com/en-us/windows/wsl/install.")
cmd.insert(0, "wsl")
logger.info(f"Running:\n{' '.join(cmd)}")
result = subprocess.run(
cmd,
cwd=os.path.dirname(DOCKER_COMPOSE_PATH),
stderr=subprocess.PIPE,
text=True,
)
if result.returncode != 0:
raise exceptions.DockerError(result.stderr.strip())
logger.info(f"Sleeping for {wait_seconds}s")
time.sleep(wait_seconds)
+60 -49
View File
@@ -84,7 +84,7 @@ Wallet features are optional and can be omitted if not required for your use cas
![Status App Wallet](./images/wallet.png)
## `Account(domain="localhost", backend_port=8080, media_port=9000, is_secure=False, backup_folder=None)`
## `Account(domain="localhost", backend_port=8080, media_port=9000, is_secure=False, backup_folder=None, volume_folder=None)`
Create a new `Account` instance ready to be logged in. The constructor wires the SDK to a running [Status Backend](https://github.com/status-im/status-go) at the given `domain` and `backend_port`, prepares the local `assets/` folder (used for image uploads, such as the [profile picture](./account.md#profile_picture)) and `backups/` folder (used for [backup uploads](./account.md#backups) and recovery).
@@ -94,12 +94,13 @@ Create a new `Account` instance ready to be logged in. The constructor wires the
| `backend_port` | `int` | No | Port exposed by Status Backend. Defaults to `8080`. If this is changed, the published port for `backend_port` must be updated to match in `docker-compose.yaml` as well. |
| `media_port` | `int` | No | Port exposed by the Status media server, used to fetch localhost images such as the [profile picture](./account.md#profile_picture). Defaults to `9000`. If this is changed, the published port for `media_port` must be updated to match in `docker-compose.yaml` as well. |
| `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). |
| `backup_folder` | `str` | No | Absolute path on the host machine where `.bkp` files will be created and loaded from. If not provided, the SDK's own `backups/` folder is used. See [Backups](./account.md#backups). |
| `volume_folder` | `str` | No | Directory containing the `docker-compose.yaml` whose `backups/` and `assets/` folders are mounted into the Status Backend container. Defaults to this package's own installation folder (e.g. the `status_sdk` folder under `site-packages` when installed via `pip`). Set this when Status Backend is launched from a different `docker-compose.yaml`, such as a local clone of the repository. |
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
from status_sdk import Account
account = Account()
```
@@ -107,7 +108,7 @@ account = Account()
Use a custom backup folder:
```python
from bot import Account
from status_sdk import Account
account = Account(backup_folder=r"C:\\Users\\me\\status-backups")
```
@@ -115,7 +116,7 @@ account = Account(backup_folder=r"C:\\Users\\me\\status-backups")
Connect to a Status Backend running on a different host or port:
```python
from bot import Account
from status_sdk import Account
account = Account(
domain="status-backend.internal",
@@ -124,10 +125,20 @@ account = Account(
)
```
Run against a local clone of the repository instead of with [`launch_docker_container`](./utils.md#launch_docker_container):
```python
from status_sdk import Account
account = Account(volume_folder="/path/to/status-python-sdk/status_sdk")
```
**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.
**Note**: `volume_folder` must match the directory containing the `docker-compose.yaml` actually used to launch Status Backend, since that is what determines where Docker mounts `backups/` and `assets/` on the host. If `volume_folder` points elsewhere, `Account` will create and use folders that are never seen by the running container.
## Methods
### `login(password, key_uid=None, display_name=None, mnemonic=None, infura_token=None, alchemy_token=None, coingecko_api_key=None)`
@@ -156,7 +167,7 @@ Returns the current `Account` instance, allowing method chaining.
#### Login with Display name
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -175,7 +186,7 @@ The code above is equivalent to the following screen on Status App:
#### Login with ENS
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -194,7 +205,7 @@ You can purchase a **universal username** on Status App:
#### Login with `key_uid`
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -207,7 +218,7 @@ account.login(**params)
#### Recover account
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -227,7 +238,7 @@ The code above is equivalent to the following screen on Status App:
#### Wallet setup
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -248,7 +259,7 @@ account.login(**params)
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.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -274,7 +285,7 @@ Returns `str` representing the **Docker path** of the generated backup file. The
The filename is generated by the Status Backend and follows the pattern `<suffix>_user_data.bkp`, where `<suffix>` is the **last 6 characters of the account's compressed public key**. For example, an account whose compressed key ends in `abc123` produces `abc123_user_data.bkp`. Because the suffix is derived deterministically from the account's key, the same account always maps to the same filename, which is how a backup is uniquely associated with its account.
```python
from bot import Account
from status_sdk import Account
account = Account(backup_folder=r"C:\\Users\\me\\status-backups")
params = {
@@ -299,7 +310,7 @@ Send a text message to a specific chat. This method currently supports **text me
| `message` | `str` | Yes | The text message to send. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -330,7 +341,7 @@ Messages can be fetched from:
Returns `list[dict]` containing message objects. Timestamp fields returned by the backend are automatically converted into `datetime.datetime` objects.
```python
from bot import Account
from status_sdk import Account
import datetime
account = Account()
@@ -358,7 +369,7 @@ for message in messages:
Listen for new incoming messages **in real time**. This method yields raw message events as they are received from the Status Backend [signal](./account.md#signallisten) `messages.new`. This method is ideal for developing real time chat applications
```python
from bot import Account
from status_sdk import Account
import datetime
# For terminal readability only
from rich import print as rprint
@@ -400,7 +411,7 @@ Modes:
Returns the current `Account` instance, allowing method chaining.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -443,7 +454,7 @@ Returns `bool`.
| `False` | The contact does not exist or was already removed. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -473,7 +484,7 @@ Send a request to join a community using its invitation URL. The method parses t
Returns `datetime.datetime` representing when the join request was submitted.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -506,7 +517,7 @@ Returns `pd.DataFrame`.
| `source_id` | `str` | Source list from which the token was fetched. |
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -539,7 +550,7 @@ Returns `pd.DataFrame`.
| `price` | `float` | Token price **for 1 `token_symbol`** in the given fiat currency (only present if `ccy` is provided). If you want to get the amount in the wallet, you must `amount * price`. |
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -566,7 +577,7 @@ data = account.get_balance(token_addresses)
Access multuple chains:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -594,7 +605,7 @@ data = account.get_balance(token_addresses, chain_ids)
Access multiple wallets:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -626,7 +637,7 @@ data = account.get_balance(token_addresses, chain_ids, wallets)
Get token prices:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -705,7 +716,7 @@ Returns `pd.DataFrame`, sorted by `timestamp` in descending order (newest first)
| `trx_fee` | `float` | Gas fee paid in the chain's native token, computed as `gas_price * gas_used / 10**18`. Populated only for `sent` rows; `0` for `received` rows since the receiver does not pay gas. |
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -744,7 +755,7 @@ Returns `str` representing the **transaction hash**. The hash can be appended to
Send ETH:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -770,7 +781,7 @@ print(f"Transaction: https://etherscan.io/tx/{tx_hash}")
Send an ERC-20 token by symbol:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -795,7 +806,7 @@ tx_hash = account.send_transaction(
Send an ERC-20 token by contract address:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -821,7 +832,7 @@ tx_hash = account.send_transaction(
Send on a different chain:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -873,7 +884,7 @@ Returns `str` representing the **transaction hash**. The hash can be appended to
Swap **ETH** for an **ERC-20** token:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -897,7 +908,7 @@ print(f"Swap: https://etherscan.io/tx/{tx_hash}")
Swap **ERC-20** for an **ETH** token:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -921,7 +932,7 @@ print(f"Swap: https://etherscan.io/tx/{tx_hash}")
Swap **ERC-20** for an **ERC-20** token:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -965,7 +976,7 @@ Returns `list[dict]`, one entry per locally available account.
| `created_at` | `datetime.datetime` | Timestamp when the account was created locally. |
```python
from bot import Account
from status_sdk import Account
# For terminal readability only
from rich import print as rprint
from rich.pretty import Pretty
@@ -994,7 +1005,7 @@ Provides information about the currently logged-in account. If `login()` has not
| `logged_in_timestamp` | `datetime.datetime` | Timestamp when the account successfully logged in. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1013,7 +1024,7 @@ Get or update the current display name of the loggedin account.
Returns `str` when reading the property.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1029,7 +1040,7 @@ print(account.display_name)
You can update the display name by assigning a new value:
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1052,7 +1063,7 @@ Get or update the **bio** of the currently loggedin account. The length of th
Returns `str` when reading the property.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1068,7 +1079,7 @@ print(account.bio)
The value assigned to `bio` will automatically be converted to a string before being sent to the backend. You can update the bio by assigning a new value:
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1085,7 +1096,7 @@ print(account.bio)
You can also **clear the bio** by deleting the property:
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1107,7 +1118,7 @@ Get or update the **profile picture** of the currently loggedin account. The
Returns `PIL.Image.Image` when reading the property, or `None` if no profile picture has been set.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1125,7 +1136,7 @@ if image:
The file path assigned to `profile_picture` will be automatically set as the latest profile picture in Status App. If the given file does not exist or the extension is not supported, an **exception will be raised**. Supported image formats are `.jpg`, `.jpeg` and `.png`.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1154,7 +1165,7 @@ The property exposes the following methods:
- `signal.expect()` - return a context manager that waits for one or more matching signals to arrive **after** you perform an action. This is the recommended way to make **async message calls**, since it removes the race conditions and infinite-loop risk of `get()`.
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1191,7 +1202,7 @@ Default logger configuration:
Example:
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -1236,7 +1247,7 @@ Returns `dict[str, dict]` where the key is the contact's **public key**. This ma
| `last_updated` | `datetime.datetime` | Timestamp when the contact information was last updated. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1304,7 +1315,7 @@ Channel permissions:
| `token_gated` | `bool` | Whether the channel requires a token to participate. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1341,7 +1352,7 @@ Returns `pd.DataFrame`.
| `status_alias` | `str` | Initial display name of the member when the account was created. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1370,7 +1381,7 @@ Returns `list[dict]` where each `dict` represents a chat that can be used with [
| `name` | `str` | Either the display name of the user or the community channel name. |
```python
from bot import Account
from status_sdk import Account
account = Account()
params = {
@@ -1400,7 +1411,7 @@ Returns `dict[int, str]`.
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -1430,7 +1441,7 @@ Returns `pd.DataFrame`.
| `symbol` | `str` | Token symbol (e.g. `ETH`, `USDT`). |
```python
from bot import Account
from status_sdk import Account
account = Account()
@@ -1448,7 +1459,7 @@ print(account.balance)
You can convert the current balance into fiat currency by using a [ISO 4217 currency code](https://www.iso.org/iso-4217-currency-codes.html) in the `[]` accessor:
```python
from bot import Account
from status_sdk import Account
account = Account()
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+65 -5
View File
@@ -6,7 +6,7 @@ Helper functions for setting up the Status Backend environment.
## Methods
### `launch_docker_container(commit=None, wait_seconds=5)`
### `launch_docker_container(commit=None, wait_seconds=5, platform="linux/amd64")`
Launch Status Backend Docker container in the background using `docker-compose.yaml`. If `docker` is not installed, or if the container fails to start, an **exception will be raised** with the error message from Docker. The container is built from [`status-im/status-go`](https://github.com/status-im/status-go) at the git ref you choose:
@@ -16,22 +16,82 @@ context: https://github.com/status-im/status-go.git#${STATUS_GO_REF:-develop}
The image is always rebuilt (`docker compose up --build`) so a newly chosen `commit` is picked up instead of reusing a previously built image.
**Note**: The container mounts the SDK's `backups/` and `assets/` folders as Docker volumes. Make sure the repository has **read and write permissions**, otherwise the container will fail to start or [backups](./account.md#backups) and [profile pictures](./account.md#profile_picture) will not be saved. On Docker Desktop the repository must also be a **shared path** (see [Windows](./utils.md#windows) and [Mac](./utils.md#mac)).
| Name | Type | Required | Description |
|-----|-----|-----|-------------|
| `commit` | `str` | No | The `status-im/status-go` git ref to build from - a commit SHA, branch, or tag. When omitted, the latest `develop` branch is built. |
| `wait_seconds` | `int` | No | Number of seconds to pause after the `docker compose up` command returns, giving Status Backend enough time to finish booting before subsequent code runs. Defaults to `5`. This matters mainly when the container already exists and is being restarted, because `docker compose up` returns immediately while the backend is still warming up - instantiating [`Account`](./account.md#accountdomainlocalhost-port8080-is_securefalse-backup_foldernone) too quickly will fail to connect. |
| `wait_seconds` | `int` | No | Number of seconds to pause after the `docker compose up` command returns, giving Status Backend enough time to finish booting before subsequent code runs. Defaults to `5`. This matters mainly when the container already exists and is being restarted, because `docker compose up` returns immediately while the backend is still warming up - instantiating [`Account`](./account.md#accountdomainlocalhost-port8080-is_securefalse-backup_foldernone) too quickly will fail to connect. On [Windows](./utils.md#windows) the same value is used to wait between retries after WSL has been restarted. |
| `platform` | `str` | No | The platform the image is built for. Defaults to `linux/amd64`. Run `docker buildx ls` to see the platforms your Docker installation supports, and pass the matching value if the default does not build on your machine. |
Wait time after container has launched:
```python
from bot import launch_docker_container
from status_sdk import launch_docker_container
# Build from the latest develop branch
launch_docker_container(wait_seconds=10)
```
Building from a specific commit:
```python
from status_sdk import launch_docker_container
# Pin a specific status-go commit
# https://github.com/status-im/status-go/commit/2bee8b6a38cdc8f92d74e2dbb8c4e77fbbeea149
launch_docker_container(commit="2bee8b6a38cdc8f92d74e2dbb8c4e77fbbeea149")
```
**Windows Note**: In Docker go to `Settings > Resources > WSL integration` and make sure `Enable integration with my default WSL distro` and `Ubuntu` are **turned on**.
Building for a specific platform:
```bash
docker buildx ls
```
```python
from status_sdk import launch_docker_container
# Build for Raspberry PI 5 - arm64
launch_docker_container(platform="linux/arm64")
```
#### Windows
In Docker go to `Settings > Resources > WSL integration` and make sure `Enable integration with my default WSL distro` and `Ubuntu` are **turned on**.
![Image of Windows Note](./images/wsl-docker.png)
Docker Desktop creates internal intermediary mounts inside its WSL 2 environment when bind-mounting paths from a WSL distribution into a container. In some cases, these mounts can become **stale**, and the container may fail to start with:
```
error while creating mount source path '/run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/Ubuntu/...': file exists
```
The only reliable way to clear the cache is to restart WSL. Method `launch_docker_container` will do the following when the container fails to start:
1. `wsl --shutdown` is run to clear the stale mounts. Keep in mind that this will shut down **every** WSL distribution, not just the one Docker uses. Any other WSL session running at the same time will be terminated.
2. The container is launched again, sleeping `wait_seconds` between each attempt, until it starts.
WSL boots back up on demand, so no manual step is needed. Docker Desktop does need a moment to bring its backend back up, which is why the retries are spaced out - **increase `wait_seconds` if the container takes a long time to come back**.
#### Mac
In Docker go to `Settings > Resources > File Sharing` and make sure the SDK repository is added to **Virtual file shares**.
![Image of Mac Note](./images/mac-docker.png)
#### Linux
Make sure the installed `status_sdk` folder has **read and write permissions**, otherwise the container will fail to start or [backups](./account.md#backups) and [profile pictures](./account.md#profile_picture) will not be saved.
If the package was installed with `sudo` (for example `sudo pip install`), the folder is owned by `root` and your user cannot write to it. Take ownership of the package so your existing permissions apply (adjust the path to where `status_sdk` is installed):
```bash
sudo chown -R $USER:$USER /path/to/status_sdk
```
`$USER` expands to your current login user, so both the owner and group of every file under `status_sdk` are set to you. If the container writes as a different user than the one that installed the package, `chown` alone is not enough - grant read and write permissions to everyone instead:
```bash
sudo chmod -R a+rw /path/to/status_sdk
```
![Image of Note](./images/wsl-docker.png)
+111
View File
@@ -0,0 +1,111 @@
# Onchain Agent
A **personal crypto assistant** that lives inside Status App. The script logs into a Status account, listens for incoming messages **in real time**, and answers them with a [Groq](https://groq.com/) that has been given tools to read and act on the account.
The agent can look up balances, tokens, contacts and transaction history - and it can also **send messages, send crypto and swap tokens** on your behalf.
## How it works
```mermaid
flowchart LR
contact[Your Status Account]
subgraph bot[Status AI Agent]
listen[listen_messages]
agent[LangChain Agent]
tools[Status Tools]
end
llm[Groq LLM]
backend[status-im/status-go]
contact -->|message| listen
listen -->|prompt| agent
agent <-->|reasoning| llm
agent -->|tool call| tools
tools <--> backend
agent -->|reply| contact
```
## Tools
Each tool is a thin wrapper around the [Python SDK](../../README.md). They are defined in [`tools.py`](./tools.py), and their arguments are validated by the [pydantic](https://docs.pydantic.dev/) models in [`models.py`](./models.py).
| Tool | SDK | What the agent can do |
|-----|-----|-------------|
| `get_balance` | [`balance`](../../docs/account.md#balance) | Read the account's wallet balance, optionally enriched with market data. |
| `get_account_info` | [`info`](../../docs/account.md#info) | Read public account details. `password` and `mnemonic` are **excluded**. |
| `get_account_contacts` | [`contacts`](../../docs/account.md#contacts) | List contacts, contact requests and group chats. |
| `manage_contact` | [`add_contact`](../../docs/account.md#add_contactpublic_key-display_namenone) / `remove_contact` | Accept, send, decline and remove contact requests. |
| `get_token_info` | [`get_tokens`](../../docs/account.md#get_tokens) | Look up chains, token symbols and token addresses. |
| `search_external_balance` | [`get_balance`](../../docs/account.md#get_balancetoken_addresses-chain_ids1-walletsnone-ccynone) | Read the balance of **any** wallet address, not just the account's. |
| `search_messages` | [`get_messages`](../../docs/account.md#get_messageschat_id-start_timestampnone-end_timestampnone) | Read chat history for a date range, including payment requests. |
| `search_transactions` | [`get_transactions`](../../docs/account.md#get_transactionsrefreshfalse) | Read historical wallet transactions. |
| `send_message` | [`send_message`](../../docs/account.md#send_messagechat_id-message) | **Send a message** to any chat. |
| `send_transaction` | [`send_transaction`](../../docs/account.md#send_transactionaddress-symbol-amount-chain_id1) | **Send crypto** to any address. |
| `swap_tokens` | [`swap_tokens`](../../docs/account.md#swap_tokensfrom_token-to_token-amount-chain_id1) | **Swap tokens** in the wallet. |
**Note**: The last three tools move real funds and send real messages. See [Security](./README.md#security).
## Setup
### 1. Install
From the **repository root**, install the SDK with the `agent` dependencies:
```
pip install ".[agent]"
```
### 2. Configure
Copy [`env.example`](./env.example) to `.env` in this folder and fill it in:
```
cp env.example .env
```
| Variable | What it is |
|-----|-------------|
| `PASSWORD` | The password of your Status account. |
| `NAME` | The [display name](../../docs/account.md#display-name) or ENS name of the account. If you have previously logged in with the SDK you can provide an ENS. For first time log ins, it is best to provide a [display name](../../docs/account.md#display-name). |
| `MNEMONIC` | The [recovery phrase](https://status.app/help/profile/understand-your-status-keys-and-recovery-phrase) of the account. Used to recover it into the container. |
| `ALCHEMY_TOKEN` | [Alchemy](https://www.alchemy.com/) token - needed for transaction history. |
| `COINGECKO_API_KEY` | [CoinGecko](https://www.coingecko.com/) key - needed for token prices. |
| `INFURA_TOKEN` | [Infura](https://www.infura.io/) token - needed for Ethereum RPC. |
| `GROQ_API_KEY` | [Groq API key](https://console.groq.com/) for the LLM. |
| `GROQ_MODEL` | The Groq model name, e.g. `llama-3.3-70b-versatile`. |
| `FROM_PUBLIC_KEY` | The public key of the account the bot will **listen and reply to**. This is the account you message the bot *from*. |
All three wallet keys (`ALCHEMY_TOKEN`, `COINGECKO_API_KEY`, `INFURA_TOKEN`) are required - without all of them the wallet tools raise a custom exception.
### 3. Run
The script imports `tools` and `models` as **top-level modules**, so it must be run from inside this folder:
```
cd examples/agents
python main.py
```
On the first run, [`launch_docker_container`](../../docs/utils.md#launch_docker_container) builds the Status Backend image, which takes a few minutes. The account is then recovered from `MNEMONIC` and the bot starts listening:
```
[INFO] Running Docker on <your-os-here>
[INFO] Successfully logged in!
[INFO] Starting messaging
[INFO] Messaging launched
```
Now message the bot from the account matching `FROM_PUBLIC_KEY`. It runs until you stop it with `Ctrl+C`.
## Security
**This agent has full control of the Status account and its wallet.** It can send messages as you, transfer crypto out of your wallet, and swap your tokens - and it decides to do so based on the output of an LLM.
The safeguards in this example are deliberately simple:
- **One sender only.** Messages are ignored unless `latest_message["from"] == FROM_PUBLIC_KEY`. Anyone else messaging the bot is not processed.
- **Secrets are withheld from prompts.** `get_account_info` strips `password` and `mnemonic` before the LLM ever sees the account details.
That is the whole boundary. There is **no** spending limit, no confirmation step and no allowlist of receiver addresses. Anyone who can send messages from `FROM_PUBLIC_KEY` - or anyone who can convince the LLM through a [prompt injection](https://en.wikipedia.org/wiki/Prompt_injection) in the chat content - can move funds.
Use a **dedicated account with a small balance**. Do not point this at a wallet you care about.
+2 -8
View File
@@ -2,15 +2,9 @@ from langchain_groq import ChatGroq
from langchain.agents import create_agent
from dotenv import load_dotenv
from typing import Optional
import os, sys
import pandas as pd
# Temp solution until repo it turned into a PyPI library
# Add the repo root to sys.path so `bot` is importable when running this
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
import os
import tools
from bot import Account, launch_docker_container
from status_sdk import Account, launch_docker_container
class StatusToolKit:
-5
View File
@@ -1,5 +0,0 @@
tabulate
langchain
langchain-core
langchain-groq
python-dotenv
+1 -1
View File
@@ -5,7 +5,7 @@ import pandas as pd
import datetime
import models
from bot import Account, exceptions
from status_sdk import Account, exceptions
class StatusBaseTool(BaseTool):
+51
View File
@@ -0,0 +1,51 @@
[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"
[project]
name = "status-sdk"
version = "1.0.0"
description = "Private chat. Communities. Multi-chain wallet. Browser. dApps all in one app, powered by SNT."
readme = "README.md"
requires-python = ">=3.12"
license = "MIT"
license-files = ["LICENSE.txt"]
authors = [{ name = "Status Research & Development GmbH" }]
keywords = ["privacy", "decentralized", "messenger", "ethereum", "blockchain", "cryptocurrency"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Topic :: Software Development :: Libraries :: Python Modules",
]
dependencies = [
"requests",
"websocket-client",
"websockets",
"pandas",
"pillow",
"eth-abi",
]
[project.optional-dependencies]
agents = [
"tabulate",
"langchain",
"langchain-core",
"langchain-groq",
"python-dotenv",
]
[project.urls]
Source = "https://github.com/status-im/status-python-sdk"
Issues = "https://github.com/status-im/status-python-sdk/issues"
"Status App" = "https://status.app/"
"Status Backend" = "https://github.com/status-im/status-go"
[tool.setuptools]
packages = ["status_sdk"]
[tool.setuptools.package-data]
status_sdk = ["docker-compose.yaml"]
-6
View File
@@ -1,6 +0,0 @@
requests
websocket-client
websockets
pandas
pillow
eth-abi
@@ -1,3 +1,5 @@
from .account import Account
from .utils import launch_docker_container
from . import exceptions
__all__ = ["Account", "launch_docker_container", "exceptions"]
+8 -6
View File
@@ -35,7 +35,7 @@ class Account:
}
__ETH_ADDRESS = "0x0000000000000000000000000000000000000000"
def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_port: int = 9000, is_secure: bool = False, backup_folder: Optional[str] = None):
def __init__(self, domain: str = "localhost", backend_port: int = 8080, media_port: int = 9000, is_secure: bool = False, backup_folder: Optional[str] = None, volume_folder: Optional[str] = None):
"""
Work with your own Status App account
@@ -44,7 +44,8 @@ class Account:
- `backend_port` - the port to connect to Status Backend. If this is changed, the published port for `backend_port` must be updated to match in `docker-compose.yaml` as well.
- `media_port` - the port to connect to Status localhost images. If this is changed, the published port for `media_port` must be updated to match in `docker-compose.yaml` as well.
- `is_secure` - if `http` or `https` should be used
- `backup_folder` - where backup files will be created and stored
- `backup_folder` - where backup files will be created and loaded
- `volume_folder` - directory containing the `backups` and `assets` folders mounted into the Status Backend Docker container (folder holding `docker-compose.yaml`). Defaults to this package's own installation folder. Set this when Status Backend is launched from a different `docker-compose.yaml` location, such as a local clone of the repo.
"""
# Wallet transactions
self.__alchemy_token = None
@@ -54,16 +55,17 @@ class Account:
# Path of the backups in the Docker container for Status Backend
self.__docker_backup_folder = "./root/.config/Status/backups"
self.__backup_folder = backup_folder
# PyPI installation folder
sdk_folder = volume_folder if volume_folder else os.path.dirname(__file__)
# As the docker-compose.yaml folder is at the moment
# NOTE: This might change for initial release
self.__backup_sdk_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), "backups")
self.__backup_sdk_folder = os.path.join(sdk_folder, "backups")
os.makedirs(self.__backup_sdk_folder, exist_ok=True)
# Path of where images will be uploaded to Status Backend
self.__docker_asset_folder = "./assets"
# As the docker-compose.yaml folder is at the moment
# NOTE: This might change for initial release
self.__assets_local_folder = os.path.join(os.path.dirname(os.path.dirname(__file__)), "assets")
self.__assets_local_folder = os.path.join(sdk_folder, "assets")
os.makedirs(self.__assets_local_folder, exist_ok=True)
self.__logger = Logger()
@@ -1379,7 +1381,7 @@ class Account:
folder = self.__backup_folder if self.__backup_folder else self.__backup_sdk_folder
file_name = self.info["compressed_key"][-6:] + "_user_data.bkp"
file_path = os.path.join(folder, self.info["compressed_key"][-6:] + "_user_data.bkp")
file_path = os.path.join(folder, file_name)
if not os.path.exists(file_path):
self.logger.warning(f"Backup file was not found in {folder}")
return

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 177 KiB

@@ -2,7 +2,7 @@ services:
backend:
build:
context: https://github.com/status-im/status-go.git#${STATUS_GO_REF:-develop}
platform: linux/amd64
platform: ${STATUS_GO_PLATFORM:-linux/amd64}
container_name: status-backend
ports:
- 8080:8080 # backend_port
+64
View File
@@ -0,0 +1,64 @@
import shutil, os, subprocess, sys, time
from pathlib import Path
from typing import Optional
from .logger import Logger
from . import exceptions
def launch_docker_container(commit: Optional[str] = None, wait_seconds: int = 5, platform: str = "linux/amd64"):
"""
Launch the Status Backend Docker container using `docker-compose.yaml`
NOTE: On Windows, Docker Desktop caches the Docker volume bind mounts in the WSL
virtual machine. When the mounts go stale the container cannot start. WSL is
restarted to clear the cache and the container is launched again until it is up.
Parameters:
- `commit` - the commit SHA. If no commit is provided, the latest version is pulled
- `wait_seconds` - number of seconds to wait before the code resumes. Sleep prevents calling `class Account` faster than launching the docker container. This only happens when the container already exists and it is must be turned on. On Windows the same value is used to wait between retries after WSL has been restarted.
- `platform` - the platform the image is built for. Defaults to `linux/amd64`. Run `docker buildx ls` to see the platforms your Docker installation supports.
"""
logger = Logger()
system = sys.platform
is_windows = system == "win32"
if not shutil.which("docker"):
raise exceptions.DockerError("Please install Docker.")
logger.info(f"Running Docker on {system}")
ref = commit if commit else "develop"
DOCKER_COMPOSE_PATH = os.path.join(os.path.dirname(__file__), "docker-compose.yaml")
docker_path = DOCKER_COMPOSE_PATH
if is_windows:
p = Path(DOCKER_COMPOSE_PATH)
drive = p.drive.rstrip(":").lower()
docker_path = f"/mnt/{drive}/" + "/".join(p.parts[1:])
cmd = ["env", f"STATUS_GO_REF={ref}", f"STATUS_GO_PLATFORM={platform}", "docker", "compose", "-f", docker_path, "up", "-d", "--build"]
if is_windows:
if not shutil.which("wsl"):
raise exceptions.DockerError("Please install wsl - https://learn.microsoft.com/en-us/windows/wsl/install.")
cmd.insert(0, "wsl")
logger.info(f"Running:\n{' '.join(cmd)}")
docker_compose_up = lambda: subprocess.run(cmd, cwd=os.path.dirname(DOCKER_COMPOSE_PATH), stderr=subprocess.PIPE, text=True)
result = docker_compose_up()
if result.returncode != 0 and is_windows:
logger.warning("Command failed! Restarting wsl...")
subprocess.run(["wsl", "--shutdown"])
attempt = 1
while result.returncode != 0:
result = docker_compose_up()
if result.returncode == 0:
logger.info(f"Container started on attempt {attempt}!")
break
logger.warning(f"Attempt {attempt} failed... Sleeping for {wait_seconds}s")
time.sleep(wait_seconds)
attempt += 1
if result.returncode != 0:
raise exceptions.DockerError(result.stderr.strip())
logger.info(f"Docker Container successfully launched! Sleeping for {wait_seconds}s")
time.sleep(wait_seconds)