Move business logic out of ExplorerWeb into context module, Explorer.Chain. No code in ExplorerWeb should access Explorer.Repo directly anymore.pull/118/head
parent
5e53958424
commit
8fb72a26ed
@ -0,0 +1,544 @@ |
|||||||
|
defmodule Explorer.Chain do |
||||||
|
@moduledoc """ |
||||||
|
The chain context. |
||||||
|
""" |
||||||
|
|
||||||
|
import Ecto.Query, only: [from: 2, order_by: 2, preload: 2, where: 2, where: 3] |
||||||
|
|
||||||
|
alias Explorer.Chain.{Address, Block, BlockTransaction, InternalTransaction, Log, Transaction} |
||||||
|
alias Explorer.Repo.NewRelic, as: Repo |
||||||
|
|
||||||
|
# Types |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
The name of an association on the `t:Ecto.Schema.t/0` |
||||||
|
""" |
||||||
|
@type association :: atom() |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
* `:optional` - the association is optional and only needs to be loaded if available |
||||||
|
* `:required` - the association is required and MUST be loaded. If it is not available, then the parent struct |
||||||
|
SHOULD NOT be returned. |
||||||
|
""" |
||||||
|
@type necessity :: :optional | :required |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
The `t:necessity/0` of each association that should be loaded |
||||||
|
""" |
||||||
|
@type necessity_by_association :: %{association => necessity} |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
Pagination params used by `scrivener` |
||||||
|
""" |
||||||
|
@type pagination :: map() |
||||||
|
|
||||||
|
@typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association} |
||||||
|
@typep pagination_option :: {:pagination, pagination} |
||||||
|
|
||||||
|
# Functions |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. |
||||||
|
* `:pagination` - pagination params to pass to scrivener. |
||||||
|
""" |
||||||
|
@spec block_to_transactions(Block.t()) :: %Scrivener.Page{entries: [Transaction.t()]} |
||||||
|
@spec block_to_transactions(Block.t(), [necessity_by_association_option | pagination_option]) :: |
||||||
|
%Scrivener.Page{entries: [Transaction.t()]} |
||||||
|
def block_to_transactions(%Block{id: block_id}, options \\ []) when is_list(options) do |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
pagination = Keyword.get(options, :pagination, %{}) |
||||||
|
|
||||||
|
query = |
||||||
|
from( |
||||||
|
transaction in Transaction, |
||||||
|
inner_join: block in assoc(transaction, :block), |
||||||
|
where: block.id == ^block_id, |
||||||
|
order_by: [desc: transaction.inserted_at] |
||||||
|
) |
||||||
|
|
||||||
|
query |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> Repo.paginate(pagination) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Counts the number of `t:Explorer.Chain.Transaction.t/0` in the `block`. |
||||||
|
""" |
||||||
|
@spec block_to_transaction_count(Block.t()) :: non_neg_integer() |
||||||
|
def block_to_transaction_count(%Block{id: block_id}) do |
||||||
|
query = |
||||||
|
from( |
||||||
|
block_transaction in BlockTransaction, |
||||||
|
join: block in assoc(block_transaction, :block), |
||||||
|
where: block_transaction.block_id == ^block_id |
||||||
|
) |
||||||
|
|
||||||
|
Repo.aggregate(query, :count, :block_id) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
How many blocks have confirmed `block` based on the current `max_block_number` |
||||||
|
""" |
||||||
|
@spec confirmations(Block.t(), [{:max_block_number, Block.block_number()}]) :: non_neg_integer() |
||||||
|
def confirmations(%Block{number: number}, named_arguments) when is_list(named_arguments) do |
||||||
|
max_block_number = Keyword.fetch!(named_arguments, :max_block_number) |
||||||
|
|
||||||
|
max_block_number - number |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Creates an address. |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
iex> Explorer.Addresses.create_address(%{field: value}) |
||||||
|
{:ok, %Address{}} |
||||||
|
|
||||||
|
iex> Explorer.Addresses.create_address(%{field: bad_value}) |
||||||
|
{:error, %Ecto.Changeset{}} |
||||||
|
|
||||||
|
""" |
||||||
|
@spec create_address(map()) :: {:ok, Address.t()} | {:error, Ecto.Changeset.t()} |
||||||
|
def create_address(attrs \\ %{}) do |
||||||
|
%Address{} |
||||||
|
|> Address.changeset(attrs) |
||||||
|
|> Repo.insert() |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Ensures that an `t:Explorer.Address.t/0` exists with the given `hash`. |
||||||
|
|
||||||
|
If a `t:Explorer.Address.t/0` with `hash` already exists, it is returned |
||||||
|
|
||||||
|
iex> Explorer.Addresses.ensure_hash_address(existing_hash) |
||||||
|
{:ok, %Address{}} |
||||||
|
|
||||||
|
If a `t:Explorer.Address.t/0` does not exist with `hash`, it is created and returned |
||||||
|
|
||||||
|
iex> Explorer.Addresses.ensure_hash_address(new_hash) |
||||||
|
{:ok, %Address{}} |
||||||
|
|
||||||
|
There is a chance of a race condition when interacting with the database: the `t:Explorer.Address.t/0` may not exist |
||||||
|
when first checked, then already exist when it is tried to be created because another connection creates the addres, |
||||||
|
then another process deletes the address after this process's connection see it was created, but before it can be |
||||||
|
retrieved. In scenario, the address may be not found as only one retry is attempted to prevent infinite loops. |
||||||
|
|
||||||
|
iex> Explorer.Addresses.ensure_hash_address(flicker_hash) |
||||||
|
{:error, :not_found} |
||||||
|
|
||||||
|
""" |
||||||
|
@spec ensure_hash_address(Address.hash()) :: {:ok, Address.t()} | {:error, :not_found} |
||||||
|
def ensure_hash_address(hash) when is_binary(hash) do |
||||||
|
with {:error, :not_found} <- hash_to_address(hash), |
||||||
|
{:error, _} <- create_address(%{hash: hash}) do |
||||||
|
# assume race condition occurred and someone else created the address between the first |
||||||
|
# hash_to_address and create_address |
||||||
|
hash_to_address(hash) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
`t:Explorer.Chain.Transaction/0`s from `address`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. |
||||||
|
* `:pagination` - pagination params to pass to scrivener. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec from_address_to_transactions(Address.t(), [ |
||||||
|
necessity_by_association_option | pagination_option |
||||||
|
]) :: %Scrivener.Page{entries: [Transaction.t()]} |
||||||
|
def from_address_to_transactions(address = %Address{}, options \\ []) |
||||||
|
when is_list(options) do |
||||||
|
address_to_transactions(address, Keyword.put(options, :direction, :from)) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Converts `t:Explorer.Chain.Address.t/0` `hash` to the `t:Explorer.Chain.Address.t/0` with that `hash`. |
||||||
|
|
||||||
|
Returns `{:ok, %Explorer.Chain.Address{}}` if found |
||||||
|
|
||||||
|
iex> hash_to_address("0x0addressaddressaddressaddressaddressaddr") |
||||||
|
{:ok, %Explorer.Chain.Address{}} |
||||||
|
|
||||||
|
Returns `{:error, :not_found}` if not found |
||||||
|
|
||||||
|
iex> hash_to_address("0x1addressaddressaddressaddressaddressaddr") |
||||||
|
{:error, :not_found} |
||||||
|
|
||||||
|
""" |
||||||
|
@spec hash_to_address(Address.hash()) :: {:ok, Address.t()} | {:error, :not_found} |
||||||
|
def hash_to_address(hash) do |
||||||
|
Address |
||||||
|
|> where_hash(hash) |
||||||
|
|> preload([:credit, :debit]) |
||||||
|
|> Repo.one() |
||||||
|
|> case do |
||||||
|
nil -> {:error, :not_found} |
||||||
|
address -> {:ok, address} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Converts `t:Explorer.Chain.Transaction.t/0` `hash` to the `t:Explorer.Chain.Transaction.t/0` with that `hash`. |
||||||
|
|
||||||
|
Returns `{:ok, %Explorer.Chain.Transaction{}}` if found |
||||||
|
|
||||||
|
iex> hash_to_transaction("0x0addressaddressaddressaddressaddressaddr") |
||||||
|
{:ok, %Explorer.Chain.Transaction{}} |
||||||
|
|
||||||
|
Returns `{:error, :not_found}` if not found |
||||||
|
|
||||||
|
iex> hash_to_transaction("0x1addressaddressaddressaddressaddressaddr") |
||||||
|
{:error, :not_found} |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. |
||||||
|
""" |
||||||
|
@spec hash_to_transaction(Transaction.hash(), [necessity_by_association_option]) :: |
||||||
|
{:ok, Transaction.t()} | {:error, :not_found} |
||||||
|
def hash_to_transaction(hash, options \\ []) when is_list(options) do |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
|
||||||
|
Transaction |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> where_hash(hash) |
||||||
|
|> Repo.one() |
||||||
|
|> case do |
||||||
|
nil -> {:error, :not_found} |
||||||
|
transaction -> {:ok, transaction} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Converts `t:Explorer.Address.t/0` `id` to the `t:Explorer.Address.t/0` with that `id`. |
||||||
|
|
||||||
|
Returns `{:ok, %Explorer.Address{}}` if found |
||||||
|
|
||||||
|
iex> id_to_address(123) |
||||||
|
{:ok, %Address{}} |
||||||
|
|
||||||
|
Returns `{:error, :not_found}` if not found |
||||||
|
|
||||||
|
iex> id_to_address(456) |
||||||
|
{:error, :not_found} |
||||||
|
|
||||||
|
""" |
||||||
|
@spec id_to_address(id :: non_neg_integer()) :: {:ok, Address.t()} | {:error, :not_found} |
||||||
|
def id_to_address(id) do |
||||||
|
Address |
||||||
|
|> Repo.get(id) |
||||||
|
|> case do |
||||||
|
nil -> |
||||||
|
{:error, :not_found} |
||||||
|
|
||||||
|
address -> |
||||||
|
{:ok, Repo.preload(address, [:credit, :debit])} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The last `t:Explorer.Chain.Transaction.t/0` `id`. |
||||||
|
""" |
||||||
|
@spec last_transaction_id([{:pending, boolean()}]) :: non_neg_integer() |
||||||
|
def last_transaction_id(options \\ []) when is_list(options) do |
||||||
|
query = |
||||||
|
from( |
||||||
|
t in Transaction, |
||||||
|
select: t.id, |
||||||
|
order_by: [desc: t.id], |
||||||
|
limit: 1 |
||||||
|
) |
||||||
|
|
||||||
|
query |
||||||
|
|> where_pending(options) |
||||||
|
|> Repo.one() |
||||||
|
|> Kernel.||(0) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Finds all `t:Explorer.Chain.Transaction.t/0` in the `t:Explorer.Chain.Block.t/0`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. |
||||||
|
* `:pagination` - pagination params to pass to scrivener. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec list_blocks([necessity_by_association_option | pagination_option]) :: %Scrivener.Page{ |
||||||
|
entries: [Block.t()] |
||||||
|
} |
||||||
|
def list_blocks(options \\ []) when is_list(options) do |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
pagination = Keyword.get(options, :pagination, %{}) |
||||||
|
|
||||||
|
Block |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> order_by(desc: :number) |
||||||
|
|> Repo.paginate(pagination) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
The maximum `t:Explorer.Chain.Block.t/0` `number` |
||||||
|
""" |
||||||
|
@spec max_block_number() :: Block.block_number() |
||||||
|
def max_block_number do |
||||||
|
Repo.aggregate(Block, :max, :number) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Finds `t:Explorer.Chain.Block.t/0` with `number` |
||||||
|
""" |
||||||
|
@spec number_to_block(Block.block_number()) :: {:ok, Block.t()} | {:error, :not_found} |
||||||
|
def number_to_block(number) do |
||||||
|
Block |
||||||
|
|> where(number: ^number) |
||||||
|
|> Repo.one() |
||||||
|
|> case do |
||||||
|
nil -> {:error, :not_found} |
||||||
|
block -> {:ok, block} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
`t:Explorer.Chain.Transaction/0`s to `address`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`. |
||||||
|
* `:pagination` - pagination params to pass to scrivener. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec to_address_to_transactions(Address.t(), [ |
||||||
|
necessity_by_association_option | pagination_option |
||||||
|
]) :: %Scrivener.Page{entries: [Transaction.t()]} |
||||||
|
def to_address_to_transactions(address = %Address{}, options \\ []) when is_list(options) do |
||||||
|
address_to_transactions(address, Keyword.put(options, :direction, :to)) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Count of `t:Explorer.Chain.Transaction.t/0`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:pending` |
||||||
|
* `true` - only count pending transactions |
||||||
|
* `false` - count all transactions |
||||||
|
|
||||||
|
""" |
||||||
|
@spec transaction_count([{:pending, boolean()}]) :: non_neg_integer() |
||||||
|
def transaction_count(options \\ []) when is_list(options) do |
||||||
|
Transaction |
||||||
|
|> where_pending(options) |
||||||
|
|> Repo.aggregate(:count, :id) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
`t:Explorer.Chain.InternalTransaction/0`s in `t:Explorer.Chain.Transaction.t/0` with `hash` |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, |
||||||
|
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec transaction_hash_to_internal_transactions(Transaction.hash()) :: [InternalTransaction.t()] |
||||||
|
def transaction_hash_to_internal_transactions(hash, options \\ []) |
||||||
|
when is_binary(hash) and is_list(options) do |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
|
||||||
|
InternalTransaction |
||||||
|
|> for_parent_transaction(hash) |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> Repo.all() |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Returns the list of transactions that occurred recently (10) before `t:Explorer.Chain.Transaction.t/0` `id`. |
||||||
|
|
||||||
|
## Examples |
||||||
|
|
||||||
|
iex> Explorer.Chain.list_transactions_before_id(id) |
||||||
|
[%Explorer.Chain.Transaction{}, ...] |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, |
||||||
|
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec transactions_recently_before_id(id :: non_neg_integer, [necessity_by_association_option]) :: |
||||||
|
[ |
||||||
|
Transaction.t() |
||||||
|
] |
||||||
|
def transactions_recently_before_id(id, options \\ []) when is_list(options) do |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
|
||||||
|
Transaction |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> recently_before_id(id) |
||||||
|
|> where_pending(options) |
||||||
|
|> Repo.all() |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Finds all `t:Explorer.Chain.Log.t/0`s for `t:Explorer.Chain.Transaction.t/0`. |
||||||
|
|
||||||
|
## Options |
||||||
|
|
||||||
|
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is |
||||||
|
`:required`, and the `t:Explorer.Chain.Log.t/0` has no associated record for that association, then the |
||||||
|
`t:Explorer.Chain.Log.t/0` will not be included in the page `entries`. |
||||||
|
* `:pagination` - pagination params to pass to scrivener. |
||||||
|
|
||||||
|
""" |
||||||
|
@spec transaction_to_logs(Transaction.t(), [ |
||||||
|
necessity_by_association_option | pagination_option |
||||||
|
]) :: %Scrivener.Page{entries: [Log.t()]} |
||||||
|
def transaction_to_logs(%Transaction{hash: hash}, options \\ []) when is_list(options) do |
||||||
|
transaction_hash_to_logs(hash, options) |
||||||
|
end |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Updates `balance` of `t:Explorer.Address.t/0` with `hash`. |
||||||
|
|
||||||
|
If `t:Explorer.Address.t/0` with `hash` does not already exist, it is created first. |
||||||
|
""" |
||||||
|
@spec update_balance(Address.hash(), Address.balance()) :: |
||||||
|
{:ok, Address.t()} | {:error, Ecto.Changeset.t()} | {:error, reason :: term} |
||||||
|
def update_balance(hash, balance) when is_binary(hash) do |
||||||
|
changes = %{ |
||||||
|
balance: balance |
||||||
|
} |
||||||
|
|
||||||
|
with {:ok, address} <- ensure_hash_address(hash) do |
||||||
|
address |
||||||
|
|> Address.balance_changeset(changes) |
||||||
|
|> Repo.update() |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
## Private Functions |
||||||
|
|
||||||
|
defp address_id_to_transactions(address_id, named_arguments) |
||||||
|
when is_integer(address_id) and is_list(named_arguments) do |
||||||
|
field = |
||||||
|
case Keyword.fetch!(named_arguments, :direction) do |
||||||
|
:to -> :to_address_id |
||||||
|
:from -> :from_address_id |
||||||
|
end |
||||||
|
|
||||||
|
necessity_by_association = Keyword.get(named_arguments, :necessity_by_association, %{}) |
||||||
|
pagination = Keyword.get(named_arguments, :pagination, %{}) |
||||||
|
|
||||||
|
Transaction |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> chronologically() |
||||||
|
|> where([t], field(t, ^field) == ^address_id) |
||||||
|
|> Repo.paginate(pagination) |
||||||
|
end |
||||||
|
|
||||||
|
defp address_to_transactions(%Address{id: address_id}, options) when is_list(options) do |
||||||
|
address_id_to_transactions(address_id, options) |
||||||
|
end |
||||||
|
|
||||||
|
defp chronologically(query) do |
||||||
|
from(q in query, order_by: [desc: q.inserted_at, desc: q.id]) |
||||||
|
end |
||||||
|
|
||||||
|
defp for_parent_transaction(query, hash) when is_binary(hash) do |
||||||
|
from( |
||||||
|
child in query, |
||||||
|
inner_join: transaction in assoc(child, :transaction), |
||||||
|
where: fragment("lower(?)", transaction.hash) == ^String.downcase(hash) |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
defp join_association(query, association, necessity) when is_atom(association) do |
||||||
|
case necessity do |
||||||
|
:optional -> |
||||||
|
preload(query, ^association) |
||||||
|
|
||||||
|
:required -> |
||||||
|
from(q in query, inner_join: a in assoc(q, ^association), preload: [{^association, a}]) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp join_associations(query, necessity_by_association) when is_map(necessity_by_association) do |
||||||
|
Enum.reduce(necessity_by_association, query, fn {association, join}, acc_query -> |
||||||
|
join_association(acc_query, association, join) |
||||||
|
end) |
||||||
|
end |
||||||
|
|
||||||
|
defp recently_before_id(query, id) do |
||||||
|
from( |
||||||
|
q in query, |
||||||
|
where: q.id < ^id, |
||||||
|
order_by: [desc: q.id], |
||||||
|
limit: 10 |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
defp transaction_hash_to_logs(transaction_hash, options) |
||||||
|
when is_binary(transaction_hash) and is_list(options) do |
||||||
|
lower_transaction_hash = String.downcase(transaction_hash) |
||||||
|
necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) |
||||||
|
pagination = Keyword.get(options, :pagination, %{}) |
||||||
|
|
||||||
|
query = |
||||||
|
from( |
||||||
|
log in Log, |
||||||
|
join: transaction in assoc(log, :transaction), |
||||||
|
where: fragment("lower(?)", transaction.hash) == ^lower_transaction_hash, |
||||||
|
order_by: [asc: :index] |
||||||
|
) |
||||||
|
|
||||||
|
query |
||||||
|
|> join_associations(necessity_by_association) |
||||||
|
|> Repo.paginate(pagination) |
||||||
|
end |
||||||
|
|
||||||
|
defp where_hash(query, hash) do |
||||||
|
from( |
||||||
|
q in query, |
||||||
|
where: fragment("lower(?)", q.hash) == ^String.downcase(hash) |
||||||
|
) |
||||||
|
end |
||||||
|
|
||||||
|
defp where_pending(query, options) when is_list(options) do |
||||||
|
pending = Keyword.get(options, :pending, false) |
||||||
|
|
||||||
|
where_pending(query, pending) |
||||||
|
end |
||||||
|
|
||||||
|
defp where_pending(query, false), do: query |
||||||
|
|
||||||
|
defp where_pending(query, true) do |
||||||
|
from( |
||||||
|
transaction in query, |
||||||
|
where: |
||||||
|
fragment( |
||||||
|
"NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)", |
||||||
|
transaction.id |
||||||
|
) |
||||||
|
) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,66 @@ |
|||||||
|
defmodule Explorer.Chain.Address do |
||||||
|
@moduledoc """ |
||||||
|
A stored representation of a web3 address. |
||||||
|
""" |
||||||
|
|
||||||
|
use Explorer.Schema |
||||||
|
|
||||||
|
alias Explorer.Chain.{Credit, Debit} |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
Hash of the public key for this address. |
||||||
|
""" |
||||||
|
@type hash :: Hash.t() |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
* `balance` - `credit.value - debit.value` |
||||||
|
* `balance_updated_at` - the last time `balance` was recalculated |
||||||
|
* `credit` - accumulation of all credits to the address `hash` |
||||||
|
* `debit` - accumulation of all debits to the address `hash` |
||||||
|
* `inserted_at` - when this address was inserted |
||||||
|
* `updated_at` when this address was last updated |
||||||
|
""" |
||||||
|
@type t :: %__MODULE__{ |
||||||
|
balance: Decimal.t(), |
||||||
|
balance_updated_at: DateTime.t(), |
||||||
|
credit: Ecto.Association.NotLoaded.t() | Credit.t() | nil, |
||||||
|
debit: Ecto.Association.NotLoaded.t() | Debit.t() | nil, |
||||||
|
hash: hash(), |
||||||
|
inserted_at: DateTime.t(), |
||||||
|
updated_at: DateTime.t() |
||||||
|
} |
||||||
|
|
||||||
|
schema "addresses" do |
||||||
|
field(:balance, :decimal) |
||||||
|
field(:balance_updated_at, Timex.Ecto.DateTime) |
||||||
|
field(:hash, :string) |
||||||
|
|
||||||
|
timestamps() |
||||||
|
|
||||||
|
has_one(:credit, Credit) |
||||||
|
has_one(:debit, Debit) |
||||||
|
end |
||||||
|
|
||||||
|
@required_attrs ~w(hash)a |
||||||
|
@optional_attrs ~w()a |
||||||
|
|
||||||
|
def changeset(%__MODULE__{} = address, attrs) do |
||||||
|
address |
||||||
|
|> cast(attrs, @required_attrs, @optional_attrs) |
||||||
|
|> validate_required(@required_attrs) |
||||||
|
|> update_change(:hash, &String.downcase/1) |
||||||
|
|> unique_constraint(:hash) |
||||||
|
end |
||||||
|
|
||||||
|
def balance_changeset(%__MODULE__{} = address, attrs) do |
||||||
|
address |
||||||
|
|> cast(attrs, [:balance]) |
||||||
|
|> validate_required([:balance]) |
||||||
|
|> put_balance_updated_at() |
||||||
|
end |
||||||
|
|
||||||
|
defp put_balance_updated_at(changeset) do |
||||||
|
changeset |
||||||
|
|> put_change(:balance_updated_at, Timex.now()) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,105 @@ |
|||||||
|
defmodule Explorer.Chain.Block do |
||||||
|
@moduledoc """ |
||||||
|
A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally |
||||||
|
other data. Because each block (except for the initial "genesis block") points to the previous block, the data |
||||||
|
structure that they form is called a "blockchain". |
||||||
|
""" |
||||||
|
|
||||||
|
use Explorer.Schema |
||||||
|
|
||||||
|
alias Explorer.Chain.{BlockTransaction, Hash, Transaction} |
||||||
|
|
||||||
|
# Types |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
How much work is required to find a hash with some number of leading 0s. It is measured in hashes for PoW |
||||||
|
(Proof-of-Work) chains like Ethereum. In PoA (Proof-of-Authority) chains, it does not apply as blocks are validated |
||||||
|
in a round-robin fashion, and so the value is always `Decimal.new(0)`. |
||||||
|
""" |
||||||
|
@type difficulty :: Decimal.t() |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
A measurement roughly equivalent to computational steps. Every operation has a gas expenditure; for most operations |
||||||
|
it is ~3-10, although some expensive operations have expenditures up to 700 and a transaction itself has an |
||||||
|
expenditure of 21000. |
||||||
|
""" |
||||||
|
@type gas :: non_neg_integer() |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
Number of the block in the chain. |
||||||
|
""" |
||||||
|
@type block_number :: non_neg_integer() |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
* `block_transactions` - The `t:Explorer.Chain.BlockTransaction.t/0`s joins this block to its `transactions` |
||||||
|
* `difficulty` - how hard the block was to mine. |
||||||
|
* `gas_limit` - If the total number of gas used by the computation spawned by the transaction, including the original |
||||||
|
message and any sub-messages that may be triggered, is less than or equal to the gas limit, then the transaction |
||||||
|
processes. If the total gas exceeds the gas limit, then all changes are reverted, except that the transaction is |
||||||
|
still valid and the fee can still be collected by the miner. |
||||||
|
* `gas_used` - The actual `t:gas/0` used to mine/validate the transactions in the block. |
||||||
|
* `hash` - the hash of the block. |
||||||
|
* `miner` - the hash of the `t:Explorer.Address.t/0` of the miner. In Proof-of-Authority chains, this is the |
||||||
|
validator. |
||||||
|
* `nonce` - the hash of the generated proof-of-work. Not used in Proof-of-Authority chains. |
||||||
|
* `number` - which block this is along the chain. |
||||||
|
* `parent_hash` - the hash of the parent block, which should have the previous `number` |
||||||
|
* `size` - The size of the block in bytes. |
||||||
|
* `timestamp` - When the block was collated |
||||||
|
* `total_diffficulty` - the total `difficulty` of the chain until this block. |
||||||
|
* `transactions` - the `t:Explorer.Chain.Transaction.t/0` in this block. |
||||||
|
""" |
||||||
|
@type t :: %__MODULE__{ |
||||||
|
block_transactions: %Ecto.Association.NotLoaded{} | [BlockTransaction.t()], |
||||||
|
difficulty: difficulty(), |
||||||
|
gas_limit: gas(), |
||||||
|
gas_used: gas(), |
||||||
|
hash: Hash.t(), |
||||||
|
miner: Address.hash(), |
||||||
|
nonce: Hash.t(), |
||||||
|
number: block_number(), |
||||||
|
parent_hash: Hash.t(), |
||||||
|
size: non_neg_integer(), |
||||||
|
timestamp: DateTime.t(), |
||||||
|
total_difficulty: difficulty(), |
||||||
|
transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()] |
||||||
|
} |
||||||
|
|
||||||
|
schema "blocks" do |
||||||
|
field(:difficulty, :decimal) |
||||||
|
field(:gas_limit, :integer) |
||||||
|
field(:gas_used, :integer) |
||||||
|
field(:hash, :string) |
||||||
|
field(:miner, :string) |
||||||
|
field(:nonce, :string) |
||||||
|
field(:number, :integer) |
||||||
|
field(:parent_hash, :string) |
||||||
|
field(:size, :integer) |
||||||
|
field(:timestamp, Timex.Ecto.DateTime) |
||||||
|
field(:total_difficulty, :decimal) |
||||||
|
|
||||||
|
timestamps() |
||||||
|
|
||||||
|
has_many(:block_transactions, BlockTransaction) |
||||||
|
many_to_many(:transactions, Transaction, join_through: "block_transactions") |
||||||
|
end |
||||||
|
|
||||||
|
@required_attrs ~w(number hash parent_hash nonce miner difficulty |
||||||
|
total_difficulty size gas_limit gas_used timestamp)a |
||||||
|
|
||||||
|
@doc false |
||||||
|
def changeset(%__MODULE__{} = block, attrs) do |
||||||
|
block |
||||||
|
|> cast(attrs, @required_attrs) |
||||||
|
|> validate_required(@required_attrs) |
||||||
|
|> update_change(:hash, &String.downcase/1) |
||||||
|
|> unique_constraint(:hash) |
||||||
|
|> cast_assoc(:transactions) |
||||||
|
end |
||||||
|
|
||||||
|
def null, do: %__MODULE__{number: -1, timestamp: :calendar.universal_time()} |
||||||
|
|
||||||
|
def latest(query) do |
||||||
|
query |> order_by(desc: :number) |
||||||
|
end |
||||||
|
end |
@ -1,20 +1,20 @@ |
|||||||
defmodule Explorer.BlockTransaction do |
defmodule Explorer.Chain.BlockTransaction do |
||||||
@moduledoc "Connects a Block to a Transaction" |
@moduledoc "Connects a Block to a Transaction" |
||||||
|
|
||||||
alias Explorer.BlockTransaction |
|
||||||
|
|
||||||
use Explorer.Schema |
use Explorer.Schema |
||||||
|
|
||||||
|
alias Explorer.Chain.{Block, Transaction} |
||||||
|
|
||||||
@primary_key false |
@primary_key false |
||||||
schema "block_transactions" do |
schema "block_transactions" do |
||||||
belongs_to(:block, Explorer.Block) |
belongs_to(:block, Block) |
||||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
belongs_to(:transaction, Transaction, primary_key: true) |
||||||
timestamps() |
timestamps() |
||||||
end |
end |
||||||
|
|
||||||
@required_attrs ~w(block_id transaction_id)a |
@required_attrs ~w(block_id transaction_id)a |
||||||
|
|
||||||
def changeset(%BlockTransaction{} = block_transaction, attrs \\ %{}) do |
def changeset(%__MODULE__{} = block_transaction, attrs \\ %{}) do |
||||||
block_transaction |
block_transaction |
||||||
|> cast(attrs, @required_attrs) |
|> cast(attrs, @required_attrs) |
||||||
|> validate_required(@required_attrs) |
|> validate_required(@required_attrs) |
@ -1,18 +1,19 @@ |
|||||||
defmodule Explorer.FromAddress do |
defmodule Explorer.Chain.FromAddress do |
||||||
@moduledoc false |
@moduledoc false |
||||||
|
|
||||||
use Explorer.Schema |
use Explorer.Schema |
||||||
|
|
||||||
alias Explorer.FromAddress |
alias Explorer.Chain.{Address, Transaction} |
||||||
|
|
||||||
@primary_key false |
@primary_key false |
||||||
schema "from_addresses" do |
schema "from_addresses" do |
||||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
belongs_to(:address, Address) |
||||||
belongs_to(:address, Explorer.Address) |
belongs_to(:transaction, Transaction, primary_key: true) |
||||||
|
|
||||||
timestamps() |
timestamps() |
||||||
end |
end |
||||||
|
|
||||||
def changeset(%FromAddress{} = to_address, attrs \\ %{}) do |
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do |
||||||
to_address |
to_address |
||||||
|> cast(attrs, [:transaction_id, :address_id]) |
|> cast(attrs, [:transaction_id, :address_id]) |
||||||
|> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index) |
|> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index) |
@ -0,0 +1,10 @@ |
|||||||
|
defmodule Explorer.Chain.Hash do |
||||||
|
@moduledoc """ |
||||||
|
Hash used throughout Ethereum chains. |
||||||
|
""" |
||||||
|
|
||||||
|
@typedoc """ |
||||||
|
[KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash as a string. |
||||||
|
""" |
||||||
|
@type t :: String.t() |
||||||
|
end |
@ -1,32 +1,32 @@ |
|||||||
defmodule Explorer.Log do |
defmodule Explorer.Chain.Log do |
||||||
@moduledoc "Captures a Web3 log entry generated by a transaction" |
@moduledoc "Captures a Web3 log entry generated by a transaction" |
||||||
|
|
||||||
use Explorer.Schema |
use Explorer.Schema |
||||||
|
|
||||||
alias Explorer.Address |
alias Explorer.Chain.{Address, Receipt} |
||||||
alias Explorer.Log |
|
||||||
alias Explorer.Receipt |
|
||||||
|
|
||||||
@required_attrs ~w(index data type)a |
@required_attrs ~w(address_id data index type)a |
||||||
@optional_attrs ~w( |
@optional_attrs ~w( |
||||||
first_topic second_topic third_topic fourth_topic address_id |
first_topic second_topic third_topic fourth_topic |
||||||
)a |
)a |
||||||
|
|
||||||
schema "logs" do |
schema "logs" do |
||||||
belongs_to(:receipt, Receipt) |
|
||||||
belongs_to(:address, Address) |
|
||||||
has_one(:transaction, through: [:receipt, :transaction]) |
|
||||||
field(:index, :integer) |
|
||||||
field(:data, :string) |
field(:data, :string) |
||||||
field(:type, :string) |
|
||||||
field(:first_topic, :string) |
field(:first_topic, :string) |
||||||
|
field(:fourth_topic, :string) |
||||||
|
field(:index, :integer) |
||||||
field(:second_topic, :string) |
field(:second_topic, :string) |
||||||
field(:third_topic, :string) |
field(:third_topic, :string) |
||||||
field(:fourth_topic, :string) |
field(:type, :string) |
||||||
|
|
||||||
timestamps() |
timestamps() |
||||||
|
|
||||||
|
belongs_to(:address, Address) |
||||||
|
belongs_to(:receipt, Receipt) |
||||||
|
has_one(:transaction, through: [:receipt, :transaction]) |
||||||
end |
end |
||||||
|
|
||||||
def changeset(%Log{} = log, attrs \\ %{}) do |
def changeset(%__MODULE__{} = log, attrs \\ %{}) do |
||||||
log |
log |
||||||
|> cast(attrs, @required_attrs) |
|> cast(attrs, @required_attrs) |
||||||
|> cast(attrs, @optional_attrs) |
|> cast(attrs, @optional_attrs) |
@ -0,0 +1,49 @@ |
|||||||
|
defmodule Explorer.Chain.Statistics.Server do |
||||||
|
@moduledoc "Stores the latest chain statistics." |
||||||
|
|
||||||
|
use GenServer |
||||||
|
|
||||||
|
alias Explorer.Chain.Statistics |
||||||
|
|
||||||
|
@interval 1_000 |
||||||
|
|
||||||
|
@spec fetch() :: Statistics.t() |
||||||
|
def fetch do |
||||||
|
case GenServer.whereis(__MODULE__) do |
||||||
|
nil -> Statistics.fetch() |
||||||
|
_ -> GenServer.call(__MODULE__, :fetch) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def start_link, do: start_link(true) |
||||||
|
|
||||||
|
def start_link(refresh) do |
||||||
|
GenServer.start_link(__MODULE__, refresh, name: __MODULE__) |
||||||
|
end |
||||||
|
|
||||||
|
def init(true) do |
||||||
|
{:noreply, chain} = handle_cast({:update, Statistics.fetch()}, %Statistics{}) |
||||||
|
{:ok, chain} |
||||||
|
end |
||||||
|
|
||||||
|
def init(false), do: {:ok, Statistics.fetch()} |
||||||
|
|
||||||
|
def handle_info(:refresh, %Statistics{} = statistics) do |
||||||
|
Task.start_link(fn -> |
||||||
|
GenServer.cast(__MODULE__, {:update, Statistics.fetch()}) |
||||||
|
end) |
||||||
|
|
||||||
|
{:noreply, statistics} |
||||||
|
end |
||||||
|
|
||||||
|
def handle_info(_, %Statistics{} = statistics), do: {:noreply, statistics} |
||||||
|
def handle_call(:fetch, _, %Statistics{} = statistics), do: {:reply, statistics, statistics} |
||||||
|
def handle_call(_, _, %Statistics{} = statistics), do: {:noreply, statistics} |
||||||
|
|
||||||
|
def handle_cast({:update, %Statistics{} = statistics}, %Statistics{} = _) do |
||||||
|
Process.send_after(self(), :refresh, @interval) |
||||||
|
{:noreply, statistics} |
||||||
|
end |
||||||
|
|
||||||
|
def handle_cast(_, %Statistics{} = statistics), do: {:noreply, statistics} |
||||||
|
end |
@ -1,17 +1,18 @@ |
|||||||
defmodule Explorer.ToAddress do |
defmodule Explorer.Chain.ToAddress do |
||||||
@moduledoc false |
@moduledoc false |
||||||
alias Explorer.ToAddress |
|
||||||
|
|
||||||
use Explorer.Schema |
use Explorer.Schema |
||||||
|
|
||||||
|
alias Explorer.Chain.{Address, Transaction} |
||||||
|
|
||||||
@primary_key false |
@primary_key false |
||||||
schema "to_addresses" do |
schema "to_addresses" do |
||||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
belongs_to(:address, Address) |
||||||
belongs_to(:address, Explorer.Address) |
belongs_to(:transaction, Transaction, primary_key: true) |
||||||
timestamps() |
timestamps() |
||||||
end |
end |
||||||
|
|
||||||
def changeset(%ToAddress{} = to_address, attrs \\ %{}) do |
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do |
||||||
to_address |
to_address |
||||||
|> cast(attrs, [:transaction_id, :address_id]) |
|> cast(attrs, [:transaction_id, :address_id]) |
||||||
|> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index) |
|> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index) |
@ -1,18 +1,17 @@ |
|||||||
defmodule Explorer.BalanceImporter do |
defmodule Explorer.BalanceImporter do |
||||||
@moduledoc "Imports a balance for a given address." |
@moduledoc "Imports a balance for a given address." |
||||||
|
|
||||||
alias Explorer.Address.Service, as: Address |
alias Explorer.{Chain, Ethereum} |
||||||
alias Explorer.Ethereum |
|
||||||
|
|
||||||
def import(hash) do |
def import(hash) do |
||||||
hash |
encoded_balance = Ethereum.download_balance(hash) |
||||||
|> Ethereum.download_balance() |
|
||||||
|> persist_balance(hash) |
persist_balance(hash, encoded_balance) |
||||||
end |
end |
||||||
|
|
||||||
defp persist_balance(balance, hash) do |
defp persist_balance(hash, encoded_balance) when is_binary(hash) do |
||||||
balance |
decoded_balance = Ethereum.decode_integer_field(encoded_balance) |
||||||
|> Ethereum.decode_integer_field() |
|
||||||
|> Address.update_balance(hash) |
Chain.update_balance(hash, decoded_balance) |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,56 +0,0 @@ |
|||||||
defmodule Explorer.Resource do |
|
||||||
@moduledoc "Looks up and fetches resource based on its handle (either an id or hash)" |
|
||||||
|
|
||||||
import Ecto.Query, only: [from: 2] |
|
||||||
|
|
||||||
alias Explorer.Block |
|
||||||
alias Explorer.Address |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
alias Explorer.Transaction |
|
||||||
|
|
||||||
def lookup(hash) when byte_size(hash) > 42, do: fetch_transaction(hash) |
|
||||||
|
|
||||||
def lookup(hash) when byte_size(hash) == 42, do: fetch_address(hash) |
|
||||||
|
|
||||||
def lookup(number), do: fetch_block(number) |
|
||||||
|
|
||||||
def fetch_address(hash) do |
|
||||||
query = |
|
||||||
from( |
|
||||||
address in Address, |
|
||||||
where: fragment("lower(?)", address.hash) == ^String.downcase(hash), |
|
||||||
limit: 1 |
|
||||||
) |
|
||||||
|
|
||||||
Repo.one(query) |
|
||||||
end |
|
||||||
|
|
||||||
def fetch_transaction(hash) do |
|
||||||
query = |
|
||||||
from( |
|
||||||
transaction in Transaction, |
|
||||||
where: fragment("lower(?)", transaction.hash) == ^String.downcase(hash), |
|
||||||
limit: 1 |
|
||||||
) |
|
||||||
|
|
||||||
Repo.one(query) |
|
||||||
end |
|
||||||
|
|
||||||
def fetch_block(block_number) when is_bitstring(block_number) do |
|
||||||
case Integer.parse(block_number) do |
|
||||||
{number, ""} -> fetch_block(number) |
|
||||||
_ -> nil |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
def fetch_block(number) when is_integer(number) do |
|
||||||
query = |
|
||||||
from( |
|
||||||
b in Block, |
|
||||||
where: b.number == ^number, |
|
||||||
limit: 1 |
|
||||||
) |
|
||||||
|
|
||||||
Repo.one(query) |
|
||||||
end |
|
||||||
end |
|
@ -1,43 +0,0 @@ |
|||||||
defmodule Explorer.Address do |
|
||||||
@moduledoc """ |
|
||||||
A stored representation of a web3 address. |
|
||||||
""" |
|
||||||
|
|
||||||
use Explorer.Schema |
|
||||||
|
|
||||||
alias Explorer.Address |
|
||||||
alias Explorer.Credit |
|
||||||
alias Explorer.Debit |
|
||||||
|
|
||||||
schema "addresses" do |
|
||||||
has_one(:credit, Credit) |
|
||||||
has_one(:debit, Debit) |
|
||||||
field(:hash, :string) |
|
||||||
field(:balance, :decimal) |
|
||||||
field(:balance_updated_at, Timex.Ecto.DateTime) |
|
||||||
timestamps() |
|
||||||
end |
|
||||||
|
|
||||||
@required_attrs ~w(hash)a |
|
||||||
@optional_attrs ~w()a |
|
||||||
|
|
||||||
def changeset(%Address{} = address, attrs) do |
|
||||||
address |
|
||||||
|> cast(attrs, @required_attrs, @optional_attrs) |
|
||||||
|> validate_required(@required_attrs) |
|
||||||
|> update_change(:hash, &String.downcase/1) |
|
||||||
|> unique_constraint(:hash) |
|
||||||
end |
|
||||||
|
|
||||||
def balance_changeset(%Address{} = address, attrs) do |
|
||||||
address |
|
||||||
|> cast(attrs, [:balance]) |
|
||||||
|> validate_required([:balance]) |
|
||||||
|> put_balance_updated_at() |
|
||||||
end |
|
||||||
|
|
||||||
defp put_balance_updated_at(changeset) do |
|
||||||
changeset |
|
||||||
|> put_change(:balance_updated_at, Timex.now()) |
|
||||||
end |
|
||||||
end |
|
@ -1,48 +0,0 @@ |
|||||||
defmodule Explorer.Block do |
|
||||||
@moduledoc """ |
|
||||||
Stores a web3 block. |
|
||||||
""" |
|
||||||
|
|
||||||
use Explorer.Schema |
|
||||||
|
|
||||||
alias Explorer.Block |
|
||||||
alias Explorer.BlockTransaction |
|
||||||
alias Explorer.Transaction |
|
||||||
|
|
||||||
schema "blocks" do |
|
||||||
has_many(:block_transactions, BlockTransaction) |
|
||||||
many_to_many(:transactions, Transaction, join_through: "block_transactions") |
|
||||||
|
|
||||||
field(:number, :integer) |
|
||||||
field(:hash, :string) |
|
||||||
field(:parent_hash, :string) |
|
||||||
field(:nonce, :string) |
|
||||||
field(:miner, :string) |
|
||||||
field(:difficulty, :decimal) |
|
||||||
field(:total_difficulty, :decimal) |
|
||||||
field(:size, :integer) |
|
||||||
field(:gas_limit, :integer) |
|
||||||
field(:gas_used, :integer) |
|
||||||
field(:timestamp, Timex.Ecto.DateTime) |
|
||||||
timestamps() |
|
||||||
end |
|
||||||
|
|
||||||
@required_attrs ~w(number hash parent_hash nonce miner difficulty |
|
||||||
total_difficulty size gas_limit gas_used timestamp)a |
|
||||||
|
|
||||||
@doc false |
|
||||||
def changeset(%Block{} = block, attrs) do |
|
||||||
block |
|
||||||
|> cast(attrs, @required_attrs) |
|
||||||
|> validate_required(@required_attrs) |
|
||||||
|> update_change(:hash, &String.downcase/1) |
|
||||||
|> unique_constraint(:hash) |
|
||||||
|> cast_assoc(:transactions) |
|
||||||
end |
|
||||||
|
|
||||||
def null, do: %Block{number: -1, timestamp: :calendar.universal_time()} |
|
||||||
|
|
||||||
def latest(query) do |
|
||||||
query |> order_by(desc: :number) |
|
||||||
end |
|
||||||
end |
|
@ -1,48 +0,0 @@ |
|||||||
defmodule Explorer.Servers.ChainStatistics do |
|
||||||
@moduledoc "Stores the latest chain statistics." |
|
||||||
|
|
||||||
use GenServer |
|
||||||
|
|
||||||
alias Explorer.Chain |
|
||||||
|
|
||||||
@interval 1_000 |
|
||||||
|
|
||||||
def fetch do |
|
||||||
case GenServer.whereis(__MODULE__) do |
|
||||||
nil -> Chain.fetch() |
|
||||||
_ -> GenServer.call(__MODULE__, :fetch) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
def start_link, do: start_link(true) |
|
||||||
|
|
||||||
def start_link(refresh) do |
|
||||||
GenServer.start_link(__MODULE__, refresh, name: __MODULE__) |
|
||||||
end |
|
||||||
|
|
||||||
def init(true) do |
|
||||||
{:noreply, chain} = handle_cast({:update, Chain.fetch()}, %Chain{}) |
|
||||||
{:ok, chain} |
|
||||||
end |
|
||||||
|
|
||||||
def init(false), do: {:ok, Chain.fetch()} |
|
||||||
|
|
||||||
def handle_info(:refresh, %Chain{} = chain) do |
|
||||||
Task.start_link(fn -> |
|
||||||
GenServer.cast(__MODULE__, {:update, Chain.fetch()}) |
|
||||||
end) |
|
||||||
|
|
||||||
{:noreply, chain} |
|
||||||
end |
|
||||||
|
|
||||||
def handle_info(_, %Chain{} = chain), do: {:noreply, chain} |
|
||||||
def handle_call(:fetch, _, %Chain{} = chain), do: {:reply, chain, chain} |
|
||||||
def handle_call(_, _, %Chain{} = chain), do: {:noreply, chain} |
|
||||||
|
|
||||||
def handle_cast({:update, %Chain{} = chain}, %Chain{} = _) do |
|
||||||
Process.send_after(self(), :refresh, @interval) |
|
||||||
{:noreply, chain} |
|
||||||
end |
|
||||||
|
|
||||||
def handle_cast(_, %Chain{} = chain), do: {:noreply, chain} |
|
||||||
end |
|
@ -1,58 +0,0 @@ |
|||||||
defmodule Explorer.Address.Service do |
|
||||||
@moduledoc "Service module for interacting with Addresses" |
|
||||||
|
|
||||||
alias Explorer.Address |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
alias Explorer.Address.Service.Query |
|
||||||
|
|
||||||
def by_hash(hash) do |
|
||||||
Address |
|
||||||
|> Query.by_hash(hash) |
|
||||||
|> Query.include_credit_and_debit() |
|
||||||
|> Repo.one() |
|
||||||
end |
|
||||||
|
|
||||||
def update_balance(balance, hash) do |
|
||||||
changes = %{ |
|
||||||
balance: balance |
|
||||||
} |
|
||||||
|
|
||||||
hash |
|
||||||
|> find_or_create_by_hash() |
|
||||||
|> Address.balance_changeset(changes) |
|
||||||
|> Repo.update() |
|
||||||
end |
|
||||||
|
|
||||||
def find_or_create_by_hash(hash) do |
|
||||||
Address |
|
||||||
|> Query.by_hash(hash) |
|
||||||
|> Repo.one() |
|
||||||
|> case do |
|
||||||
nil -> Repo.insert!(Address.changeset(%Address{}, %{hash: hash})) |
|
||||||
address -> address |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
defmodule Query do |
|
||||||
@moduledoc "Query module for pulling in aspects of Addresses." |
|
||||||
|
|
||||||
import Ecto.Query, only: [from: 2] |
|
||||||
|
|
||||||
def by_hash(query, hash) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
where: fragment("lower(?)", q.hash) == ^String.downcase(hash), |
|
||||||
limit: 1 |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def include_credit_and_debit(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
left_join: credit in assoc(q, :credit), |
|
||||||
left_join: debit in assoc(q, :debit), |
|
||||||
preload: [:credit, :debit] |
|
||||||
) |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,118 +0,0 @@ |
|||||||
defmodule Explorer.Transaction.Service do |
|
||||||
@moduledoc "Service module for interacting with Transactions" |
|
||||||
|
|
||||||
alias Explorer.InternalTransaction |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
alias Explorer.Transaction.Service.Query |
|
||||||
|
|
||||||
def internal_transactions(hash) do |
|
||||||
InternalTransaction |
|
||||||
|> Query.for_parent_transaction(hash) |
|
||||||
|> Query.join_from_and_to_addresses() |
|
||||||
|> Repo.all() |
|
||||||
end |
|
||||||
|
|
||||||
defmodule Query do |
|
||||||
@moduledoc "Helper module to hold Transaction-related query fragments" |
|
||||||
|
|
||||||
import Ecto.Query, only: [from: 2] |
|
||||||
|
|
||||||
def to_address(query, to_address_id) do |
|
||||||
from(q in query, where: q.to_address_id == ^to_address_id) |
|
||||||
end |
|
||||||
|
|
||||||
def from_address(query, from_address_id) do |
|
||||||
from(q in query, where: q.from_address_id == ^from_address_id) |
|
||||||
end |
|
||||||
|
|
||||||
def recently_seen(query, last_seen) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
where: q.id < ^last_seen, |
|
||||||
order_by: [desc: q.id], |
|
||||||
limit: 10 |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def by_hash(query, hash) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
where: fragment("lower(?)", q.hash) == ^String.downcase(hash), |
|
||||||
limit: 1 |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def include_addresses(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
inner_join: to_address in assoc(q, :to_address), |
|
||||||
inner_join: from_address in assoc(q, :from_address), |
|
||||||
preload: [ |
|
||||||
to_address: to_address, |
|
||||||
from_address: from_address |
|
||||||
] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def include_receipt(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
left_join: receipt in assoc(q, :receipt), |
|
||||||
preload: [ |
|
||||||
receipt: receipt |
|
||||||
] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def include_block(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
left_join: block in assoc(q, :block), |
|
||||||
preload: [ |
|
||||||
block: block |
|
||||||
] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def require_receipt(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
inner_join: receipt in assoc(q, :receipt), |
|
||||||
preload: [ |
|
||||||
receipt: receipt |
|
||||||
] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def require_block(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
inner_join: block in assoc(q, :block), |
|
||||||
preload: [ |
|
||||||
block: block |
|
||||||
] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def for_parent_transaction(query, hash) do |
|
||||||
from( |
|
||||||
child in query, |
|
||||||
inner_join: transaction in assoc(child, :transaction), |
|
||||||
where: fragment("lower(?)", transaction.hash) == ^String.downcase(hash) |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def join_from_and_to_addresses(query) do |
|
||||||
from( |
|
||||||
q in query, |
|
||||||
inner_join: to_address in assoc(q, :to_address), |
|
||||||
inner_join: from_address in assoc(q, :from_address), |
|
||||||
preload: [:to_address, :from_address] |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
def chron(query) do |
|
||||||
from(q in query, order_by: [desc: q.inserted_at]) |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,6 +1,7 @@ |
|||||||
defmodule Explorer.AddressTest do |
defmodule Explorer.Chain.AddressTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
alias Explorer.Address |
|
||||||
|
alias Explorer.Chain.Address |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
@ -1,9 +1,10 @@ |
|||||||
defmodule Explorer.BlockTest do |
defmodule Explorer.Chain.BlockTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Block |
|
||||||
import Ecto.Query, only: [order_by: 2] |
import Ecto.Query, only: [order_by: 2] |
||||||
|
|
||||||
|
alias Explorer.Chain.Block |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
||||||
changeset = build(:block) |> Block.changeset(%{}) |
changeset = build(:block) |> Block.changeset(%{}) |
@ -1,6 +1,7 @@ |
|||||||
defmodule Explorer.BlockTransactionTest do |
defmodule Explorer.Chain.BlockTransactionTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
alias Explorer.BlockTransaction |
|
||||||
|
alias Explorer.Chain.BlockTransaction |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with empty attributes" do |
test "with empty attributes" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.CreditTest do |
defmodule Explorer.Chain.CreditTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Credit |
alias Explorer.Chain.Credit |
||||||
|
|
||||||
describe "Repo.all/1" do |
describe "Repo.all/1" do |
||||||
test "returns no rows when there are no addresses" do |
test "returns no rows when there are no addresses" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.DebitTest do |
defmodule Explorer.Chain.DebitTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Debit |
alias Explorer.Chain.Debit |
||||||
|
|
||||||
describe "Repo.all/1" do |
describe "Repo.all/1" do |
||||||
test "returns no rows when there are no addresses" do |
test "returns no rows when there are no addresses" do |
@ -1,6 +1,7 @@ |
|||||||
defmodule Explorer.FromAddressTest do |
defmodule Explorer.Chain.FromAddressTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
alias Explorer.FromAddress |
|
||||||
|
alias Explorer.Chain.FromAddress |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.InternalTransactionTest do |
defmodule Explorer.Chain.InternalTransactionTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.InternalTransaction |
alias Explorer.Chain.InternalTransaction |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.LogTest do |
defmodule Explorer.Chain.LogTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Log |
alias Explorer.Chain.Log |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "accepts valid attributes" do |
test "accepts valid attributes" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.ReceiptTest do |
defmodule Explorer.Chain.ReceiptTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Receipt |
alias Explorer.Chain.Receipt |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "accepts valid attributes" do |
test "accepts valid attributes" do |
@ -0,0 +1,89 @@ |
|||||||
|
defmodule Explorer.Chain.Statistics.ServerTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain.Statistics |
||||||
|
alias Explorer.Chain.Statistics.Server |
||||||
|
|
||||||
|
describe "init/1" do |
||||||
|
test "returns a new chain when not told to refresh" do |
||||||
|
{:ok, statistics} = Server.init(false) |
||||||
|
|
||||||
|
assert statistics.number == Statistics.fetch().number |
||||||
|
end |
||||||
|
|
||||||
|
test "returns a new chain when told to refresh" do |
||||||
|
{:ok, statistics} = Server.init(true) |
||||||
|
|
||||||
|
assert statistics == Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "refreshes when told to refresh" do |
||||||
|
{:ok, _} = Server.init(true) |
||||||
|
|
||||||
|
assert_receive :refresh, 2_000 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "fetch/0" do |
||||||
|
test "fetches the chain when not started" do |
||||||
|
original = Statistics.fetch() |
||||||
|
|
||||||
|
assert Server.fetch() == original |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "handle_info/2" do |
||||||
|
test "returns the original chain when sent a :refresh message" do |
||||||
|
original = Statistics.fetch() |
||||||
|
|
||||||
|
assert {:noreply, ^original} = Server.handle_info(:refresh, original) |
||||||
|
end |
||||||
|
|
||||||
|
test "launches an update when sent a :refresh message" do |
||||||
|
original = Statistics.fetch() |
||||||
|
{:ok, pid} = Server.start_link() |
||||||
|
chain = Server.fetch() |
||||||
|
:ok = GenServer.stop(pid) |
||||||
|
|
||||||
|
assert original.number == chain.number |
||||||
|
end |
||||||
|
|
||||||
|
test "does not reply when sent any other message" do |
||||||
|
assert {:noreply, _} = Server.handle_info(:ham, %Statistics{}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "handle_call/3" do |
||||||
|
test "replies with statistics when sent a :fetch message" do |
||||||
|
original = Statistics.fetch() |
||||||
|
|
||||||
|
assert {:reply, _, ^original} = Server.handle_call(:fetch, self(), original) |
||||||
|
end |
||||||
|
|
||||||
|
test "does not reply when sent any other message" do |
||||||
|
assert {:noreply, _} = Server.handle_call(:ham, self(), %Statistics{}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "handle_cast/2" do |
||||||
|
test "schedules a refresh of the statistics when sent an update" do |
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
Server.handle_cast({:update, statistics}, %Statistics{}) |
||||||
|
|
||||||
|
assert_receive :refresh, 2_000 |
||||||
|
end |
||||||
|
|
||||||
|
test "returns a noreply and the new incoming chain when sent an update" do |
||||||
|
original = Statistics.fetch() |
||||||
|
|
||||||
|
assert {:noreply, ^original} = Server.handle_cast({:update, original}, %Statistics{}) |
||||||
|
end |
||||||
|
|
||||||
|
test "returns a noreply and the old chain when sent any other cast" do |
||||||
|
original = Statistics.fetch() |
||||||
|
|
||||||
|
assert {:noreply, ^original} = Server.handle_cast(:ham, original) |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,116 @@ |
|||||||
|
defmodule Explorer.Chain.StatisticsTest do |
||||||
|
use Explorer.DataCase |
||||||
|
|
||||||
|
alias Explorer.Chain.Statistics |
||||||
|
alias Timex.Duration |
||||||
|
|
||||||
|
describe "fetch/0" do |
||||||
|
test "returns -1 for the number when there are no blocks" do |
||||||
|
assert %Statistics{number: -1} = Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the highest block number when there is a block" do |
||||||
|
insert(:block, number: 1) |
||||||
|
|
||||||
|
max_number = 100 |
||||||
|
insert(:block, number: max_number) |
||||||
|
|
||||||
|
assert %Statistics{number: ^max_number} = Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the latest block timestamp" do |
||||||
|
time = DateTime.utc_now() |
||||||
|
insert(:block, timestamp: time) |
||||||
|
|
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
assert Timex.diff(statistics.timestamp, time, :seconds) == 0 |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the average time between blocks" do |
||||||
|
time = DateTime.utc_now() |
||||||
|
next_time = Timex.shift(time, seconds: 5) |
||||||
|
insert(:block, timestamp: time) |
||||||
|
insert(:block, timestamp: next_time) |
||||||
|
|
||||||
|
assert %Statistics{ |
||||||
|
average_time: %Duration{ |
||||||
|
seconds: 5, |
||||||
|
megaseconds: 0, |
||||||
|
microseconds: 0 |
||||||
|
} |
||||||
|
} = Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the count of transactions from blocks in the last day" do |
||||||
|
time = DateTime.utc_now() |
||||||
|
last_week = Timex.shift(time, days: -8) |
||||||
|
block = insert(:block, timestamp: time) |
||||||
|
old_block = insert(:block, timestamp: last_week) |
||||||
|
transaction = insert(:transaction) |
||||||
|
old_transaction = insert(:transaction) |
||||||
|
insert(:block_transaction, block: block, transaction: transaction) |
||||||
|
insert(:block_transaction, block: old_block, transaction: old_transaction) |
||||||
|
|
||||||
|
assert %Statistics{transaction_count: 1} = Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the number of skipped blocks" do |
||||||
|
insert(:block, %{number: 0}) |
||||||
|
insert(:block, %{number: 2}) |
||||||
|
|
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
assert statistics.skipped_blocks == 1 |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the lag between validation and insertion time" do |
||||||
|
validation_time = DateTime.utc_now() |
||||||
|
inserted_at = validation_time |> Timex.shift(seconds: 5) |
||||||
|
insert(:block, timestamp: validation_time, inserted_at: inserted_at) |
||||||
|
|
||||||
|
assert %Statistics{lag: %Duration{seconds: 5, megaseconds: 0, microseconds: 0}} = |
||||||
|
Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the number of blocks inserted in the last minute" do |
||||||
|
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) |
||||||
|
insert(:block, inserted_at: old_inserted_at) |
||||||
|
insert(:block) |
||||||
|
|
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
assert statistics.block_velocity == 1 |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the number of transactions inserted in the last minute" do |
||||||
|
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) |
||||||
|
insert(:transaction, inserted_at: old_inserted_at) |
||||||
|
insert(:transaction) |
||||||
|
|
||||||
|
assert %Statistics{transaction_velocity: 1} = Statistics.fetch() |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the last five blocks" do |
||||||
|
insert_list(6, :block) |
||||||
|
|
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
assert statistics.blocks |> Enum.count() == 5 |
||||||
|
end |
||||||
|
|
||||||
|
test "returns the last five transactions with blocks" do |
||||||
|
block = insert(:block) |
||||||
|
|
||||||
|
6 |
||||||
|
|> insert_list(:transaction) |
||||||
|
|> Enum.map(fn transaction -> |
||||||
|
insert(:block_transaction, block: block, transaction: transaction) |
||||||
|
end) |
||||||
|
|
||||||
|
statistics = Statistics.fetch() |
||||||
|
|
||||||
|
assert statistics.transactions |> Enum.count() == 5 |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -1,6 +1,7 @@ |
|||||||
defmodule Explorer.ToAddressTest do |
defmodule Explorer.Chain.ToAddressTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
alias Explorer.ToAddress |
|
||||||
|
alias Explorer.Chain.ToAddress |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
|||||||
defmodule Explorer.TransactionTest do |
defmodule Explorer.Chain.TransactionTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Transaction |
alias Explorer.Chain.Transaction |
||||||
|
|
||||||
describe "changeset/2" do |
describe "changeset/2" do |
||||||
test "with valid attributes" do |
test "with valid attributes" do |
@ -1,103 +1,757 @@ |
|||||||
defmodule Explorer.ChainTest do |
defmodule Explorer.ChainTest do |
||||||
use Explorer.DataCase |
use Explorer.DataCase |
||||||
|
|
||||||
alias Explorer.Chain |
alias Explorer.{Chain, Repo} |
||||||
alias Timex.Duration |
|
||||||
|
|
||||||
describe "fetch/0" do |
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Receipt, Transaction} |
||||||
test "returns -1 for the number when there are no blocks" do |
|
||||||
chain = Chain.fetch() |
# Constants |
||||||
assert chain.number == -1 |
|
||||||
|
@invalid_attrs %{hash: nil} |
||||||
|
@valid_attrs %{hash: "some hash"} |
||||||
|
|
||||||
|
# Tests |
||||||
|
|
||||||
|
describe "block_to_transactions/1" do |
||||||
|
test "without transactions" do |
||||||
|
block = insert(:block) |
||||||
|
|
||||||
|
assert Repo.aggregate(Transaction, :count, :id) == 0 |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 0 |
||||||
|
} = Chain.block_to_transactions(block) |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions" do |
||||||
|
block = %Block{id: block_id} = insert(:block) |
||||||
|
%Transaction{id: transaction_id} = insert(:transaction) |
||||||
|
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = Chain.block_to_transactions(block) |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction with receipt required without receipt does not return transaction" do |
||||||
|
block = %Block{id: block_id} = insert(:block) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id_with_receipt} = insert(:transaction) |
||||||
|
insert(:receipt, transaction_id: transaction_id_with_receipt) |
||||||
|
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id_with_receipt) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id_without_receipt} = insert(:transaction) |
||||||
|
|
||||||
|
insert( |
||||||
|
:block_transaction, |
||||||
|
block_id: block_id, |
||||||
|
transaction_id: transaction_id_without_receipt |
||||||
|
) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = |
||||||
|
Chain.block_to_transactions( |
||||||
|
block, |
||||||
|
necessity_by_association: %{receipt: :required} |
||||||
|
) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: transactions, |
||||||
|
page_number: 1, |
||||||
|
total_entries: 2 |
||||||
|
} = |
||||||
|
Chain.block_to_transactions( |
||||||
|
block, |
||||||
|
necessity_by_association: %{receipt: :optional} |
||||||
|
) |
||||||
|
|
||||||
|
assert length(transactions) == 2 |
||||||
|
|
||||||
|
transaction_by_id = |
||||||
|
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> |
||||||
|
{id, transaction} |
||||||
|
end) |
||||||
|
|
||||||
|
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] |
||||||
|
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions can be paginated" do |
||||||
|
block = %Block{id: block_id} = insert(:block) |
||||||
|
|
||||||
|
transactions = insert_list(2, :transaction) |
||||||
|
|
||||||
|
Enum.each(transactions, fn %Transaction{id: transaction_id} -> |
||||||
|
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id) |
||||||
|
end) |
||||||
|
|
||||||
|
[%Transaction{id: first_transaction_id}, %Transaction{id: second_transaction_id}] = |
||||||
|
transactions |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^first_transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.block_to_transactions(block, pagination: %{page_size: 1}) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^second_transaction_id}], |
||||||
|
page_number: 2, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.block_to_transactions(block, pagination: %{page: 2, page_size: 1}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "block_to_transaction_bound/1" do |
||||||
|
test "without transactions" do |
||||||
|
block = insert(:block) |
||||||
|
|
||||||
|
assert Chain.block_to_transaction_count(block) == 0 |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions" do |
||||||
|
block = insert(:block) |
||||||
|
%Transaction{id: transaction_id} = insert(:transaction) |
||||||
|
insert(:block_transaction, block_id: block.id, transaction_id: transaction_id) |
||||||
|
|
||||||
|
assert Chain.block_to_transaction_count(block) == 1 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "confirmations/1" do |
||||||
|
test "with block.number == max_block_number " do |
||||||
|
block = insert(:block) |
||||||
|
max_block_number = Chain.max_block_number() |
||||||
|
|
||||||
|
assert block.number == max_block_number |
||||||
|
assert Chain.confirmations(block, max_block_number: max_block_number) == 0 |
||||||
|
end |
||||||
|
|
||||||
|
test "with block.number < max_block_number" do |
||||||
|
block = insert(:block) |
||||||
|
max_block_number = block.number + 2 |
||||||
|
|
||||||
|
assert block.number < max_block_number |
||||||
|
|
||||||
|
assert Chain.confirmations(block, max_block_number: max_block_number) == |
||||||
|
max_block_number - block.number |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "create_address/1" do |
||||||
|
test "with valid data creates a address" do |
||||||
|
assert {:ok, %Address{} = address} = Chain.create_address(@valid_attrs) |
||||||
|
assert address.hash == "some hash" |
||||||
|
end |
||||||
|
|
||||||
|
test "with invalid data returns error changeset" do |
||||||
|
assert {:error, %Ecto.Changeset{}} = Chain.create_address(@invalid_attrs) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "ensure_hash_address/1" do |
||||||
|
test "creates a new address when one does not exist" do |
||||||
|
Chain.ensure_hash_address("0xFreshPrince") |
||||||
|
|
||||||
|
assert {:ok, _} = Chain.hash_to_address("0xfreshprince") |
||||||
end |
end |
||||||
|
|
||||||
test "returns the highest block number when there is a block" do |
test "when the address already exists doesn't insert a new address" do |
||||||
|
insert(:address, %{hash: "bigmouthbillybass"}) |
||||||
|
|
||||||
|
before = Repo.aggregate(Address, :count, :id) |
||||||
|
|
||||||
|
assert {:ok, _} = Chain.ensure_hash_address("bigmouthbillybass") |
||||||
|
|
||||||
|
assert Repo.aggregate(Address, :count, :id) == before |
||||||
|
end |
||||||
|
|
||||||
|
test "when there is no hash it blows up" do |
||||||
|
assert {:error, :not_found} = Chain.ensure_hash_address("") |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "from_address_to_transactions/2" do |
||||||
|
test "without transactions" do |
||||||
|
address = insert(:address) |
||||||
|
|
||||||
|
assert Repo.aggregate(Transaction, :count, :id) == 0 |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 0 |
||||||
|
} = Chain.from_address_to_transactions(address) |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions" do |
||||||
|
%Transaction{from_address_id: from_address_id, id: transaction_id} = insert(:transaction) |
||||||
|
address = Repo.get!(Address, from_address_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = Chain.from_address_to_transactions(address) |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions with receipt required without receipt does not return transaction" do |
||||||
|
address = %Address{id: from_address_id} = insert(:address) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id_with_receipt} = |
||||||
|
insert(:transaction, from_address_id: from_address_id) |
||||||
|
|
||||||
|
insert(:receipt, transaction_id: transaction_id_with_receipt) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id_without_receipt} = |
||||||
|
insert(:transaction, from_address_id: from_address_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = |
||||||
|
Chain.from_address_to_transactions( |
||||||
|
address, |
||||||
|
necessity_by_association: %{receipt: :required} |
||||||
|
) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: transactions, |
||||||
|
page_number: 1, |
||||||
|
total_entries: 2 |
||||||
|
} = |
||||||
|
Chain.from_address_to_transactions( |
||||||
|
address, |
||||||
|
necessity_by_association: %{receipt: :optional} |
||||||
|
) |
||||||
|
|
||||||
|
assert length(transactions) == 2 |
||||||
|
|
||||||
|
transaction_by_id = |
||||||
|
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> |
||||||
|
{id, transaction} |
||||||
|
end) |
||||||
|
|
||||||
|
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] |
||||||
|
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions can be paginated" do |
||||||
|
adddress = %Address{id: from_address_id} = insert(:address) |
||||||
|
transactions = insert_list(2, :transaction, from_address_id: from_address_id) |
||||||
|
|
||||||
|
[%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] = |
||||||
|
transactions |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^newest_transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.from_address_to_transactions(adddress, pagination: %{page_size: 1}) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^oldest_transaction_id}], |
||||||
|
page_number: 2, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = |
||||||
|
Chain.from_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "hash_to_address/1" do |
||||||
|
test "without address returns {:error, :not_found}" do |
||||||
|
assert {:error, :not_found} = Chain.hash_to_address("unknown") |
||||||
|
end |
||||||
|
|
||||||
|
test "with address returns {:ok, address}" do |
||||||
|
hash = "0xandesmints" |
||||||
|
%Address{id: address_id} = insert(:address, hash: hash) |
||||||
|
|
||||||
|
assert {:ok, %Address{id: ^address_id}} = Chain.hash_to_address(hash) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "hash_to_transaction/2" do |
||||||
|
test "without transaction returns {:error, :not_found}" do |
||||||
|
assert {:error, :not_found} = Chain.hash_to_transaction("unknown") |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction returns {:ok, transaction}" do |
||||||
|
hash = "0xandesmints" |
||||||
|
%Transaction{id: transaction_id} = insert(:transaction, hash: hash) |
||||||
|
|
||||||
|
assert {:ok, %Transaction{id: ^transaction_id}} = Chain.hash_to_transaction(hash) |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction with receipt required without receipt returns {:error, :not_found}" do |
||||||
|
%Transaction{hash: hash_with_receipt, id: transaction_id_with_receipt} = |
||||||
|
insert(:transaction) |
||||||
|
|
||||||
|
insert(:receipt, transaction_id: transaction_id_with_receipt) |
||||||
|
|
||||||
|
%Transaction{hash: hash_without_receipt} = insert(:transaction) |
||||||
|
|
||||||
|
assert {:ok, %Transaction{hash: ^hash_with_receipt}} = |
||||||
|
Chain.hash_to_transaction( |
||||||
|
hash_with_receipt, |
||||||
|
necessity_by_association: %{receipt: :required} |
||||||
|
) |
||||||
|
|
||||||
|
assert {:error, :not_found} = |
||||||
|
Chain.hash_to_transaction( |
||||||
|
hash_without_receipt, |
||||||
|
necessity_by_association: %{receipt: :required} |
||||||
|
) |
||||||
|
|
||||||
|
assert {:ok, %Transaction{hash: ^hash_without_receipt}} = |
||||||
|
Chain.hash_to_transaction( |
||||||
|
hash_without_receipt, |
||||||
|
necessity_by_association: %{receipt: :optional} |
||||||
|
) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "id_to_address/1" do |
||||||
|
test "returns the address with given id" do |
||||||
|
%Address{id: id} = insert(:address) |
||||||
|
|
||||||
|
assert {:ok, %Address{id: ^id}} = Chain.id_to_address(id) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "last_transaction_id/1" do |
||||||
|
test "without transactions returns 0" do |
||||||
|
assert Chain.last_transaction_id() == 0 |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction returns last created transaction's id" do |
||||||
|
insert(:transaction) |
||||||
|
%Transaction{id: id} = insert(:transaction) |
||||||
|
|
||||||
|
assert Chain.last_transaction_id() == id |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction with pending: true returns last pending transaction id, not the last transaction" do |
||||||
|
%Transaction{id: pending_transaction_id} = insert(:transaction) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id} = insert(:transaction) |
||||||
|
insert(:receipt, transaction_id: transaction_id) |
||||||
|
|
||||||
|
assert pending_transaction_id < transaction_id |
||||||
|
|
||||||
|
assert Chain.last_transaction_id(pending: true) == pending_transaction_id |
||||||
|
assert Chain.last_transaction_id(pending: false) == transaction_id |
||||||
|
assert Chain.last_transaction_id() == transaction_id |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "list_blocks/2" do |
||||||
|
test "without blocks" do |
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 0, |
||||||
|
total_pages: 1 |
||||||
|
} = Chain.list_blocks() |
||||||
|
end |
||||||
|
|
||||||
|
test "with blocks" do |
||||||
|
%Block{id: id} = insert(:block) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Block{id: ^id}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = Chain.list_blocks() |
||||||
|
end |
||||||
|
|
||||||
|
test "with blocks can be paginated" do |
||||||
|
blocks = insert_list(2, :block) |
||||||
|
|
||||||
|
[%Block{number: lesser_block_number}, %Block{number: greater_block_number}] = blocks |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Block{number: ^greater_block_number}], |
||||||
|
page_number: 1, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.list_blocks(pagination: %{page_size: 1}) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Block{number: ^lesser_block_number}], |
||||||
|
page_number: 2, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.list_blocks(pagination: %{page: 2, page_size: 1}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "max_block_number/0" do |
||||||
|
test "without blocks is nil" do |
||||||
|
assert Chain.max_block_number() == nil |
||||||
|
end |
||||||
|
|
||||||
|
test "with blocks is max number regardless of insertion order" do |
||||||
|
max_number = 2 |
||||||
|
insert(:block, number: max_number) |
||||||
|
|
||||||
insert(:block, number: 1) |
insert(:block, number: 1) |
||||||
insert(:block, number: 100) |
|
||||||
chain = Chain.fetch() |
assert Chain.max_block_number() == max_number |
||||||
assert chain.number == 100 |
|
||||||
end |
end |
||||||
|
end |
||||||
|
|
||||||
test "returns the latest block timestamp" do |
describe "number_to_block/1" do |
||||||
time = DateTime.utc_now() |
test "without block" do |
||||||
insert(:block, timestamp: time) |
assert {:error, :not_found} = Chain.number_to_block(-1) |
||||||
chain = Chain.fetch() |
|
||||||
assert Timex.diff(chain.timestamp, time, :seconds) == 0 |
|
||||||
end |
end |
||||||
|
|
||||||
test "returns the average time between blocks" do |
test "with block" do |
||||||
time = DateTime.utc_now() |
%Block{number: number} = insert(:block) |
||||||
next_time = Timex.shift(time, seconds: 5) |
|
||||||
insert(:block, timestamp: time) |
|
||||||
insert(:block, timestamp: next_time) |
|
||||||
chain = Chain.fetch() |
|
||||||
|
|
||||||
assert chain.average_time == %Duration{ |
assert {:ok, %Block{number: ^number}} = Chain.number_to_block(number) |
||||||
seconds: 5, |
|
||||||
megaseconds: 0, |
|
||||||
microseconds: 0 |
|
||||||
} |
|
||||||
end |
end |
||||||
|
end |
||||||
|
|
||||||
test "returns the count of transactions from blocks in the last day" do |
describe "to_address_to_transactions/2" do |
||||||
time = DateTime.utc_now() |
test "without transactions" do |
||||||
last_week = Timex.shift(time, days: -8) |
address = insert(:address) |
||||||
block = insert(:block, timestamp: time) |
|
||||||
old_block = insert(:block, timestamp: last_week) |
assert Repo.aggregate(Transaction, :count, :id) == 0 |
||||||
transaction = insert(:transaction) |
|
||||||
old_transaction = insert(:transaction) |
assert %Scrivener.Page{ |
||||||
insert(:block_transaction, block: block, transaction: transaction) |
entries: [], |
||||||
insert(:block_transaction, block: old_block, transaction: old_transaction) |
page_number: 1, |
||||||
chain = Chain.fetch() |
total_entries: 0 |
||||||
assert chain.transaction_count == 1 |
} = Chain.to_address_to_transactions(address) |
||||||
end |
end |
||||||
|
|
||||||
test "returns the number of skipped blocks" do |
test "with transactions" do |
||||||
insert(:block, %{number: 0}) |
%Transaction{to_address_id: to_address_id, id: transaction_id} = insert(:transaction) |
||||||
insert(:block, %{number: 2}) |
address = Repo.get!(Address, to_address_id) |
||||||
chain = Chain.fetch() |
|
||||||
assert chain.skipped_blocks == 1 |
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = Chain.to_address_to_transactions(address) |
||||||
end |
end |
||||||
|
|
||||||
test "returns the lag between validation and insertion time" do |
test "with transactions with receipt required without receipt does not return transaction" do |
||||||
validation_time = DateTime.utc_now() |
address = %Address{id: to_address_id} = insert(:address) |
||||||
inserted_at = validation_time |> Timex.shift(seconds: 5) |
|
||||||
insert(:block, timestamp: validation_time, inserted_at: inserted_at) |
%Transaction{id: transaction_id_with_receipt} = |
||||||
chain = Chain.fetch() |
insert(:transaction, to_address_id: to_address_id) |
||||||
assert chain.lag == %Duration{seconds: 5, megaseconds: 0, microseconds: 0} |
|
||||||
|
insert(:receipt, transaction_id: transaction_id_with_receipt) |
||||||
|
|
||||||
|
%Transaction{id: transaction_id_without_receipt} = |
||||||
|
insert(:transaction, to_address_id: to_address_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1 |
||||||
|
} = |
||||||
|
Chain.to_address_to_transactions( |
||||||
|
address, |
||||||
|
necessity_by_association: %{receipt: :required} |
||||||
|
) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: transactions, |
||||||
|
page_number: 1, |
||||||
|
total_entries: 2 |
||||||
|
} = |
||||||
|
Chain.to_address_to_transactions( |
||||||
|
address, |
||||||
|
necessity_by_association: %{receipt: :optional} |
||||||
|
) |
||||||
|
|
||||||
|
assert length(transactions) == 2 |
||||||
|
|
||||||
|
transaction_by_id = |
||||||
|
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> |
||||||
|
{id, transaction} |
||||||
|
end) |
||||||
|
|
||||||
|
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] |
||||||
|
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] |
||||||
end |
end |
||||||
|
|
||||||
test "returns the number of blocks inserted in the last minute" do |
test "with transactions can be paginated" do |
||||||
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) |
adddress = %Address{id: to_address_id} = insert(:address) |
||||||
insert(:block, inserted_at: old_inserted_at) |
transactions = insert_list(2, :transaction, to_address_id: to_address_id) |
||||||
insert(:block) |
|
||||||
chain = Chain.fetch() |
[%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] = |
||||||
assert chain.block_velocity == 1 |
transactions |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^newest_transaction_id}], |
||||||
|
page_number: 1, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.to_address_to_transactions(adddress, pagination: %{page_size: 1}) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Transaction{id: ^oldest_transaction_id}], |
||||||
|
page_number: 2, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.to_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1}) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "transaction_count/0" do |
||||||
|
test "without transactions" do |
||||||
|
assert Chain.transaction_count() == 0 |
||||||
|
end |
||||||
|
|
||||||
|
test "with transactions" do |
||||||
|
count = 2 |
||||||
|
insert_list(count, :transaction) |
||||||
|
|
||||||
|
assert Chain.transaction_count() == count |
||||||
end |
end |
||||||
|
|
||||||
test "returns the number of transactions inserted in the last minute" do |
test "with transaction pending: true counts only pending transactions" do |
||||||
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) |
|
||||||
insert(:transaction, inserted_at: old_inserted_at) |
|
||||||
insert(:transaction) |
insert(:transaction) |
||||||
chain = Chain.fetch() |
|
||||||
assert chain.transaction_velocity == 1 |
%Transaction{id: transaction_id} = insert(:transaction) |
||||||
|
insert(:receipt, transaction_id: transaction_id) |
||||||
|
|
||||||
|
assert Chain.transaction_count(pending: true) == 1 |
||||||
|
assert Chain.transaction_count(pending: false) == 2 |
||||||
|
assert Chain.transaction_count() == 2 |
||||||
end |
end |
||||||
|
end |
||||||
|
|
||||||
test "returns the last five blocks" do |
describe "transaction_hash_to_internal_transactions/1" do |
||||||
insert_list(6, :block) |
test "without transaction" do |
||||||
chain = Chain.fetch() |
assert Chain.transaction_hash_to_internal_transactions("unknown") == [] |
||||||
assert chain.blocks |> Enum.count() == 5 |
|
||||||
end |
end |
||||||
|
|
||||||
test "returns the last five transactions with blocks" do |
test "with transaction without internal transactions" do |
||||||
block = insert(:block) |
%Transaction{hash: hash} = insert(:transaction) |
||||||
|
|
||||||
|
assert Chain.transaction_hash_to_internal_transactions(hash) == [] |
||||||
|
end |
||||||
|
|
||||||
insert_list(6, :transaction) |
test "with transaction with internal transactions returns all internal transactions for a given transaction hash" do |
||||||
|> Enum.map(fn transaction -> |
transaction = insert(:transaction) |
||||||
insert(:block_transaction, block: block, transaction: transaction) |
internal_transaction = insert(:internal_transaction, transaction_id: transaction.id) |
||||||
|
|
||||||
|
result = hd(Chain.transaction_hash_to_internal_transactions(transaction.hash)) |
||||||
|
|
||||||
|
assert result.id == internal_transaction.id |
||||||
|
end |
||||||
|
|
||||||
|
test "with transaction with internal transactions loads associations with in necessity_by_assocation" do |
||||||
|
%Transaction{hash: hash, id: transaction_id} = insert(:transaction) |
||||||
|
insert(:internal_transaction, transaction_id: transaction_id) |
||||||
|
|
||||||
|
assert [ |
||||||
|
%InternalTransaction{ |
||||||
|
from_address: %Ecto.Association.NotLoaded{}, |
||||||
|
to_address: %Ecto.Association.NotLoaded{}, |
||||||
|
transaction: %Ecto.Association.NotLoaded{} |
||||||
|
} |
||||||
|
] = Chain.transaction_hash_to_internal_transactions(hash) |
||||||
|
|
||||||
|
assert [ |
||||||
|
%InternalTransaction{ |
||||||
|
from_address: %Address{}, |
||||||
|
to_address: %Address{}, |
||||||
|
transaction: %Transaction{} |
||||||
|
} |
||||||
|
] = |
||||||
|
Chain.transaction_hash_to_internal_transactions( |
||||||
|
hash, |
||||||
|
necessity_by_association: %{ |
||||||
|
from_address: :optional, |
||||||
|
to_address: :optional, |
||||||
|
transaction: :optional |
||||||
|
} |
||||||
|
) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "transactions_recently_before_id" do |
||||||
|
test "returns at most 10 transactions" do |
||||||
|
count = 12 |
||||||
|
|
||||||
|
assert 10 < count |
||||||
|
|
||||||
|
transactions = insert_list(count, :transaction) |
||||||
|
%Transaction{id: last_transaction_id} = List.last(transactions) |
||||||
|
|
||||||
|
recent_transactions = Chain.transactions_recently_before_id(last_transaction_id) |
||||||
|
|
||||||
|
assert length(recent_transactions) == 10 |
||||||
|
end |
||||||
|
|
||||||
|
test "with pending: true returns only pending transactions" do |
||||||
|
count = 12 |
||||||
|
|
||||||
|
transactions = insert_list(count, :transaction) |
||||||
|
%Transaction{id: last_transaction_id} = List.last(transactions) |
||||||
|
|
||||||
|
transactions |
||||||
|
|> Enum.take(3) |
||||||
|
|> Enum.each(fn %Transaction{id: id} -> |
||||||
|
insert(:receipt, transaction_id: id) |
||||||
end) |
end) |
||||||
|
|
||||||
chain = Chain.fetch() |
assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: true)) == |
||||||
assert chain.transactions |> Enum.count() == 5 |
8 |
||||||
|
|
||||||
|
assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: false)) == |
||||||
|
10 |
||||||
|
|
||||||
|
assert length(Chain.transactions_recently_before_id(last_transaction_id)) == 10 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "transaction_to_logs/2" do |
||||||
|
test "without logs" do |
||||||
|
transaction = insert(:transaction) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 0, |
||||||
|
total_pages: 1 |
||||||
|
} = Chain.transaction_to_logs(transaction) |
||||||
|
end |
||||||
|
|
||||||
|
test "with logs" do |
||||||
|
transaction = insert(:transaction) |
||||||
|
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) |
||||||
|
%Log{id: id} = insert(:log, receipt_id: receipt_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Log{id: ^id}], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1, |
||||||
|
total_pages: 1 |
||||||
|
} = Chain.transaction_to_logs(transaction) |
||||||
|
end |
||||||
|
|
||||||
|
test "with logs can be paginated" do |
||||||
|
transaction = insert(:transaction) |
||||||
|
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) |
||||||
|
logs = insert_list(2, :log, receipt_id: receipt_id) |
||||||
|
|
||||||
|
[%Log{id: first_log_id}, %Log{id: second_log_id}] = logs |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Log{id: ^first_log_id}], |
||||||
|
page_number: 1, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.transaction_to_logs(transaction, pagination: %{page_size: 1}) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [%Log{id: ^second_log_id}], |
||||||
|
page_number: 2, |
||||||
|
page_size: 1, |
||||||
|
total_entries: 2, |
||||||
|
total_pages: 2 |
||||||
|
} = Chain.transaction_to_logs(transaction, pagination: %{page: 2, page_size: 1}) |
||||||
|
end |
||||||
|
|
||||||
|
test "with logs necessity_by_association loads associations" do |
||||||
|
transaction = insert(:transaction) |
||||||
|
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) |
||||||
|
insert(:log, receipt_id: receipt_id) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [ |
||||||
|
%Log{ |
||||||
|
address: %Address{}, |
||||||
|
receipt: %Receipt{}, |
||||||
|
transaction: %Transaction{} |
||||||
|
} |
||||||
|
], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1, |
||||||
|
total_pages: 1 |
||||||
|
} = |
||||||
|
Chain.transaction_to_logs( |
||||||
|
transaction, |
||||||
|
necessity_by_association: %{ |
||||||
|
address: :optional, |
||||||
|
receipt: :optional, |
||||||
|
transaction: :optional |
||||||
|
} |
||||||
|
) |
||||||
|
|
||||||
|
assert %Scrivener.Page{ |
||||||
|
entries: [ |
||||||
|
%Log{ |
||||||
|
address: %Ecto.Association.NotLoaded{}, |
||||||
|
receipt: %Ecto.Association.NotLoaded{}, |
||||||
|
transaction: %Ecto.Association.NotLoaded{} |
||||||
|
} |
||||||
|
], |
||||||
|
page_number: 1, |
||||||
|
total_entries: 1, |
||||||
|
total_pages: 1 |
||||||
|
} = Chain.transaction_to_logs(transaction) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
describe "update_balance/2" do |
||||||
|
test "updates the balance" do |
||||||
|
hash = "0xwarheads" |
||||||
|
insert(:address, hash: hash) |
||||||
|
|
||||||
|
Chain.update_balance(hash, 5) |
||||||
|
|
||||||
|
expected_balance = Decimal.new(5) |
||||||
|
|
||||||
|
assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address(hash) |
||||||
|
end |
||||||
|
|
||||||
|
test "updates the balance timestamp" do |
||||||
|
hash = "0xtwizzlers" |
||||||
|
insert(:address, hash: hash) |
||||||
|
|
||||||
|
Chain.update_balance(hash, 88) |
||||||
|
|
||||||
|
assert {:ok, %Address{balance_updated_at: balance_updated_at}} = |
||||||
|
Chain.hash_to_address("0xtwizzlers") |
||||||
|
|
||||||
|
refute is_nil(balance_updated_at) |
||||||
|
end |
||||||
|
|
||||||
|
test "creates an address if one does not exist" do |
||||||
|
Chain.update_balance("0xtwizzlers", 88) |
||||||
|
|
||||||
|
expected_balance = Decimal.new(88) |
||||||
|
|
||||||
|
assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address("0xtwizzlers") |
||||||
end |
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,45 +0,0 @@ |
|||||||
defmodule Explorer.ResourceTest do |
|
||||||
use Explorer.DataCase |
|
||||||
|
|
||||||
alias Explorer.Resource |
|
||||||
|
|
||||||
describe "lookup/1" do |
|
||||||
test "finds a block by block number with a valid block number" do |
|
||||||
insert(:block, number: 37) |
|
||||||
block = Resource.lookup("37") |
|
||||||
|
|
||||||
assert block.number == 37 |
|
||||||
end |
|
||||||
|
|
||||||
test "finds a transaction by hash" do |
|
||||||
transaction = insert(:transaction) |
|
||||||
|
|
||||||
resource = Resource.lookup(transaction.hash) |
|
||||||
|
|
||||||
assert transaction.hash == resource.hash |
|
||||||
end |
|
||||||
|
|
||||||
test "finds an address by hash" do |
|
||||||
address = insert(:address) |
|
||||||
|
|
||||||
resource = Resource.lookup(address.hash) |
|
||||||
|
|
||||||
assert address.hash == resource.hash |
|
||||||
end |
|
||||||
|
|
||||||
test "returns nil when garbage is passed in" do |
|
||||||
item = Resource.lookup("any ol' thing") |
|
||||||
|
|
||||||
assert is_nil(item) |
|
||||||
end |
|
||||||
|
|
||||||
test "returns nil when it does not find a match" do |
|
||||||
transaction_hash = String.pad_trailing("0xnonsense", 43, "0") |
|
||||||
address_hash = String.pad_trailing("0xbaddress", 42, "0") |
|
||||||
|
|
||||||
assert is_nil(Resource.lookup("38999")) |
|
||||||
assert is_nil(Resource.lookup(transaction_hash)) |
|
||||||
assert is_nil(Resource.lookup(address_hash)) |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,85 +0,0 @@ |
|||||||
defmodule Explorer.Servers.ChainStatisticsTest do |
|
||||||
use Explorer.DataCase |
|
||||||
|
|
||||||
alias Explorer.Chain |
|
||||||
alias Explorer.Servers.ChainStatistics |
|
||||||
|
|
||||||
describe "init/1" do |
|
||||||
test "returns a new chain when not told to refresh" do |
|
||||||
{:ok, statistics} = ChainStatistics.init(false) |
|
||||||
assert statistics.number == Chain.fetch().number |
|
||||||
end |
|
||||||
|
|
||||||
test "returns a new chain when told to refresh" do |
|
||||||
{:ok, statistics} = ChainStatistics.init(true) |
|
||||||
assert statistics == Chain.fetch() |
|
||||||
end |
|
||||||
|
|
||||||
test "refreshes when told to refresh" do |
|
||||||
{:ok, _} = ChainStatistics.init(true) |
|
||||||
assert_receive :refresh, 2_000 |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "fetch/0" do |
|
||||||
test "fetches the chain when not started" do |
|
||||||
original = Chain.fetch() |
|
||||||
chain = ChainStatistics.fetch() |
|
||||||
assert chain == original |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "handle_info/2" do |
|
||||||
test "returns the original chain when sent a :refresh message" do |
|
||||||
original = Chain.fetch() |
|
||||||
{:noreply, chain} = ChainStatistics.handle_info(:refresh, original) |
|
||||||
assert chain == original |
|
||||||
end |
|
||||||
|
|
||||||
test "launches an update when sent a :refresh message" do |
|
||||||
original = Chain.fetch() |
|
||||||
{:ok, pid} = Explorer.Servers.ChainStatistics.start_link() |
|
||||||
chain = ChainStatistics.fetch() |
|
||||||
:ok = GenServer.stop(pid) |
|
||||||
assert original.number == chain.number |
|
||||||
end |
|
||||||
|
|
||||||
test "does not reply when sent any other message" do |
|
||||||
{status, _} = ChainStatistics.handle_info(:ham, %Chain{}) |
|
||||||
assert status == :noreply |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "handle_call/3" do |
|
||||||
test "replies with statistics when sent a :fetch message" do |
|
||||||
original = Chain.fetch() |
|
||||||
{:reply, _, chain} = ChainStatistics.handle_call(:fetch, self(), original) |
|
||||||
assert chain == original |
|
||||||
end |
|
||||||
|
|
||||||
test "does not reply when sent any other message" do |
|
||||||
{status, _} = ChainStatistics.handle_call(:ham, self(), %Chain{}) |
|
||||||
assert status == :noreply |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "handle_cast/2" do |
|
||||||
test "schedules a refresh of the statistics when sent an update" do |
|
||||||
chain = Chain.fetch() |
|
||||||
ChainStatistics.handle_cast({:update, chain}, %Chain{}) |
|
||||||
assert_receive :refresh, 2_000 |
|
||||||
end |
|
||||||
|
|
||||||
test "returns a noreply and the new incoming chain when sent an update" do |
|
||||||
original = Chain.fetch() |
|
||||||
{:noreply, chain} = ChainStatistics.handle_cast({:update, original}, %Chain{}) |
|
||||||
assert chain == original |
|
||||||
end |
|
||||||
|
|
||||||
test "returns a noreply and the old chain when sent any other cast" do |
|
||||||
original = Chain.fetch() |
|
||||||
{:noreply, chain} = ChainStatistics.handle_cast(:ham, original) |
|
||||||
assert chain == original |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,56 +0,0 @@ |
|||||||
defmodule Explorer.Address.ServiceTest do |
|
||||||
use Explorer.DataCase |
|
||||||
|
|
||||||
alias Explorer.Address.Service |
|
||||||
alias Explorer.Address |
|
||||||
|
|
||||||
describe "by_hash/1" do |
|
||||||
test "it returns an address with that hash" do |
|
||||||
address = insert(:address, hash: "0xandesmints") |
|
||||||
result = Service.by_hash("0xandesmints") |
|
||||||
assert result.id == address.id |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "update_balance/2" do |
|
||||||
test "it updates the balance" do |
|
||||||
insert(:address, hash: "0xwarheads") |
|
||||||
Service.update_balance(5, "0xwarheads") |
|
||||||
result = Service.by_hash("0xwarheads") |
|
||||||
assert result.balance == Decimal.new(5) |
|
||||||
end |
|
||||||
|
|
||||||
test "it updates the balance timestamp" do |
|
||||||
insert(:address, hash: "0xtwizzlers") |
|
||||||
Service.update_balance(88, "0xtwizzlers") |
|
||||||
result = Service.by_hash("0xtwizzlers") |
|
||||||
refute is_nil(result.balance_updated_at) |
|
||||||
end |
|
||||||
|
|
||||||
test "it creates an address if one does not exist" do |
|
||||||
Service.update_balance(88, "0xtwizzlers") |
|
||||||
result = Service.by_hash("0xtwizzlers") |
|
||||||
assert result.balance == Decimal.new(88) |
|
||||||
end |
|
||||||
end |
|
||||||
|
|
||||||
describe "find_or_create_by_hash/1" do |
|
||||||
test "that it creates a new address when one does not exist" do |
|
||||||
Service.find_or_create_by_hash("0xFreshPrince") |
|
||||||
assert Service.by_hash("0xfreshprince") |
|
||||||
end |
|
||||||
|
|
||||||
test "when the address already exists it doesn't insert a new address" do |
|
||||||
insert(:address, %{hash: "bigmouthbillybass"}) |
|
||||||
Service.find_or_create_by_hash("bigmouthbillybass") |
|
||||||
number_of_addresses = Address |> Repo.all() |> length |
|
||||||
assert number_of_addresses == 1 |
|
||||||
end |
|
||||||
|
|
||||||
test "when there is no hash it blows up" do |
|
||||||
assert_raise Ecto.InvalidChangesetError, fn -> |
|
||||||
Service.find_or_create_by_hash("") |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,16 +0,0 @@ |
|||||||
defmodule Explorer.Transaction.ServiceTest do |
|
||||||
use Explorer.DataCase |
|
||||||
|
|
||||||
alias Explorer.Transaction.Service |
|
||||||
|
|
||||||
describe "internal_transactions/1" do |
|
||||||
test "it returns all internal transactions for a given hash" do |
|
||||||
transaction = insert(:transaction) |
|
||||||
internal_transaction = insert(:internal_transaction, transaction_id: transaction.id) |
|
||||||
|
|
||||||
result = hd(Service.internal_transactions(transaction.hash)) |
|
||||||
|
|
||||||
assert result.id == internal_transaction.id |
|
||||||
end |
|
||||||
end |
|
||||||
end |
|
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.AddressFactory do |
defmodule Explorer.Chain.AddressFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def address_factory do |
def address_factory do |
||||||
%Explorer.Address{ |
%Explorer.Chain.Address{ |
||||||
hash: String.pad_trailing(sequence("0x"), 42, "address") |
hash: String.pad_trailing(sequence("0x"), 42, "address") |
||||||
} |
} |
||||||
end |
end |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.BlockFactory do |
defmodule Explorer.Chain.BlockFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def block_factory do |
def block_factory do |
||||||
%Explorer.Block{ |
%Explorer.Chain.Block{ |
||||||
number: sequence(""), |
number: sequence(""), |
||||||
hash: sequence("0x"), |
hash: sequence("0x"), |
||||||
parent_hash: sequence("0x"), |
parent_hash: sequence("0x"), |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.BlockTransactionFactory do |
defmodule Explorer.Chain.BlockTransactionFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def block_transaction_factory do |
def block_transaction_factory do |
||||||
%Explorer.BlockTransaction{} |
%Explorer.Chain.BlockTransaction{} |
||||||
end |
end |
||||||
end |
end |
||||||
end |
end |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.FromAddressFactory do |
defmodule Explorer.Chain.FromAddressFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def from_address_factory do |
def from_address_factory do |
||||||
%Explorer.FromAddress{} |
%Explorer.Chain.FromAddress{} |
||||||
end |
end |
||||||
end |
end |
||||||
end |
end |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.InternalTransactionFactory do |
defmodule Explorer.Chain.InternalTransactionFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def internal_transaction_factory do |
def internal_transaction_factory do |
||||||
%Explorer.InternalTransaction{ |
%Explorer.Chain.InternalTransaction{ |
||||||
index: Enum.random(0..9), |
index: Enum.random(0..9), |
||||||
call_type: Enum.random(["call", "creates", "calldelegate"]), |
call_type: Enum.random(["call", "creates", "calldelegate"]), |
||||||
trace_address: [Enum.random(0..4), Enum.random(0..4)], |
trace_address: [Enum.random(0..4), Enum.random(0..4)], |
@ -1,15 +1,16 @@ |
|||||||
defmodule Explorer.LogFactory do |
defmodule Explorer.Chain.LogFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def log_factory do |
def log_factory do |
||||||
%Explorer.Log{ |
%Explorer.Chain.Log{ |
||||||
index: sequence(""), |
address_id: insert(:address).id, |
||||||
data: sequence("0x"), |
data: sequence("0x"), |
||||||
type: sequence("0x"), |
|
||||||
first_topic: nil, |
first_topic: nil, |
||||||
|
fourth_topic: nil, |
||||||
|
index: sequence(""), |
||||||
second_topic: nil, |
second_topic: nil, |
||||||
third_topic: nil, |
third_topic: nil, |
||||||
fourth_topic: nil |
type: sequence("0x") |
||||||
} |
} |
||||||
end |
end |
||||||
end |
end |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.ReceiptFactory do |
defmodule Explorer.Chain.ReceiptFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def receipt_factory do |
def receipt_factory do |
||||||
%Explorer.Receipt{ |
%Explorer.Chain.Receipt{ |
||||||
cumulative_gas_used: Enum.random(21_000..100_000), |
cumulative_gas_used: Enum.random(21_000..100_000), |
||||||
gas_used: Enum.random(21_000..100_000), |
gas_used: Enum.random(21_000..100_000), |
||||||
status: Enum.random(1..2), |
status: Enum.random(1..2), |
@ -1,8 +1,8 @@ |
|||||||
defmodule Explorer.ToAddressFactory do |
defmodule Explorer.Chain.ToAddressFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
def to_address_factory do |
def to_address_factory do |
||||||
%Explorer.ToAddress{} |
%Explorer.Chain.ToAddress{} |
||||||
end |
end |
||||||
end |
end |
||||||
end |
end |
@ -1,12 +1,11 @@ |
|||||||
defmodule Explorer.TransactionFactory do |
defmodule Explorer.Chain.TransactionFactory do |
||||||
defmacro __using__(_opts) do |
defmacro __using__(_opts) do |
||||||
quote do |
quote do |
||||||
alias Explorer.Address |
alias Explorer.Chain.{Address, BlockTransaction, Transaction} |
||||||
alias Explorer.BlockTransaction |
|
||||||
alias Explorer.Repo |
alias Explorer.Repo |
||||||
|
|
||||||
def transaction_factory do |
def transaction_factory do |
||||||
%Explorer.Transaction{ |
%Transaction{ |
||||||
hash: String.pad_trailing(sequence("0x"), 43, "action"), |
hash: String.pad_trailing(sequence("0x"), 43, "action"), |
||||||
value: Enum.random(1..100_000), |
value: Enum.random(1..100_000), |
||||||
gas: Enum.random(21_000..100_000), |
gas: Enum.random(21_000..100_000), |
@ -1,13 +1,13 @@ |
|||||||
defmodule Explorer.Factory do |
defmodule Explorer.Factory do |
||||||
@dialyzer {:nowarn_function, fields_for: 1} |
@dialyzer {:nowarn_function, fields_for: 1} |
||||||
use ExMachina.Ecto, repo: Explorer.Repo |
use ExMachina.Ecto, repo: Explorer.Repo |
||||||
use Explorer.AddressFactory |
use Explorer.Chain.AddressFactory |
||||||
use Explorer.BlockFactory |
use Explorer.Chain.BlockFactory |
||||||
use Explorer.BlockTransactionFactory |
use Explorer.Chain.BlockTransactionFactory |
||||||
use Explorer.FromAddressFactory |
use Explorer.Chain.FromAddressFactory |
||||||
use Explorer.InternalTransactionFactory |
use Explorer.Chain.InternalTransactionFactory |
||||||
use Explorer.LogFactory |
use Explorer.Chain.LogFactory |
||||||
use Explorer.ToAddressFactory |
use Explorer.Chain.ReceiptFactory |
||||||
use Explorer.TransactionFactory |
use Explorer.Chain.ToAddressFactory |
||||||
use Explorer.ReceiptFactory |
use Explorer.Chain.TransactionFactory |
||||||
end |
end |
||||||
|
@ -0,0 +1,33 @@ |
|||||||
|
defmodule ExplorerWeb.Chain do |
||||||
|
@moduledoc """ |
||||||
|
Converts the `param` to the corresponding resource that uses that format of param. |
||||||
|
""" |
||||||
|
|
||||||
|
import Explorer.Chain, only: [hash_to_address: 1, hash_to_transaction: 1, number_to_block: 1] |
||||||
|
|
||||||
|
@spec from_param(String.t()) :: |
||||||
|
{:ok, Address.t() | Transaction.t() | Block.t()} | {:error, :not_found} |
||||||
|
def from_param(param) |
||||||
|
|
||||||
|
def from_param(hash) when byte_size(hash) > 42 do |
||||||
|
hash_to_transaction(hash) |
||||||
|
end |
||||||
|
|
||||||
|
def from_param(hash) when byte_size(hash) == 42 do |
||||||
|
hash_to_address(hash) |
||||||
|
end |
||||||
|
|
||||||
|
def from_param(formatted_number) when is_binary(formatted_number) do |
||||||
|
case param_to_block_number(formatted_number) do |
||||||
|
{:ok, number} -> number_to_block(number) |
||||||
|
{:error, :invalid} -> {:error, :not_found} |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
def param_to_block_number(formatted_number) when is_binary(formatted_number) do |
||||||
|
case Integer.parse(formatted_number) do |
||||||
|
{number, ""} -> {:ok, number} |
||||||
|
_ -> {:error, :invalid} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,18 @@ |
|||||||
|
defmodule ExplorerWeb.Controller do |
||||||
|
@moduledoc """ |
||||||
|
Common controller error responses |
||||||
|
""" |
||||||
|
|
||||||
|
import Phoenix.Controller |
||||||
|
import Plug.Conn |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Renders HTML Not Found error |
||||||
|
""" |
||||||
|
def not_found(conn) do |
||||||
|
conn |
||||||
|
|> put_status(:not_found) |
||||||
|
|> put_view(ExplorerWeb.ErrorView) |
||||||
|
|> render("404.html") |
||||||
|
end |
||||||
|
end |
@ -1,10 +1,14 @@ |
|||||||
defmodule ExplorerWeb.AddressController do |
defmodule ExplorerWeb.AddressController do |
||||||
use ExplorerWeb, :controller |
use ExplorerWeb, :controller |
||||||
|
|
||||||
alias Explorer.Address.Service, as: Address |
alias Explorer.Chain |
||||||
|
|
||||||
def show(conn, %{"id" => id}) do |
def show(conn, %{"id" => hash}) do |
||||||
address = id |> Address.by_hash() |
hash |
||||||
render(conn, "show.html", address: address) |
|> Chain.hash_to_address() |
||||||
|
|> case do |
||||||
|
{:ok, address} -> render(conn, "show.html", address: address) |
||||||
|
{:error, :not_found} -> not_found(conn) |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,31 +1,25 @@ |
|||||||
defmodule ExplorerWeb.BlockController do |
defmodule ExplorerWeb.BlockController do |
||||||
use ExplorerWeb, :controller |
use ExplorerWeb, :controller |
||||||
|
|
||||||
import Ecto.Query |
alias Explorer.Chain |
||||||
|
|
||||||
alias Explorer.Block |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
alias ExplorerWeb.BlockForm |
alias ExplorerWeb.BlockForm |
||||||
|
|
||||||
def index(conn, params) do |
def index(conn, params) do |
||||||
blocks = |
blocks = |
||||||
from( |
Chain.list_blocks(necessity_by_association: %{transactions: :optional}, pagination: params) |
||||||
block in Block, |
|
||||||
order_by: [desc: block.number], |
|
||||||
preload: :transactions |
|
||||||
) |
|
||||||
|
|
||||||
render(conn, "index.html", blocks: Repo.paginate(blocks, params)) |
render(conn, "index.html", blocks: blocks) |
||||||
end |
end |
||||||
|
|
||||||
def show(conn, %{"id" => number}) do |
def show(conn, %{"id" => number}) do |
||||||
block = |
case Chain.number_to_block(number) do |
||||||
Block |
{:ok, block} -> |
||||||
|> where(number: ^number) |
block_form = BlockForm.build(block) |
||||||
|> first |
|
||||||
|> Repo.one() |
render(conn, "show.html", block: block_form) |
||||||
|> BlockForm.build() |
|
||||||
|
|
||||||
render(conn, "show.html", block: block) |
{:error, :not_found} -> |
||||||
|
not_found(conn) |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,27 +1,34 @@ |
|||||||
defmodule ExplorerWeb.BlockTransactionController do |
defmodule ExplorerWeb.BlockTransactionController do |
||||||
use ExplorerWeb, :controller |
use ExplorerWeb, :controller |
||||||
|
|
||||||
import Ecto.Query |
import ExplorerWeb.Chain, only: [param_to_block_number: 1] |
||||||
|
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
alias Explorer.Chain |
||||||
alias Explorer.Transaction |
|
||||||
alias ExplorerWeb.TransactionForm |
alias ExplorerWeb.TransactionForm |
||||||
|
|
||||||
def index(conn, %{"block_id" => block_number} = params) do |
def index(conn, %{"block_id" => formatted_block_number} = params) do |
||||||
query = |
with {:ok, block_number} <- param_to_block_number(formatted_block_number), |
||||||
from( |
{:ok, block} <- Chain.number_to_block(block_number) do |
||||||
transaction in Transaction, |
page = |
||||||
join: block in assoc(transaction, :block), |
Chain.block_to_transactions( |
||||||
join: receipt in assoc(transaction, :receipt), |
block, |
||||||
join: from_address in assoc(transaction, :from_address), |
necessity_by_association: %{ |
||||||
join: to_address in assoc(transaction, :to_address), |
block: :required, |
||||||
preload: [:block, :receipt, :to_address, :from_address], |
from_address: :required, |
||||||
order_by: [desc: transaction.inserted_at], |
to_address: :required, |
||||||
where: block.number == ^block_number |
receipt: :required |
||||||
) |
}, |
||||||
|
pagination: params |
||||||
|
) |
||||||
|
|
||||||
page = Repo.paginate(query, params) |
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) |
||||||
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) |
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) |
||||||
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) |
else |
||||||
|
{:error, :invalid} -> |
||||||
|
not_found(conn) |
||||||
|
|
||||||
|
{:error, :not_found} -> |
||||||
|
not_found(conn) |
||||||
|
end |
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,41 +1,28 @@ |
|||||||
defmodule ExplorerWeb.TransactionLogController do |
defmodule ExplorerWeb.TransactionLogController do |
||||||
use ExplorerWeb, :controller |
use ExplorerWeb, :controller |
||||||
|
|
||||||
import Ecto.Query |
alias Explorer.Chain |
||||||
|
|
||||||
alias Explorer.Log |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
alias Explorer.Transaction |
|
||||||
alias Explorer.Transaction.Service.Query |
|
||||||
alias ExplorerWeb.TransactionForm |
alias ExplorerWeb.TransactionForm |
||||||
|
|
||||||
def index(conn, %{"transaction_id" => transaction_id}) do |
def index(conn, %{"transaction_id" => transaction_hash} = params) do |
||||||
transaction_hash = String.downcase(transaction_id) |
case Chain.hash_to_transaction( |
||||||
transaction = get_transaction(transaction_hash) |
transaction_hash, |
||||||
|
necessity_by_association: %{from_address: :required, to_address: :required} |
||||||
|
) do |
||||||
|
{:ok, transaction} -> |
||||||
|
logs = |
||||||
|
Chain.transaction_to_logs( |
||||||
|
transaction, |
||||||
|
necessity_by_association: %{address: :optional}, |
||||||
|
pagination: params |
||||||
|
) |
||||||
|
|
||||||
logs = |
transaction_form = TransactionForm.build_and_merge(transaction) |
||||||
from( |
|
||||||
log in Log, |
|
||||||
join: transaction in assoc(log, :transaction), |
|
||||||
preload: [:address], |
|
||||||
where: fragment("lower(?)", transaction.hash) == ^transaction_hash |
|
||||||
) |
|
||||||
|
|
||||||
render( |
render(conn, "index.html", logs: logs, transaction: transaction_form) |
||||||
conn, |
|
||||||
"index.html", |
|
||||||
logs: Repo.paginate(logs), |
|
||||||
transaction: transaction |
|
||||||
) |
|
||||||
end |
|
||||||
|
|
||||||
defp get_transaction(hash) do |
{:error, :not_found} -> |
||||||
Transaction |
not_found(conn) |
||||||
|> Query.by_hash(hash) |
end |
||||||
|> Query.include_addresses() |
|
||||||
|> Query.include_receipt() |
|
||||||
|> Query.include_block() |
|
||||||
|> Repo.one() |
|
||||||
|> TransactionForm.build_and_merge() |
|
||||||
end |
end |
||||||
end |
end |
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue