Skip to content

Commit 03d3f80

Browse files
authored
feat: add top miners endpoint (#2000)
1 parent bd2fd2f commit 03d3f80

File tree

9 files changed

+535
-2
lines changed

9 files changed

+535
-2
lines changed

docs/swagger_v3/stats.spec.yaml

+91
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,32 @@ schemas:
203203
example:
204204
miner: ak_2whjDhTTmbN13vU7gAUsRbosBhmycho4h8LqHVqKwyGofDetQ9
205205
total_reward: 945000000000000000000
206+
TopMinerStat:
207+
type: object
208+
description: TopMinerStat
209+
required:
210+
- miner
211+
- blocks_mined
212+
- start_date
213+
- end_date
214+
properties:
215+
miner:
216+
description: The miner (beneficiary) address
217+
$ref: '#/components/schemas/AccountAddress'
218+
blocks_mined:
219+
description: The number of blocks mined
220+
type: integer
221+
start_date:
222+
description: The statistic start date
223+
type: string
224+
end_date:
225+
description: The statistic end date
226+
type: string
227+
example:
228+
miner: ak_2whjDhTTmbN13vU7gAUsRbosBhmycho4h8LqHVqKwyGofDetQ9
229+
blocks_mined: 945
230+
start_date: "2024-02-28"
231+
end_date: "2024-02-29"
206232
Stats:
207233
type: object
208234
description: Stats
@@ -329,6 +355,71 @@ paths:
329355
application/json:
330356
schema:
331357
$ref: '#/components/schemas/ErrorResponse'
358+
/stats/miners/top:
359+
get:
360+
deprecated: false
361+
description: Get a list of top miners by blocks mined.
362+
operationId: GetTopMinerStats
363+
parameters:
364+
- name: interval_by
365+
description: The interval in which to return the stats.
366+
in: query
367+
required: false
368+
schema:
369+
type: string
370+
enum:
371+
- day
372+
- week
373+
- month
374+
example: week
375+
- name: min_start_date
376+
description: The minimum start date in YYYY-MM-DD format.
377+
in: query
378+
required: false
379+
schema:
380+
type: string
381+
example: "2023-01-01"
382+
- name: max_start_date
383+
description: The maximum start date in YYYY-MM-DD format.
384+
in: query
385+
required: false
386+
schema:
387+
type: string
388+
example: "2024-01-01"
389+
- name: type
390+
description: The type of block.
391+
in: query
392+
required: false
393+
schema:
394+
type: string
395+
enum:
396+
- key
397+
- micro
398+
example: micro
399+
- $ref: '#/components/parameters/LimitParam'
400+
- $ref: '#/components/parameters/DirectionParam'
401+
responses:
402+
'200':
403+
description: Returns paginated total stats per generation
404+
content:
405+
application/json:
406+
schema:
407+
allOf:
408+
- type: object
409+
required:
410+
- data
411+
properties:
412+
data:
413+
type: array
414+
items:
415+
$ref: '#/components/schemas/TopMinerStat'
416+
- $ref: '#/components/schemas/PaginatedResponse'
417+
'400':
418+
description: Bad request
419+
content:
420+
application/json:
421+
schema:
422+
$ref: '#/components/schemas/ErrorResponse'
332423
/stats/blocks:
333424
get:
334425
deprecated: false

lib/ae_mdw/db/int_transfer.ex

+7-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ defmodule AeMdw.Db.IntTransfer do
88
alias AeMdw.Db.Model
99
alias AeMdw.Db.MinerRewardsMutation
1010
alias AeMdw.Db.Mutation
11+
alias AeMdw.Db.TopMinerStatsMutation
1112
alias AeMdw.Db.State
1213
alias AeMdw.Collection
1314

@@ -40,6 +41,7 @@ defmodule AeMdw.Db.IntTransfer do
4041
def block_rewards_mutations(key_block) do
4142
height = :aec_blocks.height(key_block)
4243
delay = :aec_governance.beneficiary_reward_delay()
44+
time = :aec_blocks.time_in_msecs(key_block)
4345

4446
dev_benefs =
4547
for {protocol, _height} <- :aec_hard_forks.protocols(),
@@ -65,14 +67,18 @@ defmodule AeMdw.Db.IntTransfer do
6567
{@reward_block_kind, target_pk, amount}
6668
end)
6769

70+
beneficiaries =
71+
Enum.map(miners_rewards, fn {target_pk, _amount} -> target_pk end)
72+
6873
devs_transfers =
6974
Enum.map(devs_rewards, fn {target_pk, amount} ->
7075
{@reward_dev_kind, target_pk, amount}
7176
end)
7277

7378
[
7479
IntTransfersMutation.new(height, miners_transfers ++ devs_transfers),
75-
MinerRewardsMutation.new(miners_rewards)
80+
MinerRewardsMutation.new(miners_rewards),
81+
TopMinerStatsMutation.new(beneficiaries, time)
7682
]
7783
end
7884

lib/ae_mdw/db/model.ex

+20-1
Original file line numberDiff line numberDiff line change
@@ -1311,6 +1311,21 @@ defmodule AeMdw.Db.Model do
13111311
index: mempool_index(),
13121312
tx: mempool_tx()
13131313
)
1314+
@type top_miner_stats_index() ::
1315+
{Stats.interval_by(), Stats.interval_start(), pos_integer(), pubkey()}
1316+
@type top_miner_stats() ::
1317+
record(:top_miner_stats, index: top_miner_stats_index())
1318+
1319+
@top_miner_stats_defaults [:index]
1320+
defrecord :top_miner_stats, @top_miner_stats_defaults
1321+
1322+
@type top_miner_index() ::
1323+
{Stats.interval_by(), Stats.interval_start(), pubkey()}
1324+
@type top_miner() ::
1325+
record(:top_miner, index: top_miner_index, count: pos_integer())
1326+
1327+
@top_miner_defaults [index: nil, count: nil]
1328+
defrecord :top_miner, @top_miner_defaults
13141329

13151330
@hyperchain_leader_at_height_defaults [index: 0, leader: <<>>]
13161331
defrecord :hyperchain_leader_at_height, @hyperchain_leader_at_height_defaults
@@ -1516,7 +1531,9 @@ defmodule AeMdw.Db.Model do
15161531
AeMdw.Db.Model.DeltaStat,
15171532
AeMdw.Db.Model.TotalStat,
15181533
AeMdw.Db.Model.Stat,
1519-
AeMdw.Db.Model.Statistic
1534+
AeMdw.Db.Model.Statistic,
1535+
AeMdw.Db.Model.TopMinerStats,
1536+
AeMdw.Db.Model.TopMiner
15201537
]
15211538
end
15221539

@@ -1656,4 +1673,6 @@ defmodule AeMdw.Db.Model do
16561673
def record(AeMdw.Db.Model.PinInfo), do: :pin_info
16571674
def record(AeMdw.Db.Model.LeaderPinInfo), do: :leader_pin_info
16581675
def record(AeMdw.Db.Model.Delegate), do: :delegate
1676+
def record(AeMdw.Db.Model.TopMinerStats), do: :top_miner_stats
1677+
def record(AeMdw.Db.Model.TopMiner), do: :top_miner
16591678
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
defmodule AeMdw.Db.TopMinerStatsMutation do
2+
@moduledoc """
3+
Increments the top miners stats.
4+
"""
5+
6+
alias AeMdw.Node.Db
7+
alias AeMdw.Db.State
8+
alias AeMdw.Db.Model
9+
alias AeMdw.Db.Sync.Stats
10+
11+
require Model
12+
13+
@derive AeMdw.Db.Mutation
14+
defstruct [:beneficiaries, :time]
15+
16+
@opaque t() :: %__MODULE__{beneficiaries: [Db.pubkey()], time: non_neg_integer()}
17+
18+
@spec new([Db.pubkey()], non_neg_integer()) :: t()
19+
def new(beneficiaries, time), do: %__MODULE__{beneficiaries: beneficiaries, time: time}
20+
21+
@spec execute(t(), State.t()) :: State.t()
22+
def execute(%__MODULE__{beneficiaries: beneficiaries, time: time}, state) do
23+
Enum.reduce(beneficiaries, state, fn beneficiary_pk, state ->
24+
increment_top_miners(state, time, beneficiary_pk)
25+
end)
26+
end
27+
28+
defp increment_top_miners(state, time, beneficiary_pk) do
29+
time
30+
|> Stats.time_intervals()
31+
|> Enum.reduce(state, fn {interval_by, interval_start}, state ->
32+
state
33+
|> State.get(Model.TopMiner, {interval_by, interval_start, beneficiary_pk})
34+
|> case do
35+
{:ok,
36+
Model.top_miner(
37+
index: {^interval_by, ^interval_start, ^beneficiary_pk},
38+
count: count
39+
)} ->
40+
state
41+
|> State.delete(
42+
Model.TopMinerStats,
43+
{interval_by, interval_start, count, beneficiary_pk}
44+
)
45+
|> State.put(
46+
Model.TopMinerStats,
47+
Model.top_miner_stats(index: {interval_by, interval_start, count + 1, beneficiary_pk})
48+
)
49+
|> State.put(
50+
Model.TopMiner,
51+
Model.top_miner(
52+
index: {interval_by, interval_start, beneficiary_pk},
53+
count: count + 1
54+
)
55+
)
56+
57+
:not_found ->
58+
state
59+
|> State.put(
60+
Model.TopMinerStats,
61+
Model.top_miner_stats(index: {interval_by, interval_start, 1, beneficiary_pk})
62+
)
63+
|> State.put(
64+
Model.TopMiner,
65+
Model.top_miner(index: {interval_by, interval_start, beneficiary_pk}, count: 1)
66+
)
67+
end
68+
end)
69+
end
70+
end

lib/ae_mdw/stats.ex

+90
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,24 @@ defmodule AeMdw.Stats do
360360
end
361361
end
362362

363+
@spec fetch_top_miners_stats(State.t(), pagination(), query(), range(), cursor()) ::
364+
{:ok, {pagination_cursor(), [statistic()], pagination_cursor()}} | {:error, reason()}
365+
def fetch_top_miners_stats(state, pagination, query, range, cursor) do
366+
with {:ok, filters} <- Util.convert_params(query, &convert_param/1),
367+
{:ok, cursor} <- deserialize_top_miners_cursor(cursor) do
368+
paginated_top_miners =
369+
state
370+
|> build_top_miners_streamer(filters, range, cursor)
371+
|> Collection.paginate(
372+
pagination,
373+
&render_top_miner_statistic(state, &1),
374+
&serialize_top_miners_cursor/1
375+
)
376+
377+
{:ok, paginated_top_miners}
378+
end
379+
end
380+
363381
defp fetch_statistics(state, pagination, filters, range, cursor, tag) do
364382
with {:ok, cursor} <- deserialize_statistic_cursor(cursor) do
365383
paginated_statistics =
@@ -433,6 +451,21 @@ defmodule AeMdw.Stats do
433451
{{tag, interval_by, min_date}, {tag, interval_by, max_date}}
434452
end
435453

454+
defp build_top_miners_streamer(state, filters, _scope, cursor) do
455+
interval_by = Map.get(filters, :interval_by, :day)
456+
{start_network_date, end_network_date} = DbUtil.network_date_interval(state)
457+
min_date = filters |> Map.get(:min_start_date, start_network_date) |> to_interval(interval_by)
458+
max_date = filters |> Map.get(:max_start_date, end_network_date) |> to_interval(interval_by)
459+
460+
key_boundary =
461+
{{interval_by, min_date, 0, Util.min_bin()},
462+
{interval_by, max_date, Util.max_int(), Util.max_256bit_bin()}}
463+
464+
fn direction ->
465+
Collection.stream(state, Model.TopMinerStats, direction, key_boundary, cursor)
466+
end
467+
end
468+
436469
defp fill_missing_dates(stream, tag, interval_by, :backward, cursor, min_date, max_date) do
437470
max_date =
438471
case cursor do
@@ -519,6 +552,42 @@ defmodule AeMdw.Stats do
519552
render_statistic(state, {:virtual, statistic_key, count})
520553
end
521554

555+
defp render_top_miner_statistic(
556+
_state,
557+
{:month, interval_start, count, beneficiary_id}
558+
) do
559+
%{
560+
start_date: months_to_iso(interval_start),
561+
end_date: months_to_iso(interval_start + 1),
562+
miner: :aeapi.format_account_pubkey(beneficiary_id),
563+
blocks_mined: count
564+
}
565+
end
566+
567+
defp render_top_miner_statistic(
568+
_state,
569+
{:week, interval_start, count, beneficiary_id}
570+
) do
571+
%{
572+
start_date: days_to_iso(interval_start * @days_per_week),
573+
end_date: days_to_iso((interval_start + 1) * @days_per_week),
574+
miner: :aeapi.format_account_pubkey(beneficiary_id),
575+
blocks_mined: count
576+
}
577+
end
578+
579+
defp render_top_miner_statistic(
580+
_state,
581+
{:day, interval_start, count, beneficiary_id}
582+
) do
583+
%{
584+
start_date: days_to_iso(interval_start),
585+
end_date: days_to_iso(interval_start + 1),
586+
miner: :aeapi.format_account_pubkey(beneficiary_id),
587+
blocks_mined: count
588+
}
589+
end
590+
522591
defp convert_blocks_param({"type", "key"}), do: {:ok, {:block_type, :key}}
523592
defp convert_blocks_param({"type", "micro"}), do: {:ok, {:block_type, :micro}}
524593
defp convert_blocks_param(param), do: convert_param(param)
@@ -558,6 +627,12 @@ defmodule AeMdw.Stats do
558627
defp serialize_statistics_cursor({:virtual, {_tag, _interval_by, interval_start}, _count}),
559628
do: "#{interval_start}"
560629

630+
defp serialize_top_miners_cursor({_interval_by, _interval_start, _count, _ben} = cursor) do
631+
cursor
632+
|> :erlang.term_to_binary()
633+
|> Base.encode64()
634+
end
635+
561636
defp deserialize_statistic_cursor(nil), do: {:ok, nil}
562637

563638
defp deserialize_statistic_cursor(cursor_bin) do
@@ -567,6 +642,21 @@ defmodule AeMdw.Stats do
567642
end
568643
end
569644

645+
defp deserialize_top_miners_cursor(nil), do: {:ok, nil}
646+
647+
defp deserialize_top_miners_cursor(cursor_bin) do
648+
case Base.decode64(cursor_bin) do
649+
{:ok, bin} ->
650+
case :erlang.binary_to_term(bin) do
651+
cursor when is_tuple(cursor) -> {:ok, cursor}
652+
_bad_cursor -> {:error, ErrInput.Cursor.exception(value: cursor_bin)}
653+
end
654+
655+
:error ->
656+
{:error, ErrInput.Cursor.exception(value: cursor_bin)}
657+
end
658+
end
659+
570660
defp render_delta_stats(state, gens), do: Enum.map(gens, &fetch_delta_stat!(state, &1))
571661

572662
defp render_total_stats(state, gens), do: Enum.map(gens, &fetch_total_stat!(state, &1))

lib/ae_mdw_web/controllers/stats_controller.ex

+10
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,14 @@ defmodule AeMdwWeb.StatsController do
166166
Util.render(conn, paginated_stats)
167167
end
168168
end
169+
170+
@spec top_miners_stats(Conn.t(), map()) :: Conn.t()
171+
def top_miners_stats(%Conn{assigns: assigns} = conn, _params) do
172+
%{state: state, pagination: pagination, query: query, scope: scope, cursor: cursor} = assigns
173+
174+
with {:ok, paginated_stats} <-
175+
Stats.fetch_top_miners_stats(state, pagination, query, scope, cursor) do
176+
Util.render(conn, paginated_stats)
177+
end
178+
end
169179
end

lib/ae_mdw_web/router.ex

+1
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ defmodule AeMdwWeb.Router do
141141
get "/total", StatsController, :total_stats
142142
get "/delta", StatsController, :delta_stats
143143
get "/miners", StatsController, :miners_stats
144+
get "/miners/top", StatsController, :top_miners_stats
144145
get "/contracts", StatsController, :contracts_stats
145146
get "/aex9-transfers", StatsController, :aex9_transfers_stats
146147
get "/", StatsController, :stats

0 commit comments

Comments
 (0)