Security fixes, ready-to-deploy image, and rewritten docs
Audit fixes: - Wallet names were used unvalidated as file paths, so a name like "../<other-id>/x" reached another user's wallet directory or overwrote bot_settings.json. Names are now validated and every path is resolved and confirmed to sit inside the caller's own directory, which also blocks a planted symlink from escaping. - Tor mode leaked: the mempool.space fee check opened a direct clear-net connection, tying the server's IP to its Bitcoin activity. It now goes through Tor's SOCKS proxy to mempool's onion service, DNS included. - The xpub message was left in the chat forever. It reveals the wallet's whole address and balance history, so it is now deleted like the password is. - Inline buttons stay live on old messages, so a user removed from the allow-list could keep using them. Callbacks re-check authorisation, and auth_check now works for callback queries instead of raising on a null message. - Wallet names echoed back in callback_data are confirmed against the user's real wallet list. - Settings are written via temp file + atomic rename; a crash mid-write used to truncate the file and lose every user's wallet selection. - Replies were sent against messages that had just been deleted; deletion is now best-effort and cannot abort the flow. Deployment: - Allow-list can come from ALLOWED_USERS instead of a bind-mounted file, so a deployment needs no files on disk. Both sources merge when both are present. - A missing bind-mounted allowed_users.txt made Docker create a directory in its place and silently lock everyone out; the entrypoint now detects that and stops with an explanation. - Standard mode honours ELECTRUM_SERVER, which previously only worked in Tor mode despite being documented. - Added VERSION, push.sh, docker-compose.deploy.yml and .env.deploy.example for a two-file deploy off the published image. Docs rewritten in English and Persian around the name Telegtrum, leading with the security model and what it does not protect against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,299 +1,226 @@
|
||||
# Online Wallet Bot
|
||||
# Telegtrum
|
||||
|
||||
A self-hosted Telegram bot for managing Bitcoin wallets using Electrum. Designed for watch-only (xpub) wallets with air-gapped signing support.
|
||||
**Telegram + Electrum** — a self-hosted Telegram bot for managing Bitcoin wallets with Electrum.
|
||||
|
||||
## Features
|
||||
Telegtrum is built for **watch-only (xpub) wallets with air-gapped signing**. Your private keys never touch the server. The bot can see balances, hand out receiving addresses and build *unsigned* transactions; only your offline device can sign them.
|
||||
|
||||
- Per-user isolated wallets
|
||||
- Watch-only wallet restore from xpub
|
||||
- Balance display (Confirmed / Unconfirmed)
|
||||
- Receive address generation
|
||||
- Unsigned transaction (PSBT) creation with fee options
|
||||
- Unit switching: BTC / mBTC / sats
|
||||
- Persian (Farsi) and English language support
|
||||
- Persistent bottom keyboard
|
||||
- Optional Tor support (connect via .onion to a public server or your own node)
|
||||
---
|
||||
|
||||
## Security model — read this first
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **The server never holds private keys** | You import an **xpub**. It can watch, not spend. |
|
||||
| **Spending needs your offline device** | The bot produces an unsigned transaction; you sign it elsewhere and paste it back to broadcast. |
|
||||
| **Access is deny-by-default** | Only Telegram user IDs you list may use the bot. An empty list refuses everyone. |
|
||||
| **Secrets are scrubbed from chat** | Wallet passwords and your xpub are deleted from the conversation right after they're read. |
|
||||
| **Wallets are isolated per user** | Each Telegram user gets their own directory; names are validated so one user cannot reach another's files. |
|
||||
|
||||
**What this does not protect against.** Anyone who controls your Telegram account can see your balances and addresses, and can build transactions (they still cannot sign them). Anyone with root on the server can read wallet files and see wallet passwords in the process list while an Electrum command runs — so treat the server as trusted infrastructure and keep the allow-list short. Your xpub is a privacy secret: whoever has it can see your entire transaction history forever.
|
||||
|
||||
---
|
||||
|
||||
## Quick deploy (recommended)
|
||||
|
||||
No source checkout, no build. Two files and a `docker compose up`.
|
||||
|
||||
```bash
|
||||
mkdir telegtrum && cd telegtrum
|
||||
|
||||
curl -O https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/docker-compose.deploy.yml
|
||||
curl -o .env https://git.goyban.com/goyban/telegtrum_bot/raw/branch/main/.env.deploy.example
|
||||
|
||||
nano .env # add your bot token and your Telegram user ID
|
||||
docker compose -f docker-compose.deploy.yml up -d
|
||||
docker compose -f docker-compose.deploy.yml logs -f
|
||||
```
|
||||
|
||||
Your `.env` needs two things:
|
||||
|
||||
```dotenv
|
||||
TELEGRAM_BOT_TOKEN=123456:ABC... # from @BotFather
|
||||
ALLOWED_USERS=123456789 # from @userinfobot, comma-separated for several
|
||||
```
|
||||
|
||||
Compose refuses to start if `ALLOWED_USERS` is unset, rather than launching a bot nobody can use.
|
||||
|
||||
You should see `Electrum daemon started. Launching bot...`. Message your bot on Telegram and press **Add Wallet**.
|
||||
|
||||
### Updating
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.deploy.yml pull
|
||||
docker compose -f docker-compose.deploy.yml up -d
|
||||
```
|
||||
|
||||
Wallet data lives in the `telegtrum-data` volume and survives updates.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Linux server with Docker and Docker Compose installed
|
||||
- A Telegram bot token (from [@BotFather](https://t.me/BotFather))
|
||||
- Your Telegram user ID (from [@userinfobot](https://t.me/userinfobot))
|
||||
- A Linux server with Docker and Docker Compose
|
||||
- A Telegram bot token — [@BotFather](https://t.me/BotFather)
|
||||
- Your Telegram user ID — [@userinfobot](https://t.me/userinfobot)
|
||||
|
||||
---
|
||||
|
||||
## Server Setup (Step by Step)
|
||||
## Building from source
|
||||
|
||||
### 1. Clone the repository
|
||||
Only needed if you want to modify the bot.
|
||||
|
||||
```bash
|
||||
git clone https://git.goyban.com/goyban/telegtrum_bot.git
|
||||
cd telegtrum_bot
|
||||
cp .env.example .env && nano .env
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### 2. Create your `.env` file
|
||||
The source compose files mount `bot.py` read-only, so you can edit it and `docker compose restart` without rebuilding.
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
### Allow-list: file or environment
|
||||
|
||||
Either works; if you use both, the two lists are merged.
|
||||
|
||||
**Environment** (simplest — nothing to mount):
|
||||
```dotenv
|
||||
ALLOWED_USERS=123456789,987654321
|
||||
```
|
||||
|
||||
Fill in:
|
||||
|
||||
```
|
||||
ELECTRUM_VERSION=4.7.2
|
||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||
```
|
||||
|
||||
### 3. Add your Telegram user ID to the allowed users list
|
||||
|
||||
**File** — `allowed_users.txt`, one user per line, `id, Name`:
|
||||
```bash
|
||||
cp allowed_users.txt.example allowed_users.txt
|
||||
nano allowed_users.txt
|
||||
```
|
||||
|
||||
Add a line in the format `userid, Name`:
|
||||
|
||||
```
|
||||
123456789, Alice
|
||||
```
|
||||
|
||||
You can find your user ID by messaging [@userinfobot](https://t.me/userinfobot) on Telegram.
|
||||
> If you bind-mount `allowed_users.txt` and the file doesn't exist, Docker silently creates a **directory** in its place and nobody can log in. The container now detects this and stops with an explanation instead of starting unusable.
|
||||
|
||||
### 4. Build and start the container
|
||||
### Publishing a new image
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
./push.sh # tag comes from the VERSION file
|
||||
./push.sh 1.0.1 # or pass one explicitly
|
||||
```
|
||||
|
||||
This will:
|
||||
- Build the Docker image with Electrum installed
|
||||
- Start the Electrum daemon
|
||||
- Launch the Telegram bot
|
||||
|
||||
### 5. Check logs
|
||||
|
||||
```bash
|
||||
docker compose logs -f
|
||||
```
|
||||
|
||||
You should see `Electrum daemon started. Launching bot...` followed by the bot starting up.
|
||||
|
||||
---
|
||||
|
||||
## Local / Umbrel Mode
|
||||
## Connection modes
|
||||
|
||||
Use this mode to connect to your own Electrum server running on Umbrel (or any local node) on your home network.
|
||||
### Standard — public Electrum servers
|
||||
|
||||
> **Note:** `umbrel.local` does not resolve inside Docker containers — mDNS is host-only. Use the actual **IP address** of your Umbrel device (e.g. `192.168.1.100`).
|
||||
The default. Electrum picks public peers. Pin it to one server instead:
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
nano .env
|
||||
```
|
||||
|
||||
Fill in `TELEGRAM_BOT_TOKEN` and set your Umbrel's IP:
|
||||
|
||||
```
|
||||
UMBREL_IP=192.168.1.100
|
||||
```
|
||||
|
||||
```bash
|
||||
cp allowed_users.txt.example allowed_users.txt
|
||||
nano allowed_users.txt
|
||||
```
|
||||
|
||||
### Start
|
||||
```dotenv
|
||||
ELECTRUM_SERVER=192.168.1.100:50001:t
|
||||
```
|
||||
|
||||
### Local / Umbrel — your own node
|
||||
|
||||
```bash
|
||||
cp .env.example .env # set TELEGRAM_BOT_TOKEN and UMBREL_IP
|
||||
docker compose -f docker-compose.local.yml up -d --build
|
||||
```
|
||||
|
||||
### How it works
|
||||
> `umbrel.local` will **not** resolve inside a container — mDNS is host-only. Use the actual IP, e.g. `192.168.1.100`.
|
||||
|
||||
On first start, the entrypoint writes `./electrum-data/config` with:
|
||||
On first start the entrypoint writes `./electrum-data/config` pinning that server. An existing config is left alone, so manual edits survive restarts.
|
||||
|
||||
```json
|
||||
{
|
||||
"auto_connect": false,
|
||||
"enable_proxy": false,
|
||||
"oneserver": true,
|
||||
"server": "192.168.1.100:50001:t"
|
||||
}
|
||||
```
|
||||
> If it won't connect, check that the server string in `./electrum-data/config` still ends in `:t` (TCP). Electrum sometimes rewrites it to `:s` (SSL). Stop the container, change it back, restart.
|
||||
|
||||
If the config file already exists it is left untouched, so manual edits survive restarts.
|
||||
|
||||
> **If the connection doesn't work:** check `./electrum-data/config` after the first run and make sure the server string still ends in `:t` (TCP). Electrum may rewrite it to `:s` (SSL) on first connect. If it did, stop the container, change it back to `:t`, and restart.
|
||||
|
||||
### Check logs
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.local.yml logs -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tor Mode
|
||||
|
||||
Tor mode runs Electrum through the Tor network. It uses a separate Docker Compose file and Dockerfile so the standard setup is not affected.
|
||||
|
||||
### Setup
|
||||
### Tor
|
||||
|
||||
```bash
|
||||
cp .env.tor.example .env
|
||||
nano .env
|
||||
docker compose -f docker-compose.tor.yml up -d --build
|
||||
```
|
||||
|
||||
Fill in `TELEGRAM_BOT_TOKEN` and choose a connection mode:
|
||||
|
||||
**Option A — your own Electrum server over Tor:**
|
||||
|
||||
```
|
||||
Either point it at your own onion node:
|
||||
```dotenv
|
||||
ELECTRUM_SERVER=yournode123.onion:50001:t
|
||||
```
|
||||
…or leave it unset to use a public onion server (`electrums3lojbuj.onion:50001:t`, override with `ELECTRUM_ONION_SERVER`).
|
||||
|
||||
**Option B — public .onion server (default, no extra config needed):**
|
||||
|
||||
Leave `ELECTRUM_SERVER` unset. The bot will connect to `electrums3lojbuj.onion:50001:t` by default. Override with `ELECTRUM_ONION_SERVER` if needed.
|
||||
|
||||
### Start
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.tor.yml up -d --build
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- Tor runs inside the same container — no sidecar needed.
|
||||
- The container waits for the Tor SOCKS proxy to be ready before starting Electrum.
|
||||
- Wallet data is stored in a separate volume (`electrum-tor-data`) so it does not mix with the standard setup.
|
||||
- `t` in the server string means TCP — encryption is handled by the Tor layer, not SSL.
|
||||
Notes:
|
||||
- Tor runs inside the same container; the entrypoint waits for the SOCKS proxy before starting Electrum.
|
||||
- **The fee lookup goes through Tor too.** It uses mempool.space's onion service, so checking fees cannot tie your server's IP to your Bitcoin activity.
|
||||
- Wallet data uses its own volume (`electrum-tor-data`), kept separate from standard mode.
|
||||
- `:t` means TCP — Tor provides the encryption, so SSL is not needed.
|
||||
|
||||
---
|
||||
|
||||
## Updating the Bot
|
||||
## Using the bot
|
||||
|
||||
The `bot.py` file is mounted as a volume, so you can update it without rebuilding the image:
|
||||
A persistent keyboard sits at the bottom of the chat.
|
||||
|
||||
```bash
|
||||
# Edit bot.py, then:
|
||||
docker compose restart
|
||||
# or for Tor mode:
|
||||
docker compose -f docker-compose.tor.yml restart
|
||||
```
|
||||
|
||||
To update Electrum itself or system packages:
|
||||
|
||||
```bash
|
||||
docker compose up -d --build
|
||||
# or for Tor mode:
|
||||
docker compose -f docker-compose.tor.yml up -d --build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding or Removing Allowed Users
|
||||
|
||||
Edit `allowed_users.txt` and restart:
|
||||
|
||||
```bash
|
||||
nano allowed_users.txt
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Persistence
|
||||
|
||||
Wallet data is stored in a named Docker volume at `/root/.electrum` inside the container. This persists across restarts and rebuilds.
|
||||
|
||||
| Mode | Volume name |
|
||||
| Button | What it does |
|
||||
|---|---|
|
||||
| Standard | `electrum-data` |
|
||||
| Tor | `electrum-tor-data` |
|
||||
| **Add Wallet** | Import a watch-only wallet: name → xpub → password. Your xpub message is deleted once read. |
|
||||
| **List** | Your wallets. |
|
||||
| **Load** | Activate a wallet (asks for its password). Needed before Balance / Receive / Send. |
|
||||
| **Balance** | Confirmed and unconfirmed balance. Inline buttons switch BTC / mBTC / sats. |
|
||||
| **Receive** | A fresh unused receiving address. |
|
||||
| **Send** | Builds an *unsigned* transaction — see below. |
|
||||
| **Close** | Unload the active wallet from the daemon. |
|
||||
| **🌐 Language** | English ⇄ Persian. |
|
||||
|
||||
To back up wallet data (standard mode):
|
||||
Wallet names may use letters, numbers, spaces, dot, dash and underscore (1–64 characters) and must start with a letter or number. Passwords need at least 8 characters with an uppercase letter, a lowercase letter and a digit.
|
||||
|
||||
```bash
|
||||
docker run --rm -v electrum-data:/data -v $(pwd):/backup debian:bookworm-slim \
|
||||
tar czf /backup/electrum-backup.tar.gz -C /data .
|
||||
```
|
||||
### Sending
|
||||
|
||||
1. **Send** → choose a fee source: **Electrum** (its own estimate) or **mempool.space** (live)
|
||||
2. Enter the fee rate in sat/vB
|
||||
3. Enter the wallet password *(message deleted immediately)*
|
||||
4. Enter the recipient address
|
||||
5. Enter the amount, or tap **Send Max**
|
||||
6. You get the unsigned transaction as both a code block and a `.txt` file
|
||||
7. Sign it offline — Electrum Android: *Send → Paste from clipboard*; Electrum desktop: *Send → tools icon → Read text from file*
|
||||
8. **Send → Broadcast**, paste the signed transaction
|
||||
|
||||
---
|
||||
|
||||
## Bot Usage Guide
|
||||
## Backup
|
||||
|
||||
### Starting the bot
|
||||
Everything — wallets, config, blockchain headers — is in one volume.
|
||||
|
||||
Open the bot in Telegram and send any message. You'll see a persistent keyboard at the bottom with all available actions.
|
||||
```bash
|
||||
# Quick deploy
|
||||
docker run --rm -v telegtrum-data:/data -v "$PWD":/backup debian:bookworm-slim \
|
||||
tar czf /backup/telegtrum-backup.tar.gz -C /data .
|
||||
```
|
||||
|
||||
### Add Wallet
|
||||
| Mode | Volume |
|
||||
|---|---|
|
||||
| Quick deploy | `telegtrum-data` |
|
||||
| Build from source | `electrum-data` |
|
||||
| Tor | `electrum-tor-data` |
|
||||
| Local / Umbrel | `./electrum-data` (bind mount) |
|
||||
|
||||
Import a watch-only wallet from an xpub (extended public key):
|
||||
|
||||
1. Press **Add Wallet**
|
||||
2. Enter a wallet name
|
||||
3. Paste your xpub
|
||||
4. Set a password (min 8 chars, must include upper, lower, and digit)
|
||||
|
||||
### List
|
||||
|
||||
Shows all your wallets.
|
||||
|
||||
### Load
|
||||
|
||||
Load a wallet to make it active:
|
||||
|
||||
1. Press **Load**
|
||||
2. Select a wallet (auto-selects if you only have one)
|
||||
3. Enter the wallet password
|
||||
|
||||
### Balance
|
||||
|
||||
Shows the confirmed and unconfirmed balance of the loaded wallet.
|
||||
|
||||
Use the inline buttons below the balance to switch between **BTC**, **mBTC**, and **sats**.
|
||||
|
||||
### Receive
|
||||
|
||||
Shows a fresh receiving address for the loaded wallet.
|
||||
|
||||
### Send
|
||||
|
||||
Creates an unsigned transaction (PSBT) for air-gapped signing:
|
||||
|
||||
1. Press **Send**
|
||||
2. Choose a fee source: **Electrum** (recommended) or **mempool.space** (live)
|
||||
3. Enter fee rate in sat/vB
|
||||
4. Enter the wallet password
|
||||
5. Enter the recipient address
|
||||
6. Enter the amount (in your selected unit)
|
||||
7. Receive the unsigned TX as both a code block and a `.txt` file for offline signing
|
||||
8. Sign it with your offline wallet, then use **Broadcast**
|
||||
|
||||
### Close
|
||||
|
||||
Unloads the active wallet from the Electrum daemon.
|
||||
|
||||
### Language
|
||||
|
||||
Press **🌐 Language** to switch between English and Persian (Farsi).
|
||||
A backup is not a substitute for your seed phrase. These are watch-only wallets — the seed on your offline device is what actually recovers your coins.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Bot not responding:** Check logs with `docker compose logs -f`. Make sure your bot token is correct and the container is running.
|
||||
**Bot not responding** — `docker compose logs -f`. Check the token, and that the container isn't restarting.
|
||||
|
||||
**"You are not authorized":** Your Telegram user ID is not in `allowed_users.txt`. Add it and restart.
|
||||
**"You are not authorized"** — your Telegram ID isn't in `ALLOWED_USERS` or `allowed_users.txt`. Add it and restart.
|
||||
|
||||
**Wallet not loaded error:** Use **Load** before checking balance, receiving, or sending.
|
||||
**Everyone is refused** — the container logs `no allowed users configured` at startup when both sources are empty.
|
||||
|
||||
**Daemon not starting:** The container removes the lockfile on startup automatically. If issues persist, try `docker compose restart`.
|
||||
**"Wallet not loaded"** — press **Load** first; Balance, Receive and Send all need an active wallet.
|
||||
|
||||
**Tor mode — Electrum not connecting:** Tor bootstrap can take up to ~30 seconds on first run. Check logs with `docker compose -f docker-compose.tor.yml logs -f`.
|
||||
**Invalid wallet name** — stick to letters, numbers, spaces, dot, dash, underscore.
|
||||
|
||||
**Daemon won't start** — the entrypoint clears the stale lockfile each boot. If it persists, `docker compose restart`.
|
||||
|
||||
**Tor: not connecting** — bootstrap can take ~30s on first run.
|
||||
|
||||
---
|
||||
|
||||
## Verifying what you run
|
||||
|
||||
The image builds Electrum from the official source tarball and **verifies its GPG signature against pinned release-key fingerprints** (ThomasV, sombernight, Emzy). If the signature is missing or the fingerprint doesn't match, the build fails rather than shipping an unverified wallet.
|
||||
|
||||
Reproduce it yourself with `./push.sh` — or read [`install.sh`](install.sh), it's 60 lines.
|
||||
|
||||
Reference in New Issue
Block a user