From 87df37bb01b84233c7e661029bb75830ca3cd116 Mon Sep 17 00:00:00 2001 From: Sebastian Borrazas Date: Thu, 31 Aug 2023 08:43:36 -0300 Subject: [PATCH 1/2] chore: add functional error responses and tests cases for it --- lib/ae_mdw/aex9.ex | 12 +- lib/ae_mdw/aexn_transfers.ex | 44 ++++---- lib/ae_mdw/contracts.ex | 9 +- lib/ae_mdw/db/stream/query/parser.ex | 27 +++-- lib/ae_mdw/error.ex | 6 - lib/ae_mdw/txs.ex | 86 ++++++++------- lib/ae_mdw/validate.ex | 41 ------- lib/ae_mdw_web/controllers/aex9_controller.ex | 103 ++++++++---------- .../controllers/aexn_transfer_controller.ex | 81 +++++++------- lib/ae_mdw_web/controllers/name_controller.ex | 82 ++++++++------ test/ae_mdw/aex9_test.exs | 2 +- .../controllers/aex9_controller_test.exs | 22 ++++ .../aexn_transfer_controller_test.exs | 12 ++ .../controllers/name_controller_test.exs | 20 ++++ .../controllers/tx_controller_test.exs | 6 +- 15 files changed, 290 insertions(+), 263 deletions(-) diff --git a/lib/ae_mdw/aex9.ex b/lib/ae_mdw/aex9.ex index c74ab50b6..0df4f72aa 100644 --- a/lib/ae_mdw/aex9.ex +++ b/lib/ae_mdw/aex9.ex @@ -38,7 +38,7 @@ defmodule AeMdw.Aex9 do @type amounts :: map() - @spec fetch_balances(State.t(), pubkey(), boolean()) :: amounts() + @spec fetch_balances(State.t(), pubkey(), boolean()) :: {:ok, amounts()} | {:error, Error.t()} def fetch_balances(state, contract_pk, top?) do if top? do {amounts, _height} = Db.aex9_balances!(contract_pk, true) @@ -55,14 +55,16 @@ defmodule AeMdw.Aex9 do end) |> case do amounts when map_size(amounts) == 0 -> - raise ErrInput.Aex9BalanceNotAvailable, - value: "contract #{encode_contract(contract_pk)}" + {:error, + ErrInput.Aex9BalanceNotAvailable.exception( + value: "contract #{encode_contract(contract_pk)}" + )} %{{:address, <<>>} => nil} = amounts when map_size(amounts) == 1 -> - %{} + {:ok, %{}} amounts -> - Map.delete(amounts, {:address, <<>>}) + {:ok, Map.delete(amounts, {:address, <<>>})} end end end diff --git a/lib/ae_mdw/aexn_transfers.ex b/lib/ae_mdw/aexn_transfers.ex index 2cd5a3aea..e6a9ab6c6 100644 --- a/lib/ae_mdw/aexn_transfers.ex +++ b/lib/ae_mdw/aexn_transfers.ex @@ -6,6 +6,7 @@ defmodule AeMdw.AexnTransfers do alias AeMdw.Collection alias AeMdw.Db.Model alias AeMdw.Db.State + alias AeMdw.Error alias AeMdw.Error.Input, as: ErrInput alias AeMdw.Util @@ -43,7 +44,7 @@ defmodule AeMdw.AexnTransfers do pagination(), cursor() | nil ) :: - contract_paginated_transfers() + {:ok, contract_paginated_transfers()} | {:error, Error.t()} def fetch_contract_transfers(state, contract_txi, {filter_by, account_pk}, pagination, cursor) do table = if filter_by == :from, @@ -61,7 +62,7 @@ defmodule AeMdw.AexnTransfers do end @spec fetch_sender_transfers(State.t(), aexn_type(), pubkey(), pagination(), cursor() | nil) :: - account_paginated_transfers() + {:ok, account_paginated_transfers()} | {:error, Error.t()} def fetch_sender_transfers(state, aexn_type, sender_pk, pagination, cursor) do paginate_transfers( state, @@ -74,7 +75,7 @@ defmodule AeMdw.AexnTransfers do end @spec fetch_recipient_transfers(State.t(), aexn_type(), pubkey(), pagination(), cursor() | nil) :: - account_paginated_transfers() + {:ok, account_paginated_transfers()} | {:error, Error.t()} def fetch_recipient_transfers(state, aexn_type, recipient_pk, pagination, cursor) do paginate_transfers( state, @@ -94,7 +95,7 @@ defmodule AeMdw.AexnTransfers do pagination(), cursor() | nil ) :: - pair_paginated_transfers() + {:ok, pair_paginated_transfers()} | {:error, Error.t()} def fetch_pair_transfers( state, aexn_type, @@ -124,20 +125,21 @@ defmodule AeMdw.AexnTransfers do cursor, account_pk_or_pair_pks ) do - cursor_key = deserialize_cursor(cursor) - - key_boundary = key_boundary(aexn_type_or_txi, account_pk_or_pair_pks) - - {prev_cursor_key, transfer_keys, next_cursor_key} = - state - |> build_streamer(table, cursor_key, key_boundary) - |> Collection.paginate(pagination) - - { - serialize_cursor(prev_cursor_key), - transfer_keys, - serialize_cursor(next_cursor_key) - } + with {:ok, cursor_key} <- deserialize_cursor(cursor) do + key_boundary = key_boundary(aexn_type_or_txi, account_pk_or_pair_pks) + + {prev_cursor_key, transfer_keys, next_cursor_key} = + state + |> build_streamer(table, cursor_key, key_boundary) + |> Collection.paginate(pagination) + + {:ok, + { + serialize_cursor(prev_cursor_key), + transfer_keys, + serialize_cursor(next_cursor_key) + }} + end end defp build_streamer(state, table, cursor_key, key_boundary) do @@ -151,7 +153,7 @@ defmodule AeMdw.AexnTransfers do defp serialize_cursor({cursor, is_reversed?}), do: {cursor |> :erlang.term_to_binary() |> Base.encode64(), is_reversed?} - defp deserialize_cursor(nil), do: nil + defp deserialize_cursor(nil), do: {:ok, nil} defp deserialize_cursor(<>) do with {:ok, cursor_bin} <- Base.decode64(cursor_bin64), @@ -166,10 +168,10 @@ defmodule AeMdw.AexnTransfers do {_type_or_pk, <<_pk1::256>>, <<_pk2::256>>, _txi, _amount, _idx}, cursor_term )) do - cursor_term + {:ok, cursor_term} else _invalid -> - raise ErrInput.Cursor, value: cursor_bin64 + {:error, ErrInput.Cursor.exception(value: cursor_bin64)} end end diff --git a/lib/ae_mdw/contracts.ex b/lib/ae_mdw/contracts.ex index dfdf35913..eb92f6aa3 100644 --- a/lib/ae_mdw/contracts.ex +++ b/lib/ae_mdw/contracts.ex @@ -443,9 +443,6 @@ defmodule AeMdw.Contracts do end end - defp build_calls_pagination(query, _scope, _state, _cursor), - do: raise(ErrInput.Query, value: query) - defp build_grp_id_calls_stream(state, direction, scope, cursor, tx_types) do state |> Collection.stream(@grp_id_int_contract_call_table, direction, scope, cursor) @@ -527,10 +524,10 @@ defmodule AeMdw.Contracts do defp convert_param(_state, {"aexn-args", _}), do: {:ok, {:ignore, nil}} defp convert_param(_state, {id_key, id_val}) do - with {:ok, pubkey} <- Validate.id(id_val) do + with {:ok, pubkey} <- Validate.id(id_val), + {:ok, tx_types_positions} <- Parser.parse_field(id_key) do pos_types = - id_key - |> Parser.parse_field() + tx_types_positions |> Enum.flat_map(fn {tx_type, positions} -> Enum.map(positions, &{&1, tx_type}) end) |> Enum.group_by(fn {pos, _tx_type} -> pos end, fn {_pos, tx_type} -> tx_type end) diff --git a/lib/ae_mdw/db/stream/query/parser.ex b/lib/ae_mdw/db/stream/query/parser.ex index f8c63d1f3..9d9d1bfb7 100644 --- a/lib/ae_mdw/db/stream/query/parser.ex +++ b/lib/ae_mdw/db/stream/query/parser.ex @@ -1,5 +1,6 @@ defmodule AeMdw.Db.Stream.Query.Parser do @moduledoc false + alias AeMdw.Error alias AeMdw.Node, as: AE alias AeMdw.Validate alias AeMdw.Error.Input, as: ErrInput @@ -14,21 +15,31 @@ defmodule AeMdw.Db.Stream.Query.Parser do def classify_ident("name"), do: &Validate.name_id!/1 def classify_ident(_ident), do: &Validate.id!/1 - @spec parse_field(String.t()) :: map() + @spec parse_field(String.t()) :: {:ok, map()} | {:error, Error.t()} def parse_field(field) do case String.split(field, ["."]) do [field] -> - field = Validate.tx_field!(field) - for tx_type <- field_types(field), reduce: %{}, do: (acc -> add_pos(acc, tx_type, field)) + with {:ok, field} <- Validate.tx_field(field) do + tx_types_poss = + for tx_type <- field_types(field), + reduce: %{}, + do: (acc -> add_pos(acc, tx_type, field)) + + {:ok, tx_types_poss} + end [type_no_suffix, field] -> - tx_type = Validate.tx_type!(type_no_suffix) - field = Validate.tx_field!(field) - tx_type in field_types(field) || raise ErrInput.TxField, value: field - add_pos(%{}, tx_type, field) + with {:ok, tx_type} <- Validate.tx_type(type_no_suffix), + {:ok, field} <- Validate.tx_field(field) do + if tx_type in field_types(field) do + {:ok, add_pos(%{}, tx_type, field)} + else + {:error, ErrInput.TxField.exception(value: field)} + end + end _invalid -> - raise ErrInput.TxField, value: field + {:error, ErrInput.TxField.exception(value: field)} end end diff --git a/lib/ae_mdw/error.ex b/lib/ae_mdw/error.ex index 0c177da33..e94247721 100644 --- a/lib/ae_mdw/error.ex +++ b/lib/ae_mdw/error.ex @@ -27,8 +27,6 @@ defmodule AeMdw.Error do def to_string(Input.ContractReturn, x), do: concat("invalid contract return", x) def to_string(Input.ContractDryRun, x), do: concat("error calling contract", x) def to_string(Input.Aex9BalanceNotAvailable, x), do: concat("balance is not available", x) - def to_string(Input.Base64, x), do: concat("invalid base64 encoding", x) - def to_string(Input.Hex32, x), do: concat("invalid hex32 encoding", x) def to_string(Input.RangeTooBig, x), do: concat("invalid range", x) defmodule Input do @@ -53,8 +51,6 @@ defmodule AeMdw.Error do | __MODULE__.ContractReturn | __MODULE__.ContractDryRun | __MODULE__.Aex9BalanceNotAvailable - | __MODULE__.Base64 - | __MODULE__.Hex32 | __MODULE__.RangeTooBig @type message() :: binary() @type t() :: %__MODULE__{ @@ -78,8 +74,6 @@ defmodule AeMdw.Error do defexception!(ContractReturn) defexception!(ContractDryRun) defexception!(Aex9BalanceNotAvailable) - defexception!(Base64) - defexception!(Hex32) defexception!(RangeTooBig) end end diff --git a/lib/ae_mdw/txs.ex b/lib/ae_mdw/txs.ex index 4be85656f..2a7fbf568 100644 --- a/lib/ae_mdw/txs.ex +++ b/lib/ae_mdw/txs.ex @@ -158,27 +158,37 @@ defmodule AeMdw.Txs do add_spendtx_details?() ) :: {: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() - cursor = deserialize_cursor(cursor) + ids_fields = + query + |> Map.get(:ids, MapSet.new()) + |> MapSet.to_list() + |> Enum.reduce_while({:ok, []}, fn {field, id}, {:ok, acc} -> + case extract_transaction_by(String.split(field, ".")) do + {:ok, fields} -> {:cont, {:ok, [{id, fields} | acc]}} + {:error, reason} -> {:halt, {:error, reason}} + end + end) - scope = - case range do - {:gen, first_gen..last_gen} -> - {DbUtil.first_gen_to_txi(state, first_gen), DbUtil.last_gen_to_txi(state, last_gen)} + with {:ok, ids_fields} <- ids_fields do + types = query |> Map.get(:types, MapSet.new()) |> MapSet.to_list() + cursor = deserialize_cursor(cursor) - {:txi, first_txi..last_txi} -> - {first_txi, last_txi} + scope = + case range do + {:gen, first_gen..last_gen} -> + {DbUtil.first_gen_to_txi(state, first_gen), DbUtil.last_gen_to_txi(state, last_gen)} - nil -> - nil - end + {:txi, first_txi..last_txi} -> + {first_txi, last_txi} + + nil -> + nil + end - try do {prev_cursor, txis, next_cursor} = fn direction -> state - |> build_streams(ids, types, scope, cursor, direction) + |> build_streams(ids_fields, types, scope, cursor, direction) |> Collection.merge(direction) end |> Collection.paginate(pagination) @@ -186,9 +196,6 @@ defmodule AeMdw.Txs do txs = Enum.map(txis, &fetch!(state, &1, add_spendtx_details?)) {:ok, serialize_cursor(prev_cursor), txs, serialize_cursor(next_cursor)} - rescue - e in ErrInput -> - {:error, e} end end @@ -339,15 +346,10 @@ defmodule AeMdw.Txs do # the keys {:spend_tx, 1, B, X} and {:spend_tx, 2, B, X}, {:oracle_query_tx, 1, B, X} and # {:oracle_query_tx, 3, B, X} for any value of X, filtering out all those transactions that # do not include A in them. - defp build_streams(state, ids, types, scope, cursor, direction) do + defp build_streams(state, ids_fields, types, scope, cursor, direction) do extract_txi = fn {_tx_type, _field_pos, _id, tx_index} -> tx_index end initial_cursor = if direction == :backward, do: cursor, else: cursor || 0 - ids_fields = - Enum.map(ids, fn {field, id} -> - {id, extract_transaction_by(String.split(field, "."))} - end) - {[{_id, initial_fields}], ids_fields_rest} = Enum.split(ids_fields, 1) initial_tx_types = @@ -417,17 +419,18 @@ defmodule AeMdw.Txs do Node.tx_group(String.to_existing_atom(type)) end - Enum.flat_map(tx_types, fn tx_type -> - poss = tx_type |> Node.tx_ids_positions() |> Enum.map(&{tx_type, &1}) - # nil - for link - poss = if tx_type in @create_tx_types, do: [{tx_type, nil} | poss], else: poss + {:ok, + Enum.flat_map(tx_types, fn tx_type -> + poss = tx_type |> Node.tx_ids_positions() |> Enum.map(&{tx_type, &1}) + # nil - for link + poss = if tx_type in @create_tx_types, do: [{tx_type, nil} | poss], else: poss - if tx_type == :contract_create_tx, do: [{:contract_call_tx, nil} | poss], else: poss - end) + if tx_type == :contract_create_tx, do: [{:contract_call_tx, nil} | poss], else: poss + end)} end defp extract_transaction_by(["entrypoint"]) do - [{:contract_call_tx, AeMdw.Fields.mdw_field_pos("entrypoint")}] + {:ok, [{:contract_call_tx, AeMdw.Fields.mdw_field_pos("entrypoint")}]} end defp extract_transaction_by([field]) do @@ -439,20 +442,21 @@ defmodule AeMdw.Txs do {tx_type, Node.tx_ids(tx_type)[field]} end) - wrapping_tx_field_positions(:ga_meta_tx, field) ++ field_types + {:ok, wrapping_tx_field_positions(:ga_meta_tx, field) ++ field_types} else - raise ErrInput.TxField, value: ":#{field}" + {:error, ErrInput.TxField.exception(value: ":#{field}")} end end defp extract_transaction_by([type_prefix, field]) when type_prefix in ["ga_meta", "paying_for"] do if field in Node.id_fields() do - "#{type_prefix}_tx" - |> String.to_existing_atom() - |> wrapping_tx_field_positions(String.to_existing_atom(field)) + {:ok, + "#{type_prefix}_tx" + |> String.to_existing_atom() + |> wrapping_tx_field_positions(String.to_existing_atom(field))} else - raise ErrInput.TxField, value: ":#{field}" + {:error, ErrInput.TxField.exception(value: ":#{field}")} end end @@ -463,21 +467,21 @@ defmodule AeMdw.Txs do tx_field = String.to_existing_atom(field) if MapSet.member?(Parser.field_types(tx_field), tx_type) do - [{tx_type, Node.tx_ids(tx_type)[tx_field]}] + {:ok, [{tx_type, Node.tx_ids(tx_type)[tx_field]}]} else - raise ErrInput.TxField, value: ":#{field}" + {:error, ErrInput.TxField.exception(value: ":#{field}")} end type_prefix not in Node.tx_prefixes() -> - raise ErrInput.TxType, value: type_prefix + {:error, ErrInput.TxType.exception(value: type_prefix)} true -> - raise ErrInput.TxField, value: ":#{type_prefix}" + {:error, ErrInput.TxField.exception(value: ":#{type_prefix}")} end end defp extract_transaction_by(invalid_field) do - raise ErrInput.TxField, value: ":#{Enum.join(invalid_field, ".")}" + {:error, ErrInput.TxField.exception(value: ":#{Enum.join(invalid_field, ".")}")} end @spec fetch!(State.t(), txi(), add_spendtx_details?()) :: tx() diff --git a/lib/ae_mdw/validate.ex b/lib/ae_mdw/validate.ex index 246b8efcd..c623445d6 100644 --- a/lib/ae_mdw/validate.ex +++ b/lib/ae_mdw/validate.ex @@ -105,9 +105,6 @@ defmodule AeMdw.Validate do end end - def tx_type!(type), - do: unwrap!(&tx_type/1, type) - def tx_group(group) when is_atom(group), do: (group in AE.tx_groups() && {:ok, group}) || {:error, {ErrInput.TxGroup, group}} @@ -120,9 +117,6 @@ defmodule AeMdw.Validate do end end - def tx_group!(group), - do: unwrap!(&tx_group/1, group) - def tx_field(field) when is_binary(field), do: (field in AE.id_fields() && @@ -135,9 +129,6 @@ defmodule AeMdw.Validate do def tx_field(field), do: {:error, ErrInput.TxField.exception(value: field)} - def tx_field!(field), - do: unwrap!(&tx_field/1, field) - def nonneg_int(s) when is_binary(s) do case Integer.parse(s, 10) do {i, ""} when i >= 0 -> {:ok, i} @@ -148,9 +139,6 @@ defmodule AeMdw.Validate do def nonneg_int(x) when is_integer(x) and x >= 0, do: {:ok, x} def nonneg_int(x), do: {:error, {ErrInput.NonnegInt, x}} - def nonneg_int!(x), - do: unwrap!(&nonneg_int/1, x) - def block_index(x) when is_binary(x) do map_nni = fn s, f -> case nonneg_int(s) do @@ -177,35 +165,6 @@ defmodule AeMdw.Validate do end end - def block_index!(x), - do: unwrap!(&block_index/1, x) - - def base64(x) when is_binary(x) do - case Base.decode64(x) do - {:ok, bin} -> - {:ok, bin} - - :error -> - {:error, {ErrInput.Base64, x}} - end - end - - def base64!(x) when is_binary(x), - do: unwrap!(&base64/1, x) - - def hex32(x) when is_binary(x) do - case Base.hex_decode32(x) do - {:ok, bin} -> - {:ok, bin} - - :error -> - {:error, {ErrInput.Hex32, x}} - end - end - - def hex32!(x) when is_binary(x), - do: unwrap!(&hex32/1, x) - defp unwrap!(validator, value) do case validator.(value) do {:ok, res} -> diff --git a/lib/ae_mdw_web/controllers/aex9_controller.ex b/lib/ae_mdw_web/controllers/aex9_controller.ex index 9a0d21cb8..4cb75cb11 100644 --- a/lib/ae_mdw_web/controllers/aex9_controller.ex +++ b/lib/ae_mdw_web/controllers/aex9_controller.ex @@ -52,18 +52,12 @@ defmodule AeMdwWeb.Aex9Controller do do: handle_input(conn, fn -> by_symbols_reply(conn, params) end) @spec balance(Plug.Conn.t(), map()) :: Plug.Conn.t() - def balance(conn, %{"contract_id" => contract_id, "account_id" => account_id}), - do: - handle_input( - conn, - fn -> - balance_reply( - conn, - ensure_aex9_contract_pk!(contract_id), - Validate.id!(account_id, [:account_pubkey]) - ) - end - ) + def balance(conn, %{"contract_id" => contract_id, "account_id" => account_id}) do + with {:ok, contract_pk} <- ensure_aex9_contract_pk(contract_id), + {:ok, account_pk} <- Validate.id!(account_id, [:account_pubkey]) do + balance_reply(conn, contract_pk, account_pk) + end + end @spec balance_range(Plug.Conn.t(), map()) :: Plug.Conn.t() def balance_range(%Conn{assigns: %{state: state}} = conn, %{ @@ -117,38 +111,28 @@ defmodule AeMdwWeb.Aex9Controller do "height" => height, "account_id" => account_id }) do - handle_input( - conn, - fn -> - height = Validate.nonneg_int!(height) - - if nil == Util.block_txi(state, {height, -1}) do - raise ErrInput.BlockIndex, value: {height, -1} - end - - account_pk = Validate.id!(account_id, [:account_pubkey]) - - account_balances_reply(conn, account_pk, {height, -1}) - end - ) + with {:ok, height} <- Validate.nonneg_int(height), + txi when txi != nil <- Util.block_txi(state, {height, -1}), + {:ok, account_pk} <- Validate.id(account_id, [:account_pubkey]) do + account_balances_reply(conn, account_pk, {height, -1}) + else + {:error, reason} -> {:error, reason} + nil -> ErrInput.BlockIndex.exception(value: {height, -1}) + end end def balances(%Conn{assigns: %{state: state}} = conn, %{ "blockhash" => hash, "account_id" => account_id }) do - handle_input( - conn, - fn -> - account_pk = Validate.id!(account_id, [:account_pubkey]) - - block_index = - Util.block_hash_to_bi(state, Validate.id!(hash)) || - raise ErrInput.Id, value: hash - - account_balances_reply(conn, account_pk, block_index) - end - ) + with {:ok, account_pk} <- Validate.id!(account_id, [:account_pubkey]), + {:ok, hash} <- Validate.id(hash), + block_index when block_index != nil <- Util.block_hash_to_bi(state, hash) do + account_balances_reply(conn, account_pk, block_index) + else + {:error, reason} -> {:error, reason} + nil -> ErrInput.Id.exception(value: hash) + end end def balances(conn, %{"account_id" => account_id}), @@ -161,8 +145,13 @@ defmodule AeMdwWeb.Aex9Controller do end ) - def balances(conn, %{"contract_id" => contract_id}), - do: handle_input(conn, fn -> balances_reply(conn, ensure_aex9_contract_pk!(contract_id)) end) + def balances(%Conn{assigns: %{state: state, opts: opts}} = conn, %{"contract_id" => contract_id}) do + with {:ok, contract_pk} <- ensure_aex9_contract_pk(contract_id), + {:ok, amounts} <- Aex9.fetch_balances(state, contract_pk, top?(opts)) do + hash_tuple = DBN.top_height_hash(top?(opts)) + json(conn, balances_to_map({amounts, hash_tuple}, contract_pk)) + end + end @spec balances_range(Plug.Conn.t(), map()) :: Plug.Conn.t() def balances_range(%Conn{assigns: %{state: state}} = conn, %{ @@ -324,12 +313,6 @@ defmodule AeMdwWeb.Aex9Controller do json(conn, balances) end - defp balances_reply(%Conn{assigns: %{state: state, opts: opts}} = conn, contract_pk) do - amounts = Aex9.fetch_balances(state, contract_pk, top?(opts)) - hash_tuple = DBN.top_height_hash(top?(opts)) - json(conn, balances_to_map({amounts, hash_tuple}, contract_pk)) - end - defp balances_range_reply(conn, contract_pk, range) do json( conn, @@ -339,7 +322,7 @@ defmodule AeMdwWeb.Aex9Controller do map_balances_range( range, fn type_height_hash -> - {amounts, _} = DBN.aex9_balances!(contract_pk, type_height_hash) + {amounts, _height_hash} = DBN.aex9_balances!(contract_pk, type_height_hash) {:amounts, normalize_balances(amounts)} end ) @@ -348,7 +331,7 @@ defmodule AeMdwWeb.Aex9Controller do end defp balances_for_hash_reply(conn, contract_pk, {_type, _height, _hash} = height_hash) do - {amounts, _} = DBN.aex9_balances!(contract_pk, height_hash) + {amounts, _height_hash} = DBN.aex9_balances!(contract_pk, height_hash) json(conn, balances_to_map({amounts, height_hash}, contract_pk)) end @@ -371,10 +354,14 @@ defmodule AeMdwWeb.Aex9Controller do end end - defp ensure_aex9_contract_pk!(ct_ident) do - pk = Validate.id!(ct_ident, [:contract_pubkey]) - AexnContracts.is_aex9?(pk) || raise ErrInput.NotAex9, value: ct_ident - pk + defp ensure_aex9_contract_pk(ct_ident) do + with {:ok, pk} <- Validate.id(ct_ident, [:contract_pubkey]) do + if AexnContracts.is_aex9?(pk) do + {:ok, pk} + else + {:error, ErrInput.NotAex9.exception(value: ct_ident)} + end + end end defp ensure_aex9_contract_at_block(state, ct_id, block_hash) when is_binary(block_hash) do @@ -402,12 +389,12 @@ defmodule AeMdwWeb.Aex9Controller do :not_found -> # if not yet synced by Mdw but present on Node - ct_pk = Validate.id!(ct_id) - - if AexnContracts.is_aex9?(ct_pk) do - {:ok, ct_pk} - else - {:error, ErrInput.NotAex9.exception(value: ct_id)} + with {:ok, ct_pk} <- Validate.id(ct_id) do + if AexnContracts.is_aex9?(ct_pk) do + {:ok, ct_pk} + else + {:error, ErrInput.NotAex9.exception(value: ct_id)} + end end end end diff --git a/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex b/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex index f8f2a924c..51de72087 100644 --- a/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex +++ b/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex @@ -30,7 +30,7 @@ defmodule AeMdwWeb.AexnTransferController do plug(PaginatedPlug) action_fallback(FallbackController) - @spec transfers_from_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec transfers_from_v1(Conn.t(), map()) :: Conn.t() def transfers_from_v1(conn, %{"sender" => sender_id}), do: handle_input( @@ -40,7 +40,7 @@ defmodule AeMdwWeb.AexnTransferController do end ) - @spec transfers_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec transfers_to_v1(Conn.t(), map()) :: Conn.t() def transfers_to_v1(conn, %{"recipient" => recipient_id}), do: handle_input( @@ -50,7 +50,7 @@ defmodule AeMdwWeb.AexnTransferController do end ) - @spec transfers_from_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec transfers_from_to_v1(Conn.t(), map()) :: Conn.t() def transfers_from_to_v1(conn, %{"sender" => sender_id, "recipient" => recipient_id}), do: handle_input( @@ -61,27 +61,27 @@ defmodule AeMdwWeb.AexnTransferController do end ) - @spec aex9_transfers_from(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex9_transfers_from(Conn.t(), map()) :: Conn.t() def aex9_transfers_from(conn, %{"sender" => sender_id}) do transfers_from_reply(conn, :aex9, sender_id) end - @spec aex9_transfers_to(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex9_transfers_to(Conn.t(), map()) :: Conn.t() def aex9_transfers_to(conn, %{"recipient" => recipient_id}) do transfers_to_reply(conn, :aex9, recipient_id) end - @spec aex9_transfers_from_to(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex9_transfers_from_to(Conn.t(), map()) :: Conn.t() def aex9_transfers_from_to(conn, %{"sender" => sender_id, "recipient" => recipient_id}) do transfers_pair_reply(conn, :aex9, sender_id, recipient_id) end - @spec aex141_transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex141_transfers(Conn.t(), map()) :: Conn.t() def aex141_transfers(conn, %{"contract_id" => contract_id}) do contract_transfers_reply(conn, contract_id, {:from, nil}) end - @spec aex141_transfers_from(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex141_transfers_from(Conn.t(), map()) :: Conn.t() def aex141_transfers_from(conn, %{"contract_id" => contract_id, "sender" => sender_id}) do with {:ok, sender_pk} <- Validate.id(sender_id, [:account_pubkey]) do contract_transfers_reply(conn, contract_id, {:from, sender_pk}) @@ -96,7 +96,7 @@ defmodule AeMdwWeb.AexnTransferController do transfers_from_reply(conn, :aex141, sender_id) end - @spec aex141_transfers_to(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex141_transfers_to(Conn.t(), map()) :: Conn.t() def aex141_transfers_to(conn, %{"contract_id" => contract_id, "recipient" => recipient_id}) do with {:ok, recipient_pk} <- Validate.id(recipient_id, [:account_pubkey]) do contract_transfers_reply(conn, contract_id, {:to, recipient_pk}) @@ -111,7 +111,7 @@ defmodule AeMdwWeb.AexnTransferController do transfers_to_reply(conn, :aex141, recipient_id) end - @spec aex141_transfers_from_to(Plug.Conn.t(), map()) :: Plug.Conn.t() + @spec aex141_transfers_from_to(Conn.t(), map()) :: Conn.t() def aex141_transfers_from_to(conn, %{"sender" => sender_id, "recipient" => recipient_id}) do transfers_pair_reply(conn, :aex141, sender_id, recipient_id) end @@ -136,16 +136,15 @@ defmodule AeMdwWeb.AexnTransferController do %{pagination: pagination, cursor: cursor, state: state} = assigns with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]), - {:ok, create_txi} <- Origin.tx_index(state, {:contract, contract_pk}) do - {prev_cursor, transfers_keys, next_cursor} = - AexnTransfers.fetch_contract_transfers( - state, - create_txi, - tagged_account_pk, - pagination, - cursor - ) - + {:ok, create_txi} <- Origin.tx_index(state, {:contract, contract_pk}), + {:ok, {prev_cursor, transfers_keys, next_cursor}} <- + AexnTransfers.fetch_contract_transfers( + state, + create_txi, + tagged_account_pk, + pagination, + cursor + ) do data = Enum.map(transfers_keys, &contract_transfer_to_map(state, filter_by, &1)) paginate(conn, prev_cursor, data, next_cursor) @@ -159,7 +158,7 @@ defmodule AeMdwWeb.AexnTransferController do %{pagination: pagination, cursor: cursor, state: state} = assigns with {:ok, sender_pk} <- Validate.id(sender_id, [:account_pubkey]) do - {prev_cursor, transfers_keys, next_cursor} = + {:ok, {prev_cursor, transfers_keys, next_cursor}} = AexnTransfers.fetch_sender_transfers(state, aexn_type, sender_pk, pagination, cursor) data = Enum.map(transfers_keys, &sender_transfer_to_map(state, &1)) @@ -171,16 +170,15 @@ defmodule AeMdwWeb.AexnTransferController do defp transfers_to_reply(%Conn{assigns: assigns} = conn, aexn_type, recipient_id) do %{pagination: pagination, cursor: cursor, state: state} = assigns - with {:ok, recipient_pk} <- Validate.id(recipient_id, [:account_pubkey]) do - {prev_cursor, transfers_keys, next_cursor} = - AexnTransfers.fetch_recipient_transfers( - state, - aexn_type, - recipient_pk, - pagination, - cursor - ) - + with {:ok, recipient_pk} <- Validate.id(recipient_id, [:account_pubkey]), + {:ok, {prev_cursor, transfers_keys, next_cursor}} <- + AexnTransfers.fetch_recipient_transfers( + state, + aexn_type, + recipient_pk, + pagination, + cursor + ) do data = Enum.map(transfers_keys, &recipient_transfer_to_map(state, &1)) paginate(conn, prev_cursor, data, next_cursor) @@ -191,17 +189,16 @@ defmodule AeMdwWeb.AexnTransferController do %{pagination: pagination, cursor: cursor, state: state} = assigns with {:ok, sender_pk} <- Validate.id(sender_id, [:account_pubkey]), - {:ok, recipient_pk} <- Validate.id(recipient_id, [:account_pubkey]) do - {prev_cursor, transfers_keys, next_cursor} = - AexnTransfers.fetch_pair_transfers( - state, - aexn_type, - sender_pk, - recipient_pk, - pagination, - cursor - ) - + {:ok, recipient_pk} <- Validate.id(recipient_id, [:account_pubkey]), + {:ok, {prev_cursor, transfers_keys, next_cursor}} <- + AexnTransfers.fetch_pair_transfers( + state, + aexn_type, + sender_pk, + recipient_pk, + pagination, + cursor + ) do data = Enum.map(transfers_keys, &pair_transfer_to_map(state, &1)) paginate(conn, prev_cursor, data, next_cursor) diff --git a/lib/ae_mdw_web/controllers/name_controller.ex b/lib/ae_mdw_web/controllers/name_controller.ex index b7a39a6b6..39faf0617 100644 --- a/lib/ae_mdw_web/controllers/name_controller.ex +++ b/lib/ae_mdw_web/controllers/name_controller.ex @@ -176,15 +176,18 @@ defmodule AeMdwWeb.NameController do end @spec search_v1(Conn.t(), map()) :: Conn.t() - def search_v1(%Conn{assigns: %{state: state, opts: opts}} = conn, %{"prefix" => prefix}) do - handle_input(conn, fn -> - params = Map.put(query_groups(conn.query_string), "prefix", [prefix]) + def search_v1(%Conn{assigns: %{state: state, opts: opts}, query_string: query_string} = conn, %{ + "prefix" => prefix + }) do + params = + query_string + |> query_groups() + |> Map.put("prefix", [prefix]) + |> Map.delete("expand") - json( - conn, - Enum.to_list(do_prefix_stream(state, validate_search_params!(params), opts)) - ) - end) + with {:ok, search_params} <- convert_params(params, &convert_search_param/1) do + json(conn, Enum.to_list(do_prefix_stream(state, search_params, opts))) + end end @spec search(Conn.t(), map()) :: Conn.t() @@ -252,7 +255,7 @@ defmodule AeMdwWeb.NameController do defp name_reply(%Conn{assigns: %{state: state}} = conn, plain_name, opts) do case Name.locate(state, plain_name) do {info, source} -> json(conn, Format.to_map(state, info, source, expand?(opts))) - nil -> raise ErrInput.NotFound, value: plain_name + nil -> {:error, ErrInput.NotFound.exception(value: plain_name)} end end @@ -262,10 +265,10 @@ defmodule AeMdwWeb.NameController do json(conn, Name.pointers(state, m_name)) {_m_name, Model.InactiveName} -> - raise ErrInput.Expired, value: plain_name + {:error, ErrInput.Expired.exception(value: plain_name)} _no_match? -> - raise ErrInput.NotFound, value: plain_name + {:error, ErrInput.NotFound.exception(value: plain_name)} end end @@ -279,11 +282,13 @@ defmodule AeMdwWeb.NameController do end defp auction_reply(%Conn{assigns: %{state: state}} = conn, plain_name, opts) do - map_some( - Name.locate_bid(state, plain_name), - &json(conn, Format.to_map(state, &1, Model.AuctionBid, expand?(opts))) - ) || - raise ErrInput.NotFound, value: plain_name + case Name.locate_bid(state, plain_name) do + nil -> + {:error, ErrInput.NotFound.exception(value: plain_name)} + + auction_bid -> + json(conn, Format.to_map(state, auction_bid, Model.AuctionBid, expand?(opts))) + end end defp owned_by_reply(%Conn{assigns: %{state: state}} = conn, owner_pk, opts, active?) do @@ -324,8 +329,11 @@ defmodule AeMdwWeb.NameController do ########## - defp do_prefix_stream(state, {prefix, lifecycles}, opts) do - streams = Enum.map(lifecycles, &prefix_stream(state, &1, prefix, opts)) + defp do_prefix_stream(state, %{prefix: prefix} = filters, opts) do + streams = + filters + |> Map.get(:lifecycles, ~w(active inactive auction)a) + |> Enum.map(&prefix_stream(state, &1, prefix, opts)) case streams do [single] -> single @@ -335,23 +343,31 @@ defmodule AeMdwWeb.NameController do ########## - defp validate_search_params!(params), - do: do_validate_search_params!(Map.delete(params, "expand")) - - defp do_validate_search_params!(%{"prefix" => [prefix], "only" => [_ | _] = lifecycles}) do - {prefix, - lifecycles - |> Enum.map(fn - "auction" -> :auction - "active" -> :active - "inactive" -> :inactive - invalid -> raise ErrInput.Query, value: "name lifecycle #{invalid}" - end) - |> Enum.uniq()} + defp convert_search_param({"prefix", prefix}), do: {:ok, {:prefix, [prefix]}} + + defp convert_search_param({"only", [_lifecycle | _rest] = lifecycles}) do + lifecycles + |> Enum.reduce_while({:ok, []}, fn + "auction", {:ok, acc} -> + {:cont, [:auction | acc]} + + "active", {:ok, acc} -> + {:cont, [:active | acc]} + + "inactive", {:ok, acc} -> + {:cont, [:inactive | acc]} + + invalid, _acc -> + {:halt, {:error, ErrInput.Query.exception(value: "name lifecycle #{invalid}")}} + end) + |> case do + {:ok, lifecycles} -> {:ok, {:lifecycles, Enum.uniq(lifecycles)}} + {:error, reason} -> {:error, reason} + end end - defp do_validate_search_params!(%{"prefix" => [prefix]}), - do: {prefix, [:auction, :active, :inactive]} + defp convert_search_param({key, val}), + do: {:error, ErrInput.Query.exception(value: "#{key}=#{val}")} ########## diff --git a/test/ae_mdw/aex9_test.exs b/test/ae_mdw/aex9_test.exs index 32a96bb7f..dad71f72f 100644 --- a/test/ae_mdw/aex9_test.exs +++ b/test/ae_mdw/aex9_test.exs @@ -40,7 +40,7 @@ defmodule AeMdw.Aex9Test do async_state ) - assert balances == Aex9.fetch_balances(State.new(), contract_pk, false) + assert {:ok, ^balances} = Aex9.fetch_balances(State.new(), contract_pk, false) end end diff --git a/test/ae_mdw_web/controllers/aex9_controller_test.exs b/test/ae_mdw_web/controllers/aex9_controller_test.exs index 9fe035163..1fb8705c2 100644 --- a/test/ae_mdw_web/controllers/aex9_controller_test.exs +++ b/test/ae_mdw_web/controllers/aex9_controller_test.exs @@ -353,4 +353,26 @@ defmodule AeMdwWeb.Aex9ControllerTest do end end end + + test "when not a contract id, it returns 400", %{conn: conn} do + oracle_pk = :crypto.strong_rand_bytes(32) + encoded_oracle_pk = :aeser_api_encoder.encode(:oracle_pubkey, oracle_pk) + error_msg = "invalid id: #{encoded_oracle_pk}" + + assert %{"error" => ^error_msg} = + conn + |> get("/aex9/balances/#{encoded_oracle_pk}") + |> json_response(400) + end + + test "when not aex9 contract, it returns 400", %{conn: conn} do + contract_pk = :crypto.strong_rand_bytes(32) + encoded_contract_pk = :aeser_api_encoder.encode(:contract_pubkey, contract_pk) + error_msg = "not AEX9 contract: #{encoded_contract_pk}" + + assert %{"error" => ^error_msg} = + conn + |> get("/aex9/balances/#{encoded_contract_pk}") + |> json_response(400) + end end diff --git a/test/ae_mdw_web/controllers/aexn_transfer_controller_test.exs b/test/ae_mdw_web/controllers/aexn_transfer_controller_test.exs index e34e7bbc2..28ae72afc 100644 --- a/test/ae_mdw_web/controllers/aexn_transfer_controller_test.exs +++ b/test/ae_mdw_web/controllers/aexn_transfer_controller_test.exs @@ -742,6 +742,18 @@ defmodule AeMdwWeb.AexnTransferControllerTest do assert %{"error" => ^error_msg} = conn |> get("/v2/aex141/transfers/#{invalid_id}") |> json_response(400) end + + test "returns bad request when cursor is invalid", %{conn: conn, store: store} do + contract_id = encode_contract(@contract_pk2) + invalid_cursor = "foo" + error_msg = "invalid cursor: #{invalid_cursor}" + + assert %{"error" => ^error_msg} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/#{contract_id}", cursor: invalid_cursor) + |> json_response(400) + end end defp aex9_valid_sender_transfer?(sender_id, %{ diff --git a/test/ae_mdw_web/controllers/name_controller_test.exs b/test/ae_mdw_web/controllers/name_controller_test.exs index c794e8d51..7e5cf953f 100644 --- a/test/ae_mdw_web/controllers/name_controller_test.exs +++ b/test/ae_mdw_web/controllers/name_controller_test.exs @@ -3259,6 +3259,26 @@ defmodule AeMdwWeb.NameControllerTest do end end + describe "search_v1" do + test "it returns error when invalid lifecycle", %{conn: conn} do + error_msg = "invalid query: name lifecycle foo" + + %{"error" => ^error_msg} = + conn + |> get("/names/search/foo", only: "foo") + |> json_response(400) + end + + test "it returns error when invalid filter", %{conn: conn} do + error_msg = "invalid query: foo=bar" + + %{"error" => ^error_msg} = + conn + |> get("/names/search/foo", foo: "bar") + |> json_response(400) + end + end + defp name_history_store(store, active_from1, active_from2, kbi1, kbi2, expired_at, plain_name) do claim1 = {501, -1} update1 = {502, -1} diff --git a/test/integration/ae_mdw_web/controllers/tx_controller_test.exs b/test/integration/ae_mdw_web/controllers/tx_controller_test.exs index 755f074a5..c43e116d6 100644 --- a/test/integration/ae_mdw_web/controllers/tx_controller_test.exs +++ b/test/integration/ae_mdw_web/controllers/tx_controller_test.exs @@ -1775,7 +1775,11 @@ defmodule Integration.AeMdwWeb.TxControllerTest do end end - defp transform_tx_type(type), do: type |> Validate.tx_type!() |> AeMdw.Node.tx_name() + defp transform_tx_type(type) do + {:ok, tx_type} = Validate.tx_type(type) + + AeMdw.Node.tx_name(tx_type) + end defp get_txs_types_by_tx_group(tx_group) do tx_group From 3a4deadd3f8c63506a93e87ad423f54498a70a4b Mon Sep 17 00:00:00 2001 From: Sebastian Borrazas Date: Wed, 6 Sep 2023 07:42:05 -0300 Subject: [PATCH 2/2] chore: extract Txs ids_fields into private function --- lib/ae_mdw/txs.ex | 26 ++++++++++++++------------ lib/ae_mdw/util.ex | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/lib/ae_mdw/txs.ex b/lib/ae_mdw/txs.ex index 2a7fbf568..d41d36654 100644 --- a/lib/ae_mdw/txs.ex +++ b/lib/ae_mdw/txs.ex @@ -20,6 +20,7 @@ defmodule AeMdw.Txs do alias AeMdw.Error.Input, as: ErrInput alias AeMdw.Log alias AeMdw.Node + alias AeMdw.Util alias AeMdw.Validate require Logger @@ -158,20 +159,10 @@ defmodule AeMdw.Txs do add_spendtx_details?() ) :: {:ok, page_cursor(), [tx()], page_cursor()} | {:error, Error.t()} def fetch_txs(state, pagination, range, query, cursor, add_spendtx_details?) do - ids_fields = - query - |> Map.get(:ids, MapSet.new()) - |> MapSet.to_list() - |> Enum.reduce_while({:ok, []}, fn {field, id}, {:ok, acc} -> - case extract_transaction_by(String.split(field, ".")) do - {:ok, fields} -> {:cont, {:ok, [{id, fields} | acc]}} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - - with {:ok, ids_fields} <- ids_fields do + with {:ok, ids_fields} <- extract_ids_fields(query) do types = query |> Map.get(:types, MapSet.new()) |> MapSet.to_list() cursor = deserialize_cursor(cursor) + ids_fields = Enum.to_list(ids_fields) scope = case range do @@ -606,4 +597,15 @@ defmodule AeMdw.Txs do :not_found -> 0 end end + + defp extract_ids_fields(query) do + query + |> Map.get(:ids, MapSet.new()) + |> Util.convert_params(fn {field, id} -> + case extract_transaction_by(String.split(field, ".")) do + {:ok, fields} -> {:ok, {id, fields}} + {:error, reason} -> {:error, reason} + end + end) + end end diff --git a/lib/ae_mdw/util.ex b/lib/ae_mdw/util.ex index 061e6e7b2..614cfe596 100644 --- a/lib/ae_mdw/util.ex +++ b/lib/ae_mdw/util.ex @@ -171,8 +171,8 @@ defmodule AeMdw.Util do end @spec convert_params( - map(), - ({binary(), binary()} -> {:ok, {atom(), term()}} | {:error, term()}) + Enumerable.t(), + ({binary(), binary()} -> {:ok, {term(), term()}} | {:error, term()}) ) :: {:ok, map()} | {:error, term()} def convert_params(params, convert_param_fn) do Enum.reduce_while(params, {:ok, %{}}, fn param, {:ok, filters} ->