diff --git a/lib/ae_mdw/aex9.ex b/lib/ae_mdw/aex9.ex index 417d99915..37eecb53a 100644 --- a/lib/ae_mdw/aex9.ex +++ b/lib/ae_mdw/aex9.ex @@ -78,38 +78,6 @@ defmodule AeMdw.Aex9 do end end - @spec fetch_sender_transfers(State.t(), pubkey(), pagination(), cursor() | nil) :: - account_paginated_transfers() - def fetch_sender_transfers(state, sender_pk, pagination, cursor) do - paginate_account_transfers(state, pagination, Model.Aex9Transfer, cursor, sender_pk) - end - - @spec fetch_recipient_transfers(State.t(), pubkey(), pagination(), cursor() | nil) :: - account_paginated_transfers() - def fetch_recipient_transfers(state, recipient_pk, pagination, cursor) do - paginate_account_transfers(state, pagination, Model.RevAex9Transfer, cursor, recipient_pk) - end - - @spec fetch_pair_transfers(State.t(), pubkey(), pubkey(), pagination(), cursor() | nil) :: - pair_paginated_transfers() - def fetch_pair_transfers( - state, - sender_pk, - recipient_pk, - pagination, - cursor - ) do - cursor_key = deserialize_cursor(cursor) - - paginate_transfers( - state, - pagination, - Model.Aex9PairTransfer, - cursor_key, - {sender_pk, recipient_pk} - ) - end - @spec fetch_chain_balances(pubkey(), pagination(), balances_cursor() | nil) :: {:ok, balances_cursor() | nil, [aex9_balance()], balances_cursor() | nil} | {:error, Error.t()} @@ -305,51 +273,6 @@ defmodule AeMdw.Aex9 do end end - defp paginate_account_transfers( - state, - pagination, - table, - cursor, - account_pk - ) do - cursor_key = deserialize_cursor(cursor) - - paginate_transfers( - state, - pagination, - table, - cursor_key, - account_pk - ) - end - - defp paginate_transfers( - state, - pagination, - table, - cursor_key, - params - ) do - key_boundary = key_boundary(params) - - {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) - } - end - - defp build_streamer(state, table, cursor_key, key_boundary) do - fn direction -> - Collection.stream(state, table, direction, key_boundary, cursor_key) - end - end - defp render_balance(contract_pk, {:address, account_pk}, amount) do %{ contract: :aeser_api_encoder.encode(:contract_pubkey, contract_pk), @@ -393,40 +316,6 @@ defmodule AeMdw.Aex9 do end end - defp serialize_cursor(nil), do: nil - - 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(<>) do - with {:ok, cursor_bin} <- Base.decode64(cursor_bin64), - cursor_term <- :erlang.binary_to_term(cursor_bin), - true <- - match?({<<_pk1::256>>, _txi, <<_pk2::256>>, _amount, _idx}, cursor_term) or - match?({<<_pk1::256>>, <<_pk2::256>>, _txi, _amount, _idx}, cursor_term) do - cursor_term - else - _invalid -> - raise ErrInput.Cursor, value: cursor_bin64 - end - end - - defp key_boundary({sender_pk, recipient_pk}) do - { - {sender_pk, recipient_pk, 0, 0, 0}, - {sender_pk, recipient_pk, nil, 0, 0} - } - end - - defp key_boundary(account_pk) do - { - {account_pk, 0, nil, 0, 0}, - {account_pk, nil, nil, 0, 0} - } - end - defp validate_aex9(contract_pk) do if AexnContracts.is_aex9?(contract_pk) do :ok diff --git a/lib/ae_mdw/aexn_transfers.ex b/lib/ae_mdw/aexn_transfers.ex new file mode 100644 index 000000000..baffb9f78 --- /dev/null +++ b/lib/ae_mdw/aexn_transfers.ex @@ -0,0 +1,174 @@ +defmodule AeMdw.AexnTransfers do + @moduledoc """ + Fetches indexed AEX-N transfers (from Transfer AEX-9 and AEX-141 events). + """ + + alias AeMdw.Collection + alias AeMdw.Db.Model + alias AeMdw.Db.State + alias AeMdw.Error.Input, as: ErrInput + + require Model + + @type aexn_type :: :aex9 | :aex141 + @type amount :: non_neg_integer() + @type amounts :: map() + @type token_id :: non_neg_integer() + + @typep txi :: AeMdw.Txs.txi() + + @type transfer_key :: + {:aex9 | :aex141, pubkey(), txi(), pubkey(), pos_integer(), non_neg_integer()} + @type pair_transfer_key :: + {:aex9 | :aex141, pubkey(), pubkey(), txi(), pos_integer(), non_neg_integer()} + + @type cursor :: binary() + @type account_paginated_transfers :: + {cursor() | nil, [transfer_key()], {cursor() | nil, boolean()}} + @type pair_paginated_transfers :: + {cursor() | nil, [pair_transfer_key()], {cursor() | nil, boolean()}} + + @typep pagination :: Collection.direction_limit() + @typep pubkey :: AeMdw.Node.Db.pubkey() + + @spec fetch_sender_transfers(State.t(), aexn_type(), pubkey(), pagination(), cursor() | nil) :: + account_paginated_transfers() + def fetch_sender_transfers(state, aexn_type, sender_pk, pagination, cursor) do + paginate_account_transfers( + state, + aexn_type, + pagination, + Model.AexnTransfer, + cursor, + sender_pk + ) + end + + @spec fetch_recipient_transfers(State.t(), aexn_type(), pubkey(), pagination(), cursor() | nil) :: + account_paginated_transfers() + def fetch_recipient_transfers(state, aexn_type, recipient_pk, pagination, cursor) do + paginate_account_transfers( + state, + aexn_type, + pagination, + Model.RevAexnTransfer, + cursor, + recipient_pk + ) + end + + @spec fetch_pair_transfers( + State.t(), + aexn_type(), + pubkey(), + pubkey(), + pagination(), + cursor() | nil + ) :: + pair_paginated_transfers() + def fetch_pair_transfers( + state, + aexn_type, + sender_pk, + recipient_pk, + pagination, + cursor + ) do + cursor_key = deserialize_cursor(cursor) + + paginate_transfers( + state, + aexn_type, + pagination, + Model.AexnPairTransfer, + cursor_key, + {sender_pk, recipient_pk} + ) + end + + # + # Private functions + # + defp paginate_account_transfers( + state, + aexn_type, + pagination, + table, + cursor, + account_pk + ) do + cursor_key = deserialize_cursor(cursor) + + paginate_transfers( + state, + aexn_type, + pagination, + table, + cursor_key, + account_pk + ) + end + + defp paginate_transfers( + state, + aexn_type, + pagination, + table, + cursor_key, + params + ) do + key_boundary = key_boundary(aexn_type, params) + + {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) + } + end + + defp build_streamer(state, table, cursor_key, key_boundary) do + fn direction -> + Collection.stream(state, table, direction, key_boundary, cursor_key) + end + end + + defp serialize_cursor(nil), do: nil + + 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(<>) do + with {:ok, cursor_bin} <- Base.decode64(cursor_bin64), + cursor_term <- :erlang.binary_to_term(cursor_bin), + true <- + elem(cursor_term, 0) in [:aex9, :aex141] and + (match?({_type, <<_pk1::256>>, _txi, <<_pk2::256>>, _amount, _idx}, cursor_term) or + match?({_type, <<_pk1::256>>, <<_pk2::256>>, _txi, _amount, _idx}, cursor_term)) do + cursor_term + else + _invalid -> + raise ErrInput.Cursor, value: cursor_bin64 + end + end + + defp key_boundary(aexn_type, {sender_pk, recipient_pk}) do + { + {aexn_type, sender_pk, recipient_pk, 0, 0, 0}, + {aexn_type, sender_pk, recipient_pk, nil, 0, 0} + } + end + + defp key_boundary(aexn_type, account_pk) do + { + {aexn_type, account_pk, 0, nil, 0, 0}, + {aexn_type, account_pk, nil, nil, 0, 0} + } + end +end diff --git a/lib/ae_mdw/db/contract.ex b/lib/ae_mdw/db/contract.ex index 653e75b69..36652367b 100644 --- a/lib/ae_mdw/db/contract.ex +++ b/lib/ae_mdw/db/contract.ex @@ -2,9 +2,10 @@ defmodule AeMdw.Db.Contract do @moduledoc """ Data access to read and write Contract related models. """ + + alias AeMdw.AexnContracts alias AeMdw.Collection alias AeMdw.Contract - alias AeMdw.AexnContracts alias AeMdw.Db.Model alias AeMdw.Db.Origin alias AeMdw.Db.Sync @@ -191,10 +192,10 @@ defmodule AeMdw.Db.Contract do cond do is_aexn_transfer?(evt_hash) and aex9_contract_pk != nil -> - write_aex9_records(state3, txi, i, args) + write_aexn_transfer(state3, :aex9, aex9_contract_pk, txi, i, args) is_aexn_transfer?(evt_hash) and State.exists?(state3, Model.AexnContract, {:aex141, addr}) -> - write_aex141_records(state3, addr, args) + write_aex141_records(state3, addr, txi, i, args) true -> state3 @@ -237,23 +238,35 @@ defmodule AeMdw.Db.Contract do | {:from_to, pubkey(), pubkey()} ) :: Enumerable.t() def aex9_search_transfers(state, {:from, sender_pk}) do - aex9_search_transfers(state, Model.Aex9Transfer, {sender_pk, -1, nil, -1, -1}, fn key -> - elem(key, 0) == sender_pk - end) + aex9_search_transfers( + state, + Model.AexnTransfer, + {:aex9, sender_pk, -1, nil, -1, -1}, + fn key -> + elem(key, 0) == :aex9 and elem(key, 1) == sender_pk + end + ) end def aex9_search_transfers(state, {:to, recipient_pk}) do - aex9_search_transfers(state, Model.RevAex9Transfer, {recipient_pk, -1, nil, -1, -1}, fn key -> - elem(key, 0) == recipient_pk - end) + aex9_search_transfers( + state, + Model.RevAexnTransfer, + {:aex9, recipient_pk, -1, nil, -1, -1}, + fn key -> + elem(key, 0) == :aex9 and elem(key, 1) == recipient_pk + end + ) end def aex9_search_transfers(state, {:from_to, sender_pk, recipient_pk}) do aex9_search_transfers( state, - Model.Aex9PairTransfer, - {sender_pk, recipient_pk, -1, -1, -1}, - fn key -> elem(key, 0) == sender_pk && elem(key, 1) == recipient_pk end + Model.AexnPairTransfer, + {:aex9, sender_pk, recipient_pk, -1, -1, -1}, + fn key -> + elem(key, 0) == :aex9 and elem(key, 1) == sender_pk && elem(key, 2) == recipient_pk + end ) end @@ -306,51 +319,51 @@ defmodule AeMdw.Db.Contract do |> Stream.take_while(key_tester) end - defp write_aex9_records(state, txi, i, [from_pk, to_pk, <>]) do - m_transfer = Model.aex9_transfer(index: {from_pk, txi, to_pk, amount, i}) - m_rev_transfer = Model.rev_aex9_transfer(index: {to_pk, txi, from_pk, amount, i}) - m_idx_transfer = Model.idx_aex9_transfer(index: {txi, i, from_pk, to_pk, amount}) - m_pair_transfer = Model.aex9_pair_transfer(index: {from_pk, to_pk, txi, amount, i}) + defp write_aex141_records( + state, + contract_pk, + txi, + i, + [from_pk, to_pk, <>] = args + ) do + m_ownership = Model.nft_ownership(index: {to_pk, contract_pk, token_id}) - state - |> State.put(Model.Aex9Transfer, m_transfer) - |> State.put(Model.RevAex9Transfer, m_rev_transfer) - |> State.put(Model.IdxAex9Transfer, m_idx_transfer) - |> State.put(Model.Aex9PairTransfer, m_pair_transfer) + state2 = + state + |> State.put(Model.NftOwnership, m_ownership) + |> write_aexn_transfer(:aex141, contract_pk, txi, i, args) + + if State.exists?(state2, Model.NftOwnership, {from_pk, contract_pk, token_id}) do + State.delete(state2, Model.NftOwnership, {from_pk, contract_pk, token_id}) + else + state2 + end end - defp write_aex9_records(state, _txi, _i, _args), do: state + defp write_aex141_records(state, _pk, _txi, _i, _args), do: state - defp write_aex141_records(state, contract_pk, [ + defp write_aexn_transfer(state, aexn_type, contract_pk, txi, i, [ <<_pk1::256>> = from_pk, <<_pk2::256>> = to_pk, - <> + <> ]) do - do_write_aex141_records(state, contract_pk, from_pk, to_pk, token_id) - end - - defp write_aex141_records(state, contract_pk, [ - <<_pk1::256>> = from_pk, - <<_pk2::256>> = to_pk, - token_id - ]) - when is_integer(token_id) do - do_write_aex141_records(state, contract_pk, from_pk, to_pk, token_id) - end - - defp write_aex141_records(state, _pk, _args), do: state + m_transfer = + Model.aexn_transfer( + index: {aexn_type, from_pk, txi, to_pk, value, i}, + contract_pk: contract_pk + ) - defp do_write_aex141_records(state, contract_pk, from_pk, to_pk, token_id) do - m_ownership = Model.nft_ownership(index: {to_pk, contract_pk, token_id}) - state2 = State.put(state, Model.NftOwnership, m_ownership) + m_rev_transfer = Model.rev_aexn_transfer(index: {aexn_type, to_pk, txi, from_pk, value, i}) + m_pair_transfer = Model.aexn_pair_transfer(index: {aexn_type, from_pk, to_pk, txi, value, i}) - if State.exists?(state2, Model.NftOwnership, {from_pk, contract_pk, token_id}) do - State.delete(state2, Model.NftOwnership, {from_pk, contract_pk, token_id}) - else - state2 - end + state + |> State.put(Model.AexnTransfer, m_transfer) + |> State.put(Model.RevAexnTransfer, m_rev_transfer) + |> State.put(Model.AexnPairTransfer, m_pair_transfer) end + defp write_aexn_transfer(state, _contract_pk, _type, _txi, _i, _args), do: state + defp fetch_aex9_balance_or_new(state, contract_pk, account_pk) do case State.get(state, Model.Aex9Balance, {contract_pk, account_pk}) do {:ok, m_balance} -> m_balance diff --git a/lib/ae_mdw/db/model.ex b/lib/ae_mdw/db/model.ex index 3534a9826..f4c90ba97 100644 --- a/lib/ae_mdw/db/model.ex +++ b/lib/ae_mdw/db/model.ex @@ -422,6 +422,30 @@ defmodule AeMdw.Db.Model do ] defrecord :idx_aex9_transfer, @idx_aex9_transfer_defaults + # aexn transfer: + # index: {:aex9 | :aex141, from pk, call txi, to pk, amount | token_id, log idx} + @aexn_transfer_defaults [ + index: {nil, <<>>, -1, <<>>, -1, -1}, + contract_pk: <<>> + ] + defrecord :aexn_transfer, @aexn_transfer_defaults + + # rev aexn transfer: + # index: {:aex9 | :aex141, to pk, call txi, from pk, amount | token_id, log idx} + @rev_aexn_transfer_defaults [ + index: {nil, <<>>, -1, <<>>, -1, -1}, + unused: nil + ] + defrecord :rev_aexn_transfer, @rev_aexn_transfer_defaults + + # aexn pair transfer: + # index: {:aex9 | :aex141, from pk, to pk, call txi, amount | token_id, log idx} + @aexn_pair_transfer_defaults [ + index: {nil, <<>>, <<>>, -1, -1, -1}, + unused: nil + ] + defrecord :aexn_pair_transfer, @aexn_pair_transfer_defaults + # aex9 account presence: # index: {account pk, contract pk} # txi: create or call txi @@ -663,6 +687,9 @@ defmodule AeMdw.Db.Model do AeMdw.Db.Model.AexnContract, AeMdw.Db.Model.AexnContractName, AeMdw.Db.Model.AexnContractSymbol, + AeMdw.Db.Model.AexnTransfer, + AeMdw.Db.Model.RevAexnTransfer, + AeMdw.Db.Model.AexnPairTransfer, AeMdw.Db.Model.Aex9Transfer, AeMdw.Db.Model.RevAex9Transfer, AeMdw.Db.Model.Aex9PairTransfer, @@ -745,6 +772,9 @@ defmodule AeMdw.Db.Model do def record(AeMdw.Db.Model.AexnContract), do: :aexn_contract def record(AeMdw.Db.Model.AexnContractName), do: :aexn_contract_name def record(AeMdw.Db.Model.AexnContractSymbol), do: :aexn_contract_symbol + def record(AeMdw.Db.Model.AexnTransfer), do: :aexn_transfer + def record(AeMdw.Db.Model.RevAexnTransfer), do: :rev_aexn_transfer + def record(AeMdw.Db.Model.AexnPairTransfer), do: :aexn_pair_transfer def record(AeMdw.Db.Model.Aex9Transfer), do: :aex9_transfer def record(AeMdw.Db.Model.RevAex9Transfer), do: :rev_aex9_transfer def record(AeMdw.Db.Model.Aex9PairTransfer), do: :aex9_pair_transfer diff --git a/lib/ae_mdw_web/controllers/aex9_controller.ex b/lib/ae_mdw_web/controllers/aex9_controller.ex index 84a4d4132..36921cbb3 100644 --- a/lib/ae_mdw_web/controllers/aex9_controller.ex +++ b/lib/ae_mdw_web/controllers/aex9_controller.ex @@ -6,8 +6,8 @@ defmodule AeMdwWeb.Aex9Controller do use AeMdwWeb, :controller alias AeMdw.Aex9 - alias AeMdw.AexnTokens alias AeMdw.AexnContracts + alias AeMdw.AexnTokens alias AeMdw.Db.Contract alias AeMdw.Db.Model alias AeMdw.Db.Origin @@ -25,7 +25,6 @@ defmodule AeMdwWeb.Aex9Controller do import AeMdwWeb.Util, only: [ handle_input: 2, - paginate: 4, parse_range: 1 ] @@ -197,92 +196,9 @@ defmodule AeMdwWeb.Aex9Controller do end end - @spec transfers_from_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_from_v1(conn, %{"sender" => sender_id}), - do: - handle_input( - conn, - fn -> transfers_reply(conn, {:from, Validate.id!(sender_id)}, :aex9_transfer) end - ) - - @spec transfers_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_to_v1(conn, %{"recipient" => recipient_id}), - do: - handle_input( - conn, - fn -> transfers_reply(conn, {:to, Validate.id!(recipient_id)}, :rev_aex9_transfer) end - ) - - @spec transfers_from_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_from_to_v1(conn, %{"sender" => sender_id, "recipient" => recipient_id}), - do: - handle_input( - conn, - fn -> - query = {:from_to, Validate.id!(sender_id), Validate.id!(recipient_id)} - transfers_reply(conn, query, :aex9_pair_transfer) - end - ) - - @spec transfers_from(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_from(%Conn{assigns: assigns} = conn, %{"sender" => sender_id}) do - %{pagination: pagination, cursor: cursor, state: state} = assigns - - sender_id = Validate.id!(sender_id) - - {prev_cursor, transfers_keys, next_cursor} = - Aex9.fetch_sender_transfers(state, sender_id, pagination, cursor) - - data = Enum.map(transfers_keys, &sender_transfer_to_map(state, &1)) - - paginate(conn, prev_cursor, data, next_cursor) - end - - @spec transfers_to(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_to(%Conn{assigns: assigns} = conn, %{"recipient" => recipient_id}) do - %{pagination: pagination, cursor: cursor, state: state} = assigns - - recipient_id = Validate.id!(recipient_id) - - {prev_cursor, transfers_keys, next_cursor} = - Aex9.fetch_recipient_transfers(state, recipient_id, pagination, cursor) - - data = Enum.map(transfers_keys, &recipient_transfer_to_map(state, &1)) - - paginate(conn, prev_cursor, data, next_cursor) - end - - @spec transfers_from_to(Plug.Conn.t(), map()) :: Plug.Conn.t() - def transfers_from_to(%Conn{assigns: assigns} = conn, %{ - "sender" => sender_id, - "recipient" => recipient_id - }) do - %{pagination: pagination, cursor: cursor, state: state} = assigns - - sender_pk = Validate.id!(sender_id) - recipient_pk = Validate.id!(recipient_id) - - {prev_cursor, transfers_keys, next_cursor} = - Aex9.fetch_pair_transfers(state, sender_pk, recipient_pk, pagination, cursor) - - data = Enum.map(transfers_keys, &pair_transfer_to_map(state, &1)) - - paginate(conn, prev_cursor, data, next_cursor) - end - # # Private functions # - defp transfers_reply(%Conn{assigns: %{state: state}} = conn, query, key_tag) do - transfers = - state - |> Contract.aex9_search_transfers(query) - |> Stream.map(&transfer_to_map(state, &1, key_tag)) - |> Enum.sort_by(fn %{call_txi: call_txi} -> call_txi end) - - json(conn, transfers) - end - defp by_contract_reply(%Conn{assigns: %{state: state}} = conn, contract_id) do with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]), {:ok, m_aex9} <- AexnTokens.fetch_contract(state, {:aex9, contract_pk}) do diff --git a/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex b/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex new file mode 100644 index 000000000..e9d681b0a --- /dev/null +++ b/lib/ae_mdw_web/controllers/aexn_transfer_controller.ex @@ -0,0 +1,156 @@ +defmodule AeMdwWeb.AexnTransferController do + @moduledoc """ + AEX-n transfer endpoints. + """ + + use AeMdwWeb, :controller + + alias AeMdw.AexnTransfers + alias AeMdw.Db.Contract + alias AeMdw.Db.Model + alias AeMdw.Validate + + alias AeMdwWeb.FallbackController + alias AeMdwWeb.Plugs.PaginatedPlug + + alias Plug.Conn + + import AeMdwWeb.Util, + only: [ + handle_input: 2, + paginate: 4 + ] + + import AeMdwWeb.AexnView + + require Model + + plug(PaginatedPlug) + action_fallback(FallbackController) + + @spec transfers_from_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + def transfers_from_v1(conn, %{"sender" => sender_id}), + do: + handle_input( + conn, + fn -> + transfers_reply(conn, {:from, Validate.id!(sender_id)}, &sender_transfer_to_map/2) + end + ) + + @spec transfers_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + def transfers_to_v1(conn, %{"recipient" => recipient_id}), + do: + handle_input( + conn, + fn -> + transfers_reply(conn, {:to, Validate.id!(recipient_id)}, &recipient_transfer_to_map/2) + end + ) + + @spec transfers_from_to_v1(Plug.Conn.t(), map()) :: Plug.Conn.t() + def transfers_from_to_v1(conn, %{"sender" => sender_id, "recipient" => recipient_id}), + do: + handle_input( + conn, + fn -> + query = {:from_to, Validate.id!(sender_id), Validate.id!(recipient_id)} + transfers_reply(conn, query, &pair_transfer_to_map/2) + end + ) + + @spec aex9_transfers_from(Plug.Conn.t(), map()) :: Plug.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() + 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() + 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_from(Plug.Conn.t(), map()) :: Plug.Conn.t() + def aex141_transfers_from(conn, %{"sender" => sender_id}) do + transfers_from_reply(conn, :aex141, sender_id) + end + + @spec aex141_transfers_to(Plug.Conn.t(), map()) :: Plug.Conn.t() + def aex141_transfers_to(conn, %{"recipient" => recipient_id}) do + transfers_to_reply(conn, :aex141, recipient_id) + end + + @spec aex141_transfers_from_to(Plug.Conn.t(), map()) :: Plug.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 + + # + # Private functions + # + defp transfers_reply(%Conn{assigns: %{state: state}} = conn, query, transfer_to_map_fn) do + transfers = + state + |> Contract.aex9_search_transfers(query) + |> Enum.map(&transfer_to_map_fn.(state, &1)) + + json(conn, transfers) + end + + defp transfers_from_reply(%Conn{assigns: assigns} = conn, aexn_type, sender_id) 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} = + AexnTransfers.fetch_sender_transfers(state, aexn_type, sender_pk, pagination, cursor) + + data = Enum.map(transfers_keys, &sender_transfer_to_map(state, &1)) + + paginate(conn, prev_cursor, data, next_cursor) + end + end + + 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 + ) + + data = Enum.map(transfers_keys, &recipient_transfer_to_map(state, &1)) + + paginate(conn, prev_cursor, data, next_cursor) + end + end + + defp transfers_pair_reply(%Conn{assigns: assigns} = conn, aexn_type, sender_id, recipient_id) 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 + ) + + data = Enum.map(transfers_keys, &pair_transfer_to_map(state, &1)) + + paginate(conn, prev_cursor, data, next_cursor) + end + end +end diff --git a/lib/ae_mdw_web/router.ex b/lib/ae_mdw_web/router.ex index 5f0a6c3c1..59b626b23 100644 --- a/lib/ae_mdw_web/router.ex +++ b/lib/ae_mdw_web/router.ex @@ -66,9 +66,20 @@ defmodule AeMdwWeb.Router do get "/aex9/:contract_id/balances", AexnTokenController, :aex9_token_balances get "/aex9/:contract_id/balances/:account_id", AexnTokenController, :aex9_token_balance get "/aex9/account-balances/:account_id", AexnTokenController, :aex9_account_balances - get "/aex9/transfers/from/:sender", Aex9Controller, :transfers_from - get "/aex9/transfers/to/:recipient", Aex9Controller, :transfers_to - get "/aex9/transfers/from-to/:sender/:recipient", Aex9Controller, :transfers_from_to + + get "/aex9/transfers/from/:sender", AexnTransferController, :aex9_transfers_from + get "/aex9/transfers/to/:recipient", AexnTransferController, :aex9_transfers_to + + get "/aex9/transfers/from-to/:sender/:recipient", + AexnTransferController, + :aex9_transfers_from_to + + get "/aex141/transfers/from/:sender", AexnTransferController, :aex141_transfers_from + get "/aex141/transfers/to/:recipient", AexnTransferController, :aex141_transfers_to + + get "/aex141/transfers/from-to/:sender/:recipient", + AexnTransferController, + :aex141_transfers_from_to get "/aex9/:contract_id/balances/:account_id/history", AexnTokenController, @@ -120,9 +131,12 @@ defmodule AeMdwWeb.Router do get "/names", NameController, :names get "/names/:scope_type/:range", NameController, :names - get "/aex9/transfers/from/:sender", Aex9Controller, :transfers_from_v1 - get "/aex9/transfers/to/:recipient", Aex9Controller, :transfers_to_v1 - get "/aex9/transfers/from-to/:sender/:recipient", Aex9Controller, :transfers_from_to_v1 + get "/aex9/transfers/from/:sender", AexnTransferController, :transfers_from_v1 + get "/aex9/transfers/to/:recipient", AexnTransferController, :transfers_to_v1 + + get "/aex9/transfers/from-to/:sender/:recipient", + AexnTransferController, + :transfers_from_to_v1 get "/contracts/logs/:direction", ContractController, :logs get "/contracts/logs/:scope_type/:range", ContractController, :logs diff --git a/lib/ae_mdw_web/views/aexn_view.ex b/lib/ae_mdw_web/views/aexn_view.ex index 79e6c0d35..af3809c35 100644 --- a/lib/ae_mdw_web/views/aexn_view.ex +++ b/lib/ae_mdw_web/views/aexn_view.ex @@ -16,9 +16,8 @@ defmodule AeMdwWeb.AexnView do @type aexn_token() :: map() @typep pubkey :: AeMdw.Node.Db.pubkey() - @typep account_transfer_key :: AeMdw.Aex9.account_transfer_key() - @typep pair_transfer_key :: AeMdw.Aex9.pair_transfer_key() - @typep transfer_key_type :: :aex9_transfer | :rev_aex9_transfer | :aex9_pair_transfer + @typep account_transfer_key :: AeMdw.AexnTransfers.transfer_key() + @typep pair_transfer_key :: AeMdw.AexnTransfers.pair_transfer_key() @spec balance_to_map(State.t(), {non_neg_integer(), non_neg_integer(), pubkey()}) :: map() @@ -82,42 +81,19 @@ defmodule AeMdwWeb.AexnView do end @spec sender_transfer_to_map(State.t(), account_transfer_key()) :: map() - def sender_transfer_to_map(state, key), do: do_transfer_to_map(state, key) + def sender_transfer_to_map(state, key), + do: do_transfer_to_map(state, key) @spec recipient_transfer_to_map(State.t(), account_transfer_key()) :: map() - def recipient_transfer_to_map(state, {pk1, call_txi, pk2, amount, log_idx}), - do: do_transfer_to_map(state, {pk2, call_txi, pk1, amount, log_idx}) - - @spec pair_transfer_to_map(State.t(), pair_transfer_key()) :: map() - def pair_transfer_to_map(state, {pk1, pk2, call_txi, amount, log_idx}), - do: do_transfer_to_map(state, {pk1, call_txi, pk2, amount, log_idx}) - - @spec transfer_to_map( - State.t(), - account_transfer_key() | pair_transfer_key(), - transfer_key_type() - ) :: - map() - def transfer_to_map( - state, - {sender_pk, call_txi, recipient_pk, amount, log_idx}, - :aex9_transfer - ), - do: do_transfer_to_map(state, {sender_pk, call_txi, recipient_pk, amount, log_idx}) - - def transfer_to_map( + def recipient_transfer_to_map( state, - {recipient_pk, call_txi, sender_pk, amount, log_idx}, - :rev_aex9_transfer + {type, recipient_pk, call_txi, sender_pk, amount, log_idx} ), - do: do_transfer_to_map(state, {sender_pk, call_txi, recipient_pk, amount, log_idx}) + do: do_transfer_to_map(state, {type, sender_pk, call_txi, recipient_pk, amount, log_idx}) - def transfer_to_map( - state, - {sender_pk, recipient_pk, call_txi, amount, log_idx}, - :aex9_pair_transfer - ), - do: do_transfer_to_map(state, {sender_pk, call_txi, recipient_pk, amount, log_idx}) + @spec pair_transfer_to_map(State.t(), pair_transfer_key()) :: map() + def pair_transfer_to_map(state, {type, sender_pk, recipient_pk, call_txi, amount, log_idx}), + do: do_transfer_to_map(state, {type, sender_pk, call_txi, recipient_pk, amount, log_idx}) # # Private functions @@ -145,28 +121,30 @@ defmodule AeMdwWeb.AexnView do } end - defp do_transfer_to_map(state, {sender_pk, call_txi, recipient_pk, amount, log_idx}) do - Model.tx(id: hash, block_index: {kbi, mbi}, time: micro_time) = Util.read_tx!(state, call_txi) - {_block_hash, type, _signed_tx, tx_rec} = AeMdw.Node.Db.get_tx_data(hash) - - contract_pk = - if type == :contract_call_tx do - :aect_call_tx.contract_pubkey(tx_rec) - else - :aect_create_tx.contract_pubkey(tx_rec) - end + defp do_transfer_to_map( + state, + {aexn_type, sender_pk, call_txi, recipient_pk, aexn_value, log_idx} = transfer_key + ) do + Model.aexn_transfer(contract_pk: contract_pk) = + State.fetch!(state, Model.AexnTransfer, transfer_key) - %{ - sender: enc_id(sender_pk), - recipient: enc_id(recipient_pk), - amount: amount, - call_txi: call_txi, - log_idx: log_idx, - block_height: kbi, - micro_index: mbi, - micro_time: micro_time, - contract_id: enc_ct(contract_pk), - tx_hash: :aeser_api_encoder.encode(:tx_hash, hash) - } + Model.tx(id: hash, block_index: {kbi, mbi}, time: micro_time) = Util.read_tx!(state, call_txi) + aexn_key = if aexn_type == :aex9, do: :amount, else: :token_id + + Map.put( + %{ + sender: enc_id(sender_pk), + recipient: enc_id(recipient_pk), + call_txi: call_txi, + log_idx: log_idx, + block_height: kbi, + micro_index: mbi, + micro_time: micro_time, + contract_id: enc_ct(contract_pk), + tx_hash: :aeser_api_encoder.encode(:tx_hash, hash) + }, + aexn_key, + aexn_value + ) end end diff --git a/priv/migrations/20220830144500_aex9_to_aexn_transfers.ex b/priv/migrations/20220830144500_aex9_to_aexn_transfers.ex new file mode 100644 index 000000000..cf92dd1bf --- /dev/null +++ b/priv/migrations/20220830144500_aex9_to_aexn_transfers.ex @@ -0,0 +1,65 @@ +defmodule AeMdw.Migrations.Aex9toAexnTransfer do + @moduledoc """ + Converts AEX-9 transfers to AEX-n ones . + """ + + alias AeMdw.Collection + alias AeMdw.Db.Model + alias AeMdw.Db.WriteMutation + alias AeMdw.Db.State + alias AeMdw.Db.Sync.InnerTx + alias AeMdw.Db.Util + + require Model + + @spec run(boolean()) :: {:ok, {non_neg_integer(), non_neg_integer()}} + def run(_from_start?) do + state = State.new() + begin = DateTime.utc_now() + + write_mutations = + state + |> Collection.stream(Model.Aex9Transfer, nil) + |> Stream.flat_map(fn {from_pk, txi, to_pk, amount, i} -> + Model.tx(id: hash) = Util.read_tx!(state, txi) + {_block_hash, type, _signed_tx, tx_rec} = AeMdw.Node.Db.get_tx_data(hash) + contract_pk = get_contract_pk(type, tx_rec) + + m_transfer = + Model.aexn_transfer( + index: {:aex9, from_pk, txi, to_pk, amount, i}, + contract_pk: contract_pk + ) + + m_rev_transfer = Model.rev_aexn_transfer(index: {:aex9, to_pk, txi, from_pk, amount, i}) + m_pair_transfer = Model.aexn_pair_transfer(index: {:aex9, from_pk, to_pk, txi, amount, i}) + + [ + WriteMutation.new(Model.AexnTransfer, m_transfer), + WriteMutation.new(Model.RevAexnTransfer, m_rev_transfer), + WriteMutation.new(Model.AexnPairTransfer, m_pair_transfer) + ] + end) + |> Enum.to_list() + + State.commit_db(state, write_mutations, false) + duration = DateTime.diff(DateTime.utc_now(), begin) + + {:ok, {div(length(write_mutations), 3), duration}} + end + + defp get_contract_pk(type, tx) do + case type do + :contract_call_tx -> + :aect_call_tx.contract_pubkey(tx) + + :contract_create_tx -> + :aect_create_tx.contract_pubkey(tx) + + :ga_meta_tx -> + signed_tx = InnerTx.signed_tx(:ga_meta_tx, tx) + {mod, tx_rec} = :aetx.specialize_callback(:aetx_sign.tx(signed_tx)) + get_contract_pk(mod.type(), tx_rec) + end + end +end diff --git a/test/ae_mdw/db/contract_call_mutation_test.exs b/test/ae_mdw/db/contract_call_mutation_test.exs index 4f363e909..2e67f9910 100644 --- a/test/ae_mdw/db/contract_call_mutation_test.exs +++ b/test/ae_mdw/db/contract_call_mutation_test.exs @@ -201,10 +201,23 @@ defmodule AeMdw.Db.ContractCallMutationTest do [{^contract_pk, [_transfer_evt_hash | [from_pk, to_pk, <>]], _data}] = :aect_call.log(call_rec) - assert State.exists?(state, Model.Aex9Transfer, {from_pk, call_txi, to_pk, amount, 0}) - assert State.exists?(state, Model.RevAex9Transfer, {to_pk, call_txi, from_pk, amount, 0}) - assert State.exists?(state, Model.IdxAex9Transfer, {call_txi, 0, from_pk, to_pk, amount}) - assert State.exists?(state, Model.Aex9PairTransfer, {from_pk, to_pk, call_txi, amount, 0}) + assert State.exists?( + state, + Model.AexnTransfer, + {:aex9, from_pk, call_txi, to_pk, amount, 0} + ) + + assert State.exists?( + state, + Model.RevAexnTransfer, + {:aex9, to_pk, call_txi, from_pk, amount, 0} + ) + + assert State.exists?( + state, + Model.AexnPairTransfer, + {:aex9, from_pk, to_pk, call_txi, amount, 0} + ) end end end @@ -257,7 +270,7 @@ defmodule AeMdw.Db.ContractCallMutationTest do |> State.new() |> State.put(Model.AexnContract, Model.aexn_contract(index: {:aex141, contract_pk})) |> State.put( - Model.AexnContract, + Model.NftOwnership, Model.nft_ownership(index: {from_pk, contract_pk, token_id}) ) |> State.cache_put(:ct_create_sync_cache, contract_pk, call_txi - 1) @@ -265,6 +278,23 @@ defmodule AeMdw.Db.ContractCallMutationTest do assert State.exists?(state, Model.NftOwnership, {to_pk, contract_pk, token_id}) refute State.exists?(state, Model.NftOwnership, {from_pk, contract_pk, token_id}) + + key = {:aex141, from_pk, call_txi, to_pk, token_id, 0} + + assert Model.aexn_transfer(index: ^key, contract_pk: contract_pk) = + State.fetch!(state, Model.AexnTransfer, key) + + assert State.exists?( + state, + Model.RevAexnTransfer, + {:aex141, to_pk, call_txi, from_pk, token_id, 0} + ) + + assert State.exists?( + state, + Model.AexnPairTransfer, + {:aex141, from_pk, to_pk, call_txi, token_id, 0} + ) 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 new file mode 100644 index 000000000..a1cdf1d3b --- /dev/null +++ b/test/ae_mdw_web/controllers/aexn_transfer_controller_test.exs @@ -0,0 +1,633 @@ +defmodule AeMdwWeb.AexnTransferControllerTest do + use AeMdwWeb.ConnCase + @moduletag skip_store: true + + alias AeMdw.Db.Model + alias AeMdw.Db.Store + alias AeMdw.Db.MemStore + alias AeMdw.Db.NullStore + + import AeMdwWeb.Helpers.AexnHelper, only: [enc_ct: 1, enc_id: 1] + + require Model + + @contract_pk :crypto.strong_rand_bytes(32) + @contract_id enc_ct(@contract_pk) + + @from_pk1 :crypto.strong_rand_bytes(32) + @from_pk2 :crypto.strong_rand_bytes(32) + @to_pk1 :crypto.strong_rand_bytes(32) + @to_pk2 :crypto.strong_rand_bytes(32) + @senders [enc_id(@from_pk1), enc_id(@from_pk2)] + @recipients [enc_id(@to_pk1), enc_id(@to_pk2)] + + @default_limit 10 + @aexn_type_sample 10_000 + @log_index_range 0..(2 * @aexn_type_sample) + @aex9_amount_range 1_000_000_000..9_999_999_999 + @aex141_token_range 1_000..9_999 + @txi_range 10_000_000..99_999_999 + + setup_all do + store = + :aex9 + |> List.duplicate(@aexn_type_sample) + |> Kernel.++(List.duplicate(:aex141, @aexn_type_sample)) + |> Enum.with_index() + |> Enum.reduce(MemStore.new(NullStore.new()), fn {aexn_type, i}, store -> + {from_pk, to_pk, contract_pk} = + cond do + i <= 20 or (i > @aexn_type_sample and i <= @aexn_type_sample + 20) -> + {@from_pk1, @to_pk1, @contract_pk} + + i <= 40 or (i > @aexn_type_sample and i <= @aexn_type_sample + 40) -> + {@from_pk1, @to_pk2, @contract_pk} + + i <= 60 or (i > @aexn_type_sample and i <= @aexn_type_sample + 60) -> + {@from_pk2, @to_pk1, @contract_pk} + + i <= 80 or (i > @aexn_type_sample and i <= @aexn_type_sample + 80) -> + {@from_pk2, @to_pk2, @contract_pk} + + true -> + {:crypto.strong_rand_bytes(32), :crypto.strong_rand_bytes(32), + :crypto.strong_rand_bytes(32)} + end + + txi = Enum.random(@txi_range) + + value = + if aexn_type == :aex9, + do: Enum.random(@aex9_amount_range), + else: Enum.random(@aex141_token_range) + + m_transfer = + Model.aexn_transfer( + index: {aexn_type, from_pk, txi, to_pk, value, i}, + contract_pk: contract_pk + ) + + m_rev_transfer = + Model.rev_aexn_transfer(index: {aexn_type, to_pk, txi, from_pk, value, i}) + + m_pair_transfer = + Model.aexn_pair_transfer(index: {aexn_type, from_pk, to_pk, txi, value, i}) + + store + |> Store.put( + Model.Tx, + Model.tx(index: txi, id: :crypto.strong_rand_bytes(32), block_index: {i, 0}) + ) + |> Store.put(Model.AexnTransfer, m_transfer) + |> Store.put(Model.RevAexnTransfer, m_rev_transfer) + |> Store.put(Model.AexnPairTransfer, m_pair_transfer) + end) + + [store: store] + end + + describe "aex9_transfers_from" do + test "gets aex9 transfers sorted by desc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk1) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/from/#{sender_id}") + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"], :desc) + assert Enum.all?(aex9_transfers, &aex9_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"], :desc) + assert List.first(next_aex9_transfers)["call_txi"] <= List.last(aex9_transfers)["call_txi"] + assert Enum.all?(next_aex9_transfers, &aex9_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "gets aex9 transfers sorted by asc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk2) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/from/#{sender_id}", direction: "forward") + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"]) + assert Enum.all?(aex9_transfers, &aex9_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"]) + assert List.first(next_aex9_transfers)["call_txi"] >= List.last(aex9_transfers)["call_txi"] + assert Enum.all?(next_aex9_transfers, &aex9_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get("/v2/aex9/transfers/from/#{account_id_without_transfer}") + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn |> get("/v2/aex9/transfers/from/#{invalid_id}") |> json_response(400) + end + end + + describe "aex9_transfers_to" do + test "gets aex9 transfers sorted by desc txi", %{conn: conn, store: store} do + recipient_id = enc_id(@to_pk1) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/to/#{recipient_id}") + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"], :desc) + assert Enum.all?(aex9_transfers, &aex9_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"], :desc) + assert List.first(next_aex9_transfers)["call_txi"] <= List.last(aex9_transfers)["call_txi"] + assert Enum.all?(next_aex9_transfers, &aex9_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "gets aex9 transfers sorted by asc txi", %{conn: conn, store: store} do + recipient_id = enc_id(@to_pk2) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/to/#{recipient_id}", direction: "forward") + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"]) + assert Enum.all?(aex9_transfers, &aex9_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"]) + assert List.first(next_aex9_transfers)["call_txi"] >= List.last(aex9_transfers)["call_txi"] + assert Enum.all?(next_aex9_transfers, &aex9_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get("/v2/aex9/transfers/to/#{account_id_without_transfer}") + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn |> get("/v2/aex9/transfers/to/#{invalid_id}") |> json_response(400) + end + end + + describe "aex9_transfers_from_to" do + test "gets aex9 transfers sorted by desc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk1) + recipient_id = enc_id(@to_pk2) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/from-to/#{sender_id}/#{recipient_id}") + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"], :desc) + assert Enum.all?(aex9_transfers, &aex9_valid_pair_transfer?(sender_id, recipient_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"], :desc) + assert List.first(next_aex9_transfers)["call_txi"] <= List.last(aex9_transfers)["call_txi"] + + assert Enum.all?( + next_aex9_transfers, + &aex9_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "gets aex9 transfers sorted by asc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk2) + recipient_id = enc_id(@to_pk1) + + assert %{"data" => aex9_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex9/transfers/from-to/#{sender_id}/#{recipient_id}", + direction: "forward" + ) + |> json_response(200) + + assert @default_limit = length(aex9_transfers) + assert ^aex9_transfers = Enum.sort_by(aex9_transfers, & &1["call_txi"]) + assert Enum.all?(aex9_transfers, &aex9_valid_pair_transfer?(sender_id, recipient_id, &1)) + + assert %{"data" => next_aex9_transfers, "prev" => prev_aex9_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex9_transfers) + assert ^next_aex9_transfers = Enum.sort_by(next_aex9_transfers, & &1["call_txi"]) + assert List.first(next_aex9_transfers)["call_txi"] >= List.last(aex9_transfers)["call_txi"] + + assert Enum.all?( + next_aex9_transfers, + &aex9_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => ^aex9_transfers} = + conn |> with_store(store) |> get(prev_aex9_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get( + "/v2/aex9/transfers/from-to/#{account_id_without_transfer}/#{enc_id(@to_pk1)}" + ) + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn + |> get("/v2/aex9/transfers/from-to/#{invalid_id}/#{enc_id(@to_pk1)}") + |> json_response(400) + end + end + + describe "aex141_transfers_from" do + test "gets aex141 transfers sorted by desc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk1) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/from/#{sender_id}") + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"], :desc) + assert Enum.all?(aex141_transfers, &aex141_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"], :desc) + + assert List.first(next_aex141_transfers)["call_txi"] <= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?(next_aex141_transfers, &aex141_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "gets aex141 transfers sorted by asc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk2) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/from/#{sender_id}", direction: "forward") + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"]) + assert Enum.all?(aex141_transfers, &aex141_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"]) + + assert List.first(next_aex141_transfers)["call_txi"] >= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?(next_aex141_transfers, &aex141_valid_sender_transfer?(sender_id, &1)) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get("/v2/aex141/transfers/from/#{account_id_without_transfer}") + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn |> get("/v2/aex141/transfers/from/#{invalid_id}") |> json_response(400) + end + end + + describe "aex141_transfers_to" do + test "gets aex141 transfers sorted by desc txi", %{conn: conn, store: store} do + recipient_id = enc_id(@to_pk1) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/to/#{recipient_id}") + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"], :desc) + assert Enum.all?(aex141_transfers, &aex141_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"], :desc) + + assert List.first(next_aex141_transfers)["call_txi"] <= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?(next_aex141_transfers, &aex141_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "gets aex141 transfers sorted by asc txi", %{conn: conn, store: store} do + recipient_id = enc_id(@to_pk2) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/to/#{recipient_id}", direction: "forward") + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"]) + assert Enum.all?(aex141_transfers, &aex141_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"]) + + assert List.first(next_aex141_transfers)["call_txi"] >= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?(next_aex141_transfers, &aex141_valid_recipient_transfer?(recipient_id, &1)) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get("/v2/aex141/transfers/to/#{account_id_without_transfer}") + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn |> get("/v2/aex141/transfers/to/#{invalid_id}") |> json_response(400) + end + end + + describe "aex141_transfers_from_to" do + test "gets aex141 transfers sorted by desc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk1) + recipient_id = enc_id(@to_pk2) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/from-to/#{sender_id}/#{recipient_id}") + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"], :desc) + + assert Enum.all?( + aex141_transfers, + &aex141_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"], :desc) + + assert List.first(next_aex141_transfers)["call_txi"] <= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?( + next_aex141_transfers, + &aex141_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "gets aex141 transfers sorted by asc txi", %{conn: conn, store: store} do + sender_id = enc_id(@from_pk2) + recipient_id = enc_id(@to_pk1) + + assert %{"data" => aex141_transfers, "next" => next} = + conn + |> with_store(store) + |> get("/v2/aex141/transfers/from-to/#{sender_id}/#{recipient_id}", + direction: "forward" + ) + |> json_response(200) + + assert @default_limit = length(aex141_transfers) + assert ^aex141_transfers = Enum.sort_by(aex141_transfers, & &1["call_txi"]) + + assert Enum.all?( + aex141_transfers, + &aex141_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => next_aex141_transfers, "prev" => prev_aex141_transfers} = + conn |> with_store(store) |> get(next) |> json_response(200) + + assert @default_limit = length(next_aex141_transfers) + assert ^next_aex141_transfers = Enum.sort_by(next_aex141_transfers, & &1["call_txi"]) + + assert List.first(next_aex141_transfers)["call_txi"] >= + List.last(aex141_transfers)["call_txi"] + + assert Enum.all?( + next_aex141_transfers, + &aex141_valid_pair_transfer?(sender_id, recipient_id, &1) + ) + + assert %{"data" => ^aex141_transfers} = + conn |> with_store(store) |> get(prev_aex141_transfers) |> json_response(200) + end + + test "returns empty list when no transfer exists", %{conn: conn} do + account_id_without_transfer = enc_id(:crypto.strong_rand_bytes(32)) + + assert %{"prev" => nil, "data" => [], "next" => nil} = + conn + |> get( + "/v2/aex141/transfers/from-to/#{account_id_without_transfer}/#{enc_id(@to_pk1)}" + ) + |> json_response(200) + end + + test "when id is not valid, it returns 400", %{conn: conn} do + invalid_id = "ak_InvalidId" + error_msg = "invalid id: #{invalid_id}" + + assert %{"error" => ^error_msg} = + conn + |> get("/v2/aex141/transfers/from-to/#{invalid_id}/#{enc_id(@to_pk1)}") + |> json_response(400) + end + end + + defp aex9_valid_sender_transfer?(sender_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "amount" => amount, + "contract_id" => contract_id + }) do + sender == sender_id and recipient in @recipients and call_txi in @txi_range and + log_idx in @log_index_range and amount in @aex9_amount_range and contract_id == @contract_id + end + + defp aex9_valid_recipient_transfer?(recipient_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "amount" => amount, + "contract_id" => contract_id + }) do + sender in @senders and recipient == recipient_id and call_txi in @txi_range and + log_idx in @log_index_range and amount in @aex9_amount_range and contract_id == @contract_id + end + + defp aex9_valid_pair_transfer?(sender_id, recipient_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "amount" => amount, + "contract_id" => contract_id + }) do + sender == sender_id and recipient == recipient_id and call_txi in @txi_range and + log_idx in @log_index_range and amount in @aex9_amount_range and contract_id == @contract_id + end + + defp aex141_valid_sender_transfer?(sender_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "token_id" => token_id, + "contract_id" => contract_id + }) do + sender == sender_id and recipient in @recipients and call_txi in @txi_range and + log_idx in @log_index_range and token_id in @aex141_token_range and + contract_id == @contract_id + end + + defp aex141_valid_recipient_transfer?(recipient_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "token_id" => token_id, + "contract_id" => contract_id + }) do + sender in @senders and recipient == recipient_id and call_txi in @txi_range and + log_idx in @log_index_range and token_id in @aex141_token_range and + contract_id == @contract_id + end + + defp aex141_valid_pair_transfer?(sender_id, recipient_id, %{ + "sender" => sender, + "recipient" => recipient, + "call_txi" => call_txi, + "log_idx" => log_idx, + "token_id" => token_id, + "contract_id" => contract_id + }) do + sender == sender_id and recipient == recipient_id and call_txi in @txi_range and + log_idx in @log_index_range and token_id in @aex141_token_range and + contract_id == @contract_id + end +end diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex index 66144fcf8..cb3c56c30 100644 --- a/test/support/conn_case.ex +++ b/test/support/conn_case.ex @@ -36,7 +36,7 @@ defmodule AeMdwWeb.ConnCase do alias AeMdw.Db.MemStore alias AeMdw.Db.NullStore - if Map.get(tags, :integration, false) do + if Map.get(tags, :integration, false) or Map.get(tags, :skip_store, false) do {:ok, conn: ConnTest.build_conn()} else {:ok, conn: ConnTest.build_conn(), store: MemStore.new(NullStore.new())}