Skip to content

Commit

Permalink
feat: add /v2/micro-blocks/:hash/txs endpoint (#900)
Browse files Browse the repository at this point in the history
* feat: add /v2/micro-blocks/:hash/txs endpoint

* test: add integration test for /micro-blocks/:hash/txs endpoint
  • Loading branch information
sborrazas authored Sep 14, 2022
1 parent 5f66f15 commit 2312a8a
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 55 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1517,7 +1517,7 @@ $ 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
$ curl https://mainnet.aeternity.io/key-blocks/kh_2HvzkfTvRjfwbim8YZ2q2ETKLhuYK125JGpisr1Cc9m2VSa5iC/micro-blocks?limit=1
{
"data": [
{
Expand Down Expand Up @@ -1560,6 +1560,38 @@ $ curl https://mainnet.aeternity.io/mdw/v2/micro-blocks/mh_HqJKqWdJ1vaPcr82zYNue
}
```

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

```
$ curl https://mainnet.aeternity.io/mdw/v2/micro-blocks/mh_3TzzPsMhgnJBYAtSJ6c4SdbQppZi64mxP61b1u1E8g3stDQwk/txs?limit=1
{
"data": [
{
"block_hash": "mh_3TzzPsMhgnJBYAtSJ6c4SdbQppZi64mxP61b1u1E8g3stDQwk",
"block_height": 14085,
"hash": "th_2Eo84A8gYkaNnRXkkEe9gPg5jcbKGdPVkZvK9XUSEQhDD6kmqm",
"micro_index": 59,
"micro_time": 1545877257605,
"signatures": [
"sg_E6tbrssPGL4a1mXyN5EW9d3UwRfYN9pSsBDtDEQVyQqTQjhQVBPKNJV6qyc43M5zY2tLE8VQa8Jb3q1XGYKJYaHM5Q3T4"
],
"tx": {
"amount": 43734300000000000000,
"fee": 21000,
"nonce": 19631,
"payload": "ba_SGVsbG8sIE1pbmVyISAvWW91cnMgQmVlcG9vbC4vKXcQag==",
"recipient_id": "ak_2gD9eHc6AaLSgUKne5vVLrsnG4acTCDE7KPetE4PqA8MYvz8gN",
"sender_id": "ak_nv5B93FPzRHrGNmMdTDfGdd5xGZvep3MVSpJqzcQmMp59bBCv",
"type": "SpendTx",
"version": 1
},
"tx_index": 92999
}
],
"next": null,
"prev": null
}
```
---

## Naming System
Expand Down
49 changes: 5 additions & 44 deletions lib/ae_mdw/blocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ defmodule AeMdw.Blocks do
@type block :: map()
@type cursor :: binary()

@typep state() :: State.t()
@typep direction :: Database.direction()
@typep limit :: Database.limit()
@typep range :: {:gen, Range.t()} | nil
Expand Down Expand Up @@ -60,21 +61,16 @@ defmodule AeMdw.Blocks do
end
end

@spec fetch_key_block(State.t(), binary()) :: {:ok, block()} | {:error, Error.t()}
@spec fetch_key_block(state(), binary()) :: {:ok, block()} | {:error, Error.t()}
def fetch_key_block(state, hash_or_kbi) do
with {:ok, height} <- extract_height(state, hash_or_kbi) do
with {:ok, height} <- DbUtil.key_block_height(state, hash_or_kbi) do
{:ok, render_key_block(state, height)}
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()

with {:ok, height, mbi} <- DbUtil.micro_block_height_index(state, hash) do
if State.exists?(state, Model.Block, {height, mbi}) do
{:ok, render_micro_block(state, height, mbi)}
else
Expand All @@ -92,7 +88,7 @@ defmodule AeMdw.Blocks do
{: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
{:ok, height} <- DbUtil.key_block_height(state, hash_or_kbi) do
cursor = if cursor, do: {height, cursor}

{prev_cursor, mbis, next_cursor} =
Expand Down Expand Up @@ -287,41 +283,6 @@ 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, _hash, height} <- extract_height(state, :key, hash_or_kbi) do
{:ok, height}
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?}
Expand Down
54 changes: 54 additions & 0 deletions lib/ae_mdw/db/util.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ defmodule AeMdw.Db.Util do
alias AeMdw.Collection
alias AeMdw.Db.Model
alias AeMdw.Db.State
alias AeMdw.Error
alias AeMdw.Error.Input, as: ErrInput
alias AeMdw.Node.Db
alias AeMdw.Txs
alias AeMdw.Util
alias AeMdw.Validate

require Model

Expand Down Expand Up @@ -107,6 +112,55 @@ defmodule AeMdw.Db.Util do
end
end

@spec key_block_height(state(), binary()) :: {:ok, Blocks.height()} | {:error, Error.t()}
def key_block_height(state, hash_or_kbi) do
case Util.parse_int(hash_or_kbi) do
{:ok, kbi} when kbi >= 0 ->
last_gen = 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, height, _hash} <- extract_height_hash(state, :key, hash_or_kbi) do
{:ok, height}
end
end
end

@spec micro_block_height_index(state(), binary()) ::
{:ok, Blocks.height(), Blocks.mbi()} | {:error, Error.t()}
def micro_block_height_index(state, mb_hash) do
with {:ok, height, decoded_hash} <- extract_height_hash(state, :micro, mb_hash) do
mbi =
decoded_hash
|> Db.get_reverse_micro_blocks()
|> Enum.count()

{:ok, height, mbi}
end
end

defp extract_height_hash(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 <- last_gen(state),
height when height <= last_gen <- :aec_headers.height(header) do
{:ok, height, encoded_hash}
else
{:error, reason} -> {:error, reason}
_error_or_invalid_height -> {:error, ErrInput.NotFound.exception(value: hash)}
end
end

defp block_type_height(node_block) do
{type, header} =
case node_block do
Expand Down
40 changes: 32 additions & 8 deletions lib/ae_mdw/txs.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,18 @@ defmodule AeMdw.Txs do
@type txi :: non_neg_integer()
@type tx_hash() :: binary()
@type cursor :: binary()
@type query :: %{
types: term(),
ids: term()
}
@type query ::
%{
types: term(),
ids: term()
}
| %{}
@type add_spendtx_details?() :: boolean()

@typep state() :: State.t()
@typep reason :: binary()
@typep pagination :: Collection.direction_limit()
@typep range :: {:gen, Range.t()} | {:txi, Range.t()} | nil
@typep page_cursor() :: Collection.pagination_cursor()

@table Tx
@type_table Type
Expand Down Expand Up @@ -99,8 +101,7 @@ defmodule AeMdw.Txs do
query(),
cursor() | nil,
add_spendtx_details?()
) ::
{:ok, cursor() | nil, [tx()], cursor() | nil} | {:error, reason()}
) :: {:ok, page_cursor(), [tx()], page_cursor()} | {:error, Error.t()}
def fetch_txs(state, pagination, range, query, cursor, add_spendtx_details?) do
ids = query |> Map.get(:ids, MapSet.new()) |> MapSet.to_list()
types = query |> Map.get(:types, MapSet.new()) |> MapSet.to_list()
Expand Down Expand Up @@ -133,7 +134,30 @@ defmodule AeMdw.Txs do
{:ok, serialize_cursor(prev_cursor), txs, serialize_cursor(next_cursor)}
rescue
e in ErrInput ->
{:error, e.message}
{:error, e}
end
end

@spec fetch_micro_block_txs(state(), binary(), pagination(), cursor() | nil) ::
{:ok, page_cursor(), [tx()], page_cursor()} | {:error, Error.t()}
def fetch_micro_block_txs(state, hash, pagination, cursor) do
with {:ok, height, mbi} <- DbUtil.micro_block_height_index(state, hash) do
Model.block(tx_index: first_txi) = State.fetch!(state, Model.Block, {height, mbi})

case State.next(state, Model.Block, {height, mbi}) do
{:ok, next_key} ->
Model.block(tx_index: next_txi) = State.fetch!(state, Model.Block, next_key)

if first_txi < next_txi do
range = first_txi..(next_txi - 1)
fetch_txs(state, pagination, {:txi, range}, %{}, cursor, false)
else
{:ok, nil, [], nil}
end

:none ->
{:ok, nil, [], nil}
end
end
end

Expand Down
15 changes: 13 additions & 2 deletions lib/ae_mdw_web/controllers/tx_controller.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule AeMdwWeb.TxController do
use AeMdwWeb, :controller

alias AeMdw.Error.Input, as: ErrInput
alias AeMdw.Node
alias AeMdw.Validate
alias AeMdw.Db.Model
Expand Down Expand Up @@ -56,8 +57,8 @@ defmodule AeMdwWeb.TxController do
Txs.fetch_txs(state, pagination, scope, query, cursor, add_spendtx_details?) do
paginate(conn, prev_cursor, txs, next_cursor)
else
{:error, reason} ->
send_error(conn, :bad_request, reason)
{:error, reason} when is_binary(reason) -> {:error, ErrInput.Query.exception(value: reason)}
{:error, reason} -> {:error, reason}
end
end

Expand Down Expand Up @@ -100,6 +101,16 @@ defmodule AeMdwWeb.TxController do
end
end

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

with {:ok, prev_cursor, txs, next_cursor} <-
Txs.fetch_micro_block_txs(state, hash, pagination, cursor) do
paginate(conn, prev_cursor, txs, next_cursor)
end
end

defp extract_query(query_params) do
query_params
|> Enum.reject(fn {key, _val} -> key in @pagination_param_keys end)
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 @@ -54,6 +54,7 @@ defmodule AeMdwWeb.Router do
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 "/micro-blocks/:hash/txs", TxController, :micro_block_txs

get "/txs", TxController, :txs
get "/txs/:hash_or_index", TxController, :tx
Expand Down
66 changes: 66 additions & 0 deletions test/ae_mdw_web/controllers/tx_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ defmodule AeMdwWeb.TxControllerTest do

import Mock

alias :aeser_api_encoder, as: Enc
alias AeMdw.Db.Format
alias AeMdw.Db.Model
alias AeMdw.Db.Store
alias AeMdw.Db.Util
alias AeMdw.Node.Db
alias AeMdw.TestSamples, as: TS

require Model
Expand Down Expand Up @@ -102,4 +105,67 @@ defmodule AeMdwWeb.TxControllerTest do
|> json_response(400)
end
end

describe "micro_block_txs" do
test "it returns the list of txs from a single mb by mb_hash", %{conn: conn, store: store} do
mb_hash = TS.micro_block_hash(0)
tx1_hash = TS.tx_hash(0)
tx2_hash = TS.tx_hash(1)
encoded_mb_hash = Enc.encode(:micro_block_hash, mb_hash)
height = 4

store =
store
|> Store.put(Model.Block, Model.block(index: {height, -1}, tx_index: 10))
|> Store.put(Model.Block, Model.block(index: {height, 0}, tx_index: 10))
|> Store.put(Model.Block, Model.block(index: {height + 1, -1}, tx_index: 12))
|> Store.put(Model.Tx, Model.tx(index: 10, id: tx1_hash))
|> Store.put(Model.Tx, Model.tx(index: 11, id: tx2_hash))

with_mocks [
{:aec_chain, [], [get_block: fn ^mb_hash -> {:ok, :block} end]},
{:aec_blocks, [], [to_header: fn :block -> :header end]},
{:aec_headers, [],
[
type: fn :header -> :micro end,
height: fn :header -> height end
]},
{Db, [], [get_reverse_micro_blocks: fn ^mb_hash -> [] end]},
{Format, [],
[
to_map: fn
_state, Model.tx(id: ^tx1_hash) -> %{a: 1}
_state, Model.tx(id: ^tx2_hash) -> %{b: 2}
end
]}
] do
assert %{"data" => [tx2, tx1]} =
conn
|> with_store(store)
|> get("/v2/micro-blocks/#{encoded_mb_hash}/txs")
|> json_response(200)

assert %{"a" => 1} = tx1
assert %{"b" => 2} = tx2
end
end

test "if no txs, it returns an empty result", %{conn: conn, store: store} do
mb_hash = TS.micro_block_hash(0)
encoded_mb_hash = Enc.encode(:micro_block_hash, mb_hash)
error_msg = "not found: #{encoded_mb_hash}"

store = Store.put(store, Model.Block, Model.block(index: {3, 0}, tx_index: 10))

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

0 comments on commit 2312a8a

Please sign in to comment.