From c303cce348626bad3884abcc93ec04070bec4871 Mon Sep 17 00:00:00 2001 From: Sebastian Borrazas Date: Wed, 31 May 2023 09:58:30 -0300 Subject: [PATCH] feat: allow filtering channels by active/inactive (#1367) --- README.md | 2 + docs/swagger_v2/channels.spec.yaml | 18 +++- lib/ae_mdw/channels.ex | 61 ++++++++++--- .../controllers/channel_controller.ex | 8 +- .../controllers/channel_controller_test.exs | 89 +++++++++++++++++++ 5 files changed, 156 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 2efa79eea..54e7862f9 100644 --- a/README.md +++ b/README.md @@ -3768,6 +3768,8 @@ $ curl -s "https://mainnet.aeternity.io/mdw/v2/oracles?state=active&limit=1&expa Returns active channels ordered by the txi of the last update. +These can also be filtered by `state=active` or `state=inactive`. + Besides the participants balances it includes some fields intrinsic to the channel such as: - the reserve deposited in the channel for paying fees and assuring refunds; - lock parameters, in case of individual actions; and diff --git a/docs/swagger_v2/channels.spec.yaml b/docs/swagger_v2/channels.spec.yaml index 6e622fd1d..4a82b53c6 100644 --- a/docs/swagger_v2/channels.spec.yaml +++ b/docs/swagger_v2/channels.spec.yaml @@ -53,14 +53,14 @@ schemas: type: integer description: Amount owned by responder example: 4500000000000000000 - round: + round: type: integer description: Round after last transaction example: 1 - solo_round: + solo_round: type: integer description: Round of last solo transaction - example: 0 + example: 0 state_hash: type: string description: The hash of the current channel state @@ -69,7 +69,7 @@ schemas: type: string description: The amount of times the channel's been updated by any of the channel transactions example: 2 - + paths: /channels: get: @@ -80,6 +80,16 @@ paths: - $ref: '#/components/parameters/LimitParam' - $ref: '#/components/parameters/ScopeParam' - $ref: '#/components/parameters/DirectionParam' + - name: state + in: query + description: Exclusively filter by active/inactive channels. + required: false + schema: + type: string + enum: + - active + - inactive + example: inactive responses: '200': description: Returns paginated channels diff --git a/lib/ae_mdw/channels.ex b/lib/ae_mdw/channels.ex index 0e79ffbf0..4de5c9fcf 100644 --- a/lib/ae_mdw/channels.ex +++ b/lib/ae_mdw/channels.ex @@ -25,6 +25,7 @@ defmodule AeMdw.Channels do @typep type_block_hash() :: {Db.hash_type(), Db.hash()} @typep pagination() :: Collection.direction_limit() @typep range() :: {:gen, Range.t()} | nil + @typep query() :: map() @typep cursor() :: binary() @typep pagination_cursor() :: Collection.pagination_cursor() @@ -36,6 +37,8 @@ defmodule AeMdw.Channels do @table_inactive Model.InactiveChannel @table_inactive_activation Model.InactiveChannelActivation + @states ~w(active inactive) + @channel_tx_mod %{ :channel_create_tx => :aesc_create_tx, :channel_close_solo_tx => :aesc_close_solo_tx, @@ -49,20 +52,23 @@ defmodule AeMdw.Channels do :channel_snapshot_solo_tx => :aesc_snapshot_solo_tx } - @spec fetch_channels(state(), pagination(), range(), cursor()) :: - {:ok, pagination_cursor(), [channel()], pagination_cursor()} | {:error, Error.t()} - def fetch_channels(state, pagination, range, cursor) do - with {:ok, cursor} <- deserialize_cursor(cursor) do + @spec fetch_channels(state(), pagination(), range(), query(), cursor()) :: + {:ok, {pagination_cursor(), [channel()], pagination_cursor()}} + | {:error, Error.t()} + def fetch_channels(state, pagination, range, query, cursor) do + with {:ok, cursor} <- deserialize_cursor(cursor), + {:ok, filters} <- convert_params(query) do scope = deserialize_scope(range) {prev_cursor, expiration_keys, next_cursor} = - state - |> build_streamer(scope, cursor) + filters + |> Map.new() + |> build_streamer(state, scope, cursor) |> Collection.paginate(pagination) channels = render_channels(state, expiration_keys) - {:ok, serialize_cursor(prev_cursor), channels, serialize_cursor(next_cursor)} + {:ok, {serialize_cursor(prev_cursor), channels, serialize_cursor(next_cursor)}} end end @@ -131,14 +137,28 @@ defmodule AeMdw.Channels do |> Enum.count() end - defp build_streamer(state, scope, cursor) do + defp build_streamer(%{state: "active"}, state, scope, cursor) do fn direction -> - [@table_active_activation, @table_inactive_activation] - |> Enum.map(fn table -> - state - |> Collection.stream(table, direction, scope, cursor) - |> Stream.map(&{&1, table}) - end) + state + |> Collection.stream(@table_active_activation, direction, scope, cursor) + |> Stream.map(&{&1, @table_active_activation}) + end + end + + defp build_streamer(%{state: "inactive"}, state, scope, cursor) do + fn direction -> + state + |> Collection.stream(@table_inactive_activation, direction, scope, cursor) + |> Stream.map(&{&1, @table_inactive_activation}) + end + end + + defp build_streamer(_query, state, scope, cursor) do + fn direction -> + [ + build_streamer(%{state: "active"}, state, scope, cursor).(direction), + build_streamer(%{state: "inactive"}, state, scope, cursor).(direction) + ] |> Collection.merge(direction) end end @@ -317,4 +337,17 @@ defmodule AeMdw.Channels do defp get_block_index(state, :micro, block_hash) do DbUtil.micro_block_height_index(state, block_hash) end + + defp convert_params(query) do + Enum.reduce_while(query, {:ok, []}, fn param, {:ok, filters} -> + case convert_param(param) do + {:ok, filter} -> {:cont, {:ok, [filter | filters]}} + {:error, reason} -> {:halt, {:error, reason}} + end + end) + end + + defp convert_param({"state", val}) when val in @states, do: {:ok, {:state, val}} + + defp convert_param(other_param), do: {:error, ErrInput.Query.exception(value: other_param)} end diff --git a/lib/ae_mdw_web/controllers/channel_controller.ex b/lib/ae_mdw_web/controllers/channel_controller.ex index 80447a7fb..f2068ed84 100644 --- a/lib/ae_mdw_web/controllers/channel_controller.ex +++ b/lib/ae_mdw_web/controllers/channel_controller.ex @@ -13,11 +13,11 @@ defmodule AeMdwWeb.ChannelController do @spec channels(Conn.t(), map()) :: Conn.t() def channels(%Conn{assigns: assigns} = conn, _params) do - %{state: state, pagination: pagination, scope: scope, cursor: cursor} = assigns + %{state: state, pagination: pagination, scope: scope, query: query, cursor: cursor} = assigns - with {:ok, prev_cursor, channels, next_cursor} <- - Channels.fetch_channels(state, pagination, scope, cursor) do - Util.paginate(conn, prev_cursor, channels, next_cursor) + with {:ok, paginated_channels} <- + Channels.fetch_channels(state, pagination, scope, query, cursor) do + Util.paginate(conn, paginated_channels) end end diff --git a/test/ae_mdw_web/controllers/channel_controller_test.exs b/test/ae_mdw_web/controllers/channel_controller_test.exs index 8e9dc50d2..2c3d97722 100644 --- a/test/ae_mdw_web/controllers/channel_controller_test.exs +++ b/test/ae_mdw_web/controllers/channel_controller_test.exs @@ -170,6 +170,95 @@ defmodule AeMdwWeb.ChannelControllerTest do end end + test "when filtering by state=inactive, it returns inactive channels only", %{ + conn: conn, + store: store + } do + channel_pk1 = <<0::256>> + channel_pk2 = <<1::256>> + channel_pk3 = <<2::256>> + initiator_pk = <<3::256>> + responder_pk = <<4::256>> + tx_hash = <<5::256>> + block_hash = <<6::256>> + enc_tx_hash = encode(:tx_hash, tx_hash) + channel_id3 = encode(:channel, channel_pk3) + initiator = encode_account(initiator_pk) + responder = encode_account(responder_pk) + + {:ok, state_hash} = + :aeser_api_encoder.safe_decode( + :state, + "st_Wwxms0IVM7PPCHpeOXWeeZZm8h5p/SuqZL7IHIbr3CqtlCL+" + ) + + encoded_state_hash = encode(:state, state_hash) + + m_channel3 = + Model.channel( + index: channel_pk3, + active: 3, + initiator: initiator_pk, + responder: responder_pk, + state_hash: state_hash, + updates: [{{600_000, 1}, {3_000, -1}}] + ) + + store = + store + |> Store.put(Model.ActiveChannelActivation, Model.activation(index: {1, channel_pk1})) + |> Store.put(Model.ActiveChannelActivation, Model.activation(index: {2, channel_pk2})) + |> Store.put(Model.InactiveChannel, m_channel3) + |> Store.put(Model.InactiveChannelActivation, Model.activation(index: {3, channel_pk3})) + + with_mocks [ + {AeMdw.Db.Util, [:passthrough], + [ + read_node_tx_details: fn _state, {3_000, -1} -> + {:tx, :channel_deposit_tx, tx_hash, :channel_deposit_tx, block_hash} + end + ]}, + {:aec_chain, [:passthrough], + get_channel_at_hash: fn pubkey, ^block_hash -> + amount = 8_000_000 + + {:ok, + {:channel, {:id, :channel, pubkey}, {:id, :account, initiator_pk}, + {:id, :account, responder_pk}, %{initiator: [], responder: []}, amount, 3_400_000, + 3_600_000, 500_000, :basic, :basic, + <<13, 54, 141, 196, 223, 107, 172, 150, 198, 45, 62, 102, 159, 21, 123, 151, 241, + 235, 20, 175, 223, 198, 242, 127, 137, 194, 129, 204, 227, 139, 197, 132>>, 1, 2, + 3, 500_003, 3}} + end} + ] do + assert %{"data" => [channel3], "next" => nil} = + conn + |> with_store(store) + |> get("/v2/channels", state: "inactive") + |> json_response(200) + + assert %{ + "channel" => ^channel_id3, + "active" => false, + "amount" => 8_000_000, + "last_updated_height" => 600_000, + "last_updated_tx_hash" => ^enc_tx_hash, + "last_updated_tx_type" => "ChannelDepositTx", + "updates_count" => 1, + "responder" => ^responder, + "initiator" => ^initiator, + "channel_reserve" => 500_000, + "initiator_amount" => 3_400_000, + "responder_amount" => 3_600_000, + "round" => 1, + "solo_round" => 2, + "lock_period" => 3, + "locked_until" => 500_003, + "state_hash" => ^encoded_state_hash + } = channel3 + end + end + test "when no channels, it returns empty data", %{conn: conn, store: store} do assert %{"data" => []} = conn |> with_store(store) |> get("/v2/channels") |> json_response(200)