Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion lib/ae_mdw/db/model.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,12 @@ defmodule AeMdw.Db.Model do
@type miner_index() :: pubkey()
@type miner() :: record(:miner, index: miner_index(), total_reward: non_neg_integer())

@reward_miner_defaults [:index]
defrecord :reward_miner, @reward_miner_defaults

@type reward_miner_index() :: {non_neg_integer(), pubkey()}
@type reward_miner() :: record(:reward_miner, index: reward_miner_index())

### Node tables
defrecord(:mempool_tx, :tx, hash: nil, signed_tx: nil, failures: nil)

Expand Down Expand Up @@ -1440,7 +1446,8 @@ defmodule AeMdw.Db.Model do
AeMdw.Db.Model.IntTransferTx,
AeMdw.Db.Model.KindIntTransferTx,
AeMdw.Db.Model.TargetKindIntTransferTx,
AeMdw.Db.Model.Miner
AeMdw.Db.Model.Miner,
AeMdw.Db.Model.RewardMiner
]
end

Expand Down Expand Up @@ -1680,6 +1687,7 @@ defmodule AeMdw.Db.Model do
def record(AeMdw.Db.Model.Stat), do: :stat
def record(AeMdw.Db.Model.Statistic), do: :statistic
def record(AeMdw.Db.Model.Miner), do: :miner
def record(AeMdw.Db.Model.RewardMiner), do: :reward_miner
def record(AeMdw.Db.Model.AccountNamesCount), do: :account_names_count
def record(AeMdw.Db.Model.Mempool), do: :mempool
def record(AeMdw.Db.Model.DexPair), do: :dex_pair
Expand Down
34 changes: 27 additions & 7 deletions lib/ae_mdw/db/mutations/miner_rewards_mutation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,35 @@ defmodule AeMdw.Db.MinerRewardsMutation do
defp increment_total_reward(state, beneficiary_pk, reward) do
case State.get(state, Model.Miner, beneficiary_pk) do
{:ok, Model.miner(total_reward: old_reward) = miner} ->
{State.put(state, Model.Miner, Model.miner(miner, total_reward: old_reward + reward)),
false}
total_reward = old_reward + reward

state
|> State.put(
Model.Miner,
Model.miner(miner, total_reward: total_reward)
)
|> State.delete(Model.RewardMiner, {old_reward, beneficiary_pk})
|> State.put(
Model.RewardMiner,
Model.reward_miner(index: {total_reward, beneficiary_pk})
)
|> then(fn st ->
{st, false}
end)

:not_found ->
{State.put(
state,
Model.Miner,
Model.miner(index: beneficiary_pk, total_reward: reward)
), true}
state
|> State.put(
Model.RewardMiner,
Model.reward_miner(index: {reward, beneficiary_pk})
)
|> State.put(
Model.Miner,
Model.miner(index: beneficiary_pk, total_reward: reward)
)
|> then(fn st ->
{st, true}
end)
end
end

Expand Down
24 changes: 17 additions & 7 deletions lib/ae_mdw/miners.ex
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@
end
end

@spec fetch_miner!(state(), pubkey()) :: miner()

Check warning on line 33 in lib/ae_mdw/miners.ex

View workflow job for this annotation

GitHub Actions / Dialyzer

invalid_contract

Invalid type specification for function fetch_miner!.
def fetch_miner!(state, miner_pk),
def fetch_miner!(state, {_total_reward, miner_pk}),
do: render_miner(State.fetch!(state, Model.Miner, miner_pk))

defp build_streamer(state, cursor), do: &Collection.stream(state, Model.Miner, &1, nil, cursor)
defp build_streamer(state, cursor),
do: &Collection.stream(state, Model.RewardMiner, &1, nil, cursor)

defp render_miner(
Model.miner(
Expand All @@ -48,14 +49,23 @@
}
end

defp serialize_cursor(miner_pk), do: Enc.encode(:account_pubkey, miner_pk)
defp serialize_cursor({total_reward, miner_pk}) do
{total_reward, miner_pk}
|> :erlang.term_to_binary()
|> Base.hex_encode32(padding: false)
end

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

defp deserialize_cursor(cursor_bin) do
case Enc.safe_decode(:account_pubkey, cursor_bin) do
{:ok, miner_pk} -> {:ok, miner_pk}
{:error, _reason} -> {:error, ErrInput.Cursor.exception(value: cursor_bin)}
defp deserialize_cursor(cursor_hex) do
with {:ok, cursor_bin} <- Base.hex_decode32(cursor_hex, padding: false),
{total_reward, <<miner_pk::256>>}
when is_integer(total_reward) <-
:erlang.binary_to_term(cursor_bin) do
{:ok, {total_reward, <<miner_pk::256>>}}
else
_invalid_cursor ->
{:error, ErrInput.Cursor.exception(value: cursor_hex)}
end
end
end
30 changes: 30 additions & 0 deletions priv/migrations/20250430091417_index_miners_by_reward.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule AeMdw.Migrations.IndexMinersByReward do
@moduledoc false
alias AeMdw.Db.WriteMutation
alias AeMdw.Db.RocksDbCF
alias AeMdw.Db.Model
alias AeMdw.Db.State

require Model

@spec run(State.t(), boolean()) :: {:ok, non_neg_integer()}
def run(state, _from_start?) do
Model.Miner
|> RocksDbCF.stream()
|> Stream.map(fn Model.miner(index: miner_pk, total_reward: total_reward) ->
WriteMutation.new(
Model.RewardMiner,
Model.reward_miner(index: {total_reward, miner_pk})
)
end)
|> Stream.chunk_every(1000)
|> Stream.map(fn mutations ->
_state = State.commit_db(state, mutations)
length(mutations)
end)
|> Enum.sum()
|> then(fn count ->
{:ok, count}
end)
end
end
91 changes: 91 additions & 0 deletions test/ae_mdw_web/controllers/stats_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,97 @@ defmodule AeMdwWeb.StatsControllerTest do
end
end

describe "stats/miners returns sorted miners by total_reward" do
setup %{conn: conn, store: store} do
miner1 = <<1::256>>
miner2 = <<2::256>>
miner3 = <<3::256>>

miner1_pk = :aeapi.format_account_pubkey(miner1)
miner2_pk = :aeapi.format_account_pubkey(miner2)
miner3_pk = :aeapi.format_account_pubkey(miner3)

store =
store
|> Store.put(Model.Miner, Model.miner(index: miner1, total_reward: 10))
|> Store.put(Model.Miner, Model.miner(index: miner2, total_reward: 20))
|> Store.put(Model.Miner, Model.miner(index: miner3, total_reward: 30))
|> Store.put(Model.RewardMiner, Model.reward_miner(index: {10, miner1}))
|> Store.put(Model.RewardMiner, Model.reward_miner(index: {20, miner2}))
|> Store.put(Model.RewardMiner, Model.reward_miner(index: {30, miner3}))

conn = with_store(conn, store)

{:ok, %{store: store, conn: conn, miner_pks: [miner1_pk, miner2_pk, miner3_pk]}}
end

test "it returns the miners sorted by total_reward", %{
conn: conn,
miner_pks: [miner1_pk, miner2_pk, miner3_pk]
} do
assert %{"prev" => nil, "data" => [st1, st2, st3], "next" => nil} =
conn
|> get("/v3/stats/miners")
|> json_response(200)

assert %{"miner" => ^miner3_pk, "total_reward" => 30} = st1
assert %{"miner" => ^miner2_pk, "total_reward" => 20} = st2
assert %{"miner" => ^miner1_pk, "total_reward" => 10} = st3
end

test "it returns the miners sorted by total_reward with limit", %{
conn: conn,
miner_pks: [miner1_pk, miner2_pk, miner3_pk]
} do
assert statistics =
%{"prev" => nil, "data" => [st1, st2], "next" => next_url} =
conn
|> get("/v3/stats/miners", limit: 2)
|> json_response(200)

assert %{"miner" => ^miner3_pk, "total_reward" => 30} = st1
assert %{"miner" => ^miner2_pk, "total_reward" => 20} = st2

assert %{"prev" => prev_url, "data" => [st3], "next" => nil} =
conn
|> get(next_url)
|> json_response(200)

assert %{"miner" => ^miner1_pk, "total_reward" => 10} = st3

assert ^statistics =
conn
|> get(prev_url)
|> json_response(200)
end

test "it returns the miners sorted by asc total_reward", %{
conn: conn,
miner_pks: [miner1_pk, miner2_pk, miner3_pk]
} do
assert statistics =
%{"prev" => nil, "data" => [st1, st2], "next" => next_url} =
conn
|> get("/v3/stats/miners", limit: 2, direction: "forward")
|> json_response(200)

assert %{"miner" => ^miner1_pk, "total_reward" => 10} = st1
assert %{"miner" => ^miner2_pk, "total_reward" => 20} = st2

assert %{"prev" => prev_url, "data" => [st3], "next" => nil} =
conn
|> get(next_url)
|> json_response(200)

assert %{"miner" => ^miner3_pk, "total_reward" => 30} = st3

assert ^statistics =
conn
|> get(prev_url)
|> json_response(200)
end
end

defp add_transactions_every_5_hours(store, start_txi, end_txi, now) do
end_txi..start_txi
|> Enum.reduce({store, 1}, fn txi, {store, i} ->
Expand Down
Loading