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. 36
      apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex
  91. 36
      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. 43
      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. 95
      apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex
  97. 49
      apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex
  98. 25
      apps/explorer_web/lib/explorer_web/forms/block_form.ex
  99. 36
      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. # In the latter case `**/*.{ex,exs}` will be used.
# #
included: ["lib/", "src/", "web/", "apps/*/lib/**/*.{ex,exs}"], 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 # If you create your own checks, you must specify the source files for

2
.gitignore vendored

@ -1,8 +1,10 @@
# App artifacts # App artifacts
/_build /_build
/apps/*/cover
/cover /cover
/db /db
/deps /deps
/doc
/*.ez /*.ez
# Generated on crash by the VM # 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) 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 ### Testing
#### Prerequisites #### Prerequisites

@ -5,6 +5,8 @@ defmodule Explorer.Application do
use Application use Application
import Supervisor.Spec
# See https://hexdocs.pm/elixir/Application.html # See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications # for more information on OTP Applications
def start(_type, _args) do def start(_type, _args) do
@ -17,20 +19,17 @@ defmodule Explorer.Application do
defp children(:test), do: children() defp children(:test), do: children()
defp children(_) do defp children(_) do
import Supervisor.Spec
exq_options = [] |> Keyword.put(:mode, :enqueuer) exq_options = [] |> Keyword.put(:mode, :enqueuer)
children() ++ children() ++
[ [
supervisor(Exq, [exq_options]), supervisor(Exq, [exq_options]),
worker(Explorer.Servers.ChainStatistics, []), worker(Explorer.Chain.Statistics.Server, []),
Explorer.ExchangeRates Explorer.ExchangeRates
] ]
end end
defp children do defp children do
import Supervisor.Spec
[ [
supervisor(Explorer.Repo, []), supervisor(Explorer.Repo, []),
{Task.Supervisor, name: Explorer.ExchangeRateTaskSupervisor} {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" @moduledoc "Connects a Block to a Transaction"
alias Explorer.BlockTransaction
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Block, Transaction}
@primary_key false @primary_key false
schema "block_transactions" do schema "block_transactions" do
belongs_to(:block, Explorer.Block) belongs_to(:block, Block)
belongs_to(:transaction, Explorer.Transaction, primary_key: true) belongs_to(:transaction, Transaction, primary_key: true)
timestamps() timestamps()
end end
@required_attrs ~w(block_id transaction_id)a @required_attrs ~w(block_id transaction_id)a
def changeset(%BlockTransaction{} = block_transaction, attrs \\ %{}) do def changeset(%__MODULE__{} = block_transaction, attrs \\ %{}) do
block_transaction block_transaction
|> cast(attrs, @required_attrs) |> cast(attrs, @required_attrs)
|> validate_required(@required_attrs) |> validate_required(@required_attrs)

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

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

@ -1,18 +1,19 @@
defmodule Explorer.FromAddress do defmodule Explorer.Chain.FromAddress do
@moduledoc false @moduledoc false
use Explorer.Schema use Explorer.Schema
alias Explorer.FromAddress alias Explorer.Chain.{Address, Transaction}
@primary_key false @primary_key false
schema "from_addresses" do schema "from_addresses" do
belongs_to(:transaction, Explorer.Transaction, primary_key: true) belongs_to(:address, Address)
belongs_to(:address, Explorer.Address) belongs_to(:transaction, Transaction, primary_key: true)
timestamps() timestamps()
end end
def changeset(%FromAddress{} = to_address, attrs \\ %{}) do def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do
to_address to_address
|> cast(attrs, [:transaction_id, :address_id]) |> cast(attrs, [:transaction_id, :address_id])
|> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index) |> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index)

@ -0,0 +1,10 @@
defmodule Explorer.Chain.Hash do
@moduledoc """
Hash used throughout Ethereum chains.
"""
@typedoc """
[KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash as a string.
"""
@type t :: String.t()
end

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

@ -1,32 +1,32 @@
defmodule Explorer.Log do defmodule Explorer.Chain.Log do
@moduledoc "Captures a Web3 log entry generated by a transaction" @moduledoc "Captures a Web3 log entry generated by a transaction"
use Explorer.Schema use Explorer.Schema
alias Explorer.Address alias Explorer.Chain.{Address, Receipt}
alias Explorer.Log
alias Explorer.Receipt
@required_attrs ~w(index data type)a @required_attrs ~w(address_id data index type)a
@optional_attrs ~w( @optional_attrs ~w(
first_topic second_topic third_topic fourth_topic address_id first_topic second_topic third_topic fourth_topic
)a )a
schema "logs" do schema "logs" do
belongs_to(:receipt, Receipt)
belongs_to(:address, Address)
has_one(:transaction, through: [:receipt, :transaction])
field(:index, :integer)
field(:data, :string) field(:data, :string)
field(:type, :string)
field(:first_topic, :string) field(:first_topic, :string)
field(:fourth_topic, :string)
field(:index, :integer)
field(:second_topic, :string) field(:second_topic, :string)
field(:third_topic, :string) field(:third_topic, :string)
field(:fourth_topic, :string) field(:type, :string)
timestamps() timestamps()
belongs_to(:address, Address)
belongs_to(:receipt, Receipt)
has_one(:transaction, through: [:receipt, :transaction])
end end
def changeset(%Log{} = log, attrs \\ %{}) do def changeset(%__MODULE__{} = log, attrs \\ %{}) do
log log
|> cast(attrs, @required_attrs) |> cast(attrs, @required_attrs)
|> cast(attrs, @optional_attrs) |> cast(attrs, @optional_attrs)

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

@ -1,4 +1,4 @@
defmodule Explorer.Chain do defmodule Explorer.Chain.Statistics do
@moduledoc """ @moduledoc """
Represents statistics about the chain. Represents statistics about the chain.
""" """
@ -6,21 +6,11 @@ defmodule Explorer.Chain do
import Ecto.Query import Ecto.Query
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Explorer.Block alias Explorer.Chain.{Block, Transaction}
alias Explorer.Transaction alias Explorer.Repo
alias Explorer.Repo, as: Repo
alias Timex.Duration alias Timex.Duration
defstruct number: -1, # Constants
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: []
@average_time_query """ @average_time_query """
SELECT coalesce(avg(difference), interval '0 seconds') SELECT coalesce(avg(difference), interval '0 seconds')
@ -69,6 +59,60 @@ defmodule Explorer.Chain do
WHERE transactions.inserted_at > NOW() - interval '1 minute' 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 def fetch do
blocks = blocks =
from( from(
@ -90,7 +134,7 @@ defmodule Explorer.Chain do
last_block = Block |> Block.latest() |> limit(1) |> Repo.one() last_block = Block |> Block.latest() |> limit(1) |> Repo.one()
latest_block = last_block || Block.null() latest_block = last_block || Block.null()
%Explorer.Chain{ %__MODULE__{
number: latest_block.number, number: latest_block.number,
timestamp: latest_block.timestamp, timestamp: latest_block.timestamp,
average_time: query_duration(@average_time_query), 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 @moduledoc false
alias Explorer.ToAddress
use Explorer.Schema use Explorer.Schema
alias Explorer.Chain.{Address, Transaction}
@primary_key false @primary_key false
schema "to_addresses" do schema "to_addresses" do
belongs_to(:transaction, Explorer.Transaction, primary_key: true) belongs_to(:address, Address)
belongs_to(:address, Explorer.Address) belongs_to(:transaction, Transaction, primary_key: true)
timestamps() timestamps()
end end
def changeset(%ToAddress{} = to_address, attrs \\ %{}) do def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do
to_address to_address
|> cast(attrs, [:transaction_id, :address_id]) |> cast(attrs, [:transaction_id, :address_id])
|> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index) |> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index)

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

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

@ -1,18 +1,17 @@
defmodule Explorer.BalanceImporter do defmodule Explorer.BalanceImporter do
@moduledoc "Imports a balance for a given address." @moduledoc "Imports a balance for a given address."
alias Explorer.Address.Service, as: Address alias Explorer.{Chain, Ethereum}
alias Explorer.Ethereum
def import(hash) do def import(hash) do
hash encoded_balance = Ethereum.download_balance(hash)
|> Ethereum.download_balance()
|> persist_balance(hash) persist_balance(hash, encoded_balance)
end end
defp persist_balance(balance, hash) do defp persist_balance(hash, encoded_balance) when is_binary(hash) do
balance decoded_balance = Ethereum.decode_integer_field(encoded_balance)
|> Ethereum.decode_integer_field()
|> Address.update_balance(hash) Chain.update_balance(hash, decoded_balance)
end end
end end

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

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

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

@ -4,13 +4,8 @@ defmodule Explorer.TransactionImporter do
import Ecto.Query import Ecto.Query
import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1] import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1]
alias Explorer.Address.Service, as: Address alias Explorer.{Chain, Ethereum, Repo, BalanceImporter}
alias Explorer.Block alias Explorer.Chain.{Block, BlockTransaction, Transaction}
alias Explorer.BlockTransaction
alias Explorer.Ethereum
alias Explorer.Repo
alias Explorer.Transaction
alias Explorer.BalanceImporter
def import(hash) when is_binary(hash) do def import(hash) when is_binary(hash) do
hash |> download_transaction() |> persist_transaction() hash |> download_transaction() |> persist_transaction()
@ -126,7 +121,9 @@ defmodule Explorer.TransactionImporter do
def from_address(hash) when is_bitstring(hash), do: hash def from_address(hash) when is_bitstring(hash), do: hash
def fetch_address(hash) when is_bitstring(hash) do 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 end
defp refresh_account_balances(raw_transaction) do 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 quote do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.{Changeset, Query}
import Ecto.Query
@timestamps_opts [ @timestamps_opts [
type: Timex.Ecto.DateTime, 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 defmodule Explorer.SkippedBalances do
@moduledoc "Gets a list of Addresses that do not have balances." @moduledoc "Gets a list of Addresses that do not have balances."
alias Explorer.Address alias Explorer.Chain.Address
alias Explorer.Repo alias Explorer.Repo.NewRelic, as: Repo
import Ecto.Query, only: [from: 2] import Ecto.Query, only: [from: 2]

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

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

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

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

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

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

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

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

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

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

@ -1,6 +1,7 @@
defmodule Explorer.AddressTest do defmodule Explorer.Chain.AddressTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Address
alias Explorer.Chain.Address
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do

@ -1,9 +1,10 @@
defmodule Explorer.BlockTest do defmodule Explorer.Chain.BlockTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Block
import Ecto.Query, only: [order_by: 2] import Ecto.Query, only: [order_by: 2]
alias Explorer.Chain.Block
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do
changeset = build(:block) |> Block.changeset(%{}) changeset = build(:block) |> Block.changeset(%{})

@ -1,6 +1,7 @@
defmodule Explorer.BlockTransactionTest do defmodule Explorer.Chain.BlockTransactionTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.BlockTransaction
alias Explorer.Chain.BlockTransaction
describe "changeset/2" do describe "changeset/2" do
test "with empty attributes" do test "with empty attributes" do

@ -1,7 +1,7 @@
defmodule Explorer.CreditTest do defmodule Explorer.Chain.CreditTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Credit alias Explorer.Chain.Credit
describe "Repo.all/1" do describe "Repo.all/1" do
test "returns no rows when there are no addresses" do test "returns no rows when there are no addresses" do

@ -1,7 +1,7 @@
defmodule Explorer.DebitTest do defmodule Explorer.Chain.DebitTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Debit alias Explorer.Chain.Debit
describe "Repo.all/1" do describe "Repo.all/1" do
test "returns no rows when there are no addresses" do test "returns no rows when there are no addresses" do

@ -1,6 +1,7 @@
defmodule Explorer.FromAddressTest do defmodule Explorer.Chain.FromAddressTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.FromAddress
alias Explorer.Chain.FromAddress
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do

@ -1,7 +1,7 @@
defmodule Explorer.InternalTransactionTest do defmodule Explorer.Chain.InternalTransactionTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.InternalTransaction alias Explorer.Chain.InternalTransaction
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do

@ -1,7 +1,7 @@
defmodule Explorer.LogTest do defmodule Explorer.Chain.LogTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Log alias Explorer.Chain.Log
describe "changeset/2" do describe "changeset/2" do
test "accepts valid attributes" do test "accepts valid attributes" do

@ -1,7 +1,7 @@
defmodule Explorer.ReceiptTest do defmodule Explorer.Chain.ReceiptTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Receipt alias Explorer.Chain.Receipt
describe "changeset/2" do describe "changeset/2" do
test "accepts valid attributes" do test "accepts valid attributes" do

@ -0,0 +1,89 @@
defmodule Explorer.Chain.Statistics.ServerTest do
use Explorer.DataCase
alias Explorer.Chain.Statistics
alias Explorer.Chain.Statistics.Server
describe "init/1" do
test "returns a new chain when not told to refresh" do
{:ok, statistics} = Server.init(false)
assert statistics.number == Statistics.fetch().number
end
test "returns a new chain when told to refresh" do
{:ok, statistics} = Server.init(true)
assert statistics == Statistics.fetch()
end
test "refreshes when told to refresh" do
{:ok, _} = Server.init(true)
assert_receive :refresh, 2_000
end
end
describe "fetch/0" do
test "fetches the chain when not started" do
original = Statistics.fetch()
assert Server.fetch() == original
end
end
describe "handle_info/2" do
test "returns the original chain when sent a :refresh message" do
original = Statistics.fetch()
assert {:noreply, ^original} = Server.handle_info(:refresh, original)
end
test "launches an update when sent a :refresh message" do
original = Statistics.fetch()
{:ok, pid} = Server.start_link()
chain = Server.fetch()
:ok = GenServer.stop(pid)
assert original.number == chain.number
end
test "does not reply when sent any other message" do
assert {:noreply, _} = Server.handle_info(:ham, %Statistics{})
end
end
describe "handle_call/3" do
test "replies with statistics when sent a :fetch message" do
original = Statistics.fetch()
assert {:reply, _, ^original} = Server.handle_call(:fetch, self(), original)
end
test "does not reply when sent any other message" do
assert {:noreply, _} = Server.handle_call(:ham, self(), %Statistics{})
end
end
describe "handle_cast/2" do
test "schedules a refresh of the statistics when sent an update" do
statistics = Statistics.fetch()
Server.handle_cast({:update, statistics}, %Statistics{})
assert_receive :refresh, 2_000
end
test "returns a noreply and the new incoming chain when sent an update" do
original = Statistics.fetch()
assert {:noreply, ^original} = Server.handle_cast({:update, original}, %Statistics{})
end
test "returns a noreply and the old chain when sent any other cast" do
original = Statistics.fetch()
assert {:noreply, ^original} = Server.handle_cast(:ham, original)
end
end
end

@ -0,0 +1,116 @@
defmodule Explorer.Chain.StatisticsTest do
use Explorer.DataCase
alias Explorer.Chain.Statistics
alias Timex.Duration
describe "fetch/0" do
test "returns -1 for the number when there are no blocks" do
assert %Statistics{number: -1} = Statistics.fetch()
end
test "returns the highest block number when there is a block" do
insert(:block, number: 1)
max_number = 100
insert(:block, number: max_number)
assert %Statistics{number: ^max_number} = Statistics.fetch()
end
test "returns the latest block timestamp" do
time = DateTime.utc_now()
insert(:block, timestamp: time)
statistics = Statistics.fetch()
assert Timex.diff(statistics.timestamp, time, :seconds) == 0
end
test "returns the average time between blocks" do
time = DateTime.utc_now()
next_time = Timex.shift(time, seconds: 5)
insert(:block, timestamp: time)
insert(:block, timestamp: next_time)
assert %Statistics{
average_time: %Duration{
seconds: 5,
megaseconds: 0,
microseconds: 0
}
} = Statistics.fetch()
end
test "returns the count of transactions from blocks in the last day" do
time = DateTime.utc_now()
last_week = Timex.shift(time, days: -8)
block = insert(:block, timestamp: time)
old_block = insert(:block, timestamp: last_week)
transaction = insert(:transaction)
old_transaction = insert(:transaction)
insert(:block_transaction, block: block, transaction: transaction)
insert(:block_transaction, block: old_block, transaction: old_transaction)
assert %Statistics{transaction_count: 1} = Statistics.fetch()
end
test "returns the number of skipped blocks" do
insert(:block, %{number: 0})
insert(:block, %{number: 2})
statistics = Statistics.fetch()
assert statistics.skipped_blocks == 1
end
test "returns the lag between validation and insertion time" do
validation_time = DateTime.utc_now()
inserted_at = validation_time |> Timex.shift(seconds: 5)
insert(:block, timestamp: validation_time, inserted_at: inserted_at)
assert %Statistics{lag: %Duration{seconds: 5, megaseconds: 0, microseconds: 0}} =
Statistics.fetch()
end
test "returns the number of blocks inserted in the last minute" do
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1)
insert(:block, inserted_at: old_inserted_at)
insert(:block)
statistics = Statistics.fetch()
assert statistics.block_velocity == 1
end
test "returns the number of transactions inserted in the last minute" do
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1)
insert(:transaction, inserted_at: old_inserted_at)
insert(:transaction)
assert %Statistics{transaction_velocity: 1} = Statistics.fetch()
end
test "returns the last five blocks" do
insert_list(6, :block)
statistics = Statistics.fetch()
assert statistics.blocks |> Enum.count() == 5
end
test "returns the last five transactions with blocks" do
block = insert(:block)
6
|> insert_list(:transaction)
|> Enum.map(fn transaction ->
insert(:block_transaction, block: block, transaction: transaction)
end)
statistics = Statistics.fetch()
assert statistics.transactions |> Enum.count() == 5
end
end
end

@ -1,6 +1,7 @@
defmodule Explorer.ToAddressTest do defmodule Explorer.Chain.ToAddressTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.ToAddress
alias Explorer.Chain.ToAddress
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do

@ -1,7 +1,7 @@
defmodule Explorer.TransactionTest do defmodule Explorer.Chain.TransactionTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Transaction alias Explorer.Chain.Transaction
describe "changeset/2" do describe "changeset/2" do
test "with valid attributes" do test "with valid attributes" do

@ -1,103 +1,757 @@
defmodule Explorer.ChainTest do defmodule Explorer.ChainTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Chain alias Explorer.{Chain, Repo}
alias Timex.Duration
describe "fetch/0" do alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Receipt, Transaction}
test "returns -1 for the number when there are no blocks" do
chain = Chain.fetch() # Constants
assert chain.number == -1
@invalid_attrs %{hash: nil}
@valid_attrs %{hash: "some hash"}
# Tests
describe "block_to_transactions/1" do
test "without transactions" do
block = insert(:block)
assert Repo.aggregate(Transaction, :count, :id) == 0
assert %Scrivener.Page{
entries: [],
page_number: 1,
total_entries: 0
} = Chain.block_to_transactions(block)
end
test "with transactions" do
block = %Block{id: block_id} = insert(:block)
%Transaction{id: transaction_id} = insert(:transaction)
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id)
assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id}],
page_number: 1,
total_entries: 1
} = Chain.block_to_transactions(block)
end
test "with transaction with receipt required without receipt does not return transaction" do
block = %Block{id: block_id} = insert(:block)
%Transaction{id: transaction_id_with_receipt} = insert(:transaction)
insert(:receipt, transaction_id: transaction_id_with_receipt)
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id_with_receipt)
%Transaction{id: transaction_id_without_receipt} = insert(:transaction)
insert(
:block_transaction,
block_id: block_id,
transaction_id: transaction_id_without_receipt
)
assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}],
page_number: 1,
total_entries: 1
} =
Chain.block_to_transactions(
block,
necessity_by_association: %{receipt: :required}
)
assert %Scrivener.Page{
entries: transactions,
page_number: 1,
total_entries: 2
} =
Chain.block_to_transactions(
block,
necessity_by_association: %{receipt: :optional}
)
assert length(transactions) == 2
transaction_by_id =
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} ->
{id, transaction}
end)
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt]
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt]
end
test "with transactions can be paginated" do
block = %Block{id: block_id} = insert(:block)
transactions = insert_list(2, :transaction)
Enum.each(transactions, fn %Transaction{id: transaction_id} ->
insert(:block_transaction, block_id: block_id, transaction_id: transaction_id)
end)
[%Transaction{id: first_transaction_id}, %Transaction{id: second_transaction_id}] =
transactions
assert %Scrivener.Page{
entries: [%Transaction{id: ^first_transaction_id}],
page_number: 1,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.block_to_transactions(block, pagination: %{page_size: 1})
assert %Scrivener.Page{
entries: [%Transaction{id: ^second_transaction_id}],
page_number: 2,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.block_to_transactions(block, pagination: %{page: 2, page_size: 1})
end
end
describe "block_to_transaction_bound/1" do
test "without transactions" do
block = insert(:block)
assert Chain.block_to_transaction_count(block) == 0
end
test "with transactions" do
block = insert(:block)
%Transaction{id: transaction_id} = insert(:transaction)
insert(:block_transaction, block_id: block.id, transaction_id: transaction_id)
assert Chain.block_to_transaction_count(block) == 1
end
end
describe "confirmations/1" do
test "with block.number == max_block_number " do
block = insert(:block)
max_block_number = Chain.max_block_number()
assert block.number == max_block_number
assert Chain.confirmations(block, max_block_number: max_block_number) == 0
end
test "with block.number < max_block_number" do
block = insert(:block)
max_block_number = block.number + 2
assert block.number < max_block_number
assert Chain.confirmations(block, max_block_number: max_block_number) ==
max_block_number - block.number
end
end
describe "create_address/1" do
test "with valid data creates a address" do
assert {:ok, %Address{} = address} = Chain.create_address(@valid_attrs)
assert address.hash == "some hash"
end
test "with invalid data returns error changeset" do
assert {:error, %Ecto.Changeset{}} = Chain.create_address(@invalid_attrs)
end
end
describe "ensure_hash_address/1" do
test "creates a new address when one does not exist" do
Chain.ensure_hash_address("0xFreshPrince")
assert {:ok, _} = Chain.hash_to_address("0xfreshprince")
end end
test "returns the highest block number when there is a block" do test "when the address already exists doesn't insert a new address" do
insert(:address, %{hash: "bigmouthbillybass"})
before = Repo.aggregate(Address, :count, :id)
assert {:ok, _} = Chain.ensure_hash_address("bigmouthbillybass")
assert Repo.aggregate(Address, :count, :id) == before
end
test "when there is no hash it blows up" do
assert {:error, :not_found} = Chain.ensure_hash_address("")
end
end
describe "from_address_to_transactions/2" do
test "without transactions" do
address = insert(:address)
assert Repo.aggregate(Transaction, :count, :id) == 0
assert %Scrivener.Page{
entries: [],
page_number: 1,
total_entries: 0
} = Chain.from_address_to_transactions(address)
end
test "with transactions" do
%Transaction{from_address_id: from_address_id, id: transaction_id} = insert(:transaction)
address = Repo.get!(Address, from_address_id)
assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id}],
page_number: 1,
total_entries: 1
} = Chain.from_address_to_transactions(address)
end
test "with transactions with receipt required without receipt does not return transaction" do
address = %Address{id: from_address_id} = insert(:address)
%Transaction{id: transaction_id_with_receipt} =
insert(:transaction, from_address_id: from_address_id)
insert(:receipt, transaction_id: transaction_id_with_receipt)
%Transaction{id: transaction_id_without_receipt} =
insert(:transaction, from_address_id: from_address_id)
assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}],
page_number: 1,
total_entries: 1
} =
Chain.from_address_to_transactions(
address,
necessity_by_association: %{receipt: :required}
)
assert %Scrivener.Page{
entries: transactions,
page_number: 1,
total_entries: 2
} =
Chain.from_address_to_transactions(
address,
necessity_by_association: %{receipt: :optional}
)
assert length(transactions) == 2
transaction_by_id =
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} ->
{id, transaction}
end)
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt]
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt]
end
test "with transactions can be paginated" do
adddress = %Address{id: from_address_id} = insert(:address)
transactions = insert_list(2, :transaction, from_address_id: from_address_id)
[%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] =
transactions
assert %Scrivener.Page{
entries: [%Transaction{id: ^newest_transaction_id}],
page_number: 1,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.from_address_to_transactions(adddress, pagination: %{page_size: 1})
assert %Scrivener.Page{
entries: [%Transaction{id: ^oldest_transaction_id}],
page_number: 2,
page_size: 1,
total_entries: 2,
total_pages: 2
} =
Chain.from_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1})
end
end
describe "hash_to_address/1" do
test "without address returns {:error, :not_found}" do
assert {:error, :not_found} = Chain.hash_to_address("unknown")
end
test "with address returns {:ok, address}" do
hash = "0xandesmints"
%Address{id: address_id} = insert(:address, hash: hash)
assert {:ok, %Address{id: ^address_id}} = Chain.hash_to_address(hash)
end
end
describe "hash_to_transaction/2" do
test "without transaction returns {:error, :not_found}" do
assert {:error, :not_found} = Chain.hash_to_transaction("unknown")
end
test "with transaction returns {:ok, transaction}" do
hash = "0xandesmints"
%Transaction{id: transaction_id} = insert(:transaction, hash: hash)
assert {:ok, %Transaction{id: ^transaction_id}} = Chain.hash_to_transaction(hash)
end
test "with transaction with receipt required without receipt returns {:error, :not_found}" do
%Transaction{hash: hash_with_receipt, id: transaction_id_with_receipt} =
insert(:transaction)
insert(:receipt, transaction_id: transaction_id_with_receipt)
%Transaction{hash: hash_without_receipt} = insert(:transaction)
assert {:ok, %Transaction{hash: ^hash_with_receipt}} =
Chain.hash_to_transaction(
hash_with_receipt,
necessity_by_association: %{receipt: :required}
)
assert {:error, :not_found} =
Chain.hash_to_transaction(
hash_without_receipt,
necessity_by_association: %{receipt: :required}
)
assert {:ok, %Transaction{hash: ^hash_without_receipt}} =
Chain.hash_to_transaction(
hash_without_receipt,
necessity_by_association: %{receipt: :optional}
)
end
end
describe "id_to_address/1" do
test "returns the address with given id" do
%Address{id: id} = insert(:address)
assert {:ok, %Address{id: ^id}} = Chain.id_to_address(id)
end
end
describe "last_transaction_id/1" do
test "without transactions returns 0" do
assert Chain.last_transaction_id() == 0
end
test "with transaction returns last created transaction's id" do
insert(:transaction)
%Transaction{id: id} = insert(:transaction)
assert Chain.last_transaction_id() == id
end
test "with transaction with pending: true returns last pending transaction id, not the last transaction" do
%Transaction{id: pending_transaction_id} = insert(:transaction)
%Transaction{id: transaction_id} = insert(:transaction)
insert(:receipt, transaction_id: transaction_id)
assert pending_transaction_id < transaction_id
assert Chain.last_transaction_id(pending: true) == pending_transaction_id
assert Chain.last_transaction_id(pending: false) == transaction_id
assert Chain.last_transaction_id() == transaction_id
end
end
describe "list_blocks/2" do
test "without blocks" do
assert %Scrivener.Page{
entries: [],
page_number: 1,
total_entries: 0,
total_pages: 1
} = Chain.list_blocks()
end
test "with blocks" do
%Block{id: id} = insert(:block)
assert %Scrivener.Page{
entries: [%Block{id: ^id}],
page_number: 1,
total_entries: 1
} = Chain.list_blocks()
end
test "with blocks can be paginated" do
blocks = insert_list(2, :block)
[%Block{number: lesser_block_number}, %Block{number: greater_block_number}] = blocks
assert %Scrivener.Page{
entries: [%Block{number: ^greater_block_number}],
page_number: 1,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.list_blocks(pagination: %{page_size: 1})
assert %Scrivener.Page{
entries: [%Block{number: ^lesser_block_number}],
page_number: 2,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.list_blocks(pagination: %{page: 2, page_size: 1})
end
end
describe "max_block_number/0" do
test "without blocks is nil" do
assert Chain.max_block_number() == nil
end
test "with blocks is max number regardless of insertion order" do
max_number = 2
insert(:block, number: max_number)
insert(:block, number: 1) insert(:block, number: 1)
insert(:block, number: 100)
chain = Chain.fetch() assert Chain.max_block_number() == max_number
assert chain.number == 100
end end
end
test "returns the latest block timestamp" do describe "number_to_block/1" do
time = DateTime.utc_now() test "without block" do
insert(:block, timestamp: time) assert {:error, :not_found} = Chain.number_to_block(-1)
chain = Chain.fetch()
assert Timex.diff(chain.timestamp, time, :seconds) == 0
end end
test "returns the average time between blocks" do test "with block" do
time = DateTime.utc_now() %Block{number: number} = insert(:block)
next_time = Timex.shift(time, seconds: 5)
insert(:block, timestamp: time)
insert(:block, timestamp: next_time)
chain = Chain.fetch()
assert chain.average_time == %Duration{ assert {:ok, %Block{number: ^number}} = Chain.number_to_block(number)
seconds: 5,
megaseconds: 0,
microseconds: 0
}
end end
end
test "returns the count of transactions from blocks in the last day" do describe "to_address_to_transactions/2" do
time = DateTime.utc_now() test "without transactions" do
last_week = Timex.shift(time, days: -8) address = insert(:address)
block = insert(:block, timestamp: time)
old_block = insert(:block, timestamp: last_week) assert Repo.aggregate(Transaction, :count, :id) == 0
transaction = insert(:transaction)
old_transaction = insert(:transaction) assert %Scrivener.Page{
insert(:block_transaction, block: block, transaction: transaction) entries: [],
insert(:block_transaction, block: old_block, transaction: old_transaction) page_number: 1,
chain = Chain.fetch() total_entries: 0
assert chain.transaction_count == 1 } = Chain.to_address_to_transactions(address)
end end
test "returns the number of skipped blocks" do test "with transactions" do
insert(:block, %{number: 0}) %Transaction{to_address_id: to_address_id, id: transaction_id} = insert(:transaction)
insert(:block, %{number: 2}) address = Repo.get!(Address, to_address_id)
chain = Chain.fetch()
assert chain.skipped_blocks == 1 assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id}],
page_number: 1,
total_entries: 1
} = Chain.to_address_to_transactions(address)
end end
test "returns the lag between validation and insertion time" do test "with transactions with receipt required without receipt does not return transaction" do
validation_time = DateTime.utc_now() address = %Address{id: to_address_id} = insert(:address)
inserted_at = validation_time |> Timex.shift(seconds: 5)
insert(:block, timestamp: validation_time, inserted_at: inserted_at) %Transaction{id: transaction_id_with_receipt} =
chain = Chain.fetch() insert(:transaction, to_address_id: to_address_id)
assert chain.lag == %Duration{seconds: 5, megaseconds: 0, microseconds: 0}
insert(:receipt, transaction_id: transaction_id_with_receipt)
%Transaction{id: transaction_id_without_receipt} =
insert(:transaction, to_address_id: to_address_id)
assert %Scrivener.Page{
entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}],
page_number: 1,
total_entries: 1
} =
Chain.to_address_to_transactions(
address,
necessity_by_association: %{receipt: :required}
)
assert %Scrivener.Page{
entries: transactions,
page_number: 1,
total_entries: 2
} =
Chain.to_address_to_transactions(
address,
necessity_by_association: %{receipt: :optional}
)
assert length(transactions) == 2
transaction_by_id =
Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} ->
{id, transaction}
end)
assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt]
assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt]
end end
test "returns the number of blocks inserted in the last minute" do test "with transactions can be paginated" do
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) adddress = %Address{id: to_address_id} = insert(:address)
insert(:block, inserted_at: old_inserted_at) transactions = insert_list(2, :transaction, to_address_id: to_address_id)
insert(:block)
chain = Chain.fetch() [%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] =
assert chain.block_velocity == 1 transactions
assert %Scrivener.Page{
entries: [%Transaction{id: ^newest_transaction_id}],
page_number: 1,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.to_address_to_transactions(adddress, pagination: %{page_size: 1})
assert %Scrivener.Page{
entries: [%Transaction{id: ^oldest_transaction_id}],
page_number: 2,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.to_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1})
end
end
describe "transaction_count/0" do
test "without transactions" do
assert Chain.transaction_count() == 0
end
test "with transactions" do
count = 2
insert_list(count, :transaction)
assert Chain.transaction_count() == count
end end
test "returns the number of transactions inserted in the last minute" do test "with transaction pending: true counts only pending transactions" do
old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1)
insert(:transaction, inserted_at: old_inserted_at)
insert(:transaction) insert(:transaction)
chain = Chain.fetch()
assert chain.transaction_velocity == 1 %Transaction{id: transaction_id} = insert(:transaction)
insert(:receipt, transaction_id: transaction_id)
assert Chain.transaction_count(pending: true) == 1
assert Chain.transaction_count(pending: false) == 2
assert Chain.transaction_count() == 2
end end
end
test "returns the last five blocks" do describe "transaction_hash_to_internal_transactions/1" do
insert_list(6, :block) test "without transaction" do
chain = Chain.fetch() assert Chain.transaction_hash_to_internal_transactions("unknown") == []
assert chain.blocks |> Enum.count() == 5
end end
test "returns the last five transactions with blocks" do test "with transaction without internal transactions" do
block = insert(:block) %Transaction{hash: hash} = insert(:transaction)
assert Chain.transaction_hash_to_internal_transactions(hash) == []
end
insert_list(6, :transaction) test "with transaction with internal transactions returns all internal transactions for a given transaction hash" do
|> Enum.map(fn transaction -> transaction = insert(:transaction)
insert(:block_transaction, block: block, transaction: transaction) internal_transaction = insert(:internal_transaction, transaction_id: transaction.id)
result = hd(Chain.transaction_hash_to_internal_transactions(transaction.hash))
assert result.id == internal_transaction.id
end
test "with transaction with internal transactions loads associations with in necessity_by_assocation" do
%Transaction{hash: hash, id: transaction_id} = insert(:transaction)
insert(:internal_transaction, transaction_id: transaction_id)
assert [
%InternalTransaction{
from_address: %Ecto.Association.NotLoaded{},
to_address: %Ecto.Association.NotLoaded{},
transaction: %Ecto.Association.NotLoaded{}
}
] = Chain.transaction_hash_to_internal_transactions(hash)
assert [
%InternalTransaction{
from_address: %Address{},
to_address: %Address{},
transaction: %Transaction{}
}
] =
Chain.transaction_hash_to_internal_transactions(
hash,
necessity_by_association: %{
from_address: :optional,
to_address: :optional,
transaction: :optional
}
)
end
end
describe "transactions_recently_before_id" do
test "returns at most 10 transactions" do
count = 12
assert 10 < count
transactions = insert_list(count, :transaction)
%Transaction{id: last_transaction_id} = List.last(transactions)
recent_transactions = Chain.transactions_recently_before_id(last_transaction_id)
assert length(recent_transactions) == 10
end
test "with pending: true returns only pending transactions" do
count = 12
transactions = insert_list(count, :transaction)
%Transaction{id: last_transaction_id} = List.last(transactions)
transactions
|> Enum.take(3)
|> Enum.each(fn %Transaction{id: id} ->
insert(:receipt, transaction_id: id)
end) end)
chain = Chain.fetch() assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: true)) ==
assert chain.transactions |> Enum.count() == 5 8
assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: false)) ==
10
assert length(Chain.transactions_recently_before_id(last_transaction_id)) == 10
end
end
describe "transaction_to_logs/2" do
test "without logs" do
transaction = insert(:transaction)
assert %Scrivener.Page{
entries: [],
page_number: 1,
total_entries: 0,
total_pages: 1
} = Chain.transaction_to_logs(transaction)
end
test "with logs" do
transaction = insert(:transaction)
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id)
%Log{id: id} = insert(:log, receipt_id: receipt_id)
assert %Scrivener.Page{
entries: [%Log{id: ^id}],
page_number: 1,
total_entries: 1,
total_pages: 1
} = Chain.transaction_to_logs(transaction)
end
test "with logs can be paginated" do
transaction = insert(:transaction)
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id)
logs = insert_list(2, :log, receipt_id: receipt_id)
[%Log{id: first_log_id}, %Log{id: second_log_id}] = logs
assert %Scrivener.Page{
entries: [%Log{id: ^first_log_id}],
page_number: 1,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.transaction_to_logs(transaction, pagination: %{page_size: 1})
assert %Scrivener.Page{
entries: [%Log{id: ^second_log_id}],
page_number: 2,
page_size: 1,
total_entries: 2,
total_pages: 2
} = Chain.transaction_to_logs(transaction, pagination: %{page: 2, page_size: 1})
end
test "with logs necessity_by_association loads associations" do
transaction = insert(:transaction)
%Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id)
insert(:log, receipt_id: receipt_id)
assert %Scrivener.Page{
entries: [
%Log{
address: %Address{},
receipt: %Receipt{},
transaction: %Transaction{}
}
],
page_number: 1,
total_entries: 1,
total_pages: 1
} =
Chain.transaction_to_logs(
transaction,
necessity_by_association: %{
address: :optional,
receipt: :optional,
transaction: :optional
}
)
assert %Scrivener.Page{
entries: [
%Log{
address: %Ecto.Association.NotLoaded{},
receipt: %Ecto.Association.NotLoaded{},
transaction: %Ecto.Association.NotLoaded{}
}
],
page_number: 1,
total_entries: 1,
total_pages: 1
} = Chain.transaction_to_logs(transaction)
end
end
describe "update_balance/2" do
test "updates the balance" do
hash = "0xwarheads"
insert(:address, hash: hash)
Chain.update_balance(hash, 5)
expected_balance = Decimal.new(5)
assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address(hash)
end
test "updates the balance timestamp" do
hash = "0xtwizzlers"
insert(:address, hash: hash)
Chain.update_balance(hash, 88)
assert {:ok, %Address{balance_updated_at: balance_updated_at}} =
Chain.hash_to_address("0xtwizzlers")
refute is_nil(balance_updated_at)
end
test "creates an address if one does not exist" do
Chain.update_balance("0xtwizzlers", 88)
expected_balance = Decimal.new(88)
assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address("0xtwizzlers")
end end
end end
end end

@ -1,15 +1,19 @@
defmodule Explorer.BalanceImporterTest do defmodule Explorer.BalanceImporterTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Address.Service, as: Address alias Explorer.{Chain, BalanceImporter}
alias Explorer.BalanceImporter alias Explorer.Chain.Address
describe "import/1" do describe "import/1" do
test "it updates the balance for an address" do test "it updates the balance for an address" do
insert(:address, hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", balance: 5) insert(:address, hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", balance: 5)
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") 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 end
test "it updates the balance update time for an address" do test "it updates the balance update time for an address" do
@ -20,13 +24,17 @@ defmodule Explorer.BalanceImporterTest do
) )
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") 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 end
test "it creates an address if one does not exist" do test "it creates an address if one does not exist" do
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
assert Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
assert {:ok, _} = Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0")
end end
end end
end end

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

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

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

@ -1,9 +1,7 @@
defmodule Explorer.TransactionImporterTest do defmodule Explorer.TransactionImporterTest do
use Explorer.DataCase use Explorer.DataCase
alias Explorer.Address alias Explorer.Chain.{Address, BlockTransaction, Transaction}
alias Explorer.BlockTransaction
alias Explorer.Transaction
alias Explorer.TransactionImporter alias Explorer.TransactionImporter
@raw_transaction %{ @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 defmodule Explorer.Workers.ImportBalanceTest do
import Mock import Mock
alias Explorer.Chain
alias Explorer.Chain.Address
alias Explorer.Workers.ImportBalance alias Explorer.Workers.ImportBalance
alias Explorer.Address.Service, as: Address
use Explorer.DataCase use Explorer.DataCase
describe "perform/1" do describe "perform/1" do
test "imports the balance for an address" do test "imports the balance for an address" do
ImportBalance.perform("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") 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
end end
@ -25,8 +29,11 @@ defmodule Explorer.Workers.ImportBalanceTest do
) )
end do end do
ImportBalance.perform_later("0xskateboards") 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 end
end end

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

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

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

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

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

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

@ -39,20 +39,4 @@ defmodule Explorer.DataCase do
:ok :ok
end 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 end

@ -1,8 +1,8 @@
defmodule Explorer.AddressFactory do defmodule Explorer.Chain.AddressFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def address_factory do def address_factory do
%Explorer.Address{ %Explorer.Chain.Address{
hash: String.pad_trailing(sequence("0x"), 42, "address") hash: String.pad_trailing(sequence("0x"), 42, "address")
} }
end end

@ -1,8 +1,8 @@
defmodule Explorer.BlockFactory do defmodule Explorer.Chain.BlockFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def block_factory do def block_factory do
%Explorer.Block{ %Explorer.Chain.Block{
number: sequence(""), number: sequence(""),
hash: sequence("0x"), hash: sequence("0x"),
parent_hash: sequence("0x"), parent_hash: sequence("0x"),

@ -1,8 +1,8 @@
defmodule Explorer.BlockTransactionFactory do defmodule Explorer.Chain.BlockTransactionFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def block_transaction_factory do def block_transaction_factory do
%Explorer.BlockTransaction{} %Explorer.Chain.BlockTransaction{}
end end
end end
end end

@ -1,8 +1,8 @@
defmodule Explorer.FromAddressFactory do defmodule Explorer.Chain.FromAddressFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def from_address_factory do def from_address_factory do
%Explorer.FromAddress{} %Explorer.Chain.FromAddress{}
end end
end end
end end

@ -1,8 +1,8 @@
defmodule Explorer.InternalTransactionFactory do defmodule Explorer.Chain.InternalTransactionFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def internal_transaction_factory do def internal_transaction_factory do
%Explorer.InternalTransaction{ %Explorer.Chain.InternalTransaction{
index: Enum.random(0..9), index: Enum.random(0..9),
call_type: Enum.random(["call", "creates", "calldelegate"]), call_type: Enum.random(["call", "creates", "calldelegate"]),
trace_address: [Enum.random(0..4), Enum.random(0..4)], trace_address: [Enum.random(0..4), Enum.random(0..4)],

@ -1,15 +1,16 @@
defmodule Explorer.LogFactory do defmodule Explorer.Chain.LogFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def log_factory do def log_factory do
%Explorer.Log{ %Explorer.Chain.Log{
index: sequence(""), address_id: insert(:address).id,
data: sequence("0x"), data: sequence("0x"),
type: sequence("0x"),
first_topic: nil, first_topic: nil,
fourth_topic: nil,
index: sequence(""),
second_topic: nil, second_topic: nil,
third_topic: nil, third_topic: nil,
fourth_topic: nil type: sequence("0x")
} }
end end
end end

@ -1,8 +1,8 @@
defmodule Explorer.ReceiptFactory do defmodule Explorer.Chain.ReceiptFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def receipt_factory do def receipt_factory do
%Explorer.Receipt{ %Explorer.Chain.Receipt{
cumulative_gas_used: Enum.random(21_000..100_000), cumulative_gas_used: Enum.random(21_000..100_000),
gas_used: Enum.random(21_000..100_000), gas_used: Enum.random(21_000..100_000),
status: Enum.random(1..2), status: Enum.random(1..2),

@ -1,8 +1,8 @@
defmodule Explorer.ToAddressFactory do defmodule Explorer.Chain.ToAddressFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
def to_address_factory do def to_address_factory do
%Explorer.ToAddress{} %Explorer.Chain.ToAddress{}
end end
end end
end end

@ -1,12 +1,11 @@
defmodule Explorer.TransactionFactory do defmodule Explorer.Chain.TransactionFactory do
defmacro __using__(_opts) do defmacro __using__(_opts) do
quote do quote do
alias Explorer.Address alias Explorer.Chain.{Address, BlockTransaction, Transaction}
alias Explorer.BlockTransaction
alias Explorer.Repo alias Explorer.Repo
def transaction_factory do def transaction_factory do
%Explorer.Transaction{ %Transaction{
hash: String.pad_trailing(sequence("0x"), 43, "action"), hash: String.pad_trailing(sequence("0x"), 43, "action"),
value: Enum.random(1..100_000), value: Enum.random(1..100_000),
gas: Enum.random(21_000..100_000), gas: Enum.random(21_000..100_000),

@ -1,13 +1,13 @@
defmodule Explorer.Factory do defmodule Explorer.Factory do
@dialyzer {:nowarn_function, fields_for: 1} @dialyzer {:nowarn_function, fields_for: 1}
use ExMachina.Ecto, repo: Explorer.Repo use ExMachina.Ecto, repo: Explorer.Repo
use Explorer.AddressFactory use Explorer.Chain.AddressFactory
use Explorer.BlockFactory use Explorer.Chain.BlockFactory
use Explorer.BlockTransactionFactory use Explorer.Chain.BlockTransactionFactory
use Explorer.FromAddressFactory use Explorer.Chain.FromAddressFactory
use Explorer.InternalTransactionFactory use Explorer.Chain.InternalTransactionFactory
use Explorer.LogFactory use Explorer.Chain.LogFactory
use Explorer.ToAddressFactory use Explorer.Chain.ReceiptFactory
use Explorer.TransactionFactory use Explorer.Chain.ToAddressFactory
use Explorer.ReceiptFactory use Explorer.Chain.TransactionFactory
end end

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

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

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

@ -1,10 +1,14 @@
defmodule ExplorerWeb.AddressController do defmodule ExplorerWeb.AddressController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
alias Explorer.Address.Service, as: Address alias Explorer.Chain
def show(conn, %{"id" => id}) do def show(conn, %{"id" => hash}) do
address = id |> Address.by_hash() hash
render(conn, "show.html", address: address) |> Chain.hash_to_address()
|> case do
{:ok, address} -> render(conn, "show.html", address: address)
{:error, :not_found} -> not_found(conn)
end
end end
end end

@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionFromController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
alias Explorer.Address.Service, as: Address alias Explorer.Chain
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias ExplorerWeb.TransactionForm alias ExplorerWeb.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do def index(conn, %{"address_id" => from_address_hash} = params) do
address = Address.by_hash(address_id) 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 = entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1)
Transaction render(conn, "index.html", transactions: Map.put(page, :entries, entries))
|> Query.from_address(address.id)
|> Query.include_addresses()
|> Query.require_receipt()
|> Query.require_block()
|> Query.chron()
page = Repo.paginate(query, params) {:error, :not_found} ->
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) not_found(conn)
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) end
end end
end end

@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionToController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
alias Explorer.Address.Service, as: Address alias Explorer.Chain
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias ExplorerWeb.TransactionForm alias ExplorerWeb.TransactionForm
def index(conn, %{"address_id" => address_id} = params) do def index(conn, %{"address_id" => to_address_hash} = params) do
address = Address.by_hash(address_id) 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 = entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1)
Transaction render(conn, "index.html", transactions: Map.put(page, :entries, entries))
|> Query.to_address(address.id)
|> Query.include_addresses()
|> Query.require_receipt()
|> Query.require_block()
|> Query.chron()
page = Repo.paginate(query, params) {:error, :not_found} ->
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) not_found(conn)
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) end
end end
end end

@ -1,31 +1,25 @@
defmodule ExplorerWeb.BlockController do defmodule ExplorerWeb.BlockController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Chain
alias Explorer.Block
alias Explorer.Repo.NewRelic, as: Repo
alias ExplorerWeb.BlockForm alias ExplorerWeb.BlockForm
def index(conn, params) do def index(conn, params) do
blocks = blocks =
from( Chain.list_blocks(necessity_by_association: %{transactions: :optional}, pagination: params)
block in Block,
order_by: [desc: block.number],
preload: :transactions
)
render(conn, "index.html", blocks: Repo.paginate(blocks, params)) render(conn, "index.html", blocks: blocks)
end end
def show(conn, %{"id" => number}) do def show(conn, %{"id" => number}) do
block = case Chain.number_to_block(number) do
Block {:ok, block} ->
|> where(number: ^number) block_form = BlockForm.build(block)
|> first
|> Repo.one() render(conn, "show.html", block: block_form)
|> BlockForm.build()
render(conn, "show.html", block: block) {:error, :not_found} ->
not_found(conn)
end
end end
end end

@ -1,27 +1,34 @@
defmodule ExplorerWeb.BlockTransactionController do defmodule ExplorerWeb.BlockTransactionController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query import ExplorerWeb.Chain, only: [param_to_block_number: 1]
alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Chain
alias Explorer.Transaction
alias ExplorerWeb.TransactionForm alias ExplorerWeb.TransactionForm
def index(conn, %{"block_id" => block_number} = params) do def index(conn, %{"block_id" => formatted_block_number} = params) do
query = with {:ok, block_number} <- param_to_block_number(formatted_block_number),
from( {:ok, block} <- Chain.number_to_block(block_number) do
transaction in Transaction, page =
join: block in assoc(transaction, :block), Chain.block_to_transactions(
join: receipt in assoc(transaction, :receipt), block,
join: from_address in assoc(transaction, :from_address), necessity_by_association: %{
join: to_address in assoc(transaction, :to_address), block: :required,
preload: [:block, :receipt, :to_address, :from_address], from_address: :required,
order_by: [desc: transaction.inserted_at], to_address: :required,
where: block.number == ^block_number receipt: :required
) },
pagination: params
)
page = Repo.paginate(query, params) entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1)
entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) render(conn, "index.html", transactions: Map.put(page, :entries, entries))
render(conn, "index.html", transactions: Map.put(page, :entries, entries)) else
{:error, :invalid} ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end end
end end

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

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

@ -1,38 +1,23 @@
defmodule ExplorerWeb.TransactionController do defmodule ExplorerWeb.TransactionController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Chain
alias Explorer.Chain.Transaction
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service
alias Explorer.Transaction.Service.Query
alias ExplorerWeb.TransactionForm alias ExplorerWeb.TransactionForm
def index(conn, %{"last_seen" => last_seen}) do def index(conn, %{"last_seen" => last_seen_id}) do
query = total = Chain.transaction_count()
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
entries = entries =
query last_seen_id
|> Repo.all() |> 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) |> Enum.map(&TransactionForm.build_and_merge/1)
last = List.last(entries) || Transaction.null() last = List.last(entries) || Transaction.null()
@ -49,38 +34,42 @@ defmodule ExplorerWeb.TransactionController do
end end
def index(conn, params) do def index(conn, params) do
query = last_seen =
from( Chain.last_transaction_id()
t in Transaction, |> Kernel.+(1)
select: t.id, |> Integer.to_string()
order_by: [desc: t.id],
limit: 1
)
first_id = Repo.one(query) || 0
last_seen = Integer.to_string(first_id + 1)
index(conn, Map.put(params, "last_seen", last_seen)) index(conn, Map.put(params, "last_seen", last_seen))
end end
def show(conn, params) do 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( render(
conn, conn,
internal_transactions: internal_transactions, "show.html",
transaction: transaction internal_transactions: internal_transactions,
) transaction: transaction_form
end )
defp get_transaction(hash) do {:error, :not_found} ->
Transaction not_found(conn)
|> Query.by_hash(hash) end
|> Query.include_addresses()
|> Query.include_receipt()
|> Query.include_block()
|> Repo.one()
|> TransactionForm.build_and_merge()
end end
end end

@ -1,41 +1,28 @@
defmodule ExplorerWeb.TransactionLogController do defmodule ExplorerWeb.TransactionLogController do
use ExplorerWeb, :controller use ExplorerWeb, :controller
import Ecto.Query alias Explorer.Chain
alias Explorer.Log
alias Explorer.Repo.NewRelic, as: Repo
alias Explorer.Transaction
alias Explorer.Transaction.Service.Query
alias ExplorerWeb.TransactionForm alias ExplorerWeb.TransactionForm
def index(conn, %{"transaction_id" => transaction_id}) do def index(conn, %{"transaction_id" => transaction_hash} = params) do
transaction_hash = String.downcase(transaction_id) case Chain.hash_to_transaction(
transaction = get_transaction(transaction_hash) transaction_hash,
necessity_by_association: %{from_address: :required, to_address: :required}
) do
{:ok, transaction} ->
logs =
Chain.transaction_to_logs(
transaction,
necessity_by_association: %{address: :optional},
pagination: params
)
logs = transaction_form = TransactionForm.build_and_merge(transaction)
from(
log in Log,
join: transaction in assoc(log, :transaction),
preload: [:address],
where: fragment("lower(?)", transaction.hash) == ^transaction_hash
)
render( render(conn, "index.html", logs: logs, transaction: transaction_form)
conn,
"index.html",
logs: Repo.paginate(logs),
transaction: transaction
)
end
defp get_transaction(hash) do {:error, :not_found} ->
Transaction not_found(conn)
|> Query.by_hash(hash) end
|> Query.include_addresses()
|> Query.include_receipt()
|> Query.include_block()
|> Repo.one()
|> TransactionForm.build_and_merge()
end end
end end

@ -1,32 +1,17 @@
defmodule ExplorerWeb.BlockForm do defmodule ExplorerWeb.BlockForm do
@moduledoc false @moduledoc false
alias Explorer.Block
alias Explorer.BlockTransaction alias Explorer.Chain
alias Explorer.Repo
import Ecto.Query
def build(block) do def build(block) do
block block
|> Map.merge(%{ |> Map.merge(%{
transactions_count: block |> get_transactions_count, age: calculate_age(block),
age: block |> calculate_age, formatted_timestamp: format_timestamp(block),
formatted_timestamp: block |> format_timestamp transactions_count: Chain.block_to_transaction_count(block)
}) })
end 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 def calculate_age(block) do
block.timestamp |> Timex.from_now() block.timestamp |> Timex.from_now()
end end

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

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

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

Loading…
Cancel
Save