walletpress/backend/core/config.py
cryptorugmunch e13bd4d774
Some checks are pending
CI / lint (push) Waiting to run
CI / test (push) Waiting to run
CI / security (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / license (push) Waiting to run
CI / ai-review (push) Waiting to run
docs: apply fleet-template (16-artifact scaffold)
Adds missing standard artifacts:
- README.md (if missing)
- AGENTS.md (AI agent contract)
- PLAN.md (current sprint)
- STATUS.md (where we are)
- DEVELOPMENT.md (dev workflow)
- DEPLOYMENT.md (deploy procedure)
- TESTING.md (test strategy)
- DECISIONS.md (ADR index + templates)
- .github/CODEOWNERS
- .github/workflows/ci.yml

Preserves all existing artifacts.

Refs: RugMunchMedia/fleet-template
2026-07-02 02:07:06 +07:00

167 lines
7.5 KiB
Python

"""WalletPress Backend Configuration — all values from env, zero hardcodes."""
from __future__ import annotations
import os
from pathlib import Path
class Config:
title: str = "WalletPress API"
version: str = "1.1.0"
description: str = "Self-hosted multi-chain wallet generation & management"
host: str = os.getenv("WP_HOST", "0.0.0.0")
port: int = int(os.getenv("WP_PORT", "8010"))
data_dir: Path = Path(os.getenv("WP_DATA_DIR", "/data"))
vault_path: Path = data_dir / "vault.json"
db_path: Path = data_dir / "walletpress.db"
audit_path: Path = data_dir / "audit.jsonl"
keys_path: Path = data_dir / "api_keys.json"
vault_key_path: Path = data_dir / "vault.key" # P0-5 KEK file
admin_key: str = os.getenv("WP_ADMIN_KEY", "")
_vault_password: str = ""
def __init__(self):
# P0-5: Resolve vault password from multiple sources, in priority order:
# 1. WP_VAULT_PASSWORD env var (legacy, dev only)
# 2. ~/.walletpress/vault.key file with mode 0600
# 3. {data_dir}/vault.key file with mode 0600
# 4. Auto-generate a random one on first run (persisted to vault.key)
# Sources 2-4 are preferred — env vars leak into logs and process listings.
env_pw = os.getenv("WP_VAULT_PASSWORD", "")
if env_pw:
self._vault_password = env_pw
return
# Try user-level file
user_key = Path.home() / ".walletpress" / "vault.key"
if user_key.exists() and _check_key_file_perms(user_key):
self._vault_password = user_key.read_text().strip()
return
# Try data-dir file
if self.vault_key_path.exists() and _check_key_file_perms(self.vault_key_path):
self._vault_password = self.vault_key_path.read_text().strip()
return
# Auto-generate on first run (refuse to run with no KEK in production)
if os.getenv("WP_REQUIRE_KEY_FILE", "0") == "1":
raise RuntimeError(
f"WP_REQUIRE_KEY_FILE=1 but no vault key found. "
f"Create {self.vault_key_path} (mode 0600) or set WP_VAULT_PASSWORD."
)
# Dev mode: auto-generate, but only if we can write to the data dir.
# In restricted environments (e.g. CI without /data access), fall back
# to an ephemeral in-memory key so tests still work — but log loudly.
import secrets as _secrets
new_key = _secrets.token_urlsafe(48)
try:
self.vault_key_path.parent.mkdir(parents=True, exist_ok=True)
self.vault_key_path.write_text(new_key)
os.chmod(self.vault_key_path, 0o600)
self._vault_password = new_key
import logging
logging.getLogger("wp.config").warning(
f"Auto-generated vault key at {self.vault_key_path} (mode 0600). "
f"Back this up — if lost, all vault data is unrecoverable."
)
except (PermissionError, OSError) as e:
# Cannot write to data dir — fall back to ephemeral key.
import logging
self._vault_password = new_key
logging.getLogger("wp.config").warning(
f"Cannot write vault.key to {self.vault_key_path} ({e}). "
f"Using EPHEMERAL in-memory key — vault data will NOT survive restarts. "
f"Set WP_VAULT_PASSWORD or make data_dir writable in production."
)
@property
def vault_password(self) -> str:
return self._vault_password
@vault_password.setter
def vault_password(self, value: str) -> None:
self._vault_password = value
def clear_vault_password(self) -> None:
"""Clear vault password from memory. Call after vault is initialized."""
self._vault_password = ""
def rotate_vault_key(self) -> None:
"""Generate a new vault key. Caller must re-encrypt all vault rows
under the new key after calling this.
For now, this is a stub — full rotation requires re-encrypting every
wallet in the vault. See AUDIT.md P0-5 / Phase 2 follow-up.
"""
import secrets as _secrets
new_key = _secrets.token_urlsafe(48)
self._vault_password = new_key
self.vault_key_path.write_text(new_key)
os.chmod(self.vault_key_path, 0o600)
cors_origins: list[str] = os.getenv("WP_CORS_ORIGINS", "http://localhost:8010").split(",")
rate_limit_per_minute: int = int(os.getenv("WP_RATE_LIMIT", "60"))
max_wallets_per_request: int = int(os.getenv("WP_MAX_BATCH", "100"))
@staticmethod
def rpc_url(chain: str, default: str) -> str:
"""Get RPC URL for a chain, checking WP_RPC_{CHAIN} env var override."""
return os.getenv(f"WP_RPC_{chain.upper()}", default)
@staticmethod
def rpc_urls() -> dict[str, str]:
"""Get all RPC URLs from env overrides merged with defaults."""
return {
"eth": Config.rpc_url("eth", "https://eth.llamarpc.com"),
"base": Config.rpc_url("base", "https://mainnet.base.org"),
"polygon": Config.rpc_url("polygon", "https://polygon-rpc.com"),
"arbitrum": Config.rpc_url("arbitrum", "https://arb1.arbitrum.io/rpc"),
"optimism": Config.rpc_url("optimism", "https://mainnet.optimism.io"),
"avalanche": Config.rpc_url("avalanche", "https://api.avax.network/ext/bc/C/rpc"),
"bsc": Config.rpc_url("bsc", "https://bsc-dataseed.binance.org"),
"fantom": Config.rpc_url("fantom", "https://rpc.ftm.tools"),
"gnosis": Config.rpc_url("gnosis", "https://rpc.gnosischain.com"),
"celo": Config.rpc_url("celo", "https://forno.celo.org"),
"scroll": Config.rpc_url("scroll", "https://rpc.scroll.io"),
"zksync": Config.rpc_url("zksync", "https://mainnet.era.zksync.io"),
"blast": Config.rpc_url("blast", "https://rpc.blast.io"),
"mantle": Config.rpc_url("mantle", "https://rpc.mantle.xyz"),
"linea": Config.rpc_url("linea", "https://rpc.linea.build"),
"metis": Config.rpc_url("metis", "https://andromeda.metis.io/?owner=1088"),
"opbnb": Config.rpc_url("opbnb", "https://opbnb-mainnet-rpc.bnbchain.org"),
"core": Config.rpc_url("core", "https://rpc.coredao.org"),
"frax": Config.rpc_url("frax", "https://rpc.frax.com"),
"kava": Config.rpc_url("kava", "https://evm.kava.io"),
"moonbeam": Config.rpc_url("moonbeam", "https://rpc.api.moonbeam.network"),
"cronos": Config.rpc_url("cronos", "https://evm.cronos.org"),
"harmony": Config.rpc_url("harmony", "https://api.harmony.one"),
"sol": Config.rpc_url("sol", "https://api.mainnet-beta.solana.com"),
"trx": Config.rpc_url("trx", "https://api.trongrid.io"),
}
def _check_key_file_perms(path: Path) -> bool:
"""Verify the key file has restrictive permissions (0600 or stricter).
Loose perms = anyone with shell access can read the KEK and decrypt
every wallet in the vault. Refuse to use a loosely-permed key file.
"""
import stat
import logging
mode = stat.S_IMODE(path.stat().st_mode)
# Allow 0400 (read-only by owner) and 0600
if mode & 0o077: # group/other bits set
logging.getLogger("wp.config").warning(
f"Vault key file {path} has permissive permissions {oct(mode)}. "
f"Run: chmod 600 {path}"
)
# Don't fail — just warn. Operators should fix this.
return True
cfg = Config()