blockmachine 0.21.0__tar.gz → 0.22.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {blockmachine-0.21.0 → blockmachine-0.22.0}/PKG-INFO +1 -1
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/PKG-INFO +1 -1
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/SOURCES.txt +2 -0
- blockmachine-0.22.0/commands/epoch.py +191 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/main.py +2 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/pyproject.toml +1 -1
- blockmachine-0.22.0/tests/test_epoch_payout_cli.py +206 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/__init__.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/auth_config.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/dependency_links.txt +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/entry_points.txt +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/requires.txt +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/blockmachine.egg-info/top_level.txt +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/client.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/commands/__init__.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/commands/auth.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/commands/leaderboard.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/commands/miner.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/commands/validator.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/config.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/endpoints.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/settings.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/setup.cfg +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/testing.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_cli_config.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_cli_endpoints_and_testing.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_leaderboard_cli.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_add_class.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_ban_line.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_capacity.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_compat_reason.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_eligibility.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/tests/test_miner_eth_tier.py +0 -0
- {blockmachine-0.21.0 → blockmachine-0.22.0}/utils.py +0 -0
|
@@ -25,11 +25,13 @@ blockmachine.egg-info/requires.txt
|
|
|
25
25
|
blockmachine.egg-info/top_level.txt
|
|
26
26
|
commands/__init__.py
|
|
27
27
|
commands/auth.py
|
|
28
|
+
commands/epoch.py
|
|
28
29
|
commands/leaderboard.py
|
|
29
30
|
commands/miner.py
|
|
30
31
|
commands/validator.py
|
|
31
32
|
tests/test_cli_config.py
|
|
32
33
|
tests/test_cli_endpoints_and_testing.py
|
|
34
|
+
tests/test_epoch_payout_cli.py
|
|
33
35
|
tests/test_leaderboard_cli.py
|
|
34
36
|
tests/test_miner_add_class.py
|
|
35
37
|
tests/test_miner_ban_line.py
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Epoch commands: what every miner will be paid for a finalized epoch."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from cli.client import RegistryClient
|
|
13
|
+
from cli.config import load_config
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="Epoch payouts")
|
|
16
|
+
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
# Rows on the summary table before the rest are folded into one line, and
|
|
20
|
+
# breakdown entries shown per row before "+N more".
|
|
21
|
+
TOP_MINERS = 12
|
|
22
|
+
TOP_BREAKDOWN = 6
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _fmt_time(iso: Optional[str]) -> str:
|
|
26
|
+
"""``2026-09-16T20:21:59+00:00`` -> ``16 Sep 20:22 UTC``."""
|
|
27
|
+
if not iso:
|
|
28
|
+
return "?"
|
|
29
|
+
try:
|
|
30
|
+
dt = datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
|
31
|
+
except ValueError:
|
|
32
|
+
return iso
|
|
33
|
+
# Round to the minute rather than truncate: 20:21:59 reads as 20:22.
|
|
34
|
+
dt = dt.astimezone(timezone.utc) + timedelta(seconds=30)
|
|
35
|
+
return f"{dt.day} {dt.strftime('%b %H:%M')} UTC"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _fmt_usd(value: Optional[float]) -> str:
|
|
39
|
+
if value is None:
|
|
40
|
+
return "-"
|
|
41
|
+
return f"{value:,.0f}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _breakdown_summary(entries: list[dict], limit: int = TOP_BREAKDOWN) -> str:
|
|
45
|
+
shown = [
|
|
46
|
+
f"{e.get('chain')}/{e.get('node_class')} {float(e.get('alpha') or 0):.1f}"
|
|
47
|
+
for e in entries[:limit]
|
|
48
|
+
]
|
|
49
|
+
more = len(entries) - limit
|
|
50
|
+
if more > 0:
|
|
51
|
+
shown.append(f"+{more} more")
|
|
52
|
+
return ", ".join(shown)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def render_payout(body: dict, top: int = TOP_MINERS) -> list[str]:
|
|
56
|
+
"""The summary table as lines of plain text."""
|
|
57
|
+
# The registry omits zero-alpha miners; a zero row is nothing to show.
|
|
58
|
+
miners = [m for m in body.get("miners") or [] if float(m.get("alpha") or 0) > 0]
|
|
59
|
+
pool_alpha = body.get("pool_alpha")
|
|
60
|
+
price = body.get("alpha_price_usd")
|
|
61
|
+
lines = [
|
|
62
|
+
f"Epoch {body.get('epoch_number')} — finalized "
|
|
63
|
+
f"{_fmt_time(body.get('finalized_at'))}, paid out by the boundary "
|
|
64
|
+
f"~{_fmt_time(body.get('pays_at_estimate'))}",
|
|
65
|
+
]
|
|
66
|
+
pool = (
|
|
67
|
+
f"miner pool {pool_alpha:,.0f} α (${_fmt_usd(body.get('pool_usd'))})"
|
|
68
|
+
if pool_alpha is not None
|
|
69
|
+
else "miner pool ?"
|
|
70
|
+
)
|
|
71
|
+
price_txt = f"α price ${price:.2f}" if price is not None else "α price ?"
|
|
72
|
+
lines.append(f"{price_txt} {pool} {len(miners)} miners paid")
|
|
73
|
+
if body.get("caveat"):
|
|
74
|
+
lines.append(f"Note: {body['caveat']}.")
|
|
75
|
+
lines.append("")
|
|
76
|
+
|
|
77
|
+
if not miners:
|
|
78
|
+
burn = body.get("burn_pct")
|
|
79
|
+
if burn is not None and burn >= 1.0:
|
|
80
|
+
lines.append("No miners paid this epoch: the whole pool burns.")
|
|
81
|
+
else:
|
|
82
|
+
lines.append("No miners paid this epoch.")
|
|
83
|
+
return lines
|
|
84
|
+
|
|
85
|
+
lines.append(
|
|
86
|
+
f" {'UID':<4} {'alpha':>9} {'USD':>9} {'share':>5} "
|
|
87
|
+
"breakdown (chain/class → α)"
|
|
88
|
+
)
|
|
89
|
+
for m in miners[:top]:
|
|
90
|
+
lines.append(
|
|
91
|
+
f" {m.get('uid', '?'):<4} {float(m.get('alpha') or 0):>9.1f} "
|
|
92
|
+
f"{_fmt_usd(m.get('usd')):>9} {float(m.get('share') or 0) * 100:>4.1f}% "
|
|
93
|
+
f"{_breakdown_summary(m.get('breakdown') or [])}"
|
|
94
|
+
)
|
|
95
|
+
rest = miners[top:]
|
|
96
|
+
if rest:
|
|
97
|
+
rest_alpha = sum(float(m.get("alpha") or 0) for m in rest)
|
|
98
|
+
lines.append(f" … {len(rest)} more miners, {rest_alpha:.1f} α between them")
|
|
99
|
+
lines.append("")
|
|
100
|
+
lines.append(
|
|
101
|
+
"USD is an estimate at the α price the finalizer recorded; the chain pays in α."
|
|
102
|
+
)
|
|
103
|
+
return lines
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def render_miner(body: dict, miner: dict) -> list[str]:
|
|
107
|
+
"""One miner with every (chain, class) line."""
|
|
108
|
+
header = render_payout(body)
|
|
109
|
+
lines = header[: header.index("")]
|
|
110
|
+
lines.append("")
|
|
111
|
+
lines.append(
|
|
112
|
+
f"UID {miner.get('uid')} {miner.get('hotkey')}\n"
|
|
113
|
+
f" {float(miner.get('alpha') or 0):.1f} α "
|
|
114
|
+
f"(${_fmt_usd(miner.get('usd'))}, "
|
|
115
|
+
f"{float(miner.get('share') or 0) * 100:.1f}% of the pool, "
|
|
116
|
+
f"{int(miner.get('cu_total') or 0):,} CU)"
|
|
117
|
+
)
|
|
118
|
+
lines.append("")
|
|
119
|
+
lines.append(f" {'chain/class':<18} {'CU':>15} {'alpha':>9} {'USD':>9}")
|
|
120
|
+
for e in miner.get("breakdown") or []:
|
|
121
|
+
node = f"{e.get('chain')}/{e.get('node_class')}"
|
|
122
|
+
lines.append(
|
|
123
|
+
f" {node:<18} {int(e.get('cu') or 0):>15,} "
|
|
124
|
+
f"{float(e.get('alpha') or 0):>9.1f} {_fmt_usd(e.get('usd')):>9}"
|
|
125
|
+
)
|
|
126
|
+
lines.append("")
|
|
127
|
+
lines.append(
|
|
128
|
+
"USD is an estimate at the α price the finalizer recorded; the chain pays in α."
|
|
129
|
+
)
|
|
130
|
+
return lines
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@app.command("payout")
|
|
134
|
+
def payout(
|
|
135
|
+
epoch: Optional[int] = typer.Option(
|
|
136
|
+
None, "--epoch", "-e", help="Epoch number (default: the latest finalized)"
|
|
137
|
+
),
|
|
138
|
+
uid: Optional[int] = typer.Option(
|
|
139
|
+
None, "--uid", "-u", help="Show one miner with its full breakdown"
|
|
140
|
+
),
|
|
141
|
+
as_json: bool = typer.Option(False, "--json", help="Print the raw response"),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""Show what every miner will be paid for a finalized epoch.
|
|
144
|
+
|
|
145
|
+
Per UID: alpha, USD (an estimate at the price recorded when the epoch
|
|
146
|
+
was finalized), share of the miner pool, and the alpha split by chain
|
|
147
|
+
and node class. Available the moment an epoch finalizes; the chain pays
|
|
148
|
+
it out over the following epoch.
|
|
149
|
+
"""
|
|
150
|
+
path = "/epochs/latest/payout" if epoch is None else f"/epochs/{epoch}/payout"
|
|
151
|
+
with RegistryClient(load_config()) as client:
|
|
152
|
+
response = client.get(path)
|
|
153
|
+
|
|
154
|
+
if response.status_code == 404:
|
|
155
|
+
which = f"Epoch {epoch}" if epoch is not None else "No epoch"
|
|
156
|
+
console.print(f"[yellow]{which} is not finalized yet, or is unknown.[/yellow]")
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
if response.status_code == 503:
|
|
159
|
+
console.print(
|
|
160
|
+
"[yellow]Epoch payouts are not available on this registry.[/yellow]"
|
|
161
|
+
)
|
|
162
|
+
raise typer.Exit(1)
|
|
163
|
+
if response.status_code != 200:
|
|
164
|
+
detail = ""
|
|
165
|
+
try:
|
|
166
|
+
detail = str(response.json().get("detail", ""))
|
|
167
|
+
except Exception:
|
|
168
|
+
detail = response.text
|
|
169
|
+
console.print(f"[red]Error:[/red] {response.status_code} {detail}")
|
|
170
|
+
raise typer.Exit(1)
|
|
171
|
+
|
|
172
|
+
body = response.json()
|
|
173
|
+
|
|
174
|
+
if uid is not None:
|
|
175
|
+
matches = [m for m in body.get("miners") or [] if m.get("uid") == uid]
|
|
176
|
+
if not matches:
|
|
177
|
+
console.print(
|
|
178
|
+
f"[yellow]UID {uid} is not paid for epoch "
|
|
179
|
+
f"{body.get('epoch_number')}.[/yellow]"
|
|
180
|
+
)
|
|
181
|
+
raise typer.Exit(1)
|
|
182
|
+
if as_json:
|
|
183
|
+
typer.echo(json.dumps(matches[0], indent=2))
|
|
184
|
+
return
|
|
185
|
+
typer.echo("\n".join(render_miner(body, matches[0])))
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if as_json:
|
|
189
|
+
typer.echo(json.dumps(body, indent=2))
|
|
190
|
+
return
|
|
191
|
+
typer.echo("\n".join(render_payout(body)))
|
|
@@ -7,6 +7,7 @@ import typer
|
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
|
|
9
9
|
from cli import settings
|
|
10
|
+
from cli.commands.epoch import app as epoch_app
|
|
10
11
|
from cli.commands.leaderboard import app as leaderboard_app
|
|
11
12
|
from cli.commands.miner import app as miner_app
|
|
12
13
|
from cli.commands.validator import app as validator_app
|
|
@@ -31,6 +32,7 @@ def _global_options(
|
|
|
31
32
|
|
|
32
33
|
app.add_typer(miner_app, name="miner", help="Miner node management")
|
|
33
34
|
app.add_typer(leaderboard_app, name="leaderboard", help="Miner leaderboard")
|
|
35
|
+
app.add_typer(epoch_app, name="epoch", help="Epoch payouts")
|
|
34
36
|
app.add_typer(validator_app, name="validator", help="Validator authentication")
|
|
35
37
|
|
|
36
38
|
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""``bm epoch payout``: the table every miner reads to see what they will be
|
|
2
|
+
paid for the epoch that just finalized.
|
|
3
|
+
|
|
4
|
+
Rendering only — the numbers are the registry's. What is pinned: the
|
|
5
|
+
layout Ben signed off (UID, alpha, USD, share, breakdown with "+N more"),
|
|
6
|
+
zero rows never appear, the fold line for the long tail, ``--uid`` shows
|
|
7
|
+
every chain/class line, ``--json`` is the body untouched, and each failure
|
|
8
|
+
status gets a sentence rather than a traceback.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
|
|
15
|
+
from typer.testing import CliRunner
|
|
16
|
+
|
|
17
|
+
from cli.commands import epoch as epoch_mod
|
|
18
|
+
from cli.main import app
|
|
19
|
+
|
|
20
|
+
runner = CliRunner()
|
|
21
|
+
|
|
22
|
+
HK = "5F" + "a" * 46
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _miner(uid, alpha, share, breakdown, usd=None, hotkey=HK, cu=1000):
|
|
26
|
+
return {
|
|
27
|
+
"uid": uid,
|
|
28
|
+
"hotkey": hotkey,
|
|
29
|
+
"alpha": alpha,
|
|
30
|
+
"usd": alpha * 2.15 if usd is None else usd,
|
|
31
|
+
"share": share,
|
|
32
|
+
"cu_total": cu,
|
|
33
|
+
"breakdown": [
|
|
34
|
+
{"chain": c, "node_class": k, "cu": 10, "alpha": a, "usd": a * 2.15}
|
|
35
|
+
for c, k, a in breakdown
|
|
36
|
+
],
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
BODY = {
|
|
41
|
+
"epoch_number": 9075361,
|
|
42
|
+
"finalized_at": "2026-09-16T20:21:59.117368+00:00",
|
|
43
|
+
"pays_at_estimate": "2026-09-17T20:21:59.117368+00:00",
|
|
44
|
+
"alpha_price_usd": 2.15009611114,
|
|
45
|
+
"pool_alpha": 2952.0,
|
|
46
|
+
"pool_usd": 6347.08,
|
|
47
|
+
"burn_pct": 0.0,
|
|
48
|
+
"miner_count": 3,
|
|
49
|
+
"miners": [
|
|
50
|
+
_miner(
|
|
51
|
+
129,
|
|
52
|
+
776.3,
|
|
53
|
+
0.263,
|
|
54
|
+
[
|
|
55
|
+
("tao", "arch", 197.3),
|
|
56
|
+
("polygon", "arch", 165.7),
|
|
57
|
+
("base", "arch", 146.0),
|
|
58
|
+
("arbitrum", "arch", 95.6),
|
|
59
|
+
("eth", "arch", 43.1),
|
|
60
|
+
("robinhood", "arch", 34.8),
|
|
61
|
+
("scroll", "arch", 20.0),
|
|
62
|
+
("mantle", "full", 1.0),
|
|
63
|
+
],
|
|
64
|
+
),
|
|
65
|
+
_miner(178, 708.3, 0.240, [("polygon", "arch", 700.0), ("tao", "full", 8.3)]),
|
|
66
|
+
_miner(237, 327.0, 0.111, [("bsc", "arch", 327.0)]),
|
|
67
|
+
],
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class _Resp:
|
|
72
|
+
def __init__(self, payload, status_code=200, text=""):
|
|
73
|
+
self._payload = payload
|
|
74
|
+
self.status_code = status_code
|
|
75
|
+
self.text = text
|
|
76
|
+
|
|
77
|
+
def json(self):
|
|
78
|
+
return self._payload
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class _Client:
|
|
82
|
+
calls: list[str] = []
|
|
83
|
+
|
|
84
|
+
def __init__(self, *args, **kwargs):
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
def __enter__(self):
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
def __exit__(self, *args):
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
def get(self, path, params=None):
|
|
94
|
+
_Client.calls.append(path)
|
|
95
|
+
return _Client.response
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _run(monkeypatch, response, *args):
|
|
99
|
+
_Client.response = response
|
|
100
|
+
_Client.calls = []
|
|
101
|
+
monkeypatch.setattr(epoch_mod, "RegistryClient", _Client)
|
|
102
|
+
monkeypatch.setattr(epoch_mod, "load_config", lambda: None)
|
|
103
|
+
return runner.invoke(app, ["epoch", "payout", *args])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_table_matches_the_signed_off_layout(monkeypatch):
|
|
107
|
+
result = _run(monkeypatch, _Resp(BODY))
|
|
108
|
+
assert result.exit_code == 0, result.output
|
|
109
|
+
assert _Client.calls == ["/epochs/latest/payout"]
|
|
110
|
+
out = result.output
|
|
111
|
+
assert out.startswith(
|
|
112
|
+
"Epoch 9075361 — finalized 16 Sep 20:22 UTC, paid out by the boundary "
|
|
113
|
+
"~17 Sep 20:22 UTC\n"
|
|
114
|
+
)
|
|
115
|
+
assert "α price $2.15 miner pool 2,952 α ($6,347) 3 miners paid" in out
|
|
116
|
+
assert " UID alpha USD share breakdown (chain/class → α)" in out
|
|
117
|
+
assert (
|
|
118
|
+
" 129 776.3 1,669 26.3% tao/arch 197.3, polygon/arch 165.7, "
|
|
119
|
+
"base/arch 146.0, arbitrum/arch 95.6, eth/arch 43.1, robinhood/arch 34.8, "
|
|
120
|
+
"+2 more"
|
|
121
|
+
) in out
|
|
122
|
+
assert (
|
|
123
|
+
" 178 708.3 1,523 24.0% polygon/arch 700.0, tao/full 8.3\n" in out
|
|
124
|
+
)
|
|
125
|
+
assert "more miners" not in out # three rows fit; nothing folded
|
|
126
|
+
assert "USD is an estimate" in out
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_long_tail_is_folded_into_one_line():
|
|
130
|
+
miners = [_miner(i, 100.0 - i, 0.01, [("tao", "arch", 1.0)]) for i in range(15)]
|
|
131
|
+
lines = epoch_mod.render_payout({**BODY, "miners": miners})
|
|
132
|
+
assert sum(1 for line in lines if line.startswith(" 1")) >= 1
|
|
133
|
+
fold = [line for line in lines if "more miners" in line]
|
|
134
|
+
assert fold == [" … 3 more miners, 261.0 α between them"]
|
|
135
|
+
assert sum(1 for line in lines if line.startswith(" ") and "%" in line) == 12
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_epoch_option_targets_that_epoch(monkeypatch):
|
|
139
|
+
result = _run(monkeypatch, _Resp(BODY), "--epoch", "9068161")
|
|
140
|
+
assert result.exit_code == 0, result.output
|
|
141
|
+
assert _Client.calls == ["/epochs/9068161/payout"]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_uid_shows_every_breakdown_line(monkeypatch):
|
|
145
|
+
result = _run(monkeypatch, _Resp(BODY), "--uid", "129")
|
|
146
|
+
assert result.exit_code == 0, result.output
|
|
147
|
+
out = result.output
|
|
148
|
+
assert f"UID 129 {HK}" in out
|
|
149
|
+
assert "776.3 α ($1,669, 26.3% of the pool, 1,000 CU)" in out
|
|
150
|
+
for chain in ("tao", "polygon", "base", "arbitrum", "eth", "robinhood", "scroll"):
|
|
151
|
+
assert f"{chain}/arch" in out
|
|
152
|
+
assert "mantle/full" in out
|
|
153
|
+
assert "+" not in out.split("chain/class")[1] # nothing elided
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_uid_not_paid_is_a_sentence(monkeypatch):
|
|
157
|
+
result = _run(monkeypatch, _Resp(BODY), "--uid", "5")
|
|
158
|
+
assert result.exit_code == 1
|
|
159
|
+
assert "UID 5 is not paid for epoch 9075361" in result.output
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def test_json_dumps_the_body_untouched(monkeypatch):
|
|
163
|
+
result = _run(monkeypatch, _Resp(BODY), "--json")
|
|
164
|
+
assert result.exit_code == 0, result.output
|
|
165
|
+
assert json.loads(result.output) == BODY
|
|
166
|
+
|
|
167
|
+
result = _run(monkeypatch, _Resp(BODY), "--json", "--uid", "237")
|
|
168
|
+
assert json.loads(result.output) == BODY["miners"][2]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_burn_epoch_says_so(monkeypatch):
|
|
172
|
+
body = {**BODY, "miners": [], "miner_count": 0, "burn_pct": 1.0}
|
|
173
|
+
result = _run(monkeypatch, _Resp(body))
|
|
174
|
+
assert result.exit_code == 0, result.output
|
|
175
|
+
assert "0 miners paid" in result.output
|
|
176
|
+
assert "the whole pool burns" in result.output
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_failure_statuses_get_a_sentence(monkeypatch):
|
|
180
|
+
result = _run(monkeypatch, _Resp({"detail": "not found"}, 404), "--epoch", "1")
|
|
181
|
+
assert result.exit_code == 1
|
|
182
|
+
assert "Epoch 1 is not finalized yet, or is unknown." in result.output
|
|
183
|
+
|
|
184
|
+
result = _run(monkeypatch, _Resp({"detail": "x"}, 503))
|
|
185
|
+
assert result.exit_code == 1
|
|
186
|
+
assert "not available on this registry" in result.output
|
|
187
|
+
|
|
188
|
+
result = _run(monkeypatch, _Resp({"detail": "epoch 5 has no price snapshot"}, 422))
|
|
189
|
+
assert result.exit_code == 1
|
|
190
|
+
assert "422 epoch 5 has no price snapshot" in result.output
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_zero_alpha_rows_never_render():
|
|
194
|
+
body = {**BODY, "miners": [*BODY["miners"], _miner(1, 0.0, 0.0, [])]}
|
|
195
|
+
lines = epoch_mod.render_payout(body)
|
|
196
|
+
assert not any(line.startswith(" 1 ") for line in lines)
|
|
197
|
+
assert "3 miners paid" in lines[1]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_caveat_is_printed_when_the_registry_sends_one():
|
|
201
|
+
body = {**BODY, "caveat": "computed late"}
|
|
202
|
+
lines = epoch_mod.render_payout(body)
|
|
203
|
+
assert "Note: computed late." in lines
|
|
204
|
+
one = epoch_mod.render_miner(body, BODY["miners"][0])
|
|
205
|
+
assert "Note: computed late." in one
|
|
206
|
+
assert "miners paid" not in epoch_mod.render_miner(BODY, BODY["miners"][0])[-1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|