The two Mermaid diagrams under flowchart/ described the intended flow but had drifted from the code and carried no link back to it. docs/ now holds the specification instead: 15 Italian documents (flows, architecture, data model, API, security, audit log, operations, decisions and limits, bug history) with 472 numbered requirements, each pointing at the function that implements it and the test that covers it. docs/processo-di-modifica.md defines the ID convention and the spec -> code -> test change workflow, and CLAUDE.md now names docs/ as authoritative. Writing the spec from the code also corrected stale claims in CLAUDE.md (B-41's get_history check, BUGS.md still tracking B-71/B-72, Cache-Control scope, the error contract's bare-string exceptions) and in the operator guides (payouts do retry, only `drawing` fails to resume, rounds do not open without fee_address). The B-70 doc test now pins the prose in docs/scommesse-e-prelievi.md instead of the deleted diagram. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
64 lines
2.5 KiB
Python
64 lines
2.5 KiB
Python
import os
|
|
|
|
from embit import script
|
|
from embit.bip32 import HDKey
|
|
from embit.ec import PrivateKey
|
|
|
|
from app.config import settings
|
|
from app.wallet.keystore import decrypt_xprv, encrypt_xprv
|
|
from app.wallet.plm_network import ACCOUNT_PATH, PLM_MAINNET
|
|
|
|
_account_key: HDKey | None = None
|
|
|
|
|
|
def generate_master_key(overwrite: bool = False) -> None:
|
|
"""One-time ops bootstrap: create a random master seed, encrypt it, write it to
|
|
disk. Not exposed via any API endpoint — run manually before first launch."""
|
|
if os.path.exists(settings.master_key_path) and not overwrite:
|
|
raise FileExistsError(f"{settings.master_key_path} already exists")
|
|
root = HDKey.from_seed(os.urandom(32), version=PLM_MAINNET["xprv"])
|
|
with open(settings.master_key_path, "wb") as f:
|
|
f.write(encrypt_xprv(root.to_base58(version=PLM_MAINNET["xprv"])))
|
|
|
|
|
|
def _load_account_key() -> HDKey:
|
|
global _account_key
|
|
if _account_key is None:
|
|
with open(settings.master_key_path, "rb") as f:
|
|
token = f.read()
|
|
root = HDKey.from_base58(decrypt_xprv(token))
|
|
_account_key = root.derive(ACCOUNT_PATH)
|
|
return _account_key
|
|
|
|
|
|
def derive_user_key(derivation_index: int) -> HDKey:
|
|
return _load_account_key().derive(f"0/{derivation_index}")
|
|
|
|
|
|
def derive_user_address(derivation_index: int) -> str:
|
|
pub = derive_user_key(derivation_index).to_public()
|
|
return script.p2wpkh(pub).address(network=PLM_MAINNET)
|
|
|
|
|
|
def derive_user_wif(derivation_index: int) -> str:
|
|
"""Exports a user's raw private key (WIF) for manual server-side intervention
|
|
(e.g. sweeping funds back to a user, or out, if something gets stuck). This is
|
|
a custodial system — the server already holds the master key this is derived
|
|
from — but callers must still treat the result as a live secret: log access,
|
|
never persist it, never return it over an unauthenticated channel."""
|
|
key = derive_user_key(derivation_index)
|
|
return PrivateKey(key.secret, compressed=True, network=PLM_MAINNET).wif(network=PLM_MAINNET)
|
|
|
|
|
|
def derive_pool_key() -> HDKey:
|
|
"""The pool key ("indirizzo padre"): all bets are sent here, and
|
|
payouts are signed with this key. Reserved on branch 1 of the account (branch 0
|
|
is user addresses), index 0 — not a spec requirement, an implementation choice
|
|
to keep it in the same encrypted master key rather than a separate secret."""
|
|
return _load_account_key().derive("1/0")
|
|
|
|
|
|
def derive_pool_address() -> str:
|
|
pub = derive_pool_key().to_public()
|
|
return script.p2wpkh(pub).address(network=PLM_MAINNET)
|