Skip to content

Commit

Permalink
feat: allow getting block-specific AEx9 balances (#1701)
Browse files Browse the repository at this point in the history
  • Loading branch information
sborrazas committed Mar 18, 2024
1 parent 3a214d9 commit db4f45d
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 94 deletions.
109 changes: 53 additions & 56 deletions README.md
Expand Up @@ -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
}
```

Expand Down
7 changes: 7 additions & 0 deletions docs/swagger_v3/aexn.spec.yaml
Expand Up @@ -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
Expand Down
151 changes: 135 additions & 16 deletions lib/ae_mdw/aex9.ex
Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()}}
Expand Down Expand Up @@ -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

Expand All @@ -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
12 changes: 5 additions & 7 deletions lib/ae_mdw_web/controllers/aexn_token_controller.ex
Expand Up @@ -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

Expand Down

0 comments on commit db4f45d

Please sign in to comment.