Files
plm-lottery/app/wallet/hd.py
T
davideandClaude Opus 5.5 61efbb0e37 Replace flowchart/ with a traceable specification in docs/
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>
2026-09-28 12:16:01 +02:00

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)