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" |
||||
|
||||
alias Explorer.BlockTransaction |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{Block, Transaction} |
||||
|
||||
@primary_key false |
||||
schema "block_transactions" do |
||||
belongs_to(:block, Explorer.Block) |
||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
||||
belongs_to(:block, Block) |
||||
belongs_to(:transaction, Transaction, primary_key: true) |
||||
timestamps() |
||||
end |
||||
|
||||
@required_attrs ~w(block_id transaction_id)a |
||||
|
||||
def changeset(%BlockTransaction{} = block_transaction, attrs \\ %{}) do |
||||
def changeset(%__MODULE__{} = block_transaction, attrs \\ %{}) do |
||||
block_transaction |
||||
|> cast(attrs, @required_attrs) |
||||
|> validate_required(@required_attrs) |
@ -1,18 +1,19 @@ |
||||
defmodule Explorer.FromAddress do |
||||
defmodule Explorer.Chain.FromAddress do |
||||
@moduledoc false |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.FromAddress |
||||
alias Explorer.Chain.{Address, Transaction} |
||||
|
||||
@primary_key false |
||||
schema "from_addresses" do |
||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
||||
belongs_to(:address, Explorer.Address) |
||||
belongs_to(:address, Address) |
||||
belongs_to(:transaction, Transaction, primary_key: true) |
||||
|
||||
timestamps() |
||||
end |
||||
|
||||
def changeset(%FromAddress{} = to_address, attrs \\ %{}) do |
||||
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do |
||||
to_address |
||||
|> cast(attrs, [:transaction_id, :address_id]) |
||||
|> 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" |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Address |
||||
alias Explorer.Log |
||||
alias Explorer.Receipt |
||||
alias Explorer.Chain.{Address, Receipt} |
||||
|
||||
@required_attrs ~w(index data type)a |
||||
@required_attrs ~w(address_id data index type)a |
||||
@optional_attrs ~w( |
||||
first_topic second_topic third_topic fourth_topic address_id |
||||
first_topic second_topic third_topic fourth_topic |
||||
)a |
||||
|
||||
schema "logs" do |
||||
belongs_to(:receipt, Receipt) |
||||
belongs_to(:address, Address) |
||||
has_one(:transaction, through: [:receipt, :transaction]) |
||||
field(:index, :integer) |
||||
field(:data, :string) |
||||
field(:type, :string) |
||||
field(:first_topic, :string) |
||||
field(:fourth_topic, :string) |
||||
field(:index, :integer) |
||||
field(:second_topic, :string) |
||||
field(:third_topic, :string) |
||||
field(:fourth_topic, :string) |
||||
field(:type, :string) |
||||
|
||||
timestamps() |
||||
|
||||
belongs_to(:address, Address) |
||||
belongs_to(:receipt, Receipt) |
||||
has_one(:transaction, through: [:receipt, :transaction]) |
||||
end |
||||
|
||||
def changeset(%Log{} = log, attrs \\ %{}) do |
||||
def changeset(%__MODULE__{} = log, attrs \\ %{}) do |
||||
log |
||||
|> cast(attrs, @required_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 |
||||
alias Explorer.ToAddress |
||||
|
||||
use Explorer.Schema |
||||
|
||||
alias Explorer.Chain.{Address, Transaction} |
||||
|
||||
@primary_key false |
||||
schema "to_addresses" do |
||||
belongs_to(:transaction, Explorer.Transaction, primary_key: true) |
||||
belongs_to(:address, Explorer.Address) |
||||
belongs_to(:address, Address) |
||||
belongs_to(:transaction, Transaction, primary_key: true) |
||||
timestamps() |
||||
end |
||||
|
||||
def changeset(%ToAddress{} = to_address, attrs \\ %{}) do |
||||
def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do |
||||
to_address |
||||
|> cast(attrs, [:transaction_id, :address_id]) |
||||
|> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index) |
@ -1,18 +1,17 @@ |
||||
defmodule Explorer.BalanceImporter do |
||||
@moduledoc "Imports a balance for a given address." |
||||
|
||||
alias Explorer.Address.Service, as: Address |
||||
alias Explorer.Ethereum |
||||
alias Explorer.{Chain, Ethereum} |
||||
|
||||
def import(hash) do |
||||
hash |
||||
|> Ethereum.download_balance() |
||||
|> persist_balance(hash) |
||||
encoded_balance = Ethereum.download_balance(hash) |
||||
|
||||
persist_balance(hash, encoded_balance) |
||||
end |
||||
|
||||
defp persist_balance(balance, hash) do |
||||
balance |
||||
|> Ethereum.decode_integer_field() |
||||
|> Address.update_balance(hash) |
||||
defp persist_balance(hash, encoded_balance) when is_binary(hash) do |
||||
decoded_balance = Ethereum.decode_integer_field(encoded_balance) |
||||
|
||||
Chain.update_balance(hash, decoded_balance) |
||||
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 |
||||
alias Explorer.Address |
||||
|
||||
alias Explorer.Chain.Address |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
@ -1,9 +1,10 @@ |
||||
defmodule Explorer.BlockTest do |
||||
defmodule Explorer.Chain.BlockTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Block |
||||
import Ecto.Query, only: [order_by: 2] |
||||
|
||||
alias Explorer.Chain.Block |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
||||
changeset = build(:block) |> Block.changeset(%{}) |
@ -1,6 +1,7 @@ |
||||
defmodule Explorer.BlockTransactionTest do |
||||
defmodule Explorer.Chain.BlockTransactionTest do |
||||
use Explorer.DataCase |
||||
alias Explorer.BlockTransaction |
||||
|
||||
alias Explorer.Chain.BlockTransaction |
||||
|
||||
describe "changeset/2" do |
||||
test "with empty attributes" do |
@ -1,7 +1,7 @@ |
||||
defmodule Explorer.CreditTest do |
||||
defmodule Explorer.Chain.CreditTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Credit |
||||
alias Explorer.Chain.Credit |
||||
|
||||
describe "Repo.all/1" 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 |
||||
|
||||
alias Explorer.Debit |
||||
alias Explorer.Chain.Debit |
||||
|
||||
describe "Repo.all/1" 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 |
||||
alias Explorer.FromAddress |
||||
|
||||
alias Explorer.Chain.FromAddress |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
||||
defmodule Explorer.InternalTransactionTest do |
||||
defmodule Explorer.Chain.InternalTransactionTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.InternalTransaction |
||||
alias Explorer.Chain.InternalTransaction |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
||||
defmodule Explorer.LogTest do |
||||
defmodule Explorer.Chain.LogTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Log |
||||
alias Explorer.Chain.Log |
||||
|
||||
describe "changeset/2" do |
||||
test "accepts valid attributes" do |
@ -1,7 +1,7 @@ |
||||
defmodule Explorer.ReceiptTest do |
||||
defmodule Explorer.Chain.ReceiptTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Receipt |
||||
alias Explorer.Chain.Receipt |
||||
|
||||
describe "changeset/2" 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 |
||||
alias Explorer.ToAddress |
||||
|
||||
alias Explorer.Chain.ToAddress |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
@ -1,7 +1,7 @@ |
||||
defmodule Explorer.TransactionTest do |
||||
defmodule Explorer.Chain.TransactionTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Transaction |
||||
alias Explorer.Chain.Transaction |
||||
|
||||
describe "changeset/2" do |
||||
test "with valid attributes" do |
@ -1,103 +1,757 @@ |
||||
defmodule Explorer.ChainTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Chain |
||||
alias Timex.Duration |
||||
alias Explorer.{Chain, Repo} |
||||
|
||||
describe "fetch/0" do |
||||
test "returns -1 for the number when there are no blocks" do |
||||
chain = Chain.fetch() |
||||
assert chain.number == -1 |
||||
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Receipt, Transaction} |
||||
|
||||
# Constants |
||||
|
||||
@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 |
||||
|
||||
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: 100) |
||||
chain = Chain.fetch() |
||||
assert chain.number == 100 |
||||
|
||||
assert Chain.max_block_number() == max_number |
||||
end |
||||
end |
||||
|
||||
test "returns the latest block timestamp" do |
||||
time = DateTime.utc_now() |
||||
insert(:block, timestamp: time) |
||||
chain = Chain.fetch() |
||||
assert Timex.diff(chain.timestamp, time, :seconds) == 0 |
||||
describe "number_to_block/1" do |
||||
test "without block" do |
||||
assert {:error, :not_found} = Chain.number_to_block(-1) |
||||
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) |
||||
chain = Chain.fetch() |
||||
test "with block" do |
||||
%Block{number: number} = insert(:block) |
||||
|
||||
assert chain.average_time == %Duration{ |
||||
seconds: 5, |
||||
megaseconds: 0, |
||||
microseconds: 0 |
||||
} |
||||
assert {:ok, %Block{number: ^number}} = Chain.number_to_block(number) |
||||
end |
||||
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) |
||||
chain = Chain.fetch() |
||||
assert chain.transaction_count == 1 |
||||
describe "to_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.to_address_to_transactions(address) |
||||
end |
||||
|
||||
test "returns the number of skipped blocks" do |
||||
insert(:block, %{number: 0}) |
||||
insert(:block, %{number: 2}) |
||||
chain = Chain.fetch() |
||||
assert chain.skipped_blocks == 1 |
||||
test "with transactions" do |
||||
%Transaction{to_address_id: to_address_id, id: transaction_id} = insert(:transaction) |
||||
address = Repo.get!(Address, to_address_id) |
||||
|
||||
assert %Scrivener.Page{ |
||||
entries: [%Transaction{id: ^transaction_id}], |
||||
page_number: 1, |
||||
total_entries: 1 |
||||
} = Chain.to_address_to_transactions(address) |
||||
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) |
||||
chain = Chain.fetch() |
||||
assert chain.lag == %Duration{seconds: 5, megaseconds: 0, microseconds: 0} |
||||
test "with transactions with receipt required without receipt does not return transaction" do |
||||
address = %Address{id: to_address_id} = insert(:address) |
||||
|
||||
%Transaction{id: transaction_id_with_receipt} = |
||||
insert(:transaction, to_address_id: to_address_id) |
||||
|
||||
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 |
||||
|
||||
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) |
||||
chain = Chain.fetch() |
||||
assert chain.block_velocity == 1 |
||||
test "with transactions can be paginated" do |
||||
adddress = %Address{id: to_address_id} = insert(:address) |
||||
transactions = insert_list(2, :transaction, to_address_id: to_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.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 |
||||
|
||||
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) |
||||
test "with transaction pending: true counts only pending transactions" do |
||||
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 |
||||
|
||||
test "returns the last five blocks" do |
||||
insert_list(6, :block) |
||||
chain = Chain.fetch() |
||||
assert chain.blocks |> Enum.count() == 5 |
||||
describe "transaction_hash_to_internal_transactions/1" do |
||||
test "without transaction" do |
||||
assert Chain.transaction_hash_to_internal_transactions("unknown") == [] |
||||
end |
||||
|
||||
test "returns the last five transactions with blocks" do |
||||
block = insert(:block) |
||||
test "with transaction without internal transactions" do |
||||
%Transaction{hash: hash} = insert(:transaction) |
||||
|
||||
assert Chain.transaction_hash_to_internal_transactions(hash) == [] |
||||
end |
||||
|
||||
insert_list(6, :transaction) |
||||
|> Enum.map(fn transaction -> |
||||
insert(:block_transaction, block: block, transaction: transaction) |
||||
test "with transaction with internal transactions returns all internal transactions for a given transaction hash" do |
||||
transaction = insert(: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) |
||||
|
||||
chain = Chain.fetch() |
||||
assert chain.transactions |> Enum.count() == 5 |
||||
assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: true)) == |
||||
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 |
||||
|
@ -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 |
||||
quote do |
||||
def address_factory do |
||||
%Explorer.Address{ |
||||
%Explorer.Chain.Address{ |
||||
hash: String.pad_trailing(sequence("0x"), 42, "address") |
||||
} |
||||
end |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.BlockFactory do |
||||
defmodule Explorer.Chain.BlockFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def block_factory do |
||||
%Explorer.Block{ |
||||
%Explorer.Chain.Block{ |
||||
number: sequence(""), |
||||
hash: sequence("0x"), |
||||
parent_hash: sequence("0x"), |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.BlockTransactionFactory do |
||||
defmodule Explorer.Chain.BlockTransactionFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def block_transaction_factory do |
||||
%Explorer.BlockTransaction{} |
||||
%Explorer.Chain.BlockTransaction{} |
||||
end |
||||
end |
||||
end |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.FromAddressFactory do |
||||
defmodule Explorer.Chain.FromAddressFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def from_address_factory do |
||||
%Explorer.FromAddress{} |
||||
%Explorer.Chain.FromAddress{} |
||||
end |
||||
end |
||||
end |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.InternalTransactionFactory do |
||||
defmodule Explorer.Chain.InternalTransactionFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def internal_transaction_factory do |
||||
%Explorer.InternalTransaction{ |
||||
%Explorer.Chain.InternalTransaction{ |
||||
index: Enum.random(0..9), |
||||
call_type: Enum.random(["call", "creates", "calldelegate"]), |
||||
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 |
||||
quote do |
||||
def log_factory do |
||||
%Explorer.Log{ |
||||
index: sequence(""), |
||||
%Explorer.Chain.Log{ |
||||
address_id: insert(:address).id, |
||||
data: sequence("0x"), |
||||
type: sequence("0x"), |
||||
first_topic: nil, |
||||
fourth_topic: nil, |
||||
index: sequence(""), |
||||
second_topic: nil, |
||||
third_topic: nil, |
||||
fourth_topic: nil |
||||
type: sequence("0x") |
||||
} |
||||
end |
||||
end |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.ReceiptFactory do |
||||
defmodule Explorer.Chain.ReceiptFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def receipt_factory do |
||||
%Explorer.Receipt{ |
||||
%Explorer.Chain.Receipt{ |
||||
cumulative_gas_used: Enum.random(21_000..100_000), |
||||
gas_used: Enum.random(21_000..100_000), |
||||
status: Enum.random(1..2), |
@ -1,8 +1,8 @@ |
||||
defmodule Explorer.ToAddressFactory do |
||||
defmodule Explorer.Chain.ToAddressFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
def to_address_factory do |
||||
%Explorer.ToAddress{} |
||||
%Explorer.Chain.ToAddress{} |
||||
end |
||||
end |
||||
end |
@ -1,12 +1,11 @@ |
||||
defmodule Explorer.TransactionFactory do |
||||
defmodule Explorer.Chain.TransactionFactory do |
||||
defmacro __using__(_opts) do |
||||
quote do |
||||
alias Explorer.Address |
||||
alias Explorer.BlockTransaction |
||||
alias Explorer.Chain.{Address, BlockTransaction, Transaction} |
||||
alias Explorer.Repo |
||||
|
||||
def transaction_factory do |
||||
%Explorer.Transaction{ |
||||
%Transaction{ |
||||
hash: String.pad_trailing(sequence("0x"), 43, "action"), |
||||
value: Enum.random(1..100_000), |
||||
gas: Enum.random(21_000..100_000), |
@ -1,13 +1,13 @@ |
||||
defmodule Explorer.Factory do |
||||
@dialyzer {:nowarn_function, fields_for: 1} |
||||
use ExMachina.Ecto, repo: Explorer.Repo |
||||
use Explorer.AddressFactory |
||||
use Explorer.BlockFactory |
||||
use Explorer.BlockTransactionFactory |
||||
use Explorer.FromAddressFactory |
||||
use Explorer.InternalTransactionFactory |
||||
use Explorer.LogFactory |
||||
use Explorer.ToAddressFactory |
||||
use Explorer.TransactionFactory |
||||
use Explorer.ReceiptFactory |
||||
use Explorer.Chain.AddressFactory |
||||
use Explorer.Chain.BlockFactory |
||||
use Explorer.Chain.BlockTransactionFactory |
||||
use Explorer.Chain.FromAddressFactory |
||||
use Explorer.Chain.InternalTransactionFactory |
||||
use Explorer.Chain.LogFactory |
||||
use Explorer.Chain.ReceiptFactory |
||||
use Explorer.Chain.ToAddressFactory |
||||
use Explorer.Chain.TransactionFactory |
||||
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 |
||||
use ExplorerWeb, :controller |
||||
|
||||
alias Explorer.Address.Service, as: Address |
||||
alias Explorer.Chain |
||||
|
||||
def show(conn, %{"id" => id}) do |
||||
address = id |> Address.by_hash() |
||||
render(conn, "show.html", address: address) |
||||
def show(conn, %{"id" => hash}) do |
||||
hash |
||||
|> Chain.hash_to_address() |
||||
|> case do |
||||
{:ok, address} -> render(conn, "show.html", address: address) |
||||
{:error, :not_found} -> not_found(conn) |
||||
end |
||||
end |
||||
end |
||||
|
@ -1,31 +1,25 @@ |
||||
defmodule ExplorerWeb.BlockController do |
||||
use ExplorerWeb, :controller |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.Block |
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
alias Explorer.Chain |
||||
alias ExplorerWeb.BlockForm |
||||
|
||||
def index(conn, params) do |
||||
blocks = |
||||
from( |
||||
block in Block, |
||||
order_by: [desc: block.number], |
||||
preload: :transactions |
||||
) |
||||
Chain.list_blocks(necessity_by_association: %{transactions: :optional}, pagination: params) |
||||
|
||||
render(conn, "index.html", blocks: Repo.paginate(blocks, params)) |
||||
render(conn, "index.html", blocks: blocks) |
||||
end |
||||
|
||||
def show(conn, %{"id" => number}) do |
||||
block = |
||||
Block |
||||
|> where(number: ^number) |
||||
|> first |
||||
|> Repo.one() |
||||
|> BlockForm.build() |
||||
case Chain.number_to_block(number) do |
||||
{:ok, block} -> |
||||
block_form = BlockForm.build(block) |
||||
|
||||
render(conn, "show.html", block: block_form) |
||||
|
||||
render(conn, "show.html", block: block) |
||||
{:error, :not_found} -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
||||
|
@ -1,27 +1,34 @@ |
||||
defmodule ExplorerWeb.BlockTransactionController do |
||||
use ExplorerWeb, :controller |
||||
|
||||
import Ecto.Query |
||||
import ExplorerWeb.Chain, only: [param_to_block_number: 1] |
||||
|
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
alias Explorer.Transaction |
||||
alias Explorer.Chain |
||||
alias ExplorerWeb.TransactionForm |
||||
|
||||
def index(conn, %{"block_id" => block_number} = params) do |
||||
query = |
||||
from( |
||||
transaction in Transaction, |
||||
join: block in assoc(transaction, :block), |
||||
join: receipt in assoc(transaction, :receipt), |
||||
join: from_address in assoc(transaction, :from_address), |
||||
join: to_address in assoc(transaction, :to_address), |
||||
preload: [:block, :receipt, :to_address, :from_address], |
||||
order_by: [desc: transaction.inserted_at], |
||||
where: block.number == ^block_number |
||||
) |
||||
def index(conn, %{"block_id" => formatted_block_number} = params) do |
||||
with {:ok, block_number} <- param_to_block_number(formatted_block_number), |
||||
{:ok, block} <- Chain.number_to_block(block_number) do |
||||
page = |
||||
Chain.block_to_transactions( |
||||
block, |
||||
necessity_by_association: %{ |
||||
block: :required, |
||||
from_address: :required, |
||||
to_address: :required, |
||||
receipt: :required |
||||
}, |
||||
pagination: params |
||||
) |
||||
|
||||
page = Repo.paginate(query, params) |
||||
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) |
||||
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) |
||||
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) |
||||
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 |
||||
|
@ -1,41 +1,28 @@ |
||||
defmodule ExplorerWeb.TransactionLogController do |
||||
use ExplorerWeb, :controller |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.Log |
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
alias Explorer.Transaction |
||||
alias Explorer.Transaction.Service.Query |
||||
alias Explorer.Chain |
||||
alias ExplorerWeb.TransactionForm |
||||
|
||||
def index(conn, %{"transaction_id" => transaction_id}) do |
||||
transaction_hash = String.downcase(transaction_id) |
||||
transaction = get_transaction(transaction_hash) |
||||
def index(conn, %{"transaction_id" => transaction_hash} = params) do |
||||
case Chain.hash_to_transaction( |
||||
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 = |
||||
from( |
||||
log in Log, |
||||
join: transaction in assoc(log, :transaction), |
||||
preload: [:address], |
||||
where: fragment("lower(?)", transaction.hash) == ^transaction_hash |
||||
) |
||||
transaction_form = TransactionForm.build_and_merge(transaction) |
||||
|
||||
render( |
||||
conn, |
||||
"index.html", |
||||
logs: Repo.paginate(logs), |
||||
transaction: transaction |
||||
) |
||||
end |
||||
render(conn, "index.html", logs: logs, transaction: transaction_form) |
||||
|
||||
defp get_transaction(hash) do |
||||
Transaction |
||||
|> Query.by_hash(hash) |
||||
|> Query.include_addresses() |
||||
|> Query.include_receipt() |
||||
|> Query.include_block() |
||||
|> Repo.one() |
||||
|> TransactionForm.build_and_merge() |
||||
{:error, :not_found} -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue