Skip to content

Commit

Permalink
feat: add top miners endpoint (#2000)
Browse files Browse the repository at this point in the history
  • Loading branch information
yaboiishere authored Feb 5, 2025
1 parent bd2fd2f commit 03d3f80
Show file tree
Hide file tree
Showing 9 changed files with 535 additions and 2 deletions.
91 changes: 91 additions & 0 deletions docs/swagger_v3/stats.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,32 @@ schemas:
example:
miner: ak_2whjDhTTmbN13vU7gAUsRbosBhmycho4h8LqHVqKwyGofDetQ9
total_reward: 945000000000000000000
TopMinerStat:
type: object
description: TopMinerStat
required:
- miner
- blocks_mined
- start_date
- end_date
properties:
miner:
description: The miner (beneficiary) address
$ref: '#/components/schemas/AccountAddress'
blocks_mined:
description: The number of blocks mined
type: integer
start_date:
description: The statistic start date
type: string
end_date:
description: The statistic end date
type: string
example:
miner: ak_2whjDhTTmbN13vU7gAUsRbosBhmycho4h8LqHVqKwyGofDetQ9
blocks_mined: 945
start_date: "2024-02-28"
end_date: "2024-02-29"
Stats:
type: object
description: Stats
Expand Down Expand Up @@ -329,6 +355,71 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/stats/miners/top:
get:
deprecated: false
description: Get a list of top miners by blocks mined.
operationId: GetTopMinerStats
parameters:
- name: interval_by
description: The interval in which to return the stats.
in: query
required: false
schema:
type: string
enum:
- day
- week
- month
example: week
- name: min_start_date
description: The minimum start date in YYYY-MM-DD format.
in: query
required: false
schema:
type: string
example: "2023-01-01"
- name: max_start_date
description: The maximum start date in YYYY-MM-DD format.
in: query
required: false
schema:
type: string
example: "2024-01-01"
- name: type
description: The type of block.
in: query
required: false
schema:
type: string
enum:
- key
- micro
example: micro
- $ref: '#/components/parameters/LimitParam'
- $ref: '#/components/parameters/DirectionParam'
responses:
'200':
description: Returns paginated total stats per generation
content:
application/json:
schema:
allOf:
- type: object
required:
- data
properties:
data:
type: array
items:
$ref: '#/components/schemas/TopMinerStat'
- $ref: '#/components/schemas/PaginatedResponse'
'400':
description: Bad request
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/stats/blocks:
get:
deprecated: false
Expand Down
8 changes: 7 additions & 1 deletion lib/ae_mdw/db/int_transfer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ defmodule AeMdw.Db.IntTransfer do
alias AeMdw.Db.Model
alias AeMdw.Db.MinerRewardsMutation
alias AeMdw.Db.Mutation
alias AeMdw.Db.TopMinerStatsMutation
alias AeMdw.Db.State
alias AeMdw.Collection

Expand Down Expand Up @@ -40,6 +41,7 @@ defmodule AeMdw.Db.IntTransfer do
def block_rewards_mutations(key_block) do
height = :aec_blocks.height(key_block)
delay = :aec_governance.beneficiary_reward_delay()
time = :aec_blocks.time_in_msecs(key_block)

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

beneficiaries =
Enum.map(miners_rewards, fn {target_pk, _amount} -> target_pk end)

devs_transfers =
Enum.map(devs_rewards, fn {target_pk, amount} ->
{@reward_dev_kind, target_pk, amount}
end)

[
IntTransfersMutation.new(height, miners_transfers ++ devs_transfers),
MinerRewardsMutation.new(miners_rewards)
MinerRewardsMutation.new(miners_rewards),
TopMinerStatsMutation.new(beneficiaries, time)
]
end

Expand Down
21 changes: 20 additions & 1 deletion lib/ae_mdw/db/model.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,21 @@ defmodule AeMdw.Db.Model do
index: mempool_index(),
tx: mempool_tx()
)
@type top_miner_stats_index() ::
{Stats.interval_by(), Stats.interval_start(), pos_integer(), pubkey()}
@type top_miner_stats() ::
record(:top_miner_stats, index: top_miner_stats_index())

@top_miner_stats_defaults [:index]
defrecord :top_miner_stats, @top_miner_stats_defaults

@type top_miner_index() ::
{Stats.interval_by(), Stats.interval_start(), pubkey()}
@type top_miner() ::
record(:top_miner, index: top_miner_index, count: pos_integer())

@top_miner_defaults [index: nil, count: nil]
defrecord :top_miner, @top_miner_defaults

@hyperchain_leader_at_height_defaults [index: 0, leader: <<>>]
defrecord :hyperchain_leader_at_height, @hyperchain_leader_at_height_defaults
Expand Down Expand Up @@ -1516,7 +1531,9 @@ defmodule AeMdw.Db.Model do
AeMdw.Db.Model.DeltaStat,
AeMdw.Db.Model.TotalStat,
AeMdw.Db.Model.Stat,
AeMdw.Db.Model.Statistic
AeMdw.Db.Model.Statistic,
AeMdw.Db.Model.TopMinerStats,
AeMdw.Db.Model.TopMiner
]
end

Expand Down Expand Up @@ -1656,4 +1673,6 @@ defmodule AeMdw.Db.Model do
def record(AeMdw.Db.Model.PinInfo), do: :pin_info
def record(AeMdw.Db.Model.LeaderPinInfo), do: :leader_pin_info
def record(AeMdw.Db.Model.Delegate), do: :delegate
def record(AeMdw.Db.Model.TopMinerStats), do: :top_miner_stats
def record(AeMdw.Db.Model.TopMiner), do: :top_miner
end
70 changes: 70 additions & 0 deletions lib/ae_mdw/db/mutations/top_miner_stats_mutation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule AeMdw.Db.TopMinerStatsMutation do
@moduledoc """
Increments the top miners stats.
"""

alias AeMdw.Node.Db
alias AeMdw.Db.State
alias AeMdw.Db.Model
alias AeMdw.Db.Sync.Stats

require Model

@derive AeMdw.Db.Mutation
defstruct [:beneficiaries, :time]

@opaque t() :: %__MODULE__{beneficiaries: [Db.pubkey()], time: non_neg_integer()}

@spec new([Db.pubkey()], non_neg_integer()) :: t()
def new(beneficiaries, time), do: %__MODULE__{beneficiaries: beneficiaries, time: time}

@spec execute(t(), State.t()) :: State.t()
def execute(%__MODULE__{beneficiaries: beneficiaries, time: time}, state) do
Enum.reduce(beneficiaries, state, fn beneficiary_pk, state ->
increment_top_miners(state, time, beneficiary_pk)
end)
end

defp increment_top_miners(state, time, beneficiary_pk) do
time
|> Stats.time_intervals()
|> Enum.reduce(state, fn {interval_by, interval_start}, state ->
state
|> State.get(Model.TopMiner, {interval_by, interval_start, beneficiary_pk})
|> case do
{:ok,
Model.top_miner(
index: {^interval_by, ^interval_start, ^beneficiary_pk},
count: count
)} ->
state
|> State.delete(
Model.TopMinerStats,
{interval_by, interval_start, count, beneficiary_pk}
)
|> State.put(
Model.TopMinerStats,
Model.top_miner_stats(index: {interval_by, interval_start, count + 1, beneficiary_pk})
)
|> State.put(
Model.TopMiner,
Model.top_miner(
index: {interval_by, interval_start, beneficiary_pk},
count: count + 1
)
)

:not_found ->
state
|> State.put(
Model.TopMinerStats,
Model.top_miner_stats(index: {interval_by, interval_start, 1, beneficiary_pk})
)
|> State.put(
Model.TopMiner,
Model.top_miner(index: {interval_by, interval_start, beneficiary_pk}, count: 1)
)
end
end)
end
end
90 changes: 90 additions & 0 deletions lib/ae_mdw/stats.ex
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,24 @@ defmodule AeMdw.Stats do
end
end

@spec fetch_top_miners_stats(State.t(), pagination(), query(), range(), cursor()) ::
{:ok, {pagination_cursor(), [statistic()], pagination_cursor()}} | {:error, reason()}
def fetch_top_miners_stats(state, pagination, query, range, cursor) do
with {:ok, filters} <- Util.convert_params(query, &convert_param/1),
{:ok, cursor} <- deserialize_top_miners_cursor(cursor) do
paginated_top_miners =
state
|> build_top_miners_streamer(filters, range, cursor)
|> Collection.paginate(
pagination,
&render_top_miner_statistic(state, &1),
&serialize_top_miners_cursor/1
)

{:ok, paginated_top_miners}
end
end

defp fetch_statistics(state, pagination, filters, range, cursor, tag) do
with {:ok, cursor} <- deserialize_statistic_cursor(cursor) do
paginated_statistics =
Expand Down Expand Up @@ -433,6 +451,21 @@ defmodule AeMdw.Stats do
{{tag, interval_by, min_date}, {tag, interval_by, max_date}}
end

defp build_top_miners_streamer(state, filters, _scope, cursor) do
interval_by = Map.get(filters, :interval_by, :day)
{start_network_date, end_network_date} = DbUtil.network_date_interval(state)
min_date = filters |> Map.get(:min_start_date, start_network_date) |> to_interval(interval_by)
max_date = filters |> Map.get(:max_start_date, end_network_date) |> to_interval(interval_by)

key_boundary =
{{interval_by, min_date, 0, Util.min_bin()},
{interval_by, max_date, Util.max_int(), Util.max_256bit_bin()}}

fn direction ->
Collection.stream(state, Model.TopMinerStats, direction, key_boundary, cursor)
end
end

defp fill_missing_dates(stream, tag, interval_by, :backward, cursor, min_date, max_date) do
max_date =
case cursor do
Expand Down Expand Up @@ -519,6 +552,42 @@ defmodule AeMdw.Stats do
render_statistic(state, {:virtual, statistic_key, count})
end

defp render_top_miner_statistic(
_state,
{:month, interval_start, count, beneficiary_id}
) do
%{
start_date: months_to_iso(interval_start),
end_date: months_to_iso(interval_start + 1),
miner: :aeapi.format_account_pubkey(beneficiary_id),
blocks_mined: count
}
end

defp render_top_miner_statistic(
_state,
{:week, interval_start, count, beneficiary_id}
) do
%{
start_date: days_to_iso(interval_start * @days_per_week),
end_date: days_to_iso((interval_start + 1) * @days_per_week),
miner: :aeapi.format_account_pubkey(beneficiary_id),
blocks_mined: count
}
end

defp render_top_miner_statistic(
_state,
{:day, interval_start, count, beneficiary_id}
) do
%{
start_date: days_to_iso(interval_start),
end_date: days_to_iso(interval_start + 1),
miner: :aeapi.format_account_pubkey(beneficiary_id),
blocks_mined: count
}
end

defp convert_blocks_param({"type", "key"}), do: {:ok, {:block_type, :key}}
defp convert_blocks_param({"type", "micro"}), do: {:ok, {:block_type, :micro}}
defp convert_blocks_param(param), do: convert_param(param)
Expand Down Expand Up @@ -558,6 +627,12 @@ defmodule AeMdw.Stats do
defp serialize_statistics_cursor({:virtual, {_tag, _interval_by, interval_start}, _count}),
do: "#{interval_start}"

defp serialize_top_miners_cursor({_interval_by, _interval_start, _count, _ben} = cursor) do
cursor
|> :erlang.term_to_binary()
|> Base.encode64()
end

defp deserialize_statistic_cursor(nil), do: {:ok, nil}

defp deserialize_statistic_cursor(cursor_bin) do
Expand All @@ -567,6 +642,21 @@ defmodule AeMdw.Stats do
end
end

defp deserialize_top_miners_cursor(nil), do: {:ok, nil}

defp deserialize_top_miners_cursor(cursor_bin) do
case Base.decode64(cursor_bin) do
{:ok, bin} ->
case :erlang.binary_to_term(bin) do
cursor when is_tuple(cursor) -> {:ok, cursor}
_bad_cursor -> {:error, ErrInput.Cursor.exception(value: cursor_bin)}
end

:error ->
{:error, ErrInput.Cursor.exception(value: cursor_bin)}
end
end

defp render_delta_stats(state, gens), do: Enum.map(gens, &fetch_delta_stat!(state, &1))

defp render_total_stats(state, gens), do: Enum.map(gens, &fetch_total_stat!(state, &1))
Expand Down
10 changes: 10 additions & 0 deletions lib/ae_mdw_web/controllers/stats_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,14 @@ defmodule AeMdwWeb.StatsController do
Util.render(conn, paginated_stats)
end
end

@spec top_miners_stats(Conn.t(), map()) :: Conn.t()
def top_miners_stats(%Conn{assigns: assigns} = conn, _params) do
%{state: state, pagination: pagination, query: query, scope: scope, cursor: cursor} = assigns

with {:ok, paginated_stats} <-
Stats.fetch_top_miners_stats(state, pagination, query, scope, cursor) do
Util.render(conn, paginated_stats)
end
end
end
1 change: 1 addition & 0 deletions lib/ae_mdw_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ defmodule AeMdwWeb.Router do
get "/total", StatsController, :total_stats
get "/delta", StatsController, :delta_stats
get "/miners", StatsController, :miners_stats
get "/miners/top", StatsController, :top_miners_stats
get "/contracts", StatsController, :contracts_stats
get "/aex9-transfers", StatsController, :aex9_transfers_stats
get "/", StatsController, :stats
Expand Down
Loading

0 comments on commit 03d3f80

Please sign in to comment.