Chain context

Move business logic out of ExplorerWeb into context module, Explorer.Chain.  No
code in ExplorerWeb should access Explorer.Repo directly anymore.
pull/118/head
Luke Imhoff 7 years ago
parent 5e53958424
commit 8fb72a26ed
  1. 7
      .credo.exs
  2. 2
      .gitignore
  3. 5
      README.md
  4. 7
      apps/explorer/lib/explorer/application.ex
  5. 544
      apps/explorer/lib/explorer/chain.ex
  6. 66
      apps/explorer/lib/explorer/chain/address.ex
  7. 105
      apps/explorer/lib/explorer/chain/block.ex
  8. 12
      apps/explorer/lib/explorer/chain/block_transaction.ex
  9. 10
      apps/explorer/lib/explorer/chain/credit.ex
  10. 10
      apps/explorer/lib/explorer/chain/debit.ex
  11. 11
      apps/explorer/lib/explorer/chain/from_address.ex
  12. 10
      apps/explorer/lib/explorer/chain/hash.ex
  13. 24
      apps/explorer/lib/explorer/chain/internal_transaction.ex
  14. 26
      apps/explorer/lib/explorer/chain/log.ex
  15. 10
      apps/explorer/lib/explorer/chain/receipt.ex
  16. 74
      apps/explorer/lib/explorer/chain/statistics.ex
  17. 49
      apps/explorer/lib/explorer/chain/statistics/server.ex
  18. 11
      apps/explorer/lib/explorer/chain/to_address.ex
  19. 12
      apps/explorer/lib/explorer/chain/transaction.ex
  20. 6
      apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
  21. 17
      apps/explorer/lib/explorer/importers/balance_importer.ex
  22. 5
      apps/explorer/lib/explorer/importers/block_importer.ex
  23. 12
      apps/explorer/lib/explorer/importers/internal_transaction_importer.ex
  24. 8
      apps/explorer/lib/explorer/importers/receipt_importer.ex
  25. 13
      apps/explorer/lib/explorer/importers/transaction_importer.ex
  26. 56
      apps/explorer/lib/explorer/resource.ex
  27. 3
      apps/explorer/lib/explorer/schema.ex
  28. 43
      apps/explorer/lib/explorer/schemas/address.ex
  29. 48
      apps/explorer/lib/explorer/schemas/block.ex
  30. 48
      apps/explorer/lib/explorer/servers/chain_statistics.ex
  31. 58
      apps/explorer/lib/explorer/services/address.ex
  32. 118
      apps/explorer/lib/explorer/services/transaction.ex
  33. 4
      apps/explorer/lib/explorer/skipped_balances.ex
  34. 3
      apps/explorer/lib/explorer/skipped_blocks.ex
  35. 2
      apps/explorer/lib/explorer/skipped_internal_transactions.ex
  36. 2
      apps/explorer/lib/explorer/skipped_receipts.ex
  37. 3
      apps/explorer/lib/explorer/workers/import_transaction.ex
  38. 3
      apps/explorer/lib/explorer/workers/refresh_balance.ex
  39. 4
      apps/explorer/lib/mix/tasks/exq.start.ex
  40. 4
      apps/explorer/lib/mix/tasks/scrape.balances.ex
  41. 6
      apps/explorer/lib/mix/tasks/scrape.blocks.ex
  42. 5
      apps/explorer/lib/mix/tasks/scrape.internal_transactions.ex
  43. 5
      apps/explorer/lib/mix/tasks/scrape.receipts.ex
  44. 3
      apps/explorer/mix.exs
  45. 5
      apps/explorer/test/explorer/chain/address_test.exs
  46. 5
      apps/explorer/test/explorer/chain/block_test.exs
  47. 5
      apps/explorer/test/explorer/chain/block_transaction_test.exs
  48. 4
      apps/explorer/test/explorer/chain/credit_test.exs
  49. 4
      apps/explorer/test/explorer/chain/debit_test.exs
  50. 5
      apps/explorer/test/explorer/chain/from_address_test.exs
  51. 4
      apps/explorer/test/explorer/chain/internal_transaction_test.exs
  52. 4
      apps/explorer/test/explorer/chain/log_test.exs
  53. 4
      apps/explorer/test/explorer/chain/receipt_test.exs
  54. 89
      apps/explorer/test/explorer/chain/statistics/server_test.exs
  55. 116
      apps/explorer/test/explorer/chain/statistics_test.exs
  56. 5
      apps/explorer/test/explorer/chain/to_address_test.exs
  57. 4
      apps/explorer/test/explorer/chain/transaction_test.exs
  58. 794
      apps/explorer/test/explorer/chain_test.exs
  59. 22
      apps/explorer/test/explorer/importers/balance_importer_test.exs
  60. 3
      apps/explorer/test/explorer/importers/block_importer_test.exs
  61. 2
      apps/explorer/test/explorer/importers/internal_transaction_importer_test.exs
  62. 3
      apps/explorer/test/explorer/importers/receipt_importer_test.exs
  63. 4
      apps/explorer/test/explorer/importers/transaction_importer_test.exs
  64. 45
      apps/explorer/test/explorer/resource_test.exs
  65. 85
      apps/explorer/test/explorer/servers/chain_statistics_test.exs
  66. 56
      apps/explorer/test/explorer/services/address_test.exs
  67. 16
      apps/explorer/test/explorer/services/transaction_test.exs
  68. 17
      apps/explorer/test/explorer/workers/import_balance_test.exs
  69. 8
      apps/explorer/test/explorer/workers/import_block_test.exs
  70. 2
      apps/explorer/test/explorer/workers/import_internal_transaction_test.exs
  71. 2
      apps/explorer/test/explorer/workers/import_receipt_test.exs
  72. 9
      apps/explorer/test/explorer/workers/import_skipped_blocks_test.exs
  73. 4
      apps/explorer/test/explorer/workers/import_transaction_test.exs
  74. 3
      apps/explorer/test/explorer/workers/refresh_balance_test.exs
  75. 16
      apps/explorer/test/support/data_case.ex
  76. 4
      apps/explorer/test/support/factories/chain/address_factory.ex
  77. 4
      apps/explorer/test/support/factories/chain/block_factory.ex
  78. 4
      apps/explorer/test/support/factories/chain/block_transaction_factory.ex
  79. 4
      apps/explorer/test/support/factories/chain/from_address_factory.ex
  80. 4
      apps/explorer/test/support/factories/chain/internal_transaction_factory.ex
  81. 11
      apps/explorer/test/support/factories/chain/log_factory.ex
  82. 4
      apps/explorer/test/support/factories/chain/receipt_factory.ex
  83. 4
      apps/explorer/test/support/factories/chain/to_address_factory.ex
  84. 7
      apps/explorer/test/support/factories/chain/transaction_factory.ex
  85. 18
      apps/explorer/test/support/factory.ex
  86. 12
      apps/explorer_web/lib/explorer_web.ex
  87. 33
      apps/explorer_web/lib/explorer_web/chain.ex
  88. 18
      apps/explorer_web/lib/explorer_web/controller.ex
  89. 12
      apps/explorer_web/lib/explorer_web/controllers/address_controller.ex
  90. 34
      apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex
  91. 34
      apps/explorer_web/lib/explorer_web/controllers/address_transaction_to_controller.ex
  92. 28
      apps/explorer_web/lib/explorer_web/controllers/block_controller.ex
  93. 37
      apps/explorer_web/lib/explorer_web/controllers/block_transaction_controller.ex
  94. 31
      apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex
  95. 73
      apps/explorer_web/lib/explorer_web/controllers/pending_transaction_controller.ex
  96. 87
      apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex
  97. 47
      apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex
  98. 25
      apps/explorer_web/lib/explorer_web/forms/block_form.ex
  99. 34
      apps/explorer_web/lib/explorer_web/forms/pending_transaction_form.ex
  100. 81
      apps/explorer_web/lib/explorer_web/forms/transaction_form.ex
  101. Some files were not shown because too many files have changed in this diff Show More

@ -22,7 +22,12 @@
# In the latter case `**/*.{ex,exs}` will be used.
#
included: ["lib/", "src/", "web/", "apps/*/lib/**/*.{ex,exs}"],
excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"]
excluded: [
~r"/_build/",
~r"/deps/",
~r"/node_modules/",
~r"/apps/explorer_web/lib/explorer_web.ex"
]
},
#
# If you create your own checks, you must specify the source files for

2
.gitignore vendored

@ -1,8 +1,10 @@
# App artifacts
/_build
/apps/*/cover
/cover
/db
/deps
/doc
/*.ez
# Generated on crash by the VM

@ -38,6 +38,11 @@ You can also run IEx (Interactive Elixir): `iex -S mix phx.server`
Configure your local CCMenu with the following url: [`https://circleci.com/gh/poanetwork/poa-explorer.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604`](https://circleci.com/gh/poanetwork/poa-explorer.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604)
### Documentation
* `mix docs`
* `open doc/index.html`
### Testing
#### Prerequisites

@ -5,6 +5,8 @@ defmodule Explorer.Application do
use Application
import Supervisor.Spec
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
def start(_type, _args) do
@ -17,20 +19,17 @@ defmodule Explorer.Application do
defp children(:test), do: children()
defp children(_) do
import Supervisor.Spec
exq_options = [] |> Keyword.put(:mode, :enqueuer)
children() ++
[
supervisor(Exq, [exq_options]),
worker(Explorer.Servers.ChainStatistics, []),
worker(Explorer.Chain.Statistics.Server, []),
Explorer.ExchangeRates
]
end
defp children do
import Supervisor.Spec
[
supervisor(Explorer.Repo, []),
{Task.Supervisor, name: Explorer.ExchangeRateTaskSupervisor}

@ -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,4 +1,4 @@
defmodule Explorer.Credit do
defmodule Explorer.Chain.Credit do
@moduledoc """
A materialized view representing the credits to an address.
"""
@ -6,15 +6,17 @@ defmodule Explorer.Credit do
use Explorer.Schema
alias Ecto.Adapters.SQL
alias Explorer.Address
alias Explorer.Chain.Address
alias Explorer.Repo
@primary_key false
schema "credits" do
belongs_to(:address, Address, primary_key: true)
field(:value, :decimal)
field(:count, :integer)
field(:value, :decimal)
timestamps()
belongs_to(:address, Address, primary_key: true)
end
def refresh do

@ -1,4 +1,4 @@
defmodule Explorer.Debit do
defmodule Explorer.Chain.Debit do
@moduledoc """
A materialized view representing the debits from an address.
"""
@ -6,15 +6,17 @@ defmodule Explorer.Debit do
use Explorer.Schema
alias Ecto.Adapters.SQL
alias Explorer.Address
alias Explorer.Chain.Address
alias Explorer.Repo
@primary_key false
schema "debits" do
belongs_to(:address, Address, primary_key: true)
field(:value, :decimal)
field(:count, :integer)
field(:value, :decimal)
timestamps()
belongs_to(:address, Address, primary_key: true)
end
def refresh do

@ -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.InternalTransaction do
defmodule Explorer.Chain.InternalTransaction do
@moduledoc "Models internal transactions."
use Explorer.Schema
alias Explorer.InternalTransaction
alias Explorer.Transaction
alias Explorer.Address
alias Explorer.Chain.{Address, Transaction}
schema "internal_transactions" do
belongs_to(:transaction, Transaction)
belongs_to(:from_address, Address)
belongs_to(:to_address, Address)
field(:index, :integer)
field(:call_type, :string)
field(:trace_address, {:array, :integer})
field(:value, :decimal)
field(:gas, :decimal)
field(:gas_used, :decimal)
field(:index, :integer)
field(:input, :string)
field(:output, :string)
field(:trace_address, {:array, :integer})
field(:value, :decimal)
timestamps()
belongs_to(:from_address, Address)
belongs_to(:to_address, Address)
belongs_to(:transaction, Transaction)
end
@required_attrs ~w(index call_type trace_address value gas gas_used
transaction_id from_address_id to_address_id)a
@optional_attrs ~w(input output)
def changeset(%InternalTransaction{} = internal_transaction, attrs \\ %{}) do
def changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do
internal_transaction
|> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs)
@ -36,5 +36,5 @@ defmodule Explorer.InternalTransaction do
|> unique_constraint(:transaction_id, name: :internal_transactions_transaction_id_index_index)
end
def null, do: %InternalTransaction{}
def null, do: %__MODULE__{}
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)

@ -1,11 +1,9 @@
defmodule Explorer.Receipt do
defmodule Explorer.Chain.Receipt do
@moduledoc "Captures a Web3 Transaction Receipt."
use Explorer.Schema
alias Explorer.Transaction
alias Explorer.Log
alias Explorer.Receipt
alias Explorer.Chain.{Log, Transaction}
@required_attrs ~w(cumulative_gas_used gas_used status index)a
@optional_attrs ~w(transaction_id)a
@ -20,7 +18,7 @@ defmodule Explorer.Receipt do
timestamps()
end
def changeset(%Receipt{} = transaction_receipt, attrs \\ %{}) do
def changeset(%__MODULE__{} = transaction_receipt, attrs \\ %{}) do
transaction_receipt
|> cast(attrs, @required_attrs)
|> cast(attrs, @optional_attrs)
@ -31,5 +29,5 @@ defmodule Explorer.Receipt do
|> unique_constraint(:transaction_id)
end
def null, do: %Receipt{}
def null, do: %__MODULE__{}
end

@ -1,4 +1,4 @@
defmodule Explorer.Chain do
defmodule Explorer.Chain.Statistics do
@moduledoc """
Represents statistics about the chain.
"""
@ -6,21 +6,11 @@ defmodule Explorer.Chain do
import Ecto.Query
alias Ecto.Adapters.SQL
alias Explorer.Block
alias Explorer.Transaction
alias Explorer.Repo, as: Repo
alias Explorer.Chain.{Block, Transaction}
alias Explorer.Repo
alias Timex.Duration
defstruct number: -1,
timestamp: :calendar.universal_time(),
average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
lag: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
transaction_count: 0,
skipped_blocks: 0,
block_velocity: 0,
transaction_velocity: 0,
blocks: [],
transactions: []
# Constants
@average_time_query """
SELECT coalesce(avg(difference), interval '0 seconds')
@ -69,6 +59,60 @@ defmodule Explorer.Chain do
WHERE transactions.inserted_at > NOW() - interval '1 minute'
"""
# Types
@typedoc """
The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute.
"""
@type blocks_per_minute :: non_neg_integer()
@typedoc """
The number of `t:Explorer.Chain.Transaction.t/0` mined/validated per minute.
"""
@type transactions_per_minute :: non_neg_integer()
@typedoc """
* `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0`
* `block_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0`
* `lag` - the average time over the last hour between when the block was mined/validated
(`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse
(`t:Explorer.Chain.Block.t/0` `inserted_at`)
* `number` - the latest `t:Explorer.Chain.Block.t/0` `number`
* `skipped_blocks` - the number of blocks that were mined/validated, but do not exist as `t:Explorer.Chain.Block.t/0`
* `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated
* `transaction_count` - the number of transactions confirmed in blocks that were mined/validated in the last day
* `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute
* `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0`
"""
@type t :: %__MODULE__{
average_time: Duration.t(),
block_velocity: blocks_per_minute(),
blocks: [Block.t()],
lag: Duration.t(),
number: Block.number(),
skipped_blocks: non_neg_integer(),
timestamp: :calendar.datetime(),
transaction_count: non_neg_integer(),
transaction_velocity: transactions_per_minute(),
transactions: [Transaction.t()]
}
# Struct
defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
block_velocity: 0,
blocks: [],
lag: %Duration{seconds: 0, megaseconds: 0, microseconds: 0},
number: -1,
skipped_blocks: 0,
timestamp: :calendar.universal_time(),
transaction_count: 0,
transaction_velocity: 0,
transactions: []
# Functions
def fetch do
blocks =
from(
@ -90,7 +134,7 @@ defmodule Explorer.Chain do
last_block = Block |> Block.latest() |> limit(1) |> Repo.one()
latest_block = last_block || Block.null()
%Explorer.Chain{
%__MODULE__{
number: latest_block.number,
timestamp: latest_block.timestamp,
average_time: query_duration(@average_time_query),

@ -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,13 +1,9 @@
defmodule Explorer.Transaction do
defmodule Explorer.Chain.Transaction do
@moduledoc "Models a Web3 transaction."
use Explorer.Schema
alias Explorer.Address
alias Explorer.BlockTransaction
alias Explorer.InternalTransaction
alias Explorer.Receipt
alias Explorer.Transaction
alias Explorer.Chain.{Address, BlockTransaction, InternalTransaction, Receipt}
schema "transactions" do
has_one(:receipt, Receipt)
@ -37,7 +33,7 @@ defmodule Explorer.Transaction do
@optional_attrs ~w(to_address_id from_address_id)a
@doc false
def changeset(%Transaction{} = transaction, attrs \\ %{}) do
def changeset(%__MODULE__{} = transaction, attrs \\ %{}) do
transaction
|> cast(attrs, @required_attrs ++ @optional_attrs)
|> validate_required(@required_attrs)
@ -46,5 +42,5 @@ defmodule Explorer.Transaction do
|> unique_constraint(:hash)
end
def null, do: %Transaction{}
def null, do: %__MODULE__{}
end

@ -3,10 +3,8 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do
Adapter for fetching exchange rates from https://coinmarketcap.com.
"""
alias Explorer.ExchangeRates.Rate
alias Explorer.ExchangeRates.Source
alias HTTPoison.Error
alias HTTPoison.Response
alias Explorer.ExchangeRates.{Rate, Source}
alias HTTPoison.{Error, Response}
@behaviour Source

@ -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

@ -4,8 +4,8 @@ defmodule Explorer.BlockImporter do
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2]
alias Explorer.Block
alias Explorer.Ethereum
alias Explorer.{BlockImporter, Ethereum}
alias Explorer.Chain.Block
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Workers.ImportTransaction
@ -26,7 +26,6 @@ defmodule Explorer.BlockImporter do
@dialyzer {:nowarn_function, import: 1}
def import(block_number) do
alias Explorer.BlockImporter
block_number |> download_block() |> BlockImporter.import()
end

@ -3,12 +3,8 @@ defmodule Explorer.InternalTransactionImporter do
import Ecto.Query
alias Explorer.Address.Service, as: Address
alias Explorer.Ethereum
alias Explorer.EthereumexExtensions
alias Explorer.InternalTransaction
alias Explorer.Repo
alias Explorer.Transaction
alias Explorer.{Chain, Ethereum, EthereumexExtensions, Repo}
alias Explorer.Chain.{InternalTransaction, Transaction}
@dialyzer {:nowarn_function, import: 1}
def import(hash) do
@ -77,6 +73,8 @@ defmodule Explorer.InternalTransactionImporter do
end
defp address_id(hash) do
Address.find_or_create_by_hash(hash).id
{:ok, address} = Chain.ensure_hash_address(hash)
address.id
end
end

@ -4,10 +4,8 @@ defmodule Explorer.ReceiptImporter do
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1]
alias Explorer.Address.Service, as: Address
alias Explorer.Repo
alias Explorer.Transaction
alias Explorer.Receipt
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Receipt, Transaction}
def import(hash) do
transaction = hash |> find_transaction()
@ -59,7 +57,7 @@ defmodule Explorer.ReceiptImporter do
end
defp extract_log(log) do
address = Address.find_or_create_by_hash(log["address"])
{:ok, address} = Chain.ensure_hash_address(log["address"])
%{
address_id: address.id,

@ -4,13 +4,8 @@ defmodule Explorer.TransactionImporter do
import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1]
alias Explorer.Address.Service, as: Address
alias Explorer.Block
alias Explorer.BlockTransaction
alias Explorer.Ethereum
alias Explorer.Repo
alias Explorer.Transaction
alias Explorer.BalanceImporter
alias Explorer.{Chain, Ethereum, Repo, BalanceImporter}
alias Explorer.Chain.{Block, BlockTransaction, Transaction}
def import(hash) when is_binary(hash) do
hash |> download_transaction() |> persist_transaction()
@ -126,7 +121,9 @@ defmodule Explorer.TransactionImporter do
def from_address(hash) when is_bitstring(hash), do: hash
def fetch_address(hash) when is_bitstring(hash) do
Address.find_or_create_by_hash(hash)
{:ok, address} = Chain.ensure_hash_address(hash)
address
end
defp refresh_account_balances(raw_transaction) do

@ -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

@ -5,8 +5,7 @@ defmodule Explorer.Schema do
quote do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query
import Ecto.{Changeset, Query}
@timestamps_opts [
type: Timex.Ecto.DateTime,

@ -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,8 +1,8 @@
defmodule Explorer.SkippedBalances do
@moduledoc "Gets a list of Addresses that do not have balances."
alias Explorer.Address
alias Explorer.Repo
alias Explorer.Chain.Address
alias Explorer.Repo.NewRelic, as: Repo
import Ecto.Query, only: [from: 2]

@ -3,7 +3,8 @@ defmodule Explorer.SkippedBlocks do
Fill in older blocks that were skipped during processing.
"""
import Ecto.Query, only: [from: 2, limit: 2]
alias Explorer.Block
alias Explorer.Chain.Block
alias Explorer.Repo.NewRelic, as: Repo
@missing_number_query "SELECT generate_series(?, 0, -1) AS missing_number"

@ -4,7 +4,7 @@ defmodule Explorer.SkippedInternalTransactions do
"""
import Ecto.Query, only: [from: 2]
alias Explorer.Transaction
alias Explorer.Chain.Transaction
alias Explorer.Repo.NewRelic, as: Repo
def first, do: first(1)

@ -4,7 +4,7 @@ defmodule Explorer.SkippedReceipts do
"""
import Ecto.Query, only: [from: 2]
alias Explorer.Transaction
alias Explorer.Chain.Transaction
alias Explorer.Repo.NewRelic, as: Repo
def first, do: first(1)

@ -4,8 +4,7 @@ defmodule Explorer.Workers.ImportTransaction do
"""
alias Explorer.TransactionImporter
alias Explorer.Workers.ImportReceipt
alias Explorer.Workers.ImportInternalTransaction
alias Explorer.Workers.{ImportInternalTransaction, ImportReceipt}
@dialyzer {:nowarn_function, perform: 1}
def perform(hash) when is_binary(hash) do

@ -4,8 +4,7 @@ defmodule Explorer.Workers.RefreshBalance do
"""
alias Ecto.Adapters.SQL
alias Explorer.Credit
alias Explorer.Debit
alias Explorer.Chain.{Credit, Debit}
alias Explorer.Repo
def perform("credit"), do: unless(refreshing("credits"), do: Credit.refresh())

@ -1,8 +1,8 @@
defmodule Mix.Tasks.Exq.Start do
@moduledoc "Starts the Exq worker"
use Mix.Task
alias Explorer.Repo
alias Explorer.Scheduler
alias Explorer.{Repo, Scheduler}
def run(["scheduler"]) do
[:postgrex, :ecto, :ethereumex, :tzdata]

@ -3,9 +3,7 @@ defmodule Mix.Tasks.Scrape.Balances do
use Mix.Task
alias Explorer.Repo
alias Explorer.SkippedBalances
alias Explorer.BalanceImporter
alias Explorer.{BalanceImporter, Repo, SkippedBalances}
def run([]), do: run(1)

@ -1,9 +1,9 @@
defmodule Mix.Tasks.Scrape.Blocks do
@moduledoc "Scrapes blocks from web3"
use Mix.Task
alias Explorer.Repo
alias Explorer.SkippedBlocks
alias Explorer.BlockImporter
alias Explorer.{BlockImporter, Repo, SkippedBlocks}
def run([]), do: run(1)

@ -1,10 +1,9 @@
defmodule Mix.Tasks.Scrape.InternalTransactions do
@moduledoc "Backfill Internal Transactions via Parity Trace."
use Mix.Task
alias Explorer.Repo
alias Explorer.SkippedInternalTransactions
alias Explorer.InternalTransactionImporter
alias Explorer.{InternalTransactionImporter, Repo, SkippedInternalTransactions}
def run([]), do: run(1)

@ -1,10 +1,9 @@
defmodule Mix.Tasks.Scrape.Receipts do
@moduledoc "Scrapes blocks from web3"
use Mix.Task
alias Explorer.Repo
alias Explorer.SkippedReceipts
alias Explorer.ReceiptImporter
alias Explorer.{ReceiptImporter, Repo, SkippedReceipts}
def run([]), do: run(1)

@ -71,7 +71,7 @@ defmodule Explorer.Mixfile do
defp deps do
[
{:bypass, "~> 0.8", only: :test},
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},
{:credo, "0.9.1", only: [:dev, :test], runtime: false},
{:crontab, "~> 1.1"},
{:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false},
{:ethereumex, "~> 0.3"},
@ -107,6 +107,7 @@ defmodule Explorer.Mixfile do
# See the documentation for `Mix` for more info on aliases.
defp aliases do
[
compile: "compile --warnings-as-errors",
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate", "test"]

@ -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 "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)
test "returns the highest block number when there is a block" do
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
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)
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 "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
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))
insert_list(6, :transaction)
|> Enum.map(fn transaction ->
insert(:block_transaction, block: block, transaction: transaction)
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,15 +1,19 @@
defmodule Explorer.BalanceImporterTest do
use Explorer.DataCase
alias Explorer.Address.Service, as: Address
alias Explorer.BalanceImporter
alias Explorer.{Chain, BalanceImporter}
alias Explorer.Chain.Address
describe "import/1" do
test "it updates the balance for an address" do
insert(:address, hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", balance: 5)
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
assert address.balance == Decimal.new(1_572_374_181_095_000_000)
expected_balance = Decimal.new(1_572_374_181_095_000_000)
assert {:ok, %Address{balance: ^expected_balance}} =
Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
end
test "it updates the balance update time for an address" do
@ -20,13 +24,17 @@ defmodule Explorer.BalanceImporterTest do
)
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
refute is_nil(address.balance_updated_at)
assert {:ok, %Address{balance_updated_at: balance_updated_at}} =
Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
refute is_nil(balance_updated_at)
end
test "it creates an address if one does not exist" do
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
assert Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
assert {:ok, _} = Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
end
end
end

@ -3,9 +3,8 @@ defmodule Explorer.BlockImporterTest do
import Mock
alias Explorer.Block
alias Explorer.Transaction
alias Explorer.BlockImporter
alias Explorer.Chain.{Block, Transaction}
alias Explorer.Workers.ImportTransaction
describe "import/1" do

@ -1,7 +1,7 @@
defmodule Explorer.InternalTransactionImporterTest do
use Explorer.DataCase
alias Explorer.InternalTransaction
alias Explorer.Chain.InternalTransaction
alias Explorer.InternalTransactionImporter
describe "import/1" do

@ -1,8 +1,7 @@
defmodule Explorer.ReceiptImporterTest do
use Explorer.DataCase
alias Explorer.Receipt
alias Explorer.Log
alias Explorer.Chain.{Log, Receipt}
alias Explorer.ReceiptImporter
describe "import/1" do

@ -1,9 +1,7 @@
defmodule Explorer.TransactionImporterTest do
use Explorer.DataCase
alias Explorer.Address
alias Explorer.BlockTransaction
alias Explorer.Transaction
alias Explorer.Chain.{Address, BlockTransaction, Transaction}
alias Explorer.TransactionImporter
@raw_transaction %{

@ -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,16 +1,20 @@
defmodule Explorer.Workers.ImportBalanceTest do
import Mock
alias Explorer.Chain
alias Explorer.Chain.Address
alias Explorer.Workers.ImportBalance
alias Explorer.Address.Service, as: Address
use Explorer.DataCase
describe "perform/1" do
test "imports the balance for an address" do
ImportBalance.perform("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a")
address = Address.by_hash("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a")
assert address.balance == Decimal.new(1_572_374_181_095_000_000)
expected_balance = Decimal.new(1_572_374_181_095_000_000)
assert {:ok, %Address{balance: ^expected_balance}} =
Chain.hash_to_address("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a")
end
end
@ -25,8 +29,11 @@ defmodule Explorer.Workers.ImportBalanceTest do
)
end do
ImportBalance.perform_later("0xskateboards")
address = Address.by_hash("0xskateboards")
assert address.balance == Decimal.new(66)
expected_balance = Decimal.new(66)
assert {:ok, %Address{balance: ^expected_balance}} =
Chain.hash_to_address("0xskateboards")
end
end
end

@ -1,11 +1,11 @@
defmodule Explorer.Workers.ImportBlockTest do
alias Explorer.Block
alias Explorer.Repo
alias Explorer.Workers.ImportBlock
use Explorer.DataCase
import Mock
use Explorer.DataCase
alias Explorer.Chain.Block
alias Explorer.Repo
alias Explorer.Workers.ImportBlock
describe "perform/1" do
test "imports the requested block number as an integer" do

@ -2,7 +2,7 @@ defmodule Explorer.Workers.ImportInternalTransactionTest do
use Explorer.DataCase
alias Explorer.Repo
alias Explorer.InternalTransaction
alias Explorer.Chain.InternalTransaction
alias Explorer.Workers.ImportInternalTransaction
describe "perform/1" do

@ -2,7 +2,7 @@ defmodule Explorer.Workers.ImportReceiptTest do
use Explorer.DataCase
alias Explorer.Repo
alias Explorer.Receipt
alias Explorer.Chain.Receipt
alias Explorer.Workers.ImportReceipt
describe "perform/1" do

@ -1,12 +1,11 @@
defmodule Explorer.Workers.ImportSkippedBlocksTest do
alias Explorer.Block
alias Explorer.Repo
alias Explorer.Workers.ImportBlock
alias Explorer.Workers.ImportSkippedBlocks
use Explorer.DataCase
import Mock
use Explorer.DataCase
alias Explorer.Chain.Block
alias Explorer.Repo
alias Explorer.Workers.{ImportBlock, ImportSkippedBlocks}
describe "perform/1" do
test "imports the requested number of skipped blocks" do

@ -3,10 +3,8 @@ defmodule Explorer.Workers.ImportTransactionTest do
import Mock
alias Explorer.InternalTransaction
alias Explorer.Receipt
alias Explorer.Chain.{InternalTransaction, Receipt, Transaction}
alias Explorer.Repo
alias Explorer.Transaction
alias Explorer.Workers.ImportInternalTransaction
alias Explorer.Workers.ImportTransaction

@ -3,8 +3,7 @@ defmodule Explorer.Workers.RefreshBalanceTest do
import Mock
alias Explorer.Credit
alias Explorer.Debit
alias Explorer.Chain.{Credit, Debit}
alias Explorer.Workers.RefreshBalance
describe "perform/0" do

@ -39,20 +39,4 @@ defmodule Explorer.DataCase do
:ok
end
@doc """
A helper that transform changeset errors to a map of messages.
assert {:error, changeset} = Accounts.create_user(%{password: "short"})
assert "password is too short" in errors_on(changeset).password
assert %{password: ["password is too short"]} = errors_on(changeset)
"""
def errors_on(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
Enum.reduce(opts, message, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
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

@ -20,9 +20,11 @@ defmodule ExplorerWeb do
def controller do
quote do
use Phoenix.Controller, namespace: ExplorerWeb
import Plug.Conn
import ExplorerWeb.Controller
import ExplorerWeb.Router.Helpers
import ExplorerWeb.Gettext
import Plug.Conn
end
end
@ -38,17 +40,16 @@ defmodule ExplorerWeb do
# Use all HTML functionality (forms, tags, etc)
use Phoenix.HTML
import ExplorerWeb.Router.Helpers
import ExplorerWeb.ErrorHelpers
import ExplorerWeb.Gettext
import Scrivener.HTML
import ExplorerWeb.{ErrorHelpers, Gettext, Router.Helpers}
import ReactPhoenix.ClientSide
import Scrivener.HTML
end
end
def router do
quote do
use Phoenix.Router
import Plug.Conn
import Phoenix.Controller
end
@ -57,6 +58,7 @@ defmodule ExplorerWeb do
def channel do
quote do
use Phoenix.Channel
import ExplorerWeb.Gettext
end
end

@ -0,0 +1,33 @@
defmodule ExplorerWeb.Chain do
@moduledoc """
Converts the `param` to the corresponding resource that uses that format of param.
"""
import Explorer.Chain, only: [hash_to_address: 1, hash_to_transaction: 1, number_to_block: 1]
@spec from_param(String.t()) ::
{:ok, Address.t() | Transaction.t() | Block.t()} | {:error, :not_found}
def from_param(param)
def from_param(hash) when byte_size(hash) > 42 do
hash_to_transaction(hash)
end
def from_param(hash) when byte_size(hash) == 42 do
hash_to_address(hash)
end
def from_param(formatted_number) when is_binary(formatted_number) do
case param_to_block_number(formatted_number) do
{:ok, number} -> number_to_block(number)
{:error, :invalid} -> {:error, :not_found}
end
end
def param_to_block_number(formatted_number) when is_binary(formatted_number) do
case Integer.parse(formatted_number) do
{number, ""} -> {:ok, number}
_ -> {:error, :invalid}
end
end
end

@ -0,0 +1,18 @@
defmodule ExplorerWeb.Controller do
@moduledoc """
Common controller error responses
"""
import Phoenix.Controller
import Plug.Conn
@doc """
Renders HTML Not Found error
"""
def not_found(conn) do
conn
|> put_status(:not_found)
|> put_view(ExplorerWeb.ErrorView)
|> render("404.html")
end
end

@ -1,10 +1,14 @@
defmodule ExplorerWeb.AddressController do
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

@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionFromController do
use ExplorerWeb, :controller
alias Explorer.Address.Service, as: Address
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias Explorer.Chain
alias ExplorerWeb.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do
address = Address.by_hash(address_id)
def index(conn, %{"address_id" => from_address_hash} = params) do
case Chain.hash_to_address(from_address_hash) do
{:ok, from_address} ->
page =
Chain.from_address_to_transactions(
from_address,
necessity_by_association: %{
block: :required,
from_address: :optional,
to_address: :optional,
receipt: :required
},
pagination: params
)
query =
Transaction
|> Query.from_address(address.id)
|> Query.include_addresses()
|> Query.require_receipt()
|> Query.require_block()
|> Query.chron()
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))
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionToController do
use ExplorerWeb, :controller
alias Explorer.Address.Service, as: Address
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias Explorer.Chain
alias ExplorerWeb.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do
address = Address.by_hash(address_id)
def index(conn, %{"address_id" => to_address_hash} = params) do
case Chain.hash_to_address(to_address_hash) do
{:ok, to_address} ->
page =
Chain.to_address_to_transactions(
to_address,
necessity_by_association: %{
block: :required,
from_address: :optional,
to_address: :optional,
receipt: :required
},
pagination: params
)
query =
Transaction
|> Query.to_address(address.id)
|> Query.include_addresses()
|> Query.require_receipt()
|> Query.require_block()
|> Query.chron()
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))
{: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))
else
{:error, :invalid} ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -1,34 +1,35 @@
defmodule ExplorerWeb.ChainController do
use ExplorerWeb, :controller
alias Explorer.Servers.ChainStatistics
alias Explorer.Resource
alias Explorer.Chain.{Address, Block, Statistics, Transaction}
alias ExplorerWeb.Chain
def show(conn, _params) do
render(conn, "show.html", chain: ChainStatistics.fetch())
render(conn, "show.html", chain: Statistics.fetch())
end
def search(conn, %{"q" => query}) do
query
|> String.trim()
|> Resource.lookup()
|> Chain.from_param()
|> case do
nil ->
conn
|> put_status(:not_found)
|> put_view(ExplorerWeb.ErrorView)
|> render("404.html")
item ->
{:ok, item} ->
redirect_search_results(conn, item)
{:error, :not_found} ->
not_found(conn)
end
end
defp redirect_search_results(conn, %Explorer.Block{} = item) do
defp redirect_search_results(conn, %Address{} = item) do
redirect(conn, to: address_path(conn, :show, Gettext.get_locale(), item.hash))
end
defp redirect_search_results(conn, %Block{} = item) do
redirect(conn, to: block_path(conn, :show, Gettext.get_locale(), item.number))
end
defp redirect_search_results(conn, %Explorer.Transaction{} = item) do
defp redirect_search_results(conn, %Transaction{} = item) do
redirect(
conn,
to:
@ -40,8 +41,4 @@ defmodule ExplorerWeb.ChainController do
)
)
end
defp redirect_search_results(conn, %Explorer.Address{} = item) do
redirect(conn, to: address_path(conn, :show, Gettext.get_locale(), item.hash))
end
end

@ -1,55 +1,31 @@
defmodule ExplorerWeb.PendingTransactionController do
use ExplorerWeb, :controller
import Ecto.Query
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Chain
alias Explorer.Chain.Transaction
alias ExplorerWeb.PendingTransactionForm
def index(conn, %{"last_seen" => last_seen} = _) do
query =
from(
transaction in Transaction,
inner_join: to_address in assoc(transaction, :to_address),
inner_join: from_address in assoc(transaction, :from_address),
preload: [to_address: to_address, from_address: from_address],
where:
fragment(
"NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)",
transaction.id
),
where: transaction.id < ^last_seen,
order_by: [desc: transaction.id],
limit: 10
)
total_query =
from(
transaction in Transaction,
where:
fragment(
"NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)",
transaction.id
),
order_by: [desc: transaction.id],
limit: 1
def index(conn, %{"last_seen" => last_seen_id} = _) do
total = Chain.transaction_count(pending: true)
entries =
last_seen_id
|> Chain.transactions_recently_before_id(
necessity_by_association: %{
from_address: :optional,
to_address: :optional
},
pending: true
)
|> Enum.map(&PendingTransactionForm.build/1)
total =
case Repo.one(total_query) do
nil -> 0
total -> total.id
end
entries = Repo.all(query)
last = List.last(entries) || Transaction.null()
render(
conn,
"index.html",
transactions: %{
entries: entries |> Enum.map(&PendingTransactionForm.build/1),
entries: entries,
total_entries: total,
last_seen: last.id
}
@ -57,21 +33,12 @@ defmodule ExplorerWeb.PendingTransactionController do
end
def index(conn, params) do
query =
from(
transaction in Transaction,
select: transaction.id,
where:
fragment(
"NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)",
transaction.id
),
order_by: [desc: transaction.id],
limit: 1
)
last_seen =
[pending: true]
|> Chain.last_transaction_id()
|> Kernel.+(1)
|> Integer.to_string()
first_id = Repo.one(query) || 0
last_seen = Integer.to_string(first_id + 1)
index(conn, Map.put(params, "last_seen", last_seen))
end
end

@ -1,38 +1,23 @@
defmodule ExplorerWeb.TransactionController do
use ExplorerWeb, :controller
import Ecto.Query
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service
alias Explorer.Transaction.Service.Query
alias Explorer.Chain
alias Explorer.Chain.Transaction
alias ExplorerWeb.TransactionForm
def index(conn, %{"last_seen" => last_seen}) do
query =
Transaction
|> Query.recently_seen(last_seen)
|> Query.include_addresses()
|> Query.require_receipt()
|> Query.require_block()
total_query =
from(
transaction in Transaction,
order_by: [desc: transaction.id],
limit: 1
)
total =
case Repo.one(total_query) do
nil -> 0
total -> total.id
end
def index(conn, %{"last_seen" => last_seen_id}) do
total = Chain.transaction_count()
entries =
query
|> Repo.all()
last_seen_id
|> Chain.transactions_recently_before_id(
necessity_by_association: %{
block: :required,
from_address: :optional,
to_address: :optional,
receipt: :required
}
)
|> Enum.map(&TransactionForm.build_and_merge/1)
last = List.last(entries) || Transaction.null()
@ -49,38 +34,42 @@ defmodule ExplorerWeb.TransactionController do
end
def index(conn, params) do
query =
from(
t in Transaction,
select: t.id,
order_by: [desc: t.id],
limit: 1
)
last_seen =
Chain.last_transaction_id()
|> Kernel.+(1)
|> Integer.to_string()
first_id = Repo.one(query) || 0
last_seen = Integer.to_string(first_id + 1)
index(conn, Map.put(params, "last_seen", last_seen))
end
def show(conn, params) do
transaction = get_transaction(String.downcase(params["id"]))
case Chain.hash_to_transaction(
params["id"],
necessity_by_association: %{
block: :optional,
from_address: :optional,
to_address: :optional,
receipt: :optional
}
) do
{:ok, transaction} ->
internal_transactions =
Chain.transaction_hash_to_internal_transactions(
transaction.hash,
necessity_by_association: %{from_address: :required, to_address: :required}
)
internal_transactions = Service.internal_transactions(transaction.hash)
transaction_form = TransactionForm.build_and_merge(transaction)
render(
conn,
"show.html",
internal_transactions: internal_transactions,
transaction: transaction
transaction: transaction_form
)
end
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

@ -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 =
from(
log in Log,
join: transaction in assoc(log, :transaction),
preload: [:address],
where: fragment("lower(?)", transaction.hash) == ^transaction_hash
Chain.transaction_to_logs(
transaction,
necessity_by_association: %{address: :optional},
pagination: params
)
render(
conn,
"index.html",
logs: Repo.paginate(logs),
transaction: transaction
)
end
transaction_form = TransactionForm.build_and_merge(transaction)
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

@ -1,32 +1,17 @@
defmodule ExplorerWeb.BlockForm do
@moduledoc false
alias Explorer.Block
alias Explorer.BlockTransaction
alias Explorer.Repo
import Ecto.Query
alias Explorer.Chain
def build(block) do
block
|> Map.merge(%{
transactions_count: block |> get_transactions_count,
age: block |> calculate_age,
formatted_timestamp: block |> format_timestamp
age: calculate_age(block),
formatted_timestamp: format_timestamp(block),
transactions_count: Chain.block_to_transaction_count(block)
})
end
def get_transactions_count(block) do
query =
from(
block_transaction in BlockTransaction,
join: block in Block,
where: block.id == block_transaction.block_id,
where: block.id == ^block.id,
select: count(block_transaction.block_id)
)
Repo.one(query)
end
def calculate_age(block) do
block.timestamp |> Timex.from_now()
end

@ -3,30 +3,40 @@ defmodule ExplorerWeb.PendingTransactionForm do
import ExplorerWeb.Gettext
alias Explorer.Chain.{Address, Transaction}
# Functions
def build(transaction) do
Map.merge(transaction, %{
to_address_hash: transaction |> to_address_hash,
from_address_hash: transaction |> from_address_hash,
first_seen: transaction |> first_seen,
last_seen: transaction |> last_seen,
first_seen: first_seen(transaction),
formatted_status: gettext("Pending"),
from_address_hash: from_address_hash(transaction),
last_seen: last_seen(transaction),
status: :pending,
formatted_status: gettext("Pending")
to_address_hash: to_address_hash(transaction)
})
end
def to_address_hash(transaction) do
(transaction.to_address && transaction.to_address.hash) || nil
def first_seen(transaction) do
transaction.inserted_at |> Timex.from_now()
end
def from_address_hash(transaction) do
(transaction.to_address && transaction.from_address.hash) || nil
def from_address_hash(%Transaction{from_address: from_address}) do
case from_address do
%Address{hash: hash} -> hash
_ -> nil
end
def first_seen(transaction) do
transaction.inserted_at |> Timex.from_now()
end
def last_seen(transaction) do
transaction.updated_at |> Timex.from_now()
end
def to_address_hash(%Transaction{to_address: to_address}) do
case to_address do
%Address{hash: hash} -> hash
_ -> nil
end
end
end

@ -1,17 +1,15 @@
defmodule ExplorerWeb.TransactionForm do
@moduledoc "Format a Block and a Transaction for display."
import Ecto.Query
import ExplorerWeb.Gettext
alias Cldr.Number
alias Explorer.Block
alias Explorer.Receipt
alias Explorer.Repo
alias Explorer.Chain
alias Explorer.Chain.{Receipt, Transaction}
def build(transaction) do
block = (Ecto.assoc_loaded?(transaction.block) && transaction.block) || nil
receipt = Ecto.assoc_loaded?(transaction.receipt) && transaction.receipt
block = block(transaction)
receipt = receipt(transaction)
status = status(transaction, receipt || Receipt.null())
%{
@ -34,47 +32,38 @@ defmodule ExplorerWeb.TransactionForm do
Map.merge(transaction, build(transaction))
end
def block_number(block) do
(block && block.number) || ""
def block(%Transaction{block: block}) do
if Ecto.assoc_loaded?(block) do
block
else
nil
end
end
def block_age(block) do
(block && block.timestamp |> Timex.from_now()) || gettext("Pending")
end
def format_age(block) do
(block && "#{block_age(block)} (#{format_timestamp(block)})") || gettext("Pending")
def block_number(block) do
(block && block.number) || ""
end
def format_timestamp(block) do
(block && block.timestamp |> Timex.format!("%b-%d-%Y %H:%M:%S %p %Z", :strftime)) ||
gettext("Pending")
def confirmations(nil), do: 0
def confirmations(block) do
Chain.confirmations(block, max_block_number: Chain.max_block_number())
end
def cumulative_gas_used(block) do
(block && block.gas_used |> Number.to_string!()) || gettext("Pending")
end
def to_address_hash(transaction) do
(transaction.to_address && transaction.to_address.hash) || nil
end
def from_address_hash(transaction) do
(transaction.to_address && transaction.from_address.hash) || nil
end
def confirmations(block) do
query = from(block in Block, select: max(block.number))
(block && Repo.one(query) - block.number) || 0
def first_seen(transaction) do
transaction.inserted_at |> Timex.from_now()
end
def status(transaction, receipt) do
%{
0 => %{true => :out_of_gas, false => :failed},
1 => %{true => :success, false => :success}
}
|> Map.get(receipt.status, %{true: :pending, false: :pending})
|> Map.get(receipt.gas_used == transaction.gas)
def format_age(block) do
(block && "#{block_age(block)} (#{format_timestamp(block)})") || gettext("Pending")
end
def format_status(status) do
@ -87,11 +76,37 @@ defmodule ExplorerWeb.TransactionForm do
|> Map.fetch!(status)
end
def first_seen(transaction) do
transaction.inserted_at |> Timex.from_now()
def format_timestamp(block) do
(block && block.timestamp |> Timex.format!("%b-%d-%Y %H:%M:%S %p %Z", :strftime)) ||
gettext("Pending")
end
def from_address_hash(transaction) do
(transaction.to_address && transaction.from_address.hash) || nil
end
def last_seen(transaction) do
transaction.updated_at |> Timex.from_now()
end
def receipt(%Transaction{receipt: receipt}) do
if Ecto.assoc_loaded?(receipt) do
receipt
else
nil
end
end
def status(transaction, receipt) do
%{
0 => %{true => :out_of_gas, false => :failed},
1 => %{true => :success, false => :success}
}
|> Map.get(receipt.status, %{true: :pending, false: :pending})
|> Map.get(receipt.gas_used == transaction.gas)
end
def to_address_hash(transaction) do
(transaction.to_address && transaction.to_address.hash) || nil
end
end

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save