aboutsummaryrefslogtreecommitdiff
path: root/utils/monzero-miner-reporter.py
blob: cbbc3029a5503bbe8ea1cb7151aac948b27a694f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#!/usr/bin/env python3
"""Opt-in anonymous mining telemetry reporter for monzero.org."""

from __future__ import annotations

import argparse
import json
import os
import re
import stat
import time
import urllib.error
import urllib.request
import uuid
from pathlib import Path


FOUND_BLOCK = re.compile(r"Found block [0-9a-f]+ at height ", re.IGNORECASE)


def json_request(url: str, body: dict | None = None, token: str = "") -> dict:
    data = None if body is None else json.dumps(body).encode("utf-8")
    headers = {"Accept": "application/json"}
    if data is not None:
        headers["Content-Type"] = "application/json"
    if token:
        headers["Authorization"] = f"Bearer {token}"
    request = urllib.request.Request(url, data=data, headers=headers)
    with urllib.request.urlopen(request, timeout=8) as response:
        return json.load(response)


def installation_id(path: Path) -> str:
    if path.exists():
        return path.read_text(encoding="ascii").strip()
    path.parent.mkdir(parents=True, exist_ok=True)
    value = str(uuid.uuid4())
    path.write_text(value + "\n", encoding="ascii")
    path.chmod(stat.S_IRUSR | stat.S_IWUSR)
    return value


def blocks_found(log_path: Path) -> int:
    try:
        with log_path.open("r", encoding="utf-8", errors="ignore") as log:
            return sum(1 for line in log if FOUND_BLOCK.search(line))
    except FileNotFoundError:
        return 0


def report(args: argparse.Namespace) -> None:
    status = json_request(args.rpc.rstrip("/") + "/mining_status", {})
    payload = {
        "installation_id": installation_id(args.id_file),
        "hashrate": int(status.get("speed", 0)) if status.get("active") else 0,
        "blocks_found": blocks_found(args.log),
    }
    result = json_request(args.endpoint, payload, args.token)
    if result.get("status") != "OK":
        raise RuntimeError(f"telemetry rejected: {result}")
    print(
        f"reported {payload['hashrate']} H/s and "
        f"{payload['blocks_found']} block(s); no wallet or hardware ID sent"
    )


def main() -> None:
    default_data = Path.home() / ".monzero"
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--endpoint", default="https://monzero.org/api/miner-stats/")
    parser.add_argument("--rpc", default="http://127.0.0.1:6175")
    parser.add_argument("--token", default=os.environ.get("MONZERO_STATS_TOKEN", ""))
    parser.add_argument("--id-file", type=Path, default=default_data / "telemetry-id")
    parser.add_argument("--log", type=Path, default=default_data / "monzero.log")
    parser.add_argument("--interval", type=int, default=0, help="repeat every N seconds")
    args = parser.parse_args()
    if not args.token:
        parser.error("set MONZERO_STATS_TOKEN or pass --token")
    if args.interval and args.interval < 30:
        parser.error("--interval must be at least 30 seconds")

    while True:
        try:
            report(args)
        except (OSError, ValueError, urllib.error.URLError, RuntimeError) as error:
            print(f"report failed: {error}")
        if not args.interval:
            break
        time.sleep(args.interval)


if __name__ == "__main__":
    main()