Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow filtering channels by active/inactive #1367

Merged
merged 2 commits into from
May 31, 2023
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 14 additions & 4 deletions docs/swagger_v2/channels.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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
Expand Down
61 changes: 47 additions & 14 deletions lib/ae_mdw/channels.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
8 changes: 4 additions & 4 deletions lib/ae_mdw_web/controllers/channel_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
89 changes: 89 additions & 0 deletions test/ae_mdw_web/controllers/channel_controller_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading