Skip to content

Commit

Permalink
feat: add /key-blocks/:hash_or_kbi/micro-blocks endpoint (#896)
Browse files Browse the repository at this point in the history
* feat: add /key-blocks/:hash_or_kbi/micro-blocks endpoint

* refactor: remove unnecessary Stream.take_while/2

* docs: add /key-blocks/:hash/micro-blocks to README
  • Loading branch information
sborrazas committed Sep 12, 2022
1 parent b8a2e09 commit 0540074
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 22 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1506,6 +1506,32 @@ $ curl -s "https://mainnet.aeternity.io/mdw/v2/key-blocks?limit=1" | jq '.'
}
```

### `/v2/key-blocks/:hash/micro-blocks`

```
$ curl https://mainnet.aeternity.io/key-blocks/kh_2HvzkfTvRjfwbim8YZ2q2ETKLhuYK125JGpisr1Cc9m2VSa5iC/micro-blocks
{
"data": [
{
"micro_block_index": 39,
"transactions_count": 0,
"hash": "mh_HqJKqWdJ1vaPcr82zYNue99GXcKfjpYbmrEcZ7kmUHAzQoeZv",
"height": 654915,
"pof_hash": "no_fraud",
"prev_hash": "mh_G2gtKvDAkoi3HZDe5TmWYzGX2pD2AL2DrFXACTch57QEWi1Bo",
"prev_key_hash": "kh_2HvzkfTvRjfwbim8YZ2q2ETKLhuYK125JGpisr1Cc9m2VSa5iC",
"signature": "sg_Eyv2nWKwMbxga4XDHH2oCtnSCWhtD87qUjvFLqKvzt9kq2yVPMkcHSv51kr9fmHQk6TGxBHjRjm74pVZtNuHpZkvybsXX",
"state_hash": "bs_2WNN8aZ15a7pd68wWDZkTqpGUTPezUV6KTN2ra5m3v1x5vVJGC",
"time": 1662950429203,
"txs_hash": "bx_AK5hwnJdG3KAEHEvzs4gwjkRDZP5sw5sbtqgHsgJT2fp1PJka",
"version": 5
}
],
"next": "/v2/key-blocks/kh_2HvzkfTvRjfwbim8YZ2q2ETKLhuYK125JGpisr1Cc9m2VSa5iC/micro-blocks?cursor=38&limit=1",
"prev": null
}
```

---

## Naming System
Expand Down
117 changes: 95 additions & 22 deletions lib/ae_mdw/blocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ defmodule AeMdw.Blocks do
@typep direction :: Database.direction()
@typep limit :: Database.limit()
@typep range :: {:gen, Range.t()} | nil
@typep page_cursor() :: Collection.pagination_cursor()

@table Model.Block

Expand Down Expand Up @@ -61,31 +62,38 @@ defmodule AeMdw.Blocks do

@spec fetch_key_block(State.t(), binary()) :: {:ok, block()} | {:error, Error.t()}
def fetch_key_block(state, hash_or_kbi) do
case Util.parse_int(hash_or_kbi) do
{:ok, kbi} when kbi >= 0 ->
last_gen = DbUtil.last_gen(state)
with {:ok, height} <- extract_height(state, hash_or_kbi) do
{:ok, render_key_block(state, height)}
end
end

if kbi <= last_gen do
{:ok, render_key_block(state, kbi)}
else
{:error, ErrInput.NotFound.exception(value: hash_or_kbi)}
@spec fetch_key_block_micro_blocks(
State.t(),
binary(),
Collection.direction_limit(),
cursor() | nil
) ::
{:ok, page_cursor(), [block()], page_cursor()} | {:error, Error.t()}
def fetch_key_block_micro_blocks(state, hash_or_kbi, pagination, cursor) do
with {:ok, cursor} <- deserialize_cursor_err(cursor),
{:ok, height} <- extract_height(state, hash_or_kbi) do
cursor = if cursor, do: {height, cursor}

{prev_cursor, mbis, next_cursor} =
fn direction ->
state
|> Collection.stream(
Model.Block,
direction,
{{height, 0}, {height, Util.max_256bit_int()}},
cursor
)
|> Stream.map(fn {_height, mbi} -> mbi end)
end
|> Collection.paginate(pagination)

{:ok, _kbi} ->
{:error, ErrInput.NotFound.exception(value: hash_or_kbi)}

:error ->
with {:ok, encoded_hash} <- Validate.id(hash_or_kbi),
{:ok, block} <- :aec_chain.get_block(encoded_hash),
header <- :aec_blocks.to_header(block),
:key <- :aec_headers.type(header),
last_gen <- DbUtil.last_gen(state),
height when height <= last_gen <- :aec_headers.height(header) do
{:ok, render_key_block(state, height)}
else
{:error, reason} -> {:error, reason}
_error_or_invalid_height -> {:error, ErrInput.NotFound.exception(value: hash_or_kbi)}
end
{:ok, serialize_cursor(prev_cursor), render_micro_blocks(state, height, mbis),
serialize_cursor(next_cursor)}
end
end

Expand Down Expand Up @@ -182,6 +190,31 @@ defmodule AeMdw.Blocks do
|> Map.put(:transactions_count, txs_count)
end

defp render_micro_blocks(state, height, mbis),
do: Enum.map(mbis, &render_micro_block(state, height, &1))

defp render_micro_block(state, height, mbi) do
Model.block(tx_index: first_tx_index, hash: mb_hash) =
State.fetch!(state, Model.Block, {height, mbi})

txs_count =
case State.next(state, @table, {height, mbi}) do
{:ok, block_index} ->
Model.block(tx_index: next_tx_index) = State.fetch!(state, @table, block_index)
next_tx_index - first_tx_index

:none ->
0
end

header = :aec_db.get_header(mb_hash)

header
|> :aec_headers.serialize_for_client(Db.prev_block_type(header))
|> Map.put(:micro_block_index, mbi)
|> Map.put(:transactions_count, txs_count)
end

defp render_blocks(state, range, sort_mbs?) do
Enum.map(range, fn gen ->
[key_block | micro_blocks] =
Expand Down Expand Up @@ -238,8 +271,39 @@ defmodule AeMdw.Blocks do
end)
end

defp extract_height(state, hash_or_kbi) do
case Util.parse_int(hash_or_kbi) do
{:ok, kbi} when kbi >= 0 ->
last_gen = DbUtil.last_gen(state)

if kbi <= last_gen do
{:ok, kbi}
else
{:error, ErrInput.NotFound.exception(value: hash_or_kbi)}
end

{:ok, _kbi} ->
{:error, ErrInput.NotFound.exception(value: hash_or_kbi)}

:error ->
with {:ok, encoded_hash} <- Validate.id(hash_or_kbi),
{:ok, block} <- :aec_chain.get_block(encoded_hash),
header <- :aec_blocks.to_header(block),
:key <- :aec_headers.type(header),
last_gen <- DbUtil.last_gen(state),
height when height <= last_gen <- :aec_headers.height(header) do
{:ok, height}
else
{:error, reason} -> {:error, reason}
_error_or_invalid_height -> {:error, ErrInput.NotFound.exception(value: hash_or_kbi)}
end
end
end

defp serialize_cursor(nil), do: nil

defp serialize_cursor({gen, is_reversed?}), do: {Integer.to_string(gen), is_reversed?}

defp serialize_cursor(gen), do: {Integer.to_string(gen), false}

defp deserialize_cursor(nil), do: nil
Expand All @@ -251,4 +315,13 @@ defmodule AeMdw.Blocks do
:error -> nil
end
end

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

defp deserialize_cursor_err(cursor_bin) do
case deserialize_cursor(cursor_bin) do
nil -> {:error, ErrInput.Cursor.exception(value: cursor_bin)}
cursor -> {:ok, cursor}
end
end
end
14 changes: 14 additions & 0 deletions lib/ae_mdw_web/controllers/block_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,20 @@ defmodule AeMdwWeb.BlockController do
end
end

@spec key_block_micro_blocks(Conn.t(), map()) :: Conn.t()
def key_block_micro_blocks(%Conn{assigns: assigns} = conn, %{"hash_or_kbi" => hash_or_kbi}) do
%{
state: state,
pagination: pagination,
cursor: cursor
} = assigns

with {:ok, prev_cursor, micro_blocks, next_cursor} <-
Blocks.fetch_key_block_micro_blocks(state, hash_or_kbi, pagination, cursor) do
WebUtil.paginate(conn, prev_cursor, micro_blocks, next_cursor)
end
end

@doc """
Endpoint for blocks info based on pagination.
"""
Expand Down
1 change: 1 addition & 0 deletions lib/ae_mdw_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ defmodule AeMdwWeb.Router do
get "/blocks/:kbi/:mbi", BlockController, :blocki
get "/key-blocks", BlockController, :key_blocks
get "/key-blocks/:hash_or_kbi", BlockController, :key_block
get "/key-blocks/:hash_or_kbi/micro-blocks", BlockController, :key_block_micro_blocks

get "/txs", TxController, :txs
get "/txs/:hash_or_index", TxController, :tx
Expand Down
79 changes: 79 additions & 0 deletions test/ae_mdw_web/controllers/block_controller_test.exs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule AeMdwWeb.BlockControllerTest do
use AeMdwWeb.ConnCase, async: false

alias :aeser_api_encoder, as: Enc
alias AeMdw.Db.Model
alias AeMdw.Db.Store
alias AeMdw.TestSamples, as: TS
Expand Down Expand Up @@ -98,6 +99,84 @@ defmodule AeMdwWeb.BlockControllerTest do
end
end

describe "key_block_micro_blocks" do
test "it gets all micro blocks with the appropriate transactions_count", %{
conn: conn,
store: store
} do
kbi = 1
hash = TS.key_block_hash(1)
encoded_hash = Enc.encode(:key_block_hash, hash)

store =
store
|> Store.put(Model.Block, Model.block(index: {kbi, -1}, tx_index: 0, hash: hash))
|> Store.put(
Model.Block,
Model.block(index: {kbi, 0}, tx_index: 0, hash: TS.micro_block_hash(0))
)
|> Store.put(
Model.Block,
Model.block(index: {kbi, 1}, tx_index: 10, hash: TS.micro_block_hash(1))
)
|> Store.put(
Model.Block,
Model.block(index: {kbi, 2}, tx_index: 15, hash: TS.micro_block_hash(2))
)
|> Store.put(Model.Block, Model.block(index: {kbi + 1, -1}, tx_index: 17, hash: hash))

with_mocks [
{:aec_db, [], [get_header: fn _mb_hash -> :header end]},
{:aec_chain, [], [get_block: fn ^hash -> {:ok, :block} end]},
{:aec_blocks, [], [to_header: fn :block -> :header end]},
{AeMdw.Node.Db, [], [prev_block_type: fn :header -> :micro end]},
{:aec_headers, [],
[
type: fn :header -> :key end,
height: fn :header -> kbi end,
serialize_for_client: fn :header, :micro -> %{height: kbi} end
]}
] do
assert %{"data" => [block3, block2, block1]} =
conn
|> with_store(store)
|> get("/v2/key-blocks/#{encoded_hash}/micro-blocks")
|> json_response(200)

assert %{
"height" => ^kbi,
"transactions_count" => 10
} = block1

assert %{
"height" => ^kbi,
"transactions_count" => 5
} = block2

assert %{
"height" => ^kbi,
"transactions_count" => 2
} = block3
end
end

test "when key block doesn't exist, it returns 404", %{conn: conn, store: store} do
decoded_hash = TS.key_block_hash(1)
encoded_hash = Enc.encode(:key_block_hash, decoded_hash)
error_msg = "not found: #{encoded_hash}"

with_mocks [
{:aec_chain, [], [get_block: fn ^decoded_hash -> :error end]}
] do
assert %{"error" => ^error_msg} =
conn
|> with_store(store)
|> get("/v2/key-blocks/#{encoded_hash}/micro-blocks")
|> json_response(404)
end
end
end

describe "key-block" do
test "it gets blocks by kbi", %{conn: conn, store: store} do
kbi = 1
Expand Down

0 comments on commit 0540074

Please sign in to comment.