diff --git a/README.md b/README.md index 392a1724d..9fad8e041 100644 --- a/README.md +++ b/README.md @@ -1532,6 +1532,26 @@ $ curl https://mainnet.aeternity.io/key-blocks/kh_2HvzkfTvRjfwbim8YZ2q2ETKLhuYK1 } ``` +### `/v2/micro-blocks/:hash` + +``` +$ curl https://mainnet.aeternity.io/mdw/v2/micro-blocks/mh_HqJKqWdJ1vaPcr82zYNue99GXcKfjpYbmrEcZ7kmUHAzQoeZv +{ + "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 +} +``` + --- ## Naming System diff --git a/lib/ae_mdw/blocks.ex b/lib/ae_mdw/blocks.ex index 2ff636f73..5ac8ab5d6 100644 --- a/lib/ae_mdw/blocks.ex +++ b/lib/ae_mdw/blocks.ex @@ -67,6 +67,22 @@ defmodule AeMdw.Blocks do end end + @spec fetch_micro_block(State.t(), binary()) :: {:ok, block()} | {:error, Error.t()} + def fetch_micro_block(state, hash) do + with {:ok, hash, height} <- extract_height(state, :micro, hash) do + mbi = + hash + |> Db.get_reverse_micro_blocks() + |> Enum.count() + + if State.exists?(state, Model.Block, {height, mbi}) do + {:ok, render_micro_block(state, height, mbi)} + else + {:error, ErrInput.NotFound.exception(value: hash)} + end + end + end + @spec fetch_key_block_micro_blocks( State.t(), binary(), @@ -286,20 +302,26 @@ defmodule AeMdw.Blocks do {: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 + with {:ok, _hash, height} <- extract_height(state, :key, hash_or_kbi) do {:ok, height} - else - {:error, reason} -> {:error, reason} - _error_or_invalid_height -> {:error, ErrInput.NotFound.exception(value: hash_or_kbi)} end end end + defp extract_height(state, type, hash) do + with {:ok, encoded_hash} <- Validate.id(hash), + {:ok, block} <- :aec_chain.get_block(encoded_hash), + header <- :aec_blocks.to_header(block), + ^type <- :aec_headers.type(header), + last_gen <- DbUtil.last_gen(state), + height when height <= last_gen <- :aec_headers.height(header) do + {:ok, encoded_hash, height} + else + {:error, reason} -> {:error, reason} + _error_or_invalid_height -> {:error, ErrInput.NotFound.exception(value: hash)} + end + end + defp serialize_cursor(nil), do: nil defp serialize_cursor({gen, is_reversed?}), do: {Integer.to_string(gen), is_reversed?} diff --git a/lib/ae_mdw/node/db.ex b/lib/ae_mdw/node/db.ex index 133d1371e..510c061ab 100644 --- a/lib/ae_mdw/node/db.ex +++ b/lib/ae_mdw/node/db.ex @@ -81,12 +81,15 @@ defmodule AeMdw.Node.Db do end @spec get_micro_blocks(Blocks.block_hash()) :: list() - def get_micro_blocks(next_kb_hash) do - next_kb_hash + def get_micro_blocks(next_block_hash), + do: next_block_hash |> get_reverse_micro_blocks() |> Enum.reverse() + + @spec get_reverse_micro_blocks(Blocks.block_hash()) :: Enumerable.t() + def get_reverse_micro_blocks(next_block_hash) do + next_block_hash |> :aec_db.get_header() |> :aec_headers.prev_hash() |> Stream.unfold(µ_block_walker/1) - |> Enum.reverse() end @spec get_key_block_hash(Blocks.height()) :: Blocks.block_hash() | nil diff --git a/lib/ae_mdw_web/controllers/block_controller.ex b/lib/ae_mdw_web/controllers/block_controller.ex index adece5c53..77b9b5350 100644 --- a/lib/ae_mdw_web/controllers/block_controller.ex +++ b/lib/ae_mdw_web/controllers/block_controller.ex @@ -103,6 +103,13 @@ defmodule AeMdwWeb.BlockController do end end + @spec micro_block(Conn.t(), map()) :: Conn.t() + def micro_block(%Conn{assigns: %{state: state}} = conn, %{"hash" => hash}) do + with {:ok, block} <- Blocks.fetch_micro_block(state, hash) do + json(conn, block) + end + end + @doc """ Endpoint for blocks info based on pagination. """ diff --git a/lib/ae_mdw_web/router.ex b/lib/ae_mdw_web/router.ex index 315a477ba..d188f22c1 100644 --- a/lib/ae_mdw_web/router.ex +++ b/lib/ae_mdw_web/router.ex @@ -53,6 +53,7 @@ defmodule AeMdwWeb.Router do 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 "/micro-blocks/:hash", BlockController, :micro_block get "/txs", TxController, :txs get "/txs/:hash_or_index", TxController, :tx diff --git a/test/ae_mdw_web/controllers/block_controller_test.exs b/test/ae_mdw_web/controllers/block_controller_test.exs index 963d33e0e..c8b7160fa 100644 --- a/test/ae_mdw_web/controllers/block_controller_test.exs +++ b/test/ae_mdw_web/controllers/block_controller_test.exs @@ -265,6 +265,71 @@ defmodule AeMdwWeb.BlockControllerTest do end end + describe "micro-block" do + test "it returns a micro block with txs counts", %{conn: conn, store: store} do + kbi = 1 + decoded_hash = TS.micro_block_hash(0) + encoded_hash = :aeser_api_encoder.encode(:micro_block_hash, decoded_hash) + + store = + store + |> Store.put( + Model.Block, + Model.block(index: {kbi, -1}, hash: TS.key_block_hash(0), tx_index: 4) + ) + |> Store.put( + Model.Block, + Model.block(index: {kbi, 0}, hash: decoded_hash, tx_index: 4) + ) + |> Store.put( + Model.Block, + Model.block(index: {kbi, 1}, hash: TS.micro_block_hash(1), tx_index: 10) + ) + + with_mocks [ + {:aec_chain, [], [get_block: fn ^decoded_hash -> {:ok, :block} end]}, + {:aec_db, [], [get_header: fn ^decoded_hash -> :header end]}, + {:aec_blocks, [], [to_header: fn :block -> :header end]}, + {:aec_headers, [], + [ + height: fn :header -> kbi end, + serialize_for_client: fn :header, :key -> %{height: kbi} end, + type: fn :header -> :micro end + ]}, + {AeMdw.Node.Db, [], + [ + prev_block_type: fn :header -> :key end, + get_reverse_micro_blocks: fn ^decoded_hash -> [] end + ]} + ] do + assert %{ + "height" => ^kbi, + "micro_block_index" => 0, + "transactions_count" => 6 + } = + conn + |> with_store(store) + |> get("/v2/micro-blocks/#{encoded_hash}") + |> json_response(200) + end + end + + test "it returns 404 when not found", %{conn: conn, store: store} do + decoded_hash = TS.micro_block_hash(0) + encoded_hash = :aeser_api_encoder.encode(:micro_block_hash, decoded_hash) + error_msg = "not found: #{encoded_hash}" + + store = + Store.put(store, Model.Block, Model.block(index: {0, -1}, hash: TS.key_block_hash(0))) + + assert %{"error" => ^error_msg} = + conn + |> with_store(store) + |> get("/v2/micro-blocks/#{encoded_hash}") + |> json_response(404) + end + end + describe "blocks" do test "get key block by hash", %{conn: conn, store: store} do with_blockchain %{}, kb1: [] do