From db4f45d7a1aea46485cfa37a07b50c8447f7b12d Mon Sep 17 00:00:00 2001 From: Sebastian Borrazas Date: Mon, 18 Mar 2024 10:57:40 -0300 Subject: [PATCH] feat: allow getting block-specific AEx9 balances (#1701) --- README.md | 109 ++++++------- docs/swagger_v3/aexn.spec.yaml | 7 + lib/ae_mdw/aex9.ex | 151 ++++++++++++++++-- .../controllers/aexn_token_controller.ex | 12 +- .../aexn_token_controller_test.exs | 52 ++++-- 5 files changed, 237 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 95b503392..d43a2a049 100644 --- a/README.md +++ b/README.md @@ -4184,75 +4184,72 @@ $ curl -s "https://mainnet.aeternity.io/mdw/aex9/balances/hash/kh_2Ya2fM9brRoBQp } ``` -### AEX9 contract balances at height or range of heights +### AEX9 contract balances ``` -$ curl -s "https://mainnet.aeternity.io/mdw/aex9/balances/gen/350580/ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA" | jq '.' +$ curl -s "https://mainnet.aeternity.io/mdw/v2/aex9/ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA/balances" | jq '.' { - "contract_id": "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", - "range": [ + "data" : [ { - "amounts": { - "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6": 4050000000000, - "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK": 8100000000000, - "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4": 81000000000000, - "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48": 49999999999906850000000000 - }, - "block_hash": "kh_2Jv6ZekDGipPQWrZKitdqtbxgx6bGUMNvkSPmi8pvpheGynKLu", - "height": 350580 + "account_id" : "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK", + "amount" : 8100000000000, + "block_hash" : "mh_2TwVRHgyXpQpjT5Z44BJQexijf6rtweypDGK3mtCZWnBFGxTV7", + "contract_id" : "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", + "height" : 335293, + "last_log_idx" : 1, + "last_tx_hash" : "th_YkRFtLNgT9eZqfuFAihSt14L1GCHxiNSS44h2B5wiNSfvBSc5" + }, + { + "account_id" : "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6", + "amount" : 4050000000000, + "block_hash" : "mh_2TwVRHgyXpQpjT5Z44BJQexijf6rtweypDGK3mtCZWnBFGxTV7", + "contract_id" : "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", + "height" : 335293, + "last_log_idx" : 2, + "last_tx_hash" : "th_YkRFtLNgT9eZqfuFAihSt14L1GCHxiNSS44h2B5wiNSfvBSc5" + }, + { + "account_id" : "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48", + "amount" : "49999999999906850000000000", + "block_hash" : "mh_2TwVRHgyXpQpjT5Z44BJQexijf6rtweypDGK3mtCZWnBFGxTV7", + "contract_id" : "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", + "height" : 335293, + "last_log_idx" : 2, + "last_tx_hash" : "th_YkRFtLNgT9eZqfuFAihSt14L1GCHxiNSS44h2B5wiNSfvBSc5" + }, + { + "account_id" : "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4", + "amount" : 81000000000000, + "block_hash" : "mh_2TwVRHgyXpQpjT5Z44BJQexijf6rtweypDGK3mtCZWnBFGxTV7", + "contract_id" : "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", + "height" : 335293, + "last_log_idx" : 0, + "last_tx_hash" : "th_YkRFtLNgT9eZqfuFAihSt14L1GCHxiNSS44h2B5wiNSfvBSc5" } - ] + ], + "next" : null, + "prev" : null } ``` -Or, with range: +Or, at a specific block-height: ``` -$ curl -s "https://mainnet.aeternity.io/mdw/aex9/balances/gen/350600-350603/ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA" | jq '.' +$ curl -s "https://mainnet.aeternity.io/mdw/v2/aex9/ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA/balances?block_hash=mh_hmHyBsn6D5p5d8mttyT7Pc82NCySB9yVmUQhBV2EqNepsnDtv" | jq '.' { - "contract_id": "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", - "range": [ - { - "amounts": { - "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6": 4050000000000, - "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK": 8100000000000, - "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4": 81000000000000, - "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48": 49999999999906850000000000 - }, - "block_hash": "kh_wCXiE3TTbQSCboPictnY7KXH5qmm8kjUoWHJNNqM25H4BWSW8", - "height": 350600 - }, - { - "amounts": { - "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6": 4050000000000, - "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK": 8100000000000, - "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4": 81000000000000, - "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48": 49999999999906850000000000 - }, - "block_hash": "kh_wGwxc8bMfLZqSAXDGLAv7XeFs9afNxGGZ2jpBRvMQ9pWj14pj", - "height": 350601 - }, - { - "amounts": { - "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6": 4050000000000, - "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK": 8100000000000, - "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4": 81000000000000, - "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48": 49999999999906850000000000 - }, - "block_hash": "kh_avZRszDXggtiVk8oMCjZmd92JVga6Ng6BRAtuPPdaj2ntZwN6", - "height": 350602 - }, + "data": [ { - "amounts": { - "ak_2MHJv6JcdcfpNvu4wRDZXWzq8QSxGbhUfhMLR7vUPzRFYsDFw6": 4050000000000, - "ak_2Xu6d6W4UJBWyvBVJQRHASbQHQ1vjBA7d1XUeY8SwwgzssZVHK": 8100000000000, - "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4": 81000000000000, - "ak_Yc8Lr64xGiBJfm2Jo8RQpR1gwTY8KMqqXk8oWiVC9esG8ce48": 49999999999906850000000000 - }, - "block_hash": "kh_2QBikn2KuxBgbBdzJBmbydmW5dRNHEDdCKU8Psb19MuWuNLZwf", - "height": 350603 + "account_id": "ak_CNcf2oywqbgmVg3FfKdbHQJfB959wrVwqfzSpdWVKZnep7nj4", + "amount": 5e+25, + "block_hash": "mh_2TwVRHgyXpQpjT5Z44BJQexijf6rtweypDGK3mtCZWnBFGxTV7", + "contract_id": "ct_RDRJC5EySx4TcLtGRWYrXfNgyWzEDzssThJYPd9kdLeS5ECaA", + "height": 335293, + "last_log_idx": 0, + "last_tx_hash": "th_YkRFtLNgT9eZqfuFAihSt14L1GCHxiNSS44h2B5wiNSfvBSc5" } - ] + ], + "next": null, + "prev": null } ``` diff --git a/docs/swagger_v3/aexn.spec.yaml b/docs/swagger_v3/aexn.spec.yaml index 5348734d7..58e44c983 100644 --- a/docs/swagger_v3/aexn.spec.yaml +++ b/docs/swagger_v3/aexn.spec.yaml @@ -731,6 +731,13 @@ paths: schema: type: string x-example: amount + - description: Block hash + in: query + name: block_hash + required: false + schema: + type: string + example: mh_22uNd2u5ogsFCua2kU3fSag758fTcwJ4kKJwvHpRVedeKwFRHc responses: '200': description: Returns paginated contract balances diff --git a/lib/ae_mdw/aex9.ex b/lib/ae_mdw/aex9.ex index b4772b5b9..bd10998b0 100644 --- a/lib/ae_mdw/aex9.ex +++ b/lib/ae_mdw/aex9.ex @@ -3,6 +3,7 @@ defmodule AeMdw.Aex9 do Context module for dealing with Blocks. """ + alias :aeser_api_encoder, as: Enc alias AeMdw.Collection alias AeMdw.Db.AsyncStore alias AeMdw.Db.Model @@ -35,6 +36,7 @@ defmodule AeMdw.Aex9 do @typep history_cursor() :: binary() @typep range() :: {:gen, Range.t()} @typep order_by() :: :pubkey | :amount + @typep query() :: map() @type amounts :: map() @@ -74,34 +76,86 @@ defmodule AeMdw.Aex9 do pubkey(), pagination(), balances_cursor() | nil, - order_by() + order_by(), + query() ) :: {:ok, {balances_cursor() | nil, [{pubkey(), pubkey()}], balances_cursor() | nil}} | {:error, Error.t()} - def fetch_event_balances(state, contract_pk, pagination, cursor, :pubkey) do - key_boundary = {{contract_pk, <<>>}, {contract_pk, Util.max_256bit_bin()}} - - with {:ok, cursor_key} <- deserialize_event_balances_cursor(contract_pk, cursor) do + def fetch_event_balances(state, contract_id, pagination, cursor, :pubkey, query) do + with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]), + {:ok, cursor} <- deserialize_event_balances_cursor(cursor), + {:ok, filters} <- Util.convert_params(query, &convert_param/1), + {:ok, creation_txi} <- get_aex9_contract(state, contract_pk), + {:ok, streamer} <- + event_balances_streamer(state, contract_pk, creation_txi, cursor, filters) do paginated_balances = - (&Collection.stream(state, Model.Aex9EventBalance, &1, key_boundary, cursor_key)) - |> Collection.paginate(pagination, & &1, &serialize_event_balances_cursor/1) + Collection.paginate( + streamer, + pagination, + &render_aex9_balance(state, contract_id, &1), + &serialize_event_balances_cursor/1 + ) {:ok, paginated_balances} + else + {:error, reason} -> {:error, reason} + :not_found -> {:error, ErrInput.NotFound.exception(value: contract_id)} end end - def fetch_event_balances(state, contract_pk, pagination, cursor, :amount) do - key_boundary = {{contract_pk, -1, <<>>}, {contract_pk, nil, <<>>}} + def fetch_event_balances(state, contract_id, pagination, cursor, :amount, _query) do + with {:ok, contract_pk} <- Validate.id(contract_id, [:contract_pubkey]), + {:ok, cursor_key} <- deserialize_balance_account_cursor(contract_pk, cursor) do + key_boundary = {{contract_pk, -1, <<>>}, {contract_pk, nil, <<>>}} - with {:ok, cursor_key} <- deserialize_balance_account_cursor(contract_pk, cursor) do paginated_balances = (&Collection.stream(state, Model.Aex9BalanceAccount, &1, key_boundary, cursor_key)) - |> Collection.paginate(pagination, & &1, &serialize_balance_account_cursor/1) + |> Collection.paginate( + pagination, + &render_aex9_balance(state, contract_id, &1), + &serialize_balance_account_cursor/1 + ) {:ok, paginated_balances} end end + defp event_balances_streamer(state, contract_pk, creation_txi, cursor, %{block_hash: block_hash}) do + with {:ok, {height, mbi}} <- ensure_contract_at_block(state, creation_txi, block_hash) do + block_type = if mbi == -1, do: :key, else: :micro + {amounts, _height_hash} = Db.aex9_balances!(contract_pk, {block_type, height, block_hash}) + + amounts = + amounts + |> Enum.map(fn {{:address, pk}, amount} -> {pk, amount} end) + |> Enum.sort() + + {:ok, + fn + :forward when not is_nil(cursor) -> + Enum.drop_while(amounts, fn {pk, _amount} -> pk < cursor end) + + :backward when not is_nil(cursor) -> + amounts + |> Enum.reverse() + |> Enum.drop_while(fn {pk, _amount} -> pk > cursor end) + + :backward -> + Enum.reverse(amounts) + + :forward -> + amounts + end} + end + end + + defp event_balances_streamer(state, contract_pk, _creation_txi, cursor, _query) do + key_boundary = {{contract_pk, <<>>}, {contract_pk, Util.max_256bit_bin()}} + cursor = if cursor, do: {contract_pk, cursor}, else: nil + + {:ok, &Collection.stream(state, Model.Aex9EventBalance, &1, key_boundary, cursor)} + end + @spec fetch_holders_count(State.t(), pubkey()) :: non_neg_integer() def fetch_holders_count(state, contract_pk) do key_boundary = {{contract_pk, <<>>}, {contract_pk, Util.max_256bit_bin()}} @@ -301,12 +355,12 @@ defmodule AeMdw.Aex9 do defp serialize_event_balances_cursor({_contract_pk, account_pk}), do: encode_account(account_pk) - defp deserialize_event_balances_cursor(_contract_pk, nil), do: {:ok, nil} + defp deserialize_event_balances_cursor(nil), do: {:ok, nil} - defp deserialize_event_balances_cursor(contract_pk, account_pk) do - case Validate.id(account_pk, [:account_pubkey]) do - {:ok, account_pk} -> {:ok, {contract_pk, account_pk}} - {:error, _reason} -> {:error, ErrInput.Cursor.exception(value: account_pk)} + defp deserialize_event_balances_cursor(account_id) do + case Validate.id(account_id, [:account_pubkey]) do + {:ok, account_pk} -> {:ok, account_pk} + {:error, _reason} -> {:error, ErrInput.Cursor.exception(value: account_id)} end end @@ -325,4 +379,69 @@ defmodule AeMdw.Aex9 do {:error, ErrInput.Cursor.exception(value: cursor)} end end + + defp convert_param({"block_hash", block_hash}) when is_binary(block_hash) do + case Validate.hash(block_hash, :key_block_hash) do + {:ok, hash} -> + {:ok, {:block_hash, hash}} + + {:error, _error} -> + with {:ok, hash} <- Validate.hash(block_hash, :micro_block_hash) do + {:ok, {:block_hash, hash}} + end + end + end + + defp convert_param(other_param), do: {:error, ErrInput.Query.exception(value: other_param)} + + defp get_aex9_contract(state, contract_pk) do + case State.get(state, Model.AexnContract, {:aex9, contract_pk}) do + {:ok, Model.aexn_contract(txi_idx: {txi, _idx})} -> {:ok, txi} + :not_found -> :not_found + end + end + + defp ensure_contract_at_block(state, creation_txi, block_hash) do + with block_index when block_index != nil <- DbUtil.block_hash_to_bi(state, block_hash), + Model.tx(block_index: contract_block_index) when contract_block_index <= block_index <- + State.fetch!(state, Model.Tx, creation_txi) do + {:ok, block_index} + else + _error_or_not_found -> :not_found + end + end + + defp render_aex9_balance(state, contract_id, {contract_pk, account_pk}) + when is_binary(account_pk) do + Model.aex9_event_balance(amount: amount) = + State.fetch!(state, Model.Aex9EventBalance, {contract_pk, account_pk}) + + render_aex9_balance(state, contract_id, {contract_pk, amount, account_pk}) + end + + defp render_aex9_balance(state, contract_id, {contract_pk, amount, account_pk}) do + Model.aex9_balance_account(txi: txi, log_idx: log_idx) = + State.fetch!(state, Model.Aex9BalanceAccount, {contract_pk, amount, account_pk}) + + Model.tx(id: tx_hash, block_index: block_index) = State.fetch!(state, Model.Tx, txi) + + Model.block(index: {height, _mbi}, hash: block_hash) = + State.fetch!(state, Model.Block, block_index) + + %{ + contract_id: contract_id, + account_id: Enc.encode(:account_pubkey, account_pk), + block_hash: Enc.encode(:micro_block_hash, block_hash), + height: height, + last_tx_hash: Enc.encode(:tx_hash, tx_hash), + last_log_idx: log_idx, + amount: amount + } + end + + defp render_aex9_balance(state, contract_id, {account_pk, amount}) do + state + |> render_aex9_balance(contract_id, {Validate.id!(contract_id), account_pk}) + |> Map.put(:amount, amount) + end end diff --git a/lib/ae_mdw_web/controllers/aexn_token_controller.ex b/lib/ae_mdw_web/controllers/aexn_token_controller.ex index ba9d788fa..c7d670533 100644 --- a/lib/ae_mdw_web/controllers/aexn_token_controller.ex +++ b/lib/ae_mdw_web/controllers/aexn_token_controller.ex @@ -73,15 +73,13 @@ defmodule AeMdwWeb.AexnTokenController do state: state, cursor: cursor, pagination: pagination, - order_by: order_by + order_by: order_by, + query: query } = assigns - with {:ok, contract_pk} <- validate_aex9(contract_id), - {:ok, {prev_cursor, balance_keys, next_cursor}} <- - Aex9.fetch_event_balances(state, contract_pk, pagination, cursor, order_by) do - balances = Enum.map(balance_keys, &render_event_balance(state, &1)) - - Util.render(conn, {prev_cursor, balances, next_cursor}) + with {:ok, paginated_balances} <- + Aex9.fetch_event_balances(state, contract_id, pagination, cursor, order_by, query) do + Util.render(conn, paginated_balances) end end diff --git a/test/ae_mdw_web/controllers/aexn_token_controller_test.exs b/test/ae_mdw_web/controllers/aexn_token_controller_test.exs index 39b6f9152..645e68f3f 100644 --- a/test/ae_mdw_web/controllers/aexn_token_controller_test.exs +++ b/test/ae_mdw_web/controllers/aexn_token_controller_test.exs @@ -34,16 +34,17 @@ defmodule AeMdwWeb.AexnTokenControllerTest do {name, symbol, _decimals} = meta_info txi = 2_000 - i + contract_pk = <> m_aex9 = Model.aexn_contract( - index: {:aex9, <>}, + index: {:aex9, contract_pk}, txi_idx: {txi, -1}, meta_info: meta_info ) m_aexn_creation = - Model.aexn_contract_creation(index: {:aex9, {txi, -1}}, contract_pk: <>) + Model.aexn_contract_creation(index: {:aex9, {txi, -1}}, contract_pk: contract_pk) m_aexn_name = Model.aexn_contract_name(index: {:aex9, name, <>}) m_aexn_symbol = Model.aexn_contract_symbol(index: {:aex9, symbol, <>}) @@ -54,14 +55,39 @@ defmodule AeMdwWeb.AexnTokenControllerTest do |> Store.put(Model.AexnContractName, m_aexn_name) |> Store.put(Model.AexnContractSymbol, m_aexn_symbol) |> then(fn store -> - Enum.reduce(1..i, store, fn _i, store -> - Store.put( - store, + Enum.reduce(1..i, store, fn i2, store -> + balance_txi = 1_000_000 + i2 + account_pk = <<1_000 + i2::256>> + amount = 1_000_000 - i2 + + store + |> Store.put( Model.Aex9EventBalance, - Model.aex9_event_balance(index: {<>, :crypto.strong_rand_bytes(32)}) + Model.aex9_event_balance( + index: {contract_pk, account_pk}, + txi: balance_txi, + amount: amount + ) + ) + |> Store.put( + Model.Tx, + Model.tx(index: balance_txi, id: <>, block_index: {i2, -1}) + ) + |> Store.put( + Model.Block, + Model.block(index: {i2, -1}, hash: <<0::256>>) + ) + |> Store.put( + Model.Aex9BalanceAccount, + Model.aex9_balance_account( + index: {contract_pk, amount, account_pk}, + txi: balance_txi, + log_idx: i2 + ) ) end) end) + |> Store.put(Model.Tx, Model.tx(index: txi)) |> Store.put( Model.Stat, Model.stat(index: Stats.aex9_logs_count_key(<>), payload: i) @@ -295,7 +321,7 @@ defmodule AeMdwWeb.AexnTokenControllerTest do assert @default_limit = length(aex9_tokens) assert ^aex9_names = Enum.sort(aex9_names) - assert ^aex9_holders = for(i <- 200..209, do: i) + assert ^aex9_holders = Enum.to_list(200..209) assert %{"data" => next_aex9_tokens, "prev" => prev_aex9_tokens} = conn |> get(next) |> json_response(200) @@ -765,10 +791,9 @@ defmodule AeMdwWeb.AexnTokenControllerTest do describe "aex9_event_balances" do test "gets ascending event balances for a contract", %{ - conn: conn, - contract_pk: contract_pk + conn: conn } do - contract_id = encode_contract(contract_pk) + contract_id = encode_contract(<<200::256>>) assert %{"data" => balances, "next" => next} = conn @@ -846,11 +871,8 @@ defmodule AeMdwWeb.AexnTokenControllerTest do assert %{"data" => ^balances} = conn |> get(prev) |> json_response(200) end - test "gets event balances for a contract with limit", %{ - conn: conn, - contract_pk: contract_pk - } do - contract_id = encode_contract(contract_pk) + test "gets event balances for a contract with limit", %{conn: conn} do + contract_id = encode_contract(<<200::256>>) limit = 8 assert %{"data" => balances, "next" => next} =