- <%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @transaction.from_address_hash, contract: BlockScoutWeb.AddressView.contract?(@transaction.from_address) %>
+
+ <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.AddressView.render_partial() %>
→
- <%= if @transaction.to_address_hash do %>
- <%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @transaction.to_address_hash, contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %>
- <% else %>
- <%= gettext("Contract Address Pending") %>
- <% end %>
+ <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
index 5cfd4ec703..83e236f7db 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
@@ -1,10 +1,46 @@
defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view
- alias Explorer.Chain.{Address, Hash, SmartContract}
+ alias Explorer.Chain.{Address, Hash, SmartContract, TokenTransfer, Transaction}
@dialyzer :no_match
+ def address_partial_selector(struct_to_render_from, direction, current_address, truncate \\ false)
+
+ def address_partial_selector(%TokenTransfer{to_address: address}, :to, current_address, truncate) do
+ matching_address_check(current_address, address.hash, contract?(address), truncate)
+ end
+
+ def address_partial_selector(%TokenTransfer{from_address: address}, :from, current_address, truncate) do
+ matching_address_check(current_address, address.hash, contract?(address), truncate)
+ end
+
+ def address_partial_selector(
+ %Transaction{to_address_hash: nil, created_contract_address_hash: nil},
+ :to,
+ _current_address,
+ _truncate
+ ) do
+ gettext("Contract Address Pending")
+ end
+
+ def address_partial_selector(
+ %Transaction{to_address_hash: nil, created_contract_address_hash: hash},
+ :to,
+ current_address,
+ truncate
+ ) do
+ matching_address_check(current_address, hash, true, truncate)
+ end
+
+ def address_partial_selector(%Transaction{to_address: address}, :to, current_address, truncate) do
+ matching_address_check(current_address, address.hash, contract?(address), truncate)
+ end
+
+ def address_partial_selector(%Transaction{from_address: address}, :from, current_address, truncate) do
+ matching_address_check(current_address, address.hash, contract?(address), truncate)
+ end
+
def address_title(%Address{} = address) do
if contract?(address) do
gettext("Contract Address")
@@ -45,16 +81,20 @@ defmodule BlockScoutWeb.AddressView do
|> Base.encode64()
end
- def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true
+ def render_partial(%{partial: partial, address_hash: hash, contract: contract?, truncate: truncate}) do
+ render(
+ partial,
+ address_hash: hash,
+ contract: contract?,
+ truncate: truncate
+ )
+ end
- def smart_contract_verified?(%Address{smart_contract: nil}), do: false
+ def render_partial(text), do: text
- def trimmed_hash(%Hash{} = hash) do
- string_hash = to_string(hash)
- "#{String.slice(string_hash, 0..5)}–#{String.slice(string_hash, -6..-1)}"
- end
+ def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true
- def trimmed_hash(_), do: ""
+ def smart_contract_verified?(%Address{smart_contract: nil}), do: false
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(address.smart_contract.abi, & &1["constant"])
@@ -62,32 +102,28 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false
- def display_address_hash(current_address, target_address, truncate \\ false)
-
- def display_address_hash(nil, target_address, truncate) do
- render(
- "_link.html",
- address_hash: target_address.hash,
- contract: contract?(target_address),
- truncate: truncate
- )
+ def trimmed_hash(%Hash{} = hash) do
+ string_hash = to_string(hash)
+ "#{String.slice(string_hash, 0..5)}–#{String.slice(string_hash, -6..-1)}"
end
- def display_address_hash(current_address, target_address, truncate) do
- if current_address.hash == target_address.hash do
- render(
- "_responsive_hash.html",
- address_hash: current_address.hash,
- contract: contract?(current_address),
+ def trimmed_hash(_), do: ""
+
+ defp matching_address_check(current_address, hash, contract?, truncate) do
+ if current_address && current_address.hash == hash do
+ %{
+ partial: "_responsive_hash.html",
+ address_hash: hash,
+ contract: contract?,
truncate: truncate
- )
+ }
else
- render(
- "_link.html",
- address_hash: target_address.hash,
- contract: contract?(target_address),
+ %{
+ partial: "_link.html",
+ address_hash: hash,
+ contract: contract?,
truncate: truncate
- )
+ }
end
end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
new file mode 100644
index 0000000000..a69a859ab2
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
@@ -0,0 +1,50 @@
+defmodule BlockScoutWeb.Tokens.HolderView do
+ use BlockScoutWeb, :view
+
+ alias BlockScoutWeb.Tokens.{OverviewView, TokenView}
+ alias Explorer.Chain.{Token}
+
+ @doc """
+ Calculates the percentage of the value from the given total supply.
+
+ ## Examples
+
+ iex> value = Decimal.new(200)
+ iex> total_supply = Decimal.new(1000)
+ iex> BlockScoutWeb.Tokens.HolderView.total_supply_percentage(value, total_supply)
+ "20.0000%"
+
+ """
+ def total_supply_percentage(value, total_supply) do
+ result =
+ value
+ |> Decimal.div(total_supply)
+ |> Decimal.mult(100)
+ |> Decimal.round(4)
+ |> Decimal.to_string()
+
+ result <> "%"
+ end
+
+ @doc """
+ Formats the token balance value according to the Token's type.
+
+ ## Examples
+
+ iex> token = build(:token, type: "ERC-20", decimals: 2)
+ iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, token)
+ "1,000"
+
+ iex> token = build(:token, type: "ERC-721")
+ iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, token)
+ 1
+
+ """
+ def format_token_balance_value(value, %Token{type: "ERC-20", decimals: decimals}) do
+ format_according_to_decimals(value, decimals)
+ end
+
+ def format_token_balance_value(value, _token) do
+ value
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
index 402352dd95..fea9d63815 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
@@ -10,6 +10,8 @@ defmodule BlockScoutWeb.TransactionView do
defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction]
+ defdelegate formatted_timestamp(block), to: BlockView
+
def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do
case block do
nil -> 0
@@ -17,22 +19,19 @@ defmodule BlockScoutWeb.TransactionView do
end
end
- def from_or_to_address?(_token_transfer, nil), do: false
-
- def from_or_to_address?(%{from_address_hash: from_hash, to_address_hash: to_hash}, %Address{hash: hash}) do
- from_hash == hash || to_hash == hash
- end
-
- # This is the address to be shown in the to field
- def to_address_hash(%Transaction{to_address_hash: nil, created_contract_address_hash: address_hash}), do: address_hash
+ def contract_creation?(%Transaction{to_address: nil}), do: true
- def to_address_hash(%Transaction{to_address: %Address{hash: address_hash}}), do: address_hash
+ def contract_creation?(_), do: false
def fee(%Transaction{} = transaction) do
{_, value} = Chain.fee(transaction, :wei)
value
end
+ def format_gas_limit(gas) do
+ Number.to_string!(gas)
+ end
+
def formatted_fee(%Transaction{} = transaction, opts) do
transaction
|> Chain.fee(:wei)
@@ -43,34 +42,6 @@ defmodule BlockScoutWeb.TransactionView do
end
end
- def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending")
-
- def gas_used(%Transaction{gas_used: gas_used}) do
- Number.to_string!(gas_used)
- end
-
- def involves_contract?(%Transaction{from_address: from_address, to_address: to_address}) do
- AddressView.contract?(from_address) || AddressView.contract?(to_address)
- end
-
- def involves_token_transfers?(%Transaction{token_transfers: []}), do: false
- def involves_token_transfers?(%Transaction{token_transfers: transfers}) when is_list(transfers), do: true
-
- def contract_creation?(%Transaction{to_address: nil}), do: true
-
- def contract_creation?(_), do: false
-
- def qr_code(%Transaction{hash: hash}) do
- hash
- |> to_string()
- |> QRCode.to_png()
- |> Base.encode64()
- end
-
- def format_gas_limit(gas) do
- Number.to_string!(gas)
- end
-
def formatted_status(transaction) do
transaction
|> Chain.transaction_to_status()
@@ -82,7 +53,11 @@ defmodule BlockScoutWeb.TransactionView do
end
end
- defdelegate formatted_timestamp(block), to: BlockView
+ def from_or_to_address?(_token_transfer, nil), do: false
+
+ def from_or_to_address?(%{from_address_hash: from_hash, to_address_hash: to_hash}, %Address{hash: hash}) do
+ from_hash == hash || to_hash == hash
+ end
def gas(%type{gas: gas}) when is_transaction_type(type) do
Cldr.Number.to_string!(gas)
@@ -95,22 +70,39 @@ defmodule BlockScoutWeb.TransactionView do
format_wei_value(gas_price, unit)
end
+ def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending")
+
+ def gas_used(%Transaction{gas_used: gas_used}) do
+ Number.to_string!(gas_used)
+ end
+
def hash(%Transaction{hash: hash}) do
to_string(hash)
end
+ def involves_contract?(%Transaction{from_address: from_address, to_address: to_address}) do
+ AddressView.contract?(from_address) || AddressView.contract?(to_address)
+ end
+
+ def involves_token_transfers?(%Transaction{token_transfers: []}), do: false
+ def involves_token_transfers?(%Transaction{token_transfers: transfers}) when is_list(transfers), do: true
+
+ def qr_code(%Transaction{hash: hash}) do
+ hash
+ |> to_string()
+ |> QRCode.to_png()
+ |> Base.encode64()
+ end
+
def status(transaction) do
Chain.transaction_to_status(transaction)
end
- def type_suffix(%Transaction{} = transaction) do
- cond do
- involves_token_transfers?(transaction) -> "token-transfer"
- contract_creation?(transaction) -> "contract-creation"
- involves_contract?(transaction) -> "contract-call"
- true -> "transaction"
- end
- end
+ # This is the address to be shown in the to field
+ def to_address_hash(%Transaction{to_address_hash: nil, created_contract_address_hash: address_hash}),
+ do: address_hash
+
+ def to_address_hash(%Transaction{to_address: %Address{hash: address_hash}}), do: address_hash
def transaction_display_type(%Transaction{} = transaction) do
cond do
@@ -121,6 +113,15 @@ defmodule BlockScoutWeb.TransactionView do
end
end
+ def type_suffix(%Transaction{} = transaction) do
+ cond do
+ involves_token_transfers?(transaction) -> "token-transfer"
+ contract_creation?(transaction) -> "contract-creation"
+ involves_contract?(transaction) -> "contract-call"
+ true -> "transaction"
+ end
+ end
+
@doc """
Converts a transaction's Wei value to Ether and returns a formatted display value.
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index de0e2977bd..ec80196c48 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -52,7 +52,7 @@ msgstr ""
msgid "Transactions"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:91
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
msgid "Value"
msgstr ""
@@ -76,7 +76,7 @@ msgid "Miner"
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:59
-#: lib/block_scout_web/templates/transaction/overview.html.eex:59
+#: lib/block_scout_web/templates/transaction/overview.html.eex:55
msgid "Nonce"
msgstr ""
@@ -100,7 +100,7 @@ msgstr ""
msgid "Total Difficulty"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:38
+#: lib/block_scout_web/templates/transaction/overview.html.eex:34
msgid "Block Number"
msgstr ""
@@ -112,7 +112,7 @@ msgstr ""
msgid "Cumulative Gas Used"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:102
+#: lib/block_scout_web/templates/transaction/overview.html.eex:98
msgid "Gas"
msgstr ""
@@ -120,7 +120,7 @@ msgstr ""
msgid "Gas Price"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:72
+#: lib/block_scout_web/templates/transaction/overview.html.eex:68
msgid "Input"
msgstr ""
@@ -132,7 +132,7 @@ msgstr ""
msgid "%{count} transactions in this block"
msgstr ""
-#: lib/block_scout_web/views/address_view.ex:12
+#: lib/block_scout_web/views/address_view.ex:48
msgid "Address"
msgstr ""
@@ -148,7 +148,7 @@ msgstr ""
msgid "Overview"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:81
+#: lib/block_scout_web/views/transaction_view.ex:52
msgid "Success"
msgstr ""
@@ -199,9 +199,9 @@ msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35
-#: lib/block_scout_web/templates/transaction/overview.html.eex:47
-#: lib/block_scout_web/views/transaction_view.ex:46
-#: lib/block_scout_web/views/transaction_view.ex:80
+#: lib/block_scout_web/templates/transaction/overview.html.eex:43
+#: lib/block_scout_web/views/transaction_view.ex:51
+#: lib/block_scout_web/views/transaction_view.ex:73
msgid "Pending"
msgstr ""
@@ -265,15 +265,15 @@ msgstr ""
msgid "TPM"
msgstr ""
-#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:86
msgid "Next Page"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:78
+#: lib/block_scout_web/views/transaction_view.ex:49
msgid "Failed"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:79
+#: lib/block_scout_web/views/transaction_view.ex:50
msgid "Out of Gas"
msgstr ""
@@ -292,8 +292,8 @@ msgstr ""
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:24
-#: lib/block_scout_web/templates/transaction/overview.html.eex:91
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:20
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
#: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16
#: lib/block_scout_web/views/wei_helpers.ex:72
msgid "Ether"
@@ -453,7 +453,7 @@ msgstr ""
msgid "Total Gas Used"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:120
+#: lib/block_scout_web/views/transaction_view.ex:112
msgid "Transaction"
msgstr ""
@@ -467,8 +467,8 @@ msgid "View All"
msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:27
-#: lib/block_scout_web/templates/transaction/overview.html.eex:64
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:23
+#: lib/block_scout_web/templates/transaction/overview.html.eex:60
msgid "TX Fee"
msgstr ""
@@ -476,7 +476,7 @@ msgstr ""
msgid "Contract"
msgstr ""
-#: lib/block_scout_web/views/address_view.ex:10
+#: lib/block_scout_web/views/address_view.ex:46
msgid "Contract Address"
msgstr ""
@@ -493,7 +493,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:71
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:85
#: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72
msgid "Older"
@@ -540,7 +540,7 @@ msgid "Newer"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:118
+#: lib/block_scout_web/views/transaction_view.ex:110
msgid "Contract Creation"
msgstr ""
@@ -634,12 +634,12 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
-#: lib/block_scout_web/templates/transaction/overview.html.eex:23
+#: lib/block_scout_web/views/address_view.ex:24
msgid "Contract Address Pending"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:119
+#: lib/block_scout_web/views/transaction_view.ex:111
msgid "Contract Call"
msgstr ""
@@ -655,21 +655,21 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:34
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:30
msgid "Block #%{number}"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:47
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
msgid "IN"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:39
msgid "OUT"
msgstr ""
@@ -685,10 +685,12 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:50
#: lib/block_scout_web/templates/address_token/index.html.eex:58
#: lib/block_scout_web/templates/address_transaction/index.html.eex:49
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:26
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:54
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:42
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:51
#: lib/block_scout_web/templates/tokens/token/show.html.eex:26
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:45
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:54
msgid "Read Contract"
msgstr ""
@@ -714,12 +716,12 @@ msgid "Github"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:52
+#: lib/block_scout_web/templates/transaction/overview.html.eex:48
msgid "Block Confirmations"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:114
+#: lib/block_scout_web/templates/transaction/overview.html.eex:110
msgid "Limit"
msgstr ""
@@ -735,14 +737,14 @@ msgid "There are no logs for this transaction."
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:107
+#: lib/block_scout_web/templates/transaction/overview.html.eex:103
msgid "Used"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
-#: lib/block_scout_web/views/transaction_view.ex:117
+#: lib/block_scout_web/views/transaction_view.ex:109
msgid "Token Transfer"
msgstr ""
@@ -775,7 +777,7 @@ msgstr ""
msgid "Validated Transactions"
msgstr ""
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:64
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:78
msgid "There are no transfers for this Token."
msgstr ""
@@ -785,13 +787,15 @@ msgid "Token Details"
msgstr ""
#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:48
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:34
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:46
#: lib/block_scout_web/templates/tokens/token/show.html.eex:17
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:36
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:39
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:55
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:45
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:48
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:69
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42
#: lib/block_scout_web/templates/transaction_log/index.html.eex:13
@@ -830,7 +834,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:52
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:66
msgid "loading..."
msgstr ""
@@ -1001,12 +1005,12 @@ msgid "loading....."
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:67
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:63
msgid "View More Transfers"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:68
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:64
msgid "View Less Transfers"
msgstr ""
@@ -1016,7 +1020,7 @@ msgid "Less than"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:42
+#: lib/block_scout_web/views/transaction_view.ex:41
msgid "Max of"
msgstr ""
@@ -1056,3 +1060,20 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:111
msgid "There are no tokens for this address."
msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:79
+msgid "There are no holders for this Token."
+msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:34
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:45
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:59
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:70
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:32
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:55
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:34
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:59
+msgid "Token Holders"
+msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index dd61522398..c052890aed 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -64,7 +64,7 @@ msgstr "BlockScout"
msgid "Transactions"
msgstr "Transactions"
-#: lib/block_scout_web/templates/transaction/overview.html.eex:91
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
msgid "Value"
msgstr "Value"
@@ -88,7 +88,7 @@ msgid "Miner"
msgstr "Validator"
#: lib/block_scout_web/templates/block/overview.html.eex:59
-#: lib/block_scout_web/templates/transaction/overview.html.eex:59
+#: lib/block_scout_web/templates/transaction/overview.html.eex:55
msgid "Nonce"
msgstr "Nonce"
@@ -112,7 +112,7 @@ msgstr "Timestamp"
msgid "Total Difficulty"
msgstr "Total Difficulty"
-#: lib/block_scout_web/templates/transaction/overview.html.eex:38
+#: lib/block_scout_web/templates/transaction/overview.html.eex:34
msgid "Block Number"
msgstr "Block Height"
@@ -124,7 +124,7 @@ msgstr "Transaction Details"
msgid "Cumulative Gas Used"
msgstr "Cumulative Gas Used"
-#: lib/block_scout_web/templates/transaction/overview.html.eex:102
+#: lib/block_scout_web/templates/transaction/overview.html.eex:98
msgid "Gas"
msgstr "Gas"
@@ -132,7 +132,7 @@ msgstr "Gas"
msgid "Gas Price"
msgstr "Gas Price"
-#: lib/block_scout_web/templates/transaction/overview.html.eex:72
+#: lib/block_scout_web/templates/transaction/overview.html.eex:68
msgid "Input"
msgstr "Input"
@@ -144,7 +144,7 @@ msgstr "%{confirmations} block confirmations"
msgid "%{count} transactions in this block"
msgstr "%{count} transactions in this block"
-#: lib/block_scout_web/views/address_view.ex:12
+#: lib/block_scout_web/views/address_view.ex:48
msgid "Address"
msgstr "Address"
@@ -160,7 +160,7 @@ msgstr "From"
msgid "Overview"
msgstr "Overview"
-#: lib/block_scout_web/views/transaction_view.ex:81
+#: lib/block_scout_web/views/transaction_view.ex:52
msgid "Success"
msgstr "Success"
@@ -211,9 +211,9 @@ msgstr "Showing %{count} Transactions"
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35
-#: lib/block_scout_web/templates/transaction/overview.html.eex:47
-#: lib/block_scout_web/views/transaction_view.ex:46
-#: lib/block_scout_web/views/transaction_view.ex:80
+#: lib/block_scout_web/templates/transaction/overview.html.eex:43
+#: lib/block_scout_web/views/transaction_view.ex:51
+#: lib/block_scout_web/views/transaction_view.ex:73
msgid "Pending"
msgstr "Pending"
@@ -277,15 +277,15 @@ msgstr ""
msgid "TPM"
msgstr ""
-#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:86
msgid "Next Page"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:78
+#: lib/block_scout_web/views/transaction_view.ex:49
msgid "Failed"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:79
+#: lib/block_scout_web/views/transaction_view.ex:50
msgid "Out of Gas"
msgstr ""
@@ -304,8 +304,8 @@ msgstr ""
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:24
-#: lib/block_scout_web/templates/transaction/overview.html.eex:91
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:20
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
#: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16
#: lib/block_scout_web/views/wei_helpers.ex:72
msgid "Ether"
@@ -465,7 +465,7 @@ msgstr ""
msgid "Total Gas Used"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:120
+#: lib/block_scout_web/views/transaction_view.ex:112
msgid "Transaction"
msgstr ""
@@ -479,8 +479,8 @@ msgid "View All"
msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:27
-#: lib/block_scout_web/templates/transaction/overview.html.eex:64
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:23
+#: lib/block_scout_web/templates/transaction/overview.html.eex:60
msgid "TX Fee"
msgstr ""
@@ -488,7 +488,7 @@ msgstr ""
msgid "Contract"
msgstr ""
-#: lib/block_scout_web/views/address_view.ex:10
+#: lib/block_scout_web/views/address_view.ex:46
msgid "Contract Address"
msgstr ""
@@ -505,7 +505,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:71
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:85
#: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72
msgid "Older"
@@ -552,7 +552,7 @@ msgid "Newer"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:118
+#: lib/block_scout_web/views/transaction_view.ex:110
msgid "Contract Creation"
msgstr ""
@@ -646,12 +646,12 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
-#: lib/block_scout_web/templates/transaction/overview.html.eex:23
+#: lib/block_scout_web/views/address_view.ex:24
msgid "Contract Address Pending"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:119
+#: lib/block_scout_web/views/transaction_view.ex:111
msgid "Contract Call"
msgstr ""
@@ -667,21 +667,21 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:34
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:30
msgid "Block #%{number}"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:47
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
msgid "IN"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:39
msgid "OUT"
msgstr ""
@@ -697,10 +697,12 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:50
#: lib/block_scout_web/templates/address_token/index.html.eex:58
#: lib/block_scout_web/templates/address_transaction/index.html.eex:49
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:26
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:54
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:42
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:51
#: lib/block_scout_web/templates/tokens/token/show.html.eex:26
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:45
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:54
msgid "Read Contract"
msgstr ""
@@ -726,12 +728,12 @@ msgid "Github"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:52
+#: lib/block_scout_web/templates/transaction/overview.html.eex:48
msgid "Block Confirmations"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:114
+#: lib/block_scout_web/templates/transaction/overview.html.eex:110
msgid "Limit"
msgstr ""
@@ -747,14 +749,14 @@ msgid "There are no logs for this transaction."
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/overview.html.eex:107
+#: lib/block_scout_web/templates/transaction/overview.html.eex:103
msgid "Used"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
-#: lib/block_scout_web/views/transaction_view.ex:117
+#: lib/block_scout_web/views/transaction_view.ex:109
msgid "Token Transfer"
msgstr ""
@@ -787,7 +789,7 @@ msgstr ""
msgid "Validated Transactions"
msgstr ""
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:64
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:78
msgid "There are no transfers for this Token."
msgstr ""
@@ -797,13 +799,15 @@ msgid "Token Details"
msgstr ""
#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:48
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:34
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:46
#: lib/block_scout_web/templates/tokens/token/show.html.eex:17
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:36
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:39
-#: lib/block_scout_web/templates/tokens/token/show.html.eex:55
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:45
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:48
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:69
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42
#: lib/block_scout_web/templates/transaction_log/index.html.eex:13
@@ -842,7 +846,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:52
-#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:66
msgid "loading..."
msgstr ""
@@ -1013,12 +1017,12 @@ msgid "loading....."
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:67
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:63
msgid "View More Transfers"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:68
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:64
msgid "View Less Transfers"
msgstr ""
@@ -1028,7 +1032,7 @@ msgid "Less than"
msgstr ""
#, elixir-format
-#: lib/block_scout_web/views/transaction_view.ex:42
+#: lib/block_scout_web/views/transaction_view.ex:41
msgid "Max of"
msgstr ""
@@ -1068,3 +1072,20 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:111
msgid "There are no tokens for this address."
msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:79
+msgid "There are no holders for this Token."
+msgstr ""
+
+#, elixir-format
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:34
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:45
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:59
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:70
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:32
+#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:55
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:34
+#: lib/block_scout_web/templates/tokens/token/show.html.eex:59
+msgid "Token Holders"
+msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
new file mode 100644
index 0000000000..6ce8d7915b
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
@@ -0,0 +1,92 @@
+defmodule BlockScoutWeb.Tokens.HolderControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Chain.Hash
+
+ describe "GET index/3" do
+ test "with invalid address hash", %{conn: conn} do
+ conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, "invalid_address"))
+
+ assert html_response(conn, 404)
+ end
+
+ test "with a token that doesn't exist", %{conn: conn} do
+ address = build(:address)
+ conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, address.hash))
+
+ assert html_response(conn, 404)
+ end
+
+ test "successfully renders the page", %{conn: conn} do
+ token = insert(:token)
+
+ insert_list(
+ 2,
+ :token_balance,
+ token_contract_address_hash: token.contract_address_hash
+ )
+
+ conn =
+ get(
+ conn,
+ token_holder_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)
+ )
+
+ assert html_response(conn, 200)
+ end
+
+ test "returns next page of results based on last seen token balance", %{conn: conn} do
+ contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
+ token = insert(:token, contract_address: contract_address)
+
+ second_page_token_balances =
+ 1..50
+ |> Enum.map(
+ &insert(
+ :token_balance,
+ token_contract_address_hash: token.contract_address_hash,
+ value: &1 + 1000
+ )
+ )
+ |> Enum.map(& &1.value)
+
+ token_balance =
+ insert(
+ :token_balance,
+ token_contract_address_hash: token.contract_address_hash,
+ value: 50000
+ )
+
+ conn =
+ get(conn, token_holder_path(conn, :index, token.contract_address_hash), %{
+ "value" => Decimal.to_integer(token_balance.value),
+ "address_hash" => Hash.to_string(token_balance.address_hash)
+ })
+
+ actual_token_balances =
+ conn.assigns.token_balances
+ |> Enum.map(& &1.value)
+ |> Enum.reverse()
+
+ assert second_page_token_balances == actual_token_balances
+ end
+
+ test "next_page_params exists if not on last page", %{conn: conn} do
+ contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
+ token = insert(:token, contract_address: contract_address)
+
+ Enum.each(
+ 1..51,
+ &insert(
+ :token_balance,
+ token_contract_address_hash: token.contract_address_hash,
+ value: &1 + 1000
+ )
+ )
+
+ conn = get(conn, token_holder_path(conn, :index, token.contract_address_hash))
+
+ assert conn.assigns.next_page_params
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs
index 5c188d1762..5b72d3b92f 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs
@@ -31,7 +31,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractControllerTest do
assert html_response(conn, 200)
assert token.contract_address_hash == conn.assigns.token.contract_address_hash
assert conn.assigns.total_token_transfers
- assert conn.assigns.total_address_in_token_transfers
+ assert conn.assigns.total_token_holders
end
end
end
diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex
new file mode 100644
index 0000000000..6db529aff9
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex
@@ -0,0 +1,23 @@
+defmodule BlockScoutWeb.TokenPage do
+ @moduledoc false
+
+ use Wallaby.DSL
+ import Wallaby.Query, only: [css: 1, css: 2]
+ alias Explorer.Chain.{Address}
+
+ def visit_page(session, %Address{hash: address_hash}) do
+ visit_page(session, address_hash)
+ end
+
+ def visit_page(session, contract_address_hash) do
+ visit(session, "tokens/#{contract_address_hash}")
+ end
+
+ def click_tokens_holders(session) do
+ click(session, css("[data-test='token_holders_tab']"))
+ end
+
+ def token_holders(count: count) do
+ css("[data-test='token_holders']", count: count)
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
index bbd3b1a1ec..ad998dbaaf 100644
--- a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
@@ -2,7 +2,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
use BlockScoutWeb.FeatureCase, async: true
alias Explorer.Chain.Wei
- alias Explorer.Factory
alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do
@@ -41,7 +40,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
describe "viewing contract creator" do
test "see the contract creator and transaction links", %{session: session} do
address = insert(:address)
- contract = insert(:address, contract_code: Factory.data("contract_code"))
+ contract = insert(:contract_address)
transaction = insert(:transaction, from_address: address, created_contract_address: contract)
internal_transaction =
@@ -63,9 +62,9 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
test "see the contract creator and transaction links even when the creator is another contract", %{session: session} do
lincoln = insert(:address)
- contract = insert(:address, contract_code: Factory.data("contract_code"))
+ contract = insert(:contract_address)
transaction = insert(:transaction)
- another_contract = insert(:address, contract_code: Factory.data("contract_code"))
+ another_contract = insert(:contract_address)
insert(
:internal_transaction,
@@ -285,12 +284,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln
taft = addresses.taft
- contract_token_address = insert(:contract_address)
- insert(:token, contract_address: contract_token_address)
+ contract_address = insert(:contract_address)
+ insert(:token, contract_address: contract_address)
transaction =
:transaction
- |> insert(from_address: lincoln, to_address: contract_token_address)
+ |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert(
@@ -298,7 +297,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln,
to_address: taft,
transaction: transaction,
- token_contract_address: contract_token_address
+ token_contract_address: contract_address
)
session
@@ -318,12 +317,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
taft = addresses.taft
morty = build(:address)
- contract_token_address = insert(:contract_address)
- insert(:token, contract_address: contract_token_address)
+ contract_address = insert(:contract_address)
+ insert(:token, contract_address: contract_address)
transaction =
:transaction
- |> insert(from_address: lincoln, to_address: contract_token_address)
+ |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert(
@@ -331,7 +330,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln,
to_address: taft,
transaction: transaction,
- token_contract_address: contract_token_address
+ token_contract_address: contract_address
)
insert(
@@ -339,7 +338,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln,
to_address: morty,
transaction: transaction,
- token_contract_address: contract_token_address
+ token_contract_address: contract_address
)
session
@@ -358,17 +357,13 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln
taft = addresses.taft
- contract_token_address =
- insert(
- :address,
- contract_code: Factory.data("contract_code")
- )
+ contract_address = insert(:contract_address)
- insert(:token, contract_address: contract_token_address)
+ insert(:token, contract_address: contract_address)
transaction =
:transaction
- |> insert(from_address: lincoln, to_address: contract_token_address)
+ |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert_list(
@@ -377,7 +372,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln,
to_address: taft,
transaction: transaction,
- token_contract_address: contract_token_address
+ token_contract_address: contract_address
)
session
@@ -393,12 +388,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln
taft = addresses.taft
- contract_token_address = insert(:contract_address)
- insert(:token, contract_address: contract_token_address)
+ contract_address = insert(:contract_address)
+ insert(:token, contract_address: contract_address)
transaction =
:transaction
- |> insert(from_address: lincoln, to_address: contract_token_address)
+ |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert_list(
@@ -407,7 +402,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln,
to_address: taft,
transaction: transaction,
- token_contract_address: contract_token_address
+ token_contract_address: contract_address
)
session
diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
new file mode 100644
index 0000000000..8a5d769e62
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
@@ -0,0 +1,22 @@
+defmodule BlockScoutWeb.ViewingTokensTest do
+ use BlockScoutWeb.FeatureCase, async: true
+
+ alias BlockScoutWeb.TokenPage
+
+ describe "viewing token holders" do
+ test "list the token holders", %{session: session} do
+ token = insert(:token)
+
+ insert_list(
+ 2,
+ :token_balance,
+ token_contract_address_hash: token.contract_address_hash
+ )
+
+ session
+ |> TokenPage.visit_page(token.contract_address)
+ |> TokenPage.click_tokens_holders()
+ |> assert_has(TokenPage.token_holders(count: 2))
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
index 7ba5c71171..3bce75c435 100644
--- a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
@@ -1,9 +1,106 @@
defmodule BlockScoutWeb.AddressViewTest do
use BlockScoutWeb.ConnCase, async: true
- alias Explorer.Chain.Data
+ alias Explorer.Chain.{Address, Data, Transaction}
alias BlockScoutWeb.AddressView
+ describe "address_partial_selector/4" do
+ test "for a pending contract creation to address" do
+ transaction = insert(:transaction, to_address: nil, created_contract_address_hash: nil)
+ assert AddressView.address_partial_selector(transaction, :to, nil) == "Contract Address Pending"
+ end
+
+ test "will truncate address" do
+ transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_link.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: true
+ } = AddressView.address_partial_selector(transaction, :to, nil, true)
+ end
+
+ test "for a non-contract to address not on address page" do
+ transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_link.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, nil)
+ end
+
+ test "for a non-contract to address non matching address page" do
+ transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_link.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, nil)
+ end
+
+ test "for a non-contract to address matching address page" do
+ transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_responsive_hash.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
+ end
+
+ test "for a contract to address non matching address page" do
+ contract = %Address{hash: hash} = insert(:contract_address)
+ transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
+
+ assert %{
+ partial: "_link.html",
+ address_hash: ^hash,
+ contract: true,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
+ end
+
+ test "for a contract to address matching address page" do
+ contract = %Address{hash: hash} = insert(:contract_address)
+ transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
+
+ assert %{
+ partial: "_responsive_hash.html",
+ address_hash: ^hash,
+ contract: true,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, contract)
+ end
+
+ test "for a non-contract from address not on address page" do
+ transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_link.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :to, nil)
+ end
+
+ test "for a non-contract from address matching address page" do
+ transaction = %Transaction{from_address_hash: hash} = insert(:transaction)
+
+ assert %{
+ partial: "_responsive_hash.html",
+ address_hash: ^hash,
+ contract: false,
+ truncate: false
+ } = AddressView.address_partial_selector(transaction, :from, transaction.from_address)
+ end
+ end
+
describe "contract?/1" do
test "with a smart contract" do
{:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef")
@@ -15,6 +112,10 @@ defmodule BlockScoutWeb.AddressViewTest do
address = insert(:address, contract_code: nil)
refute AddressView.contract?(address)
end
+
+ test "with nil address" do
+ assert AddressView.contract?(nil)
+ end
end
describe "qr_code/1" do
@@ -24,6 +125,27 @@ defmodule BlockScoutWeb.AddressViewTest do
end
end
+ describe "render_partial/1" do
+ test "renders _link partial" do
+ %Address{hash: hash} = build(:address)
+
+ assert {:safe, _} =
+ AddressView.render_partial(%{partial: "_link.html", address_hash: hash, contract: false, truncate: false})
+ end
+
+ test "renders _responsive_hash partial" do
+ %Address{hash: hash} = build(:address)
+
+ assert {:safe, _} =
+ AddressView.render_partial(%{
+ partial: "_responsive_hash.html",
+ address_hash: hash,
+ contract: false,
+ truncate: false
+ })
+ end
+ end
+
describe "smart_contract_verified?/1" do
test "returns true when smart contract is verified" do
smart_contract = insert(:smart_contract)
diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs
new file mode 100644
index 0000000000..6cd0cd3134
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs
@@ -0,0 +1,40 @@
+defmodule BlockScoutWeb.Tokens.HolderViewTest do
+ use BlockScoutWeb.ConnCase, async: true
+
+ alias BlockScoutWeb.Tokens.HolderView
+ alias Explorer.Chain.{Address.TokenBalance, Token}
+
+ doctest BlockScoutWeb.Tokens.HolderView, import: true
+
+ describe "total_supply_percentage/2" do
+ test "returns the percentage of the Token total supply" do
+ %Token{total_supply: total_supply} = build(:token, total_supply: 1000)
+ %TokenBalance{value: value} = build(:token_balance, value: 200)
+
+ assert HolderView.total_supply_percentage(value, total_supply) == "20.0000%"
+ end
+
+ test "considers 4 decimals" do
+ %Token{total_supply: total_supply} = build(:token, total_supply: 100_000_009)
+ %TokenBalance{value: value} = build(:token_balance, value: 500)
+
+ assert HolderView.total_supply_percentage(value, total_supply) == "0.0005%"
+ end
+ end
+
+ describe "format_token_balance_value/1" do
+ test "formats according to token decimals when it's a ERC-20" do
+ token = build(:token, type: "ERC-20", decimals: 2)
+ token_balance = build(:token_balance, value: 2_000_000)
+
+ assert HolderView.format_token_balance_value(token_balance.value, token) == "20,000"
+ end
+
+ test "returns the value when it's ERC-721" do
+ token = build(:token, type: "ERC-721")
+ token_balance = build(:token_balance, value: 1)
+
+ assert HolderView.format_token_balance_value(token_balance.value, token) == 1
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
index cef595b5aa..ee32c5df4e 100644
--- a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
@@ -5,6 +5,35 @@ defmodule BlockScoutWeb.TransactionViewTest do
alias Explorer.Repo
alias BlockScoutWeb.TransactionView
+ describe "confirmations/2" do
+ test "returns 0 if pending transaction" do
+ transaction = build(:transaction, block: nil)
+
+ assert 0 == TransactionView.confirmations(transaction, [])
+ end
+
+ test "returns string of number of blocks validated since subject block" do
+ block = insert(:block)
+
+ transaction =
+ :transaction
+ |> insert()
+ |> with_block(block)
+
+ assert "1" == TransactionView.confirmations(transaction, max_block_number: block.number + 1)
+ end
+ end
+
+ describe "contract_creation?/1" do
+ test "returns true if contract creation transaction" do
+ assert TransactionView.contract_creation?(build(:transaction, to_address: nil))
+ end
+
+ test "returns false if not contract" do
+ refute TransactionView.contract_creation?(build(:transaction))
+ end
+ end
+
describe "formatted_fee/2" do
test "pending transaction with no Receipt" do
{:ok, gas_price} = Wei.cast(3_000_000_000)
@@ -78,10 +107,32 @@ defmodule BlockScoutWeb.TransactionViewTest do
end
end
+ test "gas/1 returns the gas as a string" do
+ assert "2" == TransactionView.gas(build(:transaction, gas: 2))
+ end
+
+ test "hash/1 returns the hash as a string" do
+ assert "test" == TransactionView.hash(build(:transaction, hash: "test"))
+ end
+
describe "qr_code/1" do
test "it returns an encoded value" do
transaction = build(:transaction)
assert {:ok, _} = Base.decode64(TransactionView.qr_code(transaction))
end
end
+
+ describe "to_address_hash/1" do
+ test "returns contract address for created contract transaction" do
+ contract = insert(:contract_address)
+ transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
+ assert contract.hash == TransactionView.to_address_hash(transaction)
+ end
+
+ test "returns hash for transaction" do
+ address = insert(:address)
+ transaction = insert(:transaction, to_address: address)
+ assert address.hash == TransactionView.to_address_hash(transaction)
+ end
+ end
end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index d2c7ec0aaa..6816f16ba3 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -1635,11 +1635,6 @@ defmodule Explorer.Chain do
TokenTransfer.count_token_transfers_from_token_hash(token_address_hash)
end
- @spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
- def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
- TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_address_hash)
- end
-
@spec transaction_has_token_transfers?(Hash.t()) :: boolean()
def transaction_has_token_transfers?(transaction_hash) do
query = from(tt in TokenTransfer, where: tt.transaction_hash == ^transaction_hash, limit: 1, select: 1)
@@ -1718,4 +1713,18 @@ defmodule Explorer.Chain do
|> TokenBalance.last_token_balances()
|> Repo.all()
end
+
+ @spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
+ def fetch_token_holders_from_token_hash(contract_address_hash, options) do
+ contract_address_hash
+ |> TokenBalance.token_holders_ordered_by_value(options)
+ |> Repo.all()
+ end
+
+ @spec count_token_holders_from_token_hash(Hash.Address.t()) :: non_neg_integer()
+ def count_token_holders_from_token_hash(contract_address_hash) do
+ contract_address_hash
+ |> TokenBalance.token_holders_from_token_hash()
+ |> Repo.aggregate(:count, :address_hash)
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex
index 0ce0981da3..c8dec08d45 100644
--- a/apps/explorer/lib/explorer/chain/address/token_balance.ex
+++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex
@@ -5,11 +5,14 @@ defmodule Explorer.Chain.Address.TokenBalance do
use Ecto.Schema
import Ecto.Changeset
- import Ecto.Query, only: [from: 2]
+ import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, preload: 2]
+ alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.Address.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token}
+ @default_paging_options %PagingOptions{page_size: 50}
+
@typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
* `address_hash` - The address hash foreign key.
@@ -62,17 +65,68 @@ defmodule Explorer.Chain.Address.TokenBalance do
end
@doc """
- Builds an `Ecto.Query` to fetch the last token balances.
+ Builds an `Ecto.Query` to fetch the last token balances that have value greater than 0.
The last token balances from an Address is the last block indexed.
"""
def last_token_balances(address_hash) do
+ query =
+ from(
+ tb in TokenBalance,
+ where: tb.address_hash == ^address_hash,
+ distinct: :token_contract_address_hash,
+ order_by: [desc: :block_number]
+ )
+
+ from(tb in subquery(query), where: tb.value > 0, preload: :token)
+ end
+
+ @doc """
+ Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash.
+
+ The Token Holders are the addresses that own a positive amount of the Token. So this query is
+ considering the following conditions:
+
+ * The token balance from the last block.
+ * Balances greater than 0.
+ * Excluding the burn address (0x0000000000000000000000000000000000000000).
+
+ """
+ def token_holders_from_token_hash(token_contract_address_hash) do
+ query = token_holders_query(token_contract_address_hash)
+
+ from(tb in subquery(query), where: tb.value > 0)
+ end
+
+ def token_holders_ordered_by_value(token_contract_address_hash, options) do
+ paging_options = Keyword.get(options, :paging_options, @default_paging_options)
+
+ token_contract_address_hash
+ |> token_holders_from_token_hash()
+ |> order_by([tb], desc: tb.value, desc: tb.address_hash)
+ |> preload(:address)
+ |> page_token_balances(paging_options)
+ |> limit(^paging_options.page_size)
+ end
+
+ defp token_holders_query(contract_address_hash) do
+ {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
+
from(
tb in TokenBalance,
- where: tb.address_hash == ^address_hash and tb.value > 0,
- distinct: :token_contract_address_hash,
- order_by: [desc: :block_number],
- preload: :token
+ distinct: :address_hash,
+ where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^burn_address_hash,
+ order_by: [desc: :block_number]
+ )
+ end
+
+ defp page_token_balances(query, %PagingOptions{key: nil}), do: query
+
+ defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
+ where(
+ query,
+ [tb],
+ tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
)
end
end
diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex
index 1723c45ace..fd012da665 100644
--- a/apps/explorer/lib/explorer/chain/token_transfer.ex
+++ b/apps/explorer/lib/explorer/chain/token_transfer.ex
@@ -28,7 +28,6 @@ defmodule Explorer.Chain.TokenTransfer do
alias Explorer.Chain.{Address, Block, Hash, Transaction, TokenTransfer}
alias Explorer.{PagingOptions, Repo}
- alias Ecto.Adapters.SQL
@default_paging_options %PagingOptions{page_size: 50}
@@ -140,32 +139,6 @@ defmodule Explorer.Chain.TokenTransfer do
Repo.one(query)
end
- @spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
- def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
- {:ok, %{rows: [[result]]}} =
- SQL.query(
- Repo,
- """
- select count(*) as "addresses"
- from
- (
- select to_address_hash as "address_hash"
- from token_transfers tt1
- where tt1.token_contract_address_hash = $1
-
- union
-
- select from_address_hash as "address_hash"
- from token_transfers tt2
- where tt2.token_contract_address_hash = $1
- ) as addresses_count
- """,
- [token_address_hash.bytes]
- )
-
- result
- end
-
def page_token_transfer(query, %PagingOptions{key: nil}), do: query
def page_token_transfer(query, %PagingOptions{key: inserted_at}) do
diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs
index 3a2ec6a13c..a0ec6cf378 100644
--- a/apps/explorer/test/explorer/chain/token_transfer_test.exs
+++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs
@@ -142,53 +142,4 @@ defmodule Explorer.Chain.TokenTransferTest do
assert TokenTransfer.count_token_transfers_from_token_hash(token_contract_address.hash) == 2
end
end
-
- describe "count_addresses_in_transfers/1" do
- test "counts how many unique addresses that appeared at `to` or `from`" do
- token_contract_address = insert(:contract_address)
-
- transaction =
- :transaction
- |> insert()
- |> with_block()
-
- john_address = insert(:address)
- jane_address = insert(:address)
- bob_address = insert(:address)
-
- insert(
- :token_transfer,
- from_address: jane_address,
- to_address: john_address,
- transaction: transaction,
- token_contract_address: token_contract_address
- )
-
- insert(
- :token_transfer,
- from_address: john_address,
- to_address: jane_address,
- transaction: transaction,
- token_contract_address: token_contract_address
- )
-
- insert(
- :token_transfer,
- from_address: bob_address,
- to_address: jane_address,
- transaction: transaction,
- token_contract_address: token_contract_address
- )
-
- insert(
- :token_transfer,
- from_address: jane_address,
- to_address: bob_address,
- transaction: transaction,
- token_contract_address: token_contract_address
- )
-
- assert TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_contract_address.hash) == 3
- end
- end
end
diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs
index 3270d9aa4c..ce7c6351c7 100644
--- a/apps/explorer/test/explorer/chain_test.exs
+++ b/apps/explorer/test/explorer/chain_test.exs
@@ -507,28 +507,6 @@ defmodule Explorer.ChainTest do
end
end
- describe "count_addresses_in_token_transfers_from_token_hash/1" do
- test "without token transfers" do
- %Token{contract_address_hash: contract_address_hash} = insert(:token)
-
- assert Chain.count_addresses_in_token_transfers_from_token_hash(contract_address_hash) == 0
- end
-
- test "with token transfers" do
- address = insert(:address)
-
- transaction =
- :transaction
- |> insert()
- |> with_block()
-
- %TokenTransfer{token_contract_address_hash: token_contract_address_hash} =
- insert(:token_transfer, to_address: address, transaction: transaction)
-
- assert Chain.count_addresses_in_token_transfers_from_token_hash(token_contract_address_hash) == 2
- end
- end
-
describe "gas_price/2" do
test ":wei unit" do
assert Chain.gas_price(%Transaction{gas_price: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1)
@@ -2687,5 +2665,274 @@ defmodule Explorer.ChainTest do
assert Chain.fetch_last_token_balances(address.hash) == []
end
+
+ test "does not consider other blocks when the last block has the value 0" do
+ address = insert(:address)
+ token = insert(:token, contract_address: build(:contract_address))
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1000,
+ token_contract_address_hash: token.contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1001,
+ token_contract_address_hash: token.contract_address_hash,
+ value: 0
+ )
+
+ assert Chain.fetch_last_token_balances(address.hash) == []
+ end
+ end
+
+ describe "fetch_token_holders_from_token_hash/2" do
+ test "returns the last value for each address" do
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+ address = insert(:address)
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ block_number: 1001,
+ token_contract_address_hash: contract_address_hash,
+ value: 4000
+ )
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 2000
+ )
+
+ values =
+ contract_address_hash
+ |> Chain.fetch_token_holders_from_token_hash([])
+ |> Enum.map(&Decimal.to_integer(&1.value))
+
+ assert values == [4000, 2000]
+ end
+
+ test "sort by the hightest value" do
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 2000
+ )
+
+ insert(
+ :token_balance,
+ block_number: 1001,
+ token_contract_address_hash: contract_address_hash,
+ value: 1000
+ )
+
+ insert(
+ :token_balance,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 4000
+ )
+
+ insert(
+ :token_balance,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 3000
+ )
+
+ values =
+ contract_address_hash
+ |> Chain.fetch_token_holders_from_token_hash([])
+ |> Enum.map(&Decimal.to_integer(&1.value))
+
+ assert values == [4000, 3000, 2000, 1000]
+ end
+
+ test "returns only token balances that have value" do
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ token_contract_address_hash: contract_address_hash,
+ value: 0
+ )
+
+ assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
+ end
+
+ test "returns an empty list when there are no address with value greater than 0" do
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(:token_balance, value: 1000)
+
+ assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
+ end
+
+ test "ignores the burn address" do
+ {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
+
+ burn_address = insert(:address, hash: burn_address_hash)
+
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ address: burn_address,
+ token_contract_address_hash: contract_address_hash,
+ value: 1000
+ )
+
+ assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
+ end
+
+ test "paginates the result by value and different address" do
+ address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a")
+ address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
+
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ first_page =
+ insert(
+ :token_balance,
+ address: address_a,
+ token_contract_address_hash: contract_address_hash,
+ value: 4000
+ )
+
+ second_page =
+ insert(
+ :token_balance,
+ address: address_b,
+ token_contract_address_hash: contract_address_hash,
+ value: 4000
+ )
+
+ paging_options = %PagingOptions{
+ key: {first_page.value, first_page.address_hash},
+ page_size: 2
+ }
+
+ holders_paginated =
+ contract_address_hash
+ |> Chain.fetch_token_holders_from_token_hash(paging_options: paging_options)
+ |> Enum.map(& &1.address_hash)
+
+ assert holders_paginated == [second_page.address_hash]
+ end
+
+ test "considers the last block only if it has value" do
+ address = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 0
+ )
+
+ assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
+ end
+ end
+
+ describe "count_token_holders_from_token_hash" do
+ test "counts different addresses that have the token" do
+ address_a = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
+ address_b = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
+
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ address: address_a,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ address: address_b,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 1000
+ )
+
+ assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 2
+ end
+
+ test "counts only the last block" do
+ address = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
+
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 1000
+ )
+
+ assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 1
+ end
+
+ test "counts only the last block that has value greater than 0" do
+ address = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
+
+ %Token{contract_address_hash: contract_address_hash} = insert(:token)
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1000,
+ token_contract_address_hash: contract_address_hash,
+ value: 5000
+ )
+
+ insert(
+ :token_balance,
+ address: address,
+ block_number: 1002,
+ token_contract_address_hash: contract_address_hash,
+ value: 0
+ )
+
+ assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 0
+ end
end
end