diff --git a/.credo.exs b/.credo.exs index 4bcf3c0828..b3b4f2d737 100644 --- a/.credo.exs +++ b/.credo.exs @@ -22,7 +22,12 @@ # In the latter case `**/*.{ex,exs}` will be used. # included: ["lib/", "src/", "web/", "apps/*/lib/**/*.{ex,exs}"], - excluded: [~r"/_build/", ~r"/deps/", ~r"/node_modules/"] + excluded: [ + ~r"/_build/", + ~r"/deps/", + ~r"/node_modules/", + ~r"/apps/explorer_web/lib/explorer_web.ex" + ] }, # # If you create your own checks, you must specify the source files for diff --git a/.gitignore b/.gitignore index f9e2d2fd4f..41fe7c4743 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # App artifacts /_build +/apps/*/cover /cover /db /deps +/doc /*.ez # Generated on crash by the VM diff --git a/README.md b/README.md index c4a91e5446..cf88ae03a8 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ You can also run IEx (Interactive Elixir): `iex -S mix phx.server` Configure your local CCMenu with the following url: [`https://circleci.com/gh/poanetwork/poa-explorer.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604`](https://circleci.com/gh/poanetwork/poa-explorer.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604) +### Documentation + +* `mix docs` +* `open doc/index.html` + ### Testing #### Prerequisites diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 8b532705f2..a72f1c79c7 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -5,6 +5,8 @@ defmodule Explorer.Application do use Application + import Supervisor.Spec + # See https://hexdocs.pm/elixir/Application.html # for more information on OTP Applications def start(_type, _args) do @@ -17,20 +19,17 @@ defmodule Explorer.Application do defp children(:test), do: children() defp children(_) do - import Supervisor.Spec exq_options = [] |> Keyword.put(:mode, :enqueuer) children() ++ [ supervisor(Exq, [exq_options]), - worker(Explorer.Servers.ChainStatistics, []), + worker(Explorer.Chain.Statistics.Server, []), Explorer.ExchangeRates ] end defp children do - import Supervisor.Spec - [ supervisor(Explorer.Repo, []), {Task.Supervisor, name: Explorer.ExchangeRateTaskSupervisor} diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex new file mode 100644 index 0000000000..f23493db37 --- /dev/null +++ b/apps/explorer/lib/explorer/chain.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex new file mode 100644 index 0000000000..3cc6d7c30d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex new file mode 100644 index 0000000000..1ad0bcc1de --- /dev/null +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -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 diff --git a/apps/explorer/lib/explorer/schemas/block_transaction.ex b/apps/explorer/lib/explorer/chain/block_transaction.ex similarity index 64% rename from apps/explorer/lib/explorer/schemas/block_transaction.ex rename to apps/explorer/lib/explorer/chain/block_transaction.ex index 61c1c082d4..1ece36cb72 100644 --- a/apps/explorer/lib/explorer/schemas/block_transaction.ex +++ b/apps/explorer/lib/explorer/chain/block_transaction.ex @@ -1,20 +1,20 @@ -defmodule Explorer.BlockTransaction do +defmodule Explorer.Chain.BlockTransaction do @moduledoc "Connects a Block to a Transaction" - alias Explorer.BlockTransaction - use Explorer.Schema + alias Explorer.Chain.{Block, Transaction} + @primary_key false schema "block_transactions" do - belongs_to(:block, Explorer.Block) - belongs_to(:transaction, Explorer.Transaction, primary_key: true) + belongs_to(:block, Block) + belongs_to(:transaction, Transaction, primary_key: true) timestamps() end @required_attrs ~w(block_id transaction_id)a - def changeset(%BlockTransaction{} = block_transaction, attrs \\ %{}) do + def changeset(%__MODULE__{} = block_transaction, attrs \\ %{}) do block_transaction |> cast(attrs, @required_attrs) |> validate_required(@required_attrs) diff --git a/apps/explorer/lib/explorer/schemas/credit.ex b/apps/explorer/lib/explorer/chain/credit.ex similarity index 88% rename from apps/explorer/lib/explorer/schemas/credit.ex rename to apps/explorer/lib/explorer/chain/credit.ex index 5007646605..1477ca91a9 100644 --- a/apps/explorer/lib/explorer/schemas/credit.ex +++ b/apps/explorer/lib/explorer/chain/credit.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Credit do +defmodule Explorer.Chain.Credit do @moduledoc """ A materialized view representing the credits to an address. """ @@ -6,15 +6,17 @@ defmodule Explorer.Credit do use Explorer.Schema alias Ecto.Adapters.SQL - alias Explorer.Address + alias Explorer.Chain.Address alias Explorer.Repo @primary_key false schema "credits" do - belongs_to(:address, Address, primary_key: true) - field(:value, :decimal) field(:count, :integer) + field(:value, :decimal) + timestamps() + + belongs_to(:address, Address, primary_key: true) end def refresh do diff --git a/apps/explorer/lib/explorer/schemas/debit.ex b/apps/explorer/lib/explorer/chain/debit.ex similarity index 88% rename from apps/explorer/lib/explorer/schemas/debit.ex rename to apps/explorer/lib/explorer/chain/debit.ex index be0fcfb12d..0f709c081d 100644 --- a/apps/explorer/lib/explorer/schemas/debit.ex +++ b/apps/explorer/lib/explorer/chain/debit.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Debit do +defmodule Explorer.Chain.Debit do @moduledoc """ A materialized view representing the debits from an address. """ @@ -6,15 +6,17 @@ defmodule Explorer.Debit do use Explorer.Schema alias Ecto.Adapters.SQL - alias Explorer.Address + alias Explorer.Chain.Address alias Explorer.Repo @primary_key false schema "debits" do - belongs_to(:address, Address, primary_key: true) - field(:value, :decimal) field(:count, :integer) + field(:value, :decimal) + timestamps() + + belongs_to(:address, Address, primary_key: true) end def refresh do diff --git a/apps/explorer/lib/explorer/schemas/from_address.ex b/apps/explorer/lib/explorer/chain/from_address.ex similarity index 53% rename from apps/explorer/lib/explorer/schemas/from_address.ex rename to apps/explorer/lib/explorer/chain/from_address.ex index 0a1c5d0fa2..7b44d2a194 100644 --- a/apps/explorer/lib/explorer/schemas/from_address.ex +++ b/apps/explorer/lib/explorer/chain/from_address.ex @@ -1,18 +1,19 @@ -defmodule Explorer.FromAddress do +defmodule Explorer.Chain.FromAddress do @moduledoc false use Explorer.Schema - alias Explorer.FromAddress + alias Explorer.Chain.{Address, Transaction} @primary_key false schema "from_addresses" do - belongs_to(:transaction, Explorer.Transaction, primary_key: true) - belongs_to(:address, Explorer.Address) + belongs_to(:address, Address) + belongs_to(:transaction, Transaction, primary_key: true) + timestamps() end - def changeset(%FromAddress{} = to_address, attrs \\ %{}) do + def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do to_address |> cast(attrs, [:transaction_id, :address_id]) |> unique_constraint(:transaction_id, name: :from_addresses_transaction_id_index) diff --git a/apps/explorer/lib/explorer/chain/hash.ex b/apps/explorer/lib/explorer/chain/hash.ex new file mode 100644 index 0000000000..6ad8fefd86 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/hash.ex @@ -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 diff --git a/apps/explorer/lib/explorer/schemas/internal_transaction.ex b/apps/explorer/lib/explorer/chain/internal_transaction.ex similarity index 80% rename from apps/explorer/lib/explorer/schemas/internal_transaction.ex rename to apps/explorer/lib/explorer/chain/internal_transaction.ex index 70fa7b76db..c3eb25aea9 100644 --- a/apps/explorer/lib/explorer/schemas/internal_transaction.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction.ex @@ -1,32 +1,32 @@ -defmodule Explorer.InternalTransaction do +defmodule Explorer.Chain.InternalTransaction do @moduledoc "Models internal transactions." use Explorer.Schema - alias Explorer.InternalTransaction - alias Explorer.Transaction - alias Explorer.Address + alias Explorer.Chain.{Address, Transaction} schema "internal_transactions" do - belongs_to(:transaction, Transaction) - belongs_to(:from_address, Address) - belongs_to(:to_address, Address) - field(:index, :integer) field(:call_type, :string) - field(:trace_address, {:array, :integer}) - field(:value, :decimal) field(:gas, :decimal) field(:gas_used, :decimal) + field(:index, :integer) field(:input, :string) field(:output, :string) + field(:trace_address, {:array, :integer}) + field(:value, :decimal) + timestamps() + + belongs_to(:from_address, Address) + belongs_to(:to_address, Address) + belongs_to(:transaction, Transaction) end @required_attrs ~w(index call_type trace_address value gas gas_used transaction_id from_address_id to_address_id)a @optional_attrs ~w(input output) - def changeset(%InternalTransaction{} = internal_transaction, attrs \\ %{}) do + def changeset(%__MODULE__{} = internal_transaction, attrs \\ %{}) do internal_transaction |> cast(attrs, @required_attrs ++ @optional_attrs) |> validate_required(@required_attrs) @@ -36,5 +36,5 @@ defmodule Explorer.InternalTransaction do |> unique_constraint(:transaction_id, name: :internal_transactions_transaction_id_index_index) end - def null, do: %InternalTransaction{} + def null, do: %__MODULE__{} end diff --git a/apps/explorer/lib/explorer/schemas/log.ex b/apps/explorer/lib/explorer/chain/log.ex similarity index 73% rename from apps/explorer/lib/explorer/schemas/log.ex rename to apps/explorer/lib/explorer/chain/log.ex index 6e6272c32d..6b81c2139f 100644 --- a/apps/explorer/lib/explorer/schemas/log.ex +++ b/apps/explorer/lib/explorer/chain/log.ex @@ -1,32 +1,32 @@ -defmodule Explorer.Log do +defmodule Explorer.Chain.Log do @moduledoc "Captures a Web3 log entry generated by a transaction" use Explorer.Schema - alias Explorer.Address - alias Explorer.Log - alias Explorer.Receipt + alias Explorer.Chain.{Address, Receipt} - @required_attrs ~w(index data type)a + @required_attrs ~w(address_id data index type)a @optional_attrs ~w( - first_topic second_topic third_topic fourth_topic address_id + first_topic second_topic third_topic fourth_topic )a schema "logs" do - belongs_to(:receipt, Receipt) - belongs_to(:address, Address) - has_one(:transaction, through: [:receipt, :transaction]) - field(:index, :integer) field(:data, :string) - field(:type, :string) field(:first_topic, :string) + field(:fourth_topic, :string) + field(:index, :integer) field(:second_topic, :string) field(:third_topic, :string) - field(:fourth_topic, :string) + field(:type, :string) + timestamps() + + belongs_to(:address, Address) + belongs_to(:receipt, Receipt) + has_one(:transaction, through: [:receipt, :transaction]) end - def changeset(%Log{} = log, attrs \\ %{}) do + def changeset(%__MODULE__{} = log, attrs \\ %{}) do log |> cast(attrs, @required_attrs) |> cast(attrs, @optional_attrs) diff --git a/apps/explorer/lib/explorer/schemas/receipt.ex b/apps/explorer/lib/explorer/chain/receipt.ex similarity index 78% rename from apps/explorer/lib/explorer/schemas/receipt.ex rename to apps/explorer/lib/explorer/chain/receipt.ex index 6fbb867b92..b81c935667 100644 --- a/apps/explorer/lib/explorer/schemas/receipt.ex +++ b/apps/explorer/lib/explorer/chain/receipt.ex @@ -1,11 +1,9 @@ -defmodule Explorer.Receipt do +defmodule Explorer.Chain.Receipt do @moduledoc "Captures a Web3 Transaction Receipt." use Explorer.Schema - alias Explorer.Transaction - alias Explorer.Log - alias Explorer.Receipt + alias Explorer.Chain.{Log, Transaction} @required_attrs ~w(cumulative_gas_used gas_used status index)a @optional_attrs ~w(transaction_id)a @@ -20,7 +18,7 @@ defmodule Explorer.Receipt do timestamps() end - def changeset(%Receipt{} = transaction_receipt, attrs \\ %{}) do + def changeset(%__MODULE__{} = transaction_receipt, attrs \\ %{}) do transaction_receipt |> cast(attrs, @required_attrs) |> cast(attrs, @optional_attrs) @@ -31,5 +29,5 @@ defmodule Explorer.Receipt do |> unique_constraint(:transaction_id) end - def null, do: %Receipt{} + def null, do: %__MODULE__{} end diff --git a/apps/explorer/lib/explorer/schemas/chain.ex b/apps/explorer/lib/explorer/chain/statistics.ex similarity index 60% rename from apps/explorer/lib/explorer/schemas/chain.ex rename to apps/explorer/lib/explorer/chain/statistics.ex index 9d1cea3862..59d3adafa6 100644 --- a/apps/explorer/lib/explorer/schemas/chain.ex +++ b/apps/explorer/lib/explorer/chain/statistics.ex @@ -1,4 +1,4 @@ -defmodule Explorer.Chain do +defmodule Explorer.Chain.Statistics do @moduledoc """ Represents statistics about the chain. """ @@ -6,21 +6,11 @@ defmodule Explorer.Chain do import Ecto.Query alias Ecto.Adapters.SQL - alias Explorer.Block - alias Explorer.Transaction - alias Explorer.Repo, as: Repo + alias Explorer.Chain.{Block, Transaction} + alias Explorer.Repo alias Timex.Duration - defstruct number: -1, - timestamp: :calendar.universal_time(), - average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0}, - lag: %Duration{seconds: 0, megaseconds: 0, microseconds: 0}, - transaction_count: 0, - skipped_blocks: 0, - block_velocity: 0, - transaction_velocity: 0, - blocks: [], - transactions: [] + # Constants @average_time_query """ SELECT coalesce(avg(difference), interval '0 seconds') @@ -69,6 +59,60 @@ defmodule Explorer.Chain do WHERE transactions.inserted_at > NOW() - interval '1 minute' """ + # Types + + @typedoc """ + The number of `t:Explorer.Chain.Block.t/0` mined/validated per minute. + """ + @type blocks_per_minute :: non_neg_integer() + + @typedoc """ + The number of `t:Explorer.Chain.Transaction.t/0` mined/validated per minute. + """ + @type transactions_per_minute :: non_neg_integer() + + @typedoc """ + * `average_time` - the average time it took to mine/validate the last <= 100 `t:Explorer.Chain.Block.t/0` + * `block_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute + * `blocks` - the last <= 5 `t:Explorer.Chain.Block.t/0` + * `lag` - the average time over the last hour between when the block was mined/validated + (`t:Explorer.Chain.Block.t/0` `timestamp`) and when it was inserted into the databasse + (`t:Explorer.Chain.Block.t/0` `inserted_at`) + * `number` - the latest `t:Explorer.Chain.Block.t/0` `number` + * `skipped_blocks` - the number of blocks that were mined/validated, but do not exist as `t:Explorer.Chain.Block.t/0` + * `timestamp` - when the last `t:Explorer.Chain.Block.t/0` was mined/validated + * `transaction_count` - the number of transactions confirmed in blocks that were mined/validated in the last day + * `transaction_velocity` - the number of `t:Explorer.Chain.Block.t/0` mined/validated in the last minute + * `transactions` - the last <= 5 `t:Explorer.Chain.Transaction.t/0` + """ + @type t :: %__MODULE__{ + average_time: Duration.t(), + block_velocity: blocks_per_minute(), + blocks: [Block.t()], + lag: Duration.t(), + number: Block.number(), + skipped_blocks: non_neg_integer(), + timestamp: :calendar.datetime(), + transaction_count: non_neg_integer(), + transaction_velocity: transactions_per_minute(), + transactions: [Transaction.t()] + } + + # Struct + + defstruct average_time: %Duration{seconds: 0, megaseconds: 0, microseconds: 0}, + block_velocity: 0, + blocks: [], + lag: %Duration{seconds: 0, megaseconds: 0, microseconds: 0}, + number: -1, + skipped_blocks: 0, + timestamp: :calendar.universal_time(), + transaction_count: 0, + transaction_velocity: 0, + transactions: [] + + # Functions + def fetch do blocks = from( @@ -90,7 +134,7 @@ defmodule Explorer.Chain do last_block = Block |> Block.latest() |> limit(1) |> Repo.one() latest_block = last_block || Block.null() - %Explorer.Chain{ + %__MODULE__{ number: latest_block.number, timestamp: latest_block.timestamp, average_time: query_duration(@average_time_query), diff --git a/apps/explorer/lib/explorer/chain/statistics/server.ex b/apps/explorer/lib/explorer/chain/statistics/server.ex new file mode 100644 index 0000000000..8463f46e5b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/statistics/server.ex @@ -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 diff --git a/apps/explorer/lib/explorer/schemas/to_address.ex b/apps/explorer/lib/explorer/chain/to_address.ex similarity index 53% rename from apps/explorer/lib/explorer/schemas/to_address.ex rename to apps/explorer/lib/explorer/chain/to_address.ex index 2bba63fc1d..190ced36cd 100644 --- a/apps/explorer/lib/explorer/schemas/to_address.ex +++ b/apps/explorer/lib/explorer/chain/to_address.ex @@ -1,17 +1,18 @@ -defmodule Explorer.ToAddress do +defmodule Explorer.Chain.ToAddress do @moduledoc false - alias Explorer.ToAddress use Explorer.Schema + alias Explorer.Chain.{Address, Transaction} + @primary_key false schema "to_addresses" do - belongs_to(:transaction, Explorer.Transaction, primary_key: true) - belongs_to(:address, Explorer.Address) + belongs_to(:address, Address) + belongs_to(:transaction, Transaction, primary_key: true) timestamps() end - def changeset(%ToAddress{} = to_address, attrs \\ %{}) do + def changeset(%__MODULE__{} = to_address, attrs \\ %{}) do to_address |> cast(attrs, [:transaction_id, :address_id]) |> unique_constraint(:transaction_id, name: :to_addresses_transaction_id_index) diff --git a/apps/explorer/lib/explorer/schemas/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex similarity index 80% rename from apps/explorer/lib/explorer/schemas/transaction.ex rename to apps/explorer/lib/explorer/chain/transaction.ex index f37df28675..c12e910917 100644 --- a/apps/explorer/lib/explorer/schemas/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -1,13 +1,9 @@ -defmodule Explorer.Transaction do +defmodule Explorer.Chain.Transaction do @moduledoc "Models a Web3 transaction." use Explorer.Schema - alias Explorer.Address - alias Explorer.BlockTransaction - alias Explorer.InternalTransaction - alias Explorer.Receipt - alias Explorer.Transaction + alias Explorer.Chain.{Address, BlockTransaction, InternalTransaction, Receipt} schema "transactions" do has_one(:receipt, Receipt) @@ -37,7 +33,7 @@ defmodule Explorer.Transaction do @optional_attrs ~w(to_address_id from_address_id)a @doc false - def changeset(%Transaction{} = transaction, attrs \\ %{}) do + def changeset(%__MODULE__{} = transaction, attrs \\ %{}) do transaction |> cast(attrs, @required_attrs ++ @optional_attrs) |> validate_required(@required_attrs) @@ -46,5 +42,5 @@ defmodule Explorer.Transaction do |> unique_constraint(:hash) end - def null, do: %Transaction{} + def null, do: %__MODULE__{} end diff --git a/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex b/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex index 16ef4d77cd..609d16f74a 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex @@ -3,10 +3,8 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do Adapter for fetching exchange rates from https://coinmarketcap.com. """ - alias Explorer.ExchangeRates.Rate - alias Explorer.ExchangeRates.Source - alias HTTPoison.Error - alias HTTPoison.Response + alias Explorer.ExchangeRates.{Rate, Source} + alias HTTPoison.{Error, Response} @behaviour Source diff --git a/apps/explorer/lib/explorer/importers/balance_importer.ex b/apps/explorer/lib/explorer/importers/balance_importer.ex index 1cb817ff41..8c81e14e06 100644 --- a/apps/explorer/lib/explorer/importers/balance_importer.ex +++ b/apps/explorer/lib/explorer/importers/balance_importer.ex @@ -1,18 +1,17 @@ defmodule Explorer.BalanceImporter do @moduledoc "Imports a balance for a given address." - alias Explorer.Address.Service, as: Address - alias Explorer.Ethereum + alias Explorer.{Chain, Ethereum} def import(hash) do - hash - |> Ethereum.download_balance() - |> persist_balance(hash) + encoded_balance = Ethereum.download_balance(hash) + + persist_balance(hash, encoded_balance) end - defp persist_balance(balance, hash) do - balance - |> Ethereum.decode_integer_field() - |> Address.update_balance(hash) + defp persist_balance(hash, encoded_balance) when is_binary(hash) do + decoded_balance = Ethereum.decode_integer_field(encoded_balance) + + Chain.update_balance(hash, decoded_balance) end end diff --git a/apps/explorer/lib/explorer/importers/block_importer.ex b/apps/explorer/lib/explorer/importers/block_importer.ex index 3c86c449d6..d189c80269 100644 --- a/apps/explorer/lib/explorer/importers/block_importer.ex +++ b/apps/explorer/lib/explorer/importers/block_importer.ex @@ -4,8 +4,8 @@ defmodule Explorer.BlockImporter do import Ecto.Query import Ethereumex.HttpClient, only: [eth_get_block_by_number: 2] - alias Explorer.Block - alias Explorer.Ethereum + alias Explorer.{BlockImporter, Ethereum} + alias Explorer.Chain.Block alias Explorer.Repo.NewRelic, as: Repo alias Explorer.Workers.ImportTransaction @@ -26,7 +26,6 @@ defmodule Explorer.BlockImporter do @dialyzer {:nowarn_function, import: 1} def import(block_number) do - alias Explorer.BlockImporter block_number |> download_block() |> BlockImporter.import() end diff --git a/apps/explorer/lib/explorer/importers/internal_transaction_importer.ex b/apps/explorer/lib/explorer/importers/internal_transaction_importer.ex index c09c6fd79f..c9f8225098 100644 --- a/apps/explorer/lib/explorer/importers/internal_transaction_importer.ex +++ b/apps/explorer/lib/explorer/importers/internal_transaction_importer.ex @@ -3,12 +3,8 @@ defmodule Explorer.InternalTransactionImporter do import Ecto.Query - alias Explorer.Address.Service, as: Address - alias Explorer.Ethereum - alias Explorer.EthereumexExtensions - alias Explorer.InternalTransaction - alias Explorer.Repo - alias Explorer.Transaction + alias Explorer.{Chain, Ethereum, EthereumexExtensions, Repo} + alias Explorer.Chain.{InternalTransaction, Transaction} @dialyzer {:nowarn_function, import: 1} def import(hash) do @@ -77,6 +73,8 @@ defmodule Explorer.InternalTransactionImporter do end defp address_id(hash) do - Address.find_or_create_by_hash(hash).id + {:ok, address} = Chain.ensure_hash_address(hash) + + address.id end end diff --git a/apps/explorer/lib/explorer/importers/receipt_importer.ex b/apps/explorer/lib/explorer/importers/receipt_importer.ex index 2ff01b7e2d..c81861fc28 100644 --- a/apps/explorer/lib/explorer/importers/receipt_importer.ex +++ b/apps/explorer/lib/explorer/importers/receipt_importer.ex @@ -4,10 +4,8 @@ defmodule Explorer.ReceiptImporter do import Ecto.Query import Ethereumex.HttpClient, only: [eth_get_transaction_receipt: 1] - alias Explorer.Address.Service, as: Address - alias Explorer.Repo - alias Explorer.Transaction - alias Explorer.Receipt + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Receipt, Transaction} def import(hash) do transaction = hash |> find_transaction() @@ -59,7 +57,7 @@ defmodule Explorer.ReceiptImporter do end defp extract_log(log) do - address = Address.find_or_create_by_hash(log["address"]) + {:ok, address} = Chain.ensure_hash_address(log["address"]) %{ address_id: address.id, diff --git a/apps/explorer/lib/explorer/importers/transaction_importer.ex b/apps/explorer/lib/explorer/importers/transaction_importer.ex index ae02a67953..f3becbf31c 100644 --- a/apps/explorer/lib/explorer/importers/transaction_importer.ex +++ b/apps/explorer/lib/explorer/importers/transaction_importer.ex @@ -4,13 +4,8 @@ defmodule Explorer.TransactionImporter do import Ecto.Query import Ethereumex.HttpClient, only: [eth_get_transaction_by_hash: 1] - alias Explorer.Address.Service, as: Address - alias Explorer.Block - alias Explorer.BlockTransaction - alias Explorer.Ethereum - alias Explorer.Repo - alias Explorer.Transaction - alias Explorer.BalanceImporter + alias Explorer.{Chain, Ethereum, Repo, BalanceImporter} + alias Explorer.Chain.{Block, BlockTransaction, Transaction} def import(hash) when is_binary(hash) do hash |> download_transaction() |> persist_transaction() @@ -126,7 +121,9 @@ defmodule Explorer.TransactionImporter do def from_address(hash) when is_bitstring(hash), do: hash def fetch_address(hash) when is_bitstring(hash) do - Address.find_or_create_by_hash(hash) + {:ok, address} = Chain.ensure_hash_address(hash) + + address end defp refresh_account_balances(raw_transaction) do diff --git a/apps/explorer/lib/explorer/resource.ex b/apps/explorer/lib/explorer/resource.ex deleted file mode 100644 index 6fee0b0785..0000000000 --- a/apps/explorer/lib/explorer/resource.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/schemas/schema.ex b/apps/explorer/lib/explorer/schema.ex similarity index 85% rename from apps/explorer/lib/explorer/schemas/schema.ex rename to apps/explorer/lib/explorer/schema.ex index 752e4e9374..958a2ebe90 100644 --- a/apps/explorer/lib/explorer/schemas/schema.ex +++ b/apps/explorer/lib/explorer/schema.ex @@ -5,8 +5,7 @@ defmodule Explorer.Schema do quote do use Ecto.Schema - import Ecto.Changeset - import Ecto.Query + import Ecto.{Changeset, Query} @timestamps_opts [ type: Timex.Ecto.DateTime, diff --git a/apps/explorer/lib/explorer/schemas/address.ex b/apps/explorer/lib/explorer/schemas/address.ex deleted file mode 100644 index 4e757d1dfc..0000000000 --- a/apps/explorer/lib/explorer/schemas/address.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/schemas/block.ex b/apps/explorer/lib/explorer/schemas/block.ex deleted file mode 100644 index 28dda46909..0000000000 --- a/apps/explorer/lib/explorer/schemas/block.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/servers/chain_statistics.ex b/apps/explorer/lib/explorer/servers/chain_statistics.ex deleted file mode 100644 index 6bbe2098e3..0000000000 --- a/apps/explorer/lib/explorer/servers/chain_statistics.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/services/address.ex b/apps/explorer/lib/explorer/services/address.ex deleted file mode 100644 index 91c86089ae..0000000000 --- a/apps/explorer/lib/explorer/services/address.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/services/transaction.ex b/apps/explorer/lib/explorer/services/transaction.ex deleted file mode 100644 index 0763fce7f6..0000000000 --- a/apps/explorer/lib/explorer/services/transaction.ex +++ /dev/null @@ -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 diff --git a/apps/explorer/lib/explorer/skipped_balances.ex b/apps/explorer/lib/explorer/skipped_balances.ex index 380c879115..849d9e3988 100644 --- a/apps/explorer/lib/explorer/skipped_balances.ex +++ b/apps/explorer/lib/explorer/skipped_balances.ex @@ -1,8 +1,8 @@ defmodule Explorer.SkippedBalances do @moduledoc "Gets a list of Addresses that do not have balances." - alias Explorer.Address - alias Explorer.Repo + alias Explorer.Chain.Address + alias Explorer.Repo.NewRelic, as: Repo import Ecto.Query, only: [from: 2] diff --git a/apps/explorer/lib/explorer/skipped_blocks.ex b/apps/explorer/lib/explorer/skipped_blocks.ex index 9ccf27c393..51cd40229d 100644 --- a/apps/explorer/lib/explorer/skipped_blocks.ex +++ b/apps/explorer/lib/explorer/skipped_blocks.ex @@ -3,7 +3,8 @@ defmodule Explorer.SkippedBlocks do Fill in older blocks that were skipped during processing. """ import Ecto.Query, only: [from: 2, limit: 2] - alias Explorer.Block + + alias Explorer.Chain.Block alias Explorer.Repo.NewRelic, as: Repo @missing_number_query "SELECT generate_series(?, 0, -1) AS missing_number" diff --git a/apps/explorer/lib/explorer/skipped_internal_transactions.ex b/apps/explorer/lib/explorer/skipped_internal_transactions.ex index 0506e408d9..1c30e25464 100644 --- a/apps/explorer/lib/explorer/skipped_internal_transactions.ex +++ b/apps/explorer/lib/explorer/skipped_internal_transactions.ex @@ -4,7 +4,7 @@ defmodule Explorer.SkippedInternalTransactions do """ import Ecto.Query, only: [from: 2] - alias Explorer.Transaction + alias Explorer.Chain.Transaction alias Explorer.Repo.NewRelic, as: Repo def first, do: first(1) diff --git a/apps/explorer/lib/explorer/skipped_receipts.ex b/apps/explorer/lib/explorer/skipped_receipts.ex index a7ddc5bf70..7729d729ac 100644 --- a/apps/explorer/lib/explorer/skipped_receipts.ex +++ b/apps/explorer/lib/explorer/skipped_receipts.ex @@ -4,7 +4,7 @@ defmodule Explorer.SkippedReceipts do """ import Ecto.Query, only: [from: 2] - alias Explorer.Transaction + alias Explorer.Chain.Transaction alias Explorer.Repo.NewRelic, as: Repo def first, do: first(1) diff --git a/apps/explorer/lib/explorer/workers/import_transaction.ex b/apps/explorer/lib/explorer/workers/import_transaction.ex index 22a3c3fef6..8dcd5d33f7 100644 --- a/apps/explorer/lib/explorer/workers/import_transaction.ex +++ b/apps/explorer/lib/explorer/workers/import_transaction.ex @@ -4,8 +4,7 @@ defmodule Explorer.Workers.ImportTransaction do """ alias Explorer.TransactionImporter - alias Explorer.Workers.ImportReceipt - alias Explorer.Workers.ImportInternalTransaction + alias Explorer.Workers.{ImportInternalTransaction, ImportReceipt} @dialyzer {:nowarn_function, perform: 1} def perform(hash) when is_binary(hash) do diff --git a/apps/explorer/lib/explorer/workers/refresh_balance.ex b/apps/explorer/lib/explorer/workers/refresh_balance.ex index 856d2494e1..c32960cb55 100644 --- a/apps/explorer/lib/explorer/workers/refresh_balance.ex +++ b/apps/explorer/lib/explorer/workers/refresh_balance.ex @@ -4,8 +4,7 @@ defmodule Explorer.Workers.RefreshBalance do """ alias Ecto.Adapters.SQL - alias Explorer.Credit - alias Explorer.Debit + alias Explorer.Chain.{Credit, Debit} alias Explorer.Repo def perform("credit"), do: unless(refreshing("credits"), do: Credit.refresh()) diff --git a/apps/explorer/lib/mix/tasks/exq.start.ex b/apps/explorer/lib/mix/tasks/exq.start.ex index 2200c77d25..ff792ab3a5 100644 --- a/apps/explorer/lib/mix/tasks/exq.start.ex +++ b/apps/explorer/lib/mix/tasks/exq.start.ex @@ -1,8 +1,8 @@ defmodule Mix.Tasks.Exq.Start do @moduledoc "Starts the Exq worker" use Mix.Task - alias Explorer.Repo - alias Explorer.Scheduler + + alias Explorer.{Repo, Scheduler} def run(["scheduler"]) do [:postgrex, :ecto, :ethereumex, :tzdata] diff --git a/apps/explorer/lib/mix/tasks/scrape.balances.ex b/apps/explorer/lib/mix/tasks/scrape.balances.ex index bd282b7592..7028073656 100644 --- a/apps/explorer/lib/mix/tasks/scrape.balances.ex +++ b/apps/explorer/lib/mix/tasks/scrape.balances.ex @@ -3,9 +3,7 @@ defmodule Mix.Tasks.Scrape.Balances do use Mix.Task - alias Explorer.Repo - alias Explorer.SkippedBalances - alias Explorer.BalanceImporter + alias Explorer.{BalanceImporter, Repo, SkippedBalances} def run([]), do: run(1) diff --git a/apps/explorer/lib/mix/tasks/scrape.blocks.ex b/apps/explorer/lib/mix/tasks/scrape.blocks.ex index 42271116ab..6ed93b727c 100644 --- a/apps/explorer/lib/mix/tasks/scrape.blocks.ex +++ b/apps/explorer/lib/mix/tasks/scrape.blocks.ex @@ -1,9 +1,9 @@ defmodule Mix.Tasks.Scrape.Blocks do @moduledoc "Scrapes blocks from web3" + use Mix.Task - alias Explorer.Repo - alias Explorer.SkippedBlocks - alias Explorer.BlockImporter + + alias Explorer.{BlockImporter, Repo, SkippedBlocks} def run([]), do: run(1) diff --git a/apps/explorer/lib/mix/tasks/scrape.internal_transactions.ex b/apps/explorer/lib/mix/tasks/scrape.internal_transactions.ex index 7d32eb7e14..151a75f402 100644 --- a/apps/explorer/lib/mix/tasks/scrape.internal_transactions.ex +++ b/apps/explorer/lib/mix/tasks/scrape.internal_transactions.ex @@ -1,10 +1,9 @@ defmodule Mix.Tasks.Scrape.InternalTransactions do @moduledoc "Backfill Internal Transactions via Parity Trace." + use Mix.Task - alias Explorer.Repo - alias Explorer.SkippedInternalTransactions - alias Explorer.InternalTransactionImporter + alias Explorer.{InternalTransactionImporter, Repo, SkippedInternalTransactions} def run([]), do: run(1) diff --git a/apps/explorer/lib/mix/tasks/scrape.receipts.ex b/apps/explorer/lib/mix/tasks/scrape.receipts.ex index b69b2eed53..f56c9861a3 100644 --- a/apps/explorer/lib/mix/tasks/scrape.receipts.ex +++ b/apps/explorer/lib/mix/tasks/scrape.receipts.ex @@ -1,10 +1,9 @@ defmodule Mix.Tasks.Scrape.Receipts do @moduledoc "Scrapes blocks from web3" + use Mix.Task - alias Explorer.Repo - alias Explorer.SkippedReceipts - alias Explorer.ReceiptImporter + alias Explorer.{ReceiptImporter, Repo, SkippedReceipts} def run([]), do: run(1) diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs index 3c179037cc..556b2f4c5e 100644 --- a/apps/explorer/mix.exs +++ b/apps/explorer/mix.exs @@ -71,7 +71,7 @@ defmodule Explorer.Mixfile do defp deps do [ {:bypass, "~> 0.8", only: :test}, - {:credo, "~> 0.8", only: [:dev, :test], runtime: false}, + {:credo, "0.9.1", only: [:dev, :test], runtime: false}, {:crontab, "~> 1.1"}, {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, {:ethereumex, "~> 0.3"}, @@ -107,6 +107,7 @@ defmodule Explorer.Mixfile do # See the documentation for `Mix` for more info on aliases. defp aliases do [ + compile: "compile --warnings-as-errors", "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"] diff --git a/apps/explorer/test/explorer/address_test.exs b/apps/explorer/test/explorer/chain/address_test.exs similarity index 92% rename from apps/explorer/test/explorer/address_test.exs rename to apps/explorer/test/explorer/chain/address_test.exs index 206a72f5cd..a916db7a3a 100644 --- a/apps/explorer/test/explorer/address_test.exs +++ b/apps/explorer/test/explorer/chain/address_test.exs @@ -1,6 +1,7 @@ -defmodule Explorer.AddressTest do +defmodule Explorer.Chain.AddressTest do use Explorer.DataCase - alias Explorer.Address + + alias Explorer.Chain.Address describe "changeset/2" do test "with valid attributes" do diff --git a/apps/explorer/test/explorer/block_test.exs b/apps/explorer/test/explorer/chain/block_test.exs similarity index 95% rename from apps/explorer/test/explorer/block_test.exs rename to apps/explorer/test/explorer/chain/block_test.exs index 8e01cfd7cf..5442515376 100644 --- a/apps/explorer/test/explorer/block_test.exs +++ b/apps/explorer/test/explorer/chain/block_test.exs @@ -1,9 +1,10 @@ -defmodule Explorer.BlockTest do +defmodule Explorer.Chain.BlockTest do use Explorer.DataCase - alias Explorer.Block import Ecto.Query, only: [order_by: 2] + alias Explorer.Chain.Block + describe "changeset/2" do test "with valid attributes" do changeset = build(:block) |> Block.changeset(%{}) diff --git a/apps/explorer/test/explorer/block_transaction_test.exs b/apps/explorer/test/explorer/chain/block_transaction_test.exs similarity index 81% rename from apps/explorer/test/explorer/block_transaction_test.exs rename to apps/explorer/test/explorer/chain/block_transaction_test.exs index f9da5e6c8e..32aab6e388 100644 --- a/apps/explorer/test/explorer/block_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/block_transaction_test.exs @@ -1,6 +1,7 @@ -defmodule Explorer.BlockTransactionTest do +defmodule Explorer.Chain.BlockTransactionTest do use Explorer.DataCase - alias Explorer.BlockTransaction + + alias Explorer.Chain.BlockTransaction describe "changeset/2" do test "with empty attributes" do diff --git a/apps/explorer/test/explorer/credit_test.exs b/apps/explorer/test/explorer/chain/credit_test.exs similarity index 95% rename from apps/explorer/test/explorer/credit_test.exs rename to apps/explorer/test/explorer/chain/credit_test.exs index cc548d1c4e..ec1ea530b5 100644 --- a/apps/explorer/test/explorer/credit_test.exs +++ b/apps/explorer/test/explorer/chain/credit_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.CreditTest do +defmodule Explorer.Chain.CreditTest do use Explorer.DataCase - alias Explorer.Credit + alias Explorer.Chain.Credit describe "Repo.all/1" do test "returns no rows when there are no addresses" do diff --git a/apps/explorer/test/explorer/debit_test.exs b/apps/explorer/test/explorer/chain/debit_test.exs similarity index 96% rename from apps/explorer/test/explorer/debit_test.exs rename to apps/explorer/test/explorer/chain/debit_test.exs index f3e7e38ca0..884a961c89 100644 --- a/apps/explorer/test/explorer/debit_test.exs +++ b/apps/explorer/test/explorer/chain/debit_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.DebitTest do +defmodule Explorer.Chain.DebitTest do use Explorer.DataCase - alias Explorer.Debit + alias Explorer.Chain.Debit describe "Repo.all/1" do test "returns no rows when there are no addresses" do diff --git a/apps/explorer/test/explorer/from_address_test.exs b/apps/explorer/test/explorer/chain/from_address_test.exs similarity index 75% rename from apps/explorer/test/explorer/from_address_test.exs rename to apps/explorer/test/explorer/chain/from_address_test.exs index 3cd77a3ac3..1cf4c5cc06 100644 --- a/apps/explorer/test/explorer/from_address_test.exs +++ b/apps/explorer/test/explorer/chain/from_address_test.exs @@ -1,6 +1,7 @@ -defmodule Explorer.FromAddressTest do +defmodule Explorer.Chain.FromAddressTest do use Explorer.DataCase - alias Explorer.FromAddress + + alias Explorer.Chain.FromAddress describe "changeset/2" do test "with valid attributes" do diff --git a/apps/explorer/test/explorer/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs similarity index 92% rename from apps/explorer/test/explorer/internal_transaction_test.exs rename to apps/explorer/test/explorer/chain/internal_transaction_test.exs index ac1de8467d..ce7acf640a 100644 --- a/apps/explorer/test/explorer/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.InternalTransactionTest do +defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase - alias Explorer.InternalTransaction + alias Explorer.Chain.InternalTransaction describe "changeset/2" do test "with valid attributes" do diff --git a/apps/explorer/test/explorer/log_test.exs b/apps/explorer/test/explorer/chain/log_test.exs similarity index 92% rename from apps/explorer/test/explorer/log_test.exs rename to apps/explorer/test/explorer/chain/log_test.exs index 5135d6df01..66b16833ab 100644 --- a/apps/explorer/test/explorer/log_test.exs +++ b/apps/explorer/test/explorer/chain/log_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.LogTest do +defmodule Explorer.Chain.LogTest do use Explorer.DataCase - alias Explorer.Log + alias Explorer.Chain.Log describe "changeset/2" do test "accepts valid attributes" do diff --git a/apps/explorer/test/explorer/receipt_test.exs b/apps/explorer/test/explorer/chain/receipt_test.exs similarity index 94% rename from apps/explorer/test/explorer/receipt_test.exs rename to apps/explorer/test/explorer/chain/receipt_test.exs index 1555665812..86162b0f79 100644 --- a/apps/explorer/test/explorer/receipt_test.exs +++ b/apps/explorer/test/explorer/chain/receipt_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.ReceiptTest do +defmodule Explorer.Chain.ReceiptTest do use Explorer.DataCase - alias Explorer.Receipt + alias Explorer.Chain.Receipt describe "changeset/2" do test "accepts valid attributes" do diff --git a/apps/explorer/test/explorer/chain/statistics/server_test.exs b/apps/explorer/test/explorer/chain/statistics/server_test.exs new file mode 100644 index 0000000000..fb1ad654be --- /dev/null +++ b/apps/explorer/test/explorer/chain/statistics/server_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/statistics_test.exs b/apps/explorer/test/explorer/chain/statistics_test.exs new file mode 100644 index 0000000000..adb1d43152 --- /dev/null +++ b/apps/explorer/test/explorer/chain/statistics_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/to_address_test.exs b/apps/explorer/test/explorer/chain/to_address_test.exs similarity index 75% rename from apps/explorer/test/explorer/to_address_test.exs rename to apps/explorer/test/explorer/chain/to_address_test.exs index c267482811..78a3fa59f3 100644 --- a/apps/explorer/test/explorer/to_address_test.exs +++ b/apps/explorer/test/explorer/chain/to_address_test.exs @@ -1,6 +1,7 @@ -defmodule Explorer.ToAddressTest do +defmodule Explorer.Chain.ToAddressTest do use Explorer.DataCase - alias Explorer.ToAddress + + alias Explorer.Chain.ToAddress describe "changeset/2" do test "with valid attributes" do diff --git a/apps/explorer/test/explorer/transaction_test.exs b/apps/explorer/test/explorer/chain/transaction_test.exs similarity index 92% rename from apps/explorer/test/explorer/transaction_test.exs rename to apps/explorer/test/explorer/chain/transaction_test.exs index 64d47d8931..d1f3e15257 100644 --- a/apps/explorer/test/explorer/transaction_test.exs +++ b/apps/explorer/test/explorer/chain/transaction_test.exs @@ -1,7 +1,7 @@ -defmodule Explorer.TransactionTest do +defmodule Explorer.Chain.TransactionTest do use Explorer.DataCase - alias Explorer.Transaction + alias Explorer.Chain.Transaction describe "changeset/2" do test "with valid attributes" do diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 336d0cf912..806962a09f 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1,103 +1,757 @@ defmodule Explorer.ChainTest do use Explorer.DataCase - alias Explorer.Chain - alias Timex.Duration + alias Explorer.{Chain, Repo} - describe "fetch/0" do - test "returns -1 for the number when there are no blocks" do - chain = Chain.fetch() - assert chain.number == -1 + alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Receipt, Transaction} + + # Constants + + @invalid_attrs %{hash: nil} + @valid_attrs %{hash: "some hash"} + + # Tests + + describe "block_to_transactions/1" do + test "without transactions" do + block = insert(:block) + + assert Repo.aggregate(Transaction, :count, :id) == 0 + + assert %Scrivener.Page{ + entries: [], + page_number: 1, + total_entries: 0 + } = Chain.block_to_transactions(block) + end + + test "with transactions" do + block = %Block{id: block_id} = insert(:block) + %Transaction{id: transaction_id} = insert(:transaction) + insert(:block_transaction, block_id: block_id, transaction_id: transaction_id) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id}], + page_number: 1, + total_entries: 1 + } = Chain.block_to_transactions(block) + end + + test "with transaction with receipt required without receipt does not return transaction" do + block = %Block{id: block_id} = insert(:block) + + %Transaction{id: transaction_id_with_receipt} = insert(:transaction) + insert(:receipt, transaction_id: transaction_id_with_receipt) + insert(:block_transaction, block_id: block_id, transaction_id: transaction_id_with_receipt) + + %Transaction{id: transaction_id_without_receipt} = insert(:transaction) + + insert( + :block_transaction, + block_id: block_id, + transaction_id: transaction_id_without_receipt + ) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], + page_number: 1, + total_entries: 1 + } = + Chain.block_to_transactions( + block, + necessity_by_association: %{receipt: :required} + ) + + assert %Scrivener.Page{ + entries: transactions, + page_number: 1, + total_entries: 2 + } = + Chain.block_to_transactions( + block, + necessity_by_association: %{receipt: :optional} + ) + + assert length(transactions) == 2 + + transaction_by_id = + Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> + {id, transaction} + end) + + assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] + assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] + end + + test "with transactions can be paginated" do + block = %Block{id: block_id} = insert(:block) + + transactions = insert_list(2, :transaction) + + Enum.each(transactions, fn %Transaction{id: transaction_id} -> + insert(:block_transaction, block_id: block_id, transaction_id: transaction_id) + end) + + [%Transaction{id: first_transaction_id}, %Transaction{id: second_transaction_id}] = + transactions + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^first_transaction_id}], + page_number: 1, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.block_to_transactions(block, pagination: %{page_size: 1}) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^second_transaction_id}], + page_number: 2, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.block_to_transactions(block, pagination: %{page: 2, page_size: 1}) + end + end + + describe "block_to_transaction_bound/1" do + test "without transactions" do + block = insert(:block) + + assert Chain.block_to_transaction_count(block) == 0 + end + + test "with transactions" do + block = insert(:block) + %Transaction{id: transaction_id} = insert(:transaction) + insert(:block_transaction, block_id: block.id, transaction_id: transaction_id) + + assert Chain.block_to_transaction_count(block) == 1 + end + end + + describe "confirmations/1" do + test "with block.number == max_block_number " do + block = insert(:block) + max_block_number = Chain.max_block_number() + + assert block.number == max_block_number + assert Chain.confirmations(block, max_block_number: max_block_number) == 0 + end + + test "with block.number < max_block_number" do + block = insert(:block) + max_block_number = block.number + 2 + + assert block.number < max_block_number + + assert Chain.confirmations(block, max_block_number: max_block_number) == + max_block_number - block.number + end + end + + describe "create_address/1" do + test "with valid data creates a address" do + assert {:ok, %Address{} = address} = Chain.create_address(@valid_attrs) + assert address.hash == "some hash" + end + + test "with invalid data returns error changeset" do + assert {:error, %Ecto.Changeset{}} = Chain.create_address(@invalid_attrs) + end + end + + describe "ensure_hash_address/1" do + test "creates a new address when one does not exist" do + Chain.ensure_hash_address("0xFreshPrince") + + assert {:ok, _} = Chain.hash_to_address("0xfreshprince") end - test "returns the highest block number when there is a block" do + test "when the address already exists doesn't insert a new address" do + insert(:address, %{hash: "bigmouthbillybass"}) + + before = Repo.aggregate(Address, :count, :id) + + assert {:ok, _} = Chain.ensure_hash_address("bigmouthbillybass") + + assert Repo.aggregate(Address, :count, :id) == before + end + + test "when there is no hash it blows up" do + assert {:error, :not_found} = Chain.ensure_hash_address("") + end + end + + describe "from_address_to_transactions/2" do + test "without transactions" do + address = insert(:address) + + assert Repo.aggregate(Transaction, :count, :id) == 0 + + assert %Scrivener.Page{ + entries: [], + page_number: 1, + total_entries: 0 + } = Chain.from_address_to_transactions(address) + end + + test "with transactions" do + %Transaction{from_address_id: from_address_id, id: transaction_id} = insert(:transaction) + address = Repo.get!(Address, from_address_id) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id}], + page_number: 1, + total_entries: 1 + } = Chain.from_address_to_transactions(address) + end + + test "with transactions with receipt required without receipt does not return transaction" do + address = %Address{id: from_address_id} = insert(:address) + + %Transaction{id: transaction_id_with_receipt} = + insert(:transaction, from_address_id: from_address_id) + + insert(:receipt, transaction_id: transaction_id_with_receipt) + + %Transaction{id: transaction_id_without_receipt} = + insert(:transaction, from_address_id: from_address_id) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], + page_number: 1, + total_entries: 1 + } = + Chain.from_address_to_transactions( + address, + necessity_by_association: %{receipt: :required} + ) + + assert %Scrivener.Page{ + entries: transactions, + page_number: 1, + total_entries: 2 + } = + Chain.from_address_to_transactions( + address, + necessity_by_association: %{receipt: :optional} + ) + + assert length(transactions) == 2 + + transaction_by_id = + Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> + {id, transaction} + end) + + assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] + assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] + end + + test "with transactions can be paginated" do + adddress = %Address{id: from_address_id} = insert(:address) + transactions = insert_list(2, :transaction, from_address_id: from_address_id) + + [%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] = + transactions + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^newest_transaction_id}], + page_number: 1, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.from_address_to_transactions(adddress, pagination: %{page_size: 1}) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^oldest_transaction_id}], + page_number: 2, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = + Chain.from_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1}) + end + end + + describe "hash_to_address/1" do + test "without address returns {:error, :not_found}" do + assert {:error, :not_found} = Chain.hash_to_address("unknown") + end + + test "with address returns {:ok, address}" do + hash = "0xandesmints" + %Address{id: address_id} = insert(:address, hash: hash) + + assert {:ok, %Address{id: ^address_id}} = Chain.hash_to_address(hash) + end + end + + describe "hash_to_transaction/2" do + test "without transaction returns {:error, :not_found}" do + assert {:error, :not_found} = Chain.hash_to_transaction("unknown") + end + + test "with transaction returns {:ok, transaction}" do + hash = "0xandesmints" + %Transaction{id: transaction_id} = insert(:transaction, hash: hash) + + assert {:ok, %Transaction{id: ^transaction_id}} = Chain.hash_to_transaction(hash) + end + + test "with transaction with receipt required without receipt returns {:error, :not_found}" do + %Transaction{hash: hash_with_receipt, id: transaction_id_with_receipt} = + insert(:transaction) + + insert(:receipt, transaction_id: transaction_id_with_receipt) + + %Transaction{hash: hash_without_receipt} = insert(:transaction) + + assert {:ok, %Transaction{hash: ^hash_with_receipt}} = + Chain.hash_to_transaction( + hash_with_receipt, + necessity_by_association: %{receipt: :required} + ) + + assert {:error, :not_found} = + Chain.hash_to_transaction( + hash_without_receipt, + necessity_by_association: %{receipt: :required} + ) + + assert {:ok, %Transaction{hash: ^hash_without_receipt}} = + Chain.hash_to_transaction( + hash_without_receipt, + necessity_by_association: %{receipt: :optional} + ) + end + end + + describe "id_to_address/1" do + test "returns the address with given id" do + %Address{id: id} = insert(:address) + + assert {:ok, %Address{id: ^id}} = Chain.id_to_address(id) + end + end + + describe "last_transaction_id/1" do + test "without transactions returns 0" do + assert Chain.last_transaction_id() == 0 + end + + test "with transaction returns last created transaction's id" do + insert(:transaction) + %Transaction{id: id} = insert(:transaction) + + assert Chain.last_transaction_id() == id + end + + test "with transaction with pending: true returns last pending transaction id, not the last transaction" do + %Transaction{id: pending_transaction_id} = insert(:transaction) + + %Transaction{id: transaction_id} = insert(:transaction) + insert(:receipt, transaction_id: transaction_id) + + assert pending_transaction_id < transaction_id + + assert Chain.last_transaction_id(pending: true) == pending_transaction_id + assert Chain.last_transaction_id(pending: false) == transaction_id + assert Chain.last_transaction_id() == transaction_id + end + end + + describe "list_blocks/2" do + test "without blocks" do + assert %Scrivener.Page{ + entries: [], + page_number: 1, + total_entries: 0, + total_pages: 1 + } = Chain.list_blocks() + end + + test "with blocks" do + %Block{id: id} = insert(:block) + + assert %Scrivener.Page{ + entries: [%Block{id: ^id}], + page_number: 1, + total_entries: 1 + } = Chain.list_blocks() + end + + test "with blocks can be paginated" do + blocks = insert_list(2, :block) + + [%Block{number: lesser_block_number}, %Block{number: greater_block_number}] = blocks + + assert %Scrivener.Page{ + entries: [%Block{number: ^greater_block_number}], + page_number: 1, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.list_blocks(pagination: %{page_size: 1}) + + assert %Scrivener.Page{ + entries: [%Block{number: ^lesser_block_number}], + page_number: 2, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.list_blocks(pagination: %{page: 2, page_size: 1}) + end + end + + describe "max_block_number/0" do + test "without blocks is nil" do + assert Chain.max_block_number() == nil + end + + test "with blocks is max number regardless of insertion order" do + max_number = 2 + insert(:block, number: max_number) + insert(:block, number: 1) - insert(:block, number: 100) - chain = Chain.fetch() - assert chain.number == 100 + + assert Chain.max_block_number() == max_number end + end - test "returns the latest block timestamp" do - time = DateTime.utc_now() - insert(:block, timestamp: time) - chain = Chain.fetch() - assert Timex.diff(chain.timestamp, time, :seconds) == 0 + describe "number_to_block/1" do + test "without block" do + assert {:error, :not_found} = Chain.number_to_block(-1) end - test "returns the average time between blocks" do - time = DateTime.utc_now() - next_time = Timex.shift(time, seconds: 5) - insert(:block, timestamp: time) - insert(:block, timestamp: next_time) - chain = Chain.fetch() + test "with block" do + %Block{number: number} = insert(:block) - assert chain.average_time == %Duration{ - seconds: 5, - megaseconds: 0, - microseconds: 0 - } + assert {:ok, %Block{number: ^number}} = Chain.number_to_block(number) end + end - test "returns the count of transactions from blocks in the last day" do - time = DateTime.utc_now() - last_week = Timex.shift(time, days: -8) - block = insert(:block, timestamp: time) - old_block = insert(:block, timestamp: last_week) - transaction = insert(:transaction) - old_transaction = insert(:transaction) - insert(:block_transaction, block: block, transaction: transaction) - insert(:block_transaction, block: old_block, transaction: old_transaction) - chain = Chain.fetch() - assert chain.transaction_count == 1 + describe "to_address_to_transactions/2" do + test "without transactions" do + address = insert(:address) + + assert Repo.aggregate(Transaction, :count, :id) == 0 + + assert %Scrivener.Page{ + entries: [], + page_number: 1, + total_entries: 0 + } = Chain.to_address_to_transactions(address) end - test "returns the number of skipped blocks" do - insert(:block, %{number: 0}) - insert(:block, %{number: 2}) - chain = Chain.fetch() - assert chain.skipped_blocks == 1 + test "with transactions" do + %Transaction{to_address_id: to_address_id, id: transaction_id} = insert(:transaction) + address = Repo.get!(Address, to_address_id) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id}], + page_number: 1, + total_entries: 1 + } = Chain.to_address_to_transactions(address) end - test "returns the lag between validation and insertion time" do - validation_time = DateTime.utc_now() - inserted_at = validation_time |> Timex.shift(seconds: 5) - insert(:block, timestamp: validation_time, inserted_at: inserted_at) - chain = Chain.fetch() - assert chain.lag == %Duration{seconds: 5, megaseconds: 0, microseconds: 0} + test "with transactions with receipt required without receipt does not return transaction" do + address = %Address{id: to_address_id} = insert(:address) + + %Transaction{id: transaction_id_with_receipt} = + insert(:transaction, to_address_id: to_address_id) + + insert(:receipt, transaction_id: transaction_id_with_receipt) + + %Transaction{id: transaction_id_without_receipt} = + insert(:transaction, to_address_id: to_address_id) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^transaction_id_with_receipt, receipt: %Receipt{}}], + page_number: 1, + total_entries: 1 + } = + Chain.to_address_to_transactions( + address, + necessity_by_association: %{receipt: :required} + ) + + assert %Scrivener.Page{ + entries: transactions, + page_number: 1, + total_entries: 2 + } = + Chain.to_address_to_transactions( + address, + necessity_by_association: %{receipt: :optional} + ) + + assert length(transactions) == 2 + + transaction_by_id = + Enum.into(transactions, %{}, fn transaction = %Transaction{id: id} -> + {id, transaction} + end) + + assert %Transaction{receipt: %Receipt{}} = transaction_by_id[transaction_id_with_receipt] + assert %Transaction{receipt: nil} = transaction_by_id[transaction_id_without_receipt] end - test "returns the number of blocks inserted in the last minute" do - old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) - insert(:block, inserted_at: old_inserted_at) - insert(:block) - chain = Chain.fetch() - assert chain.block_velocity == 1 + test "with transactions can be paginated" do + adddress = %Address{id: to_address_id} = insert(:address) + transactions = insert_list(2, :transaction, to_address_id: to_address_id) + + [%Transaction{id: oldest_transaction_id}, %Transaction{id: newest_transaction_id}] = + transactions + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^newest_transaction_id}], + page_number: 1, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.to_address_to_transactions(adddress, pagination: %{page_size: 1}) + + assert %Scrivener.Page{ + entries: [%Transaction{id: ^oldest_transaction_id}], + page_number: 2, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.to_address_to_transactions(adddress, pagination: %{page: 2, page_size: 1}) + end + end + + describe "transaction_count/0" do + test "without transactions" do + assert Chain.transaction_count() == 0 + end + + test "with transactions" do + count = 2 + insert_list(count, :transaction) + + assert Chain.transaction_count() == count end - test "returns the number of transactions inserted in the last minute" do - old_inserted_at = Timex.shift(DateTime.utc_now(), days: -1) - insert(:transaction, inserted_at: old_inserted_at) + test "with transaction pending: true counts only pending transactions" do insert(:transaction) - chain = Chain.fetch() - assert chain.transaction_velocity == 1 + + %Transaction{id: transaction_id} = insert(:transaction) + insert(:receipt, transaction_id: transaction_id) + + assert Chain.transaction_count(pending: true) == 1 + assert Chain.transaction_count(pending: false) == 2 + assert Chain.transaction_count() == 2 end + end - test "returns the last five blocks" do - insert_list(6, :block) - chain = Chain.fetch() - assert chain.blocks |> Enum.count() == 5 + describe "transaction_hash_to_internal_transactions/1" do + test "without transaction" do + assert Chain.transaction_hash_to_internal_transactions("unknown") == [] end - test "returns the last five transactions with blocks" do - block = insert(:block) + test "with transaction without internal transactions" do + %Transaction{hash: hash} = insert(:transaction) + + assert Chain.transaction_hash_to_internal_transactions(hash) == [] + end - insert_list(6, :transaction) - |> Enum.map(fn transaction -> - insert(:block_transaction, block: block, transaction: transaction) + test "with transaction with internal transactions returns all internal transactions for a given transaction hash" do + transaction = insert(:transaction) + internal_transaction = insert(:internal_transaction, transaction_id: transaction.id) + + result = hd(Chain.transaction_hash_to_internal_transactions(transaction.hash)) + + assert result.id == internal_transaction.id + end + + test "with transaction with internal transactions loads associations with in necessity_by_assocation" do + %Transaction{hash: hash, id: transaction_id} = insert(:transaction) + insert(:internal_transaction, transaction_id: transaction_id) + + assert [ + %InternalTransaction{ + from_address: %Ecto.Association.NotLoaded{}, + to_address: %Ecto.Association.NotLoaded{}, + transaction: %Ecto.Association.NotLoaded{} + } + ] = Chain.transaction_hash_to_internal_transactions(hash) + + assert [ + %InternalTransaction{ + from_address: %Address{}, + to_address: %Address{}, + transaction: %Transaction{} + } + ] = + Chain.transaction_hash_to_internal_transactions( + hash, + necessity_by_association: %{ + from_address: :optional, + to_address: :optional, + transaction: :optional + } + ) + end + end + + describe "transactions_recently_before_id" do + test "returns at most 10 transactions" do + count = 12 + + assert 10 < count + + transactions = insert_list(count, :transaction) + %Transaction{id: last_transaction_id} = List.last(transactions) + + recent_transactions = Chain.transactions_recently_before_id(last_transaction_id) + + assert length(recent_transactions) == 10 + end + + test "with pending: true returns only pending transactions" do + count = 12 + + transactions = insert_list(count, :transaction) + %Transaction{id: last_transaction_id} = List.last(transactions) + + transactions + |> Enum.take(3) + |> Enum.each(fn %Transaction{id: id} -> + insert(:receipt, transaction_id: id) end) - chain = Chain.fetch() - assert chain.transactions |> Enum.count() == 5 + assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: true)) == + 8 + + assert length(Chain.transactions_recently_before_id(last_transaction_id, pending: false)) == + 10 + + assert length(Chain.transactions_recently_before_id(last_transaction_id)) == 10 + end + end + + describe "transaction_to_logs/2" do + test "without logs" do + transaction = insert(:transaction) + + assert %Scrivener.Page{ + entries: [], + page_number: 1, + total_entries: 0, + total_pages: 1 + } = Chain.transaction_to_logs(transaction) + end + + test "with logs" do + transaction = insert(:transaction) + %Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) + %Log{id: id} = insert(:log, receipt_id: receipt_id) + + assert %Scrivener.Page{ + entries: [%Log{id: ^id}], + page_number: 1, + total_entries: 1, + total_pages: 1 + } = Chain.transaction_to_logs(transaction) + end + + test "with logs can be paginated" do + transaction = insert(:transaction) + %Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) + logs = insert_list(2, :log, receipt_id: receipt_id) + + [%Log{id: first_log_id}, %Log{id: second_log_id}] = logs + + assert %Scrivener.Page{ + entries: [%Log{id: ^first_log_id}], + page_number: 1, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.transaction_to_logs(transaction, pagination: %{page_size: 1}) + + assert %Scrivener.Page{ + entries: [%Log{id: ^second_log_id}], + page_number: 2, + page_size: 1, + total_entries: 2, + total_pages: 2 + } = Chain.transaction_to_logs(transaction, pagination: %{page: 2, page_size: 1}) + end + + test "with logs necessity_by_association loads associations" do + transaction = insert(:transaction) + %Receipt{id: receipt_id} = insert(:receipt, transaction_id: transaction.id) + insert(:log, receipt_id: receipt_id) + + assert %Scrivener.Page{ + entries: [ + %Log{ + address: %Address{}, + receipt: %Receipt{}, + transaction: %Transaction{} + } + ], + page_number: 1, + total_entries: 1, + total_pages: 1 + } = + Chain.transaction_to_logs( + transaction, + necessity_by_association: %{ + address: :optional, + receipt: :optional, + transaction: :optional + } + ) + + assert %Scrivener.Page{ + entries: [ + %Log{ + address: %Ecto.Association.NotLoaded{}, + receipt: %Ecto.Association.NotLoaded{}, + transaction: %Ecto.Association.NotLoaded{} + } + ], + page_number: 1, + total_entries: 1, + total_pages: 1 + } = Chain.transaction_to_logs(transaction) + end + end + + describe "update_balance/2" do + test "updates the balance" do + hash = "0xwarheads" + insert(:address, hash: hash) + + Chain.update_balance(hash, 5) + + expected_balance = Decimal.new(5) + + assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address(hash) + end + + test "updates the balance timestamp" do + hash = "0xtwizzlers" + insert(:address, hash: hash) + + Chain.update_balance(hash, 88) + + assert {:ok, %Address{balance_updated_at: balance_updated_at}} = + Chain.hash_to_address("0xtwizzlers") + + refute is_nil(balance_updated_at) + end + + test "creates an address if one does not exist" do + Chain.update_balance("0xtwizzlers", 88) + + expected_balance = Decimal.new(88) + + assert {:ok, %Address{balance: ^expected_balance}} = Chain.hash_to_address("0xtwizzlers") end end end diff --git a/apps/explorer/test/explorer/importers/balance_importer_test.exs b/apps/explorer/test/explorer/importers/balance_importer_test.exs index 698b0609a1..9b2635eaa1 100644 --- a/apps/explorer/test/explorer/importers/balance_importer_test.exs +++ b/apps/explorer/test/explorer/importers/balance_importer_test.exs @@ -1,15 +1,19 @@ defmodule Explorer.BalanceImporterTest do use Explorer.DataCase - alias Explorer.Address.Service, as: Address - alias Explorer.BalanceImporter + alias Explorer.{Chain, BalanceImporter} + alias Explorer.Chain.Address describe "import/1" do test "it updates the balance for an address" do insert(:address, hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", balance: 5) + BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") - address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") - assert address.balance == Decimal.new(1_572_374_181_095_000_000) + + expected_balance = Decimal.new(1_572_374_181_095_000_000) + + assert {:ok, %Address{balance: ^expected_balance}} = + Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") end test "it updates the balance update time for an address" do @@ -20,13 +24,17 @@ defmodule Explorer.BalanceImporterTest do ) BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") - address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") - refute is_nil(address.balance_updated_at) + + assert {:ok, %Address{balance_updated_at: balance_updated_at}} = + Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + + refute is_nil(balance_updated_at) end test "it creates an address if one does not exist" do BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") - assert Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") + + assert {:ok, _} = Chain.hash_to_address("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") end end end diff --git a/apps/explorer/test/explorer/importers/block_importer_test.exs b/apps/explorer/test/explorer/importers/block_importer_test.exs index 74f34711fc..94e167d82c 100644 --- a/apps/explorer/test/explorer/importers/block_importer_test.exs +++ b/apps/explorer/test/explorer/importers/block_importer_test.exs @@ -3,9 +3,8 @@ defmodule Explorer.BlockImporterTest do import Mock - alias Explorer.Block - alias Explorer.Transaction alias Explorer.BlockImporter + alias Explorer.Chain.{Block, Transaction} alias Explorer.Workers.ImportTransaction describe "import/1" do diff --git a/apps/explorer/test/explorer/importers/internal_transaction_importer_test.exs b/apps/explorer/test/explorer/importers/internal_transaction_importer_test.exs index bf816dbfe3..dc29b3e648 100644 --- a/apps/explorer/test/explorer/importers/internal_transaction_importer_test.exs +++ b/apps/explorer/test/explorer/importers/internal_transaction_importer_test.exs @@ -1,7 +1,7 @@ defmodule Explorer.InternalTransactionImporterTest do use Explorer.DataCase - alias Explorer.InternalTransaction + alias Explorer.Chain.InternalTransaction alias Explorer.InternalTransactionImporter describe "import/1" do diff --git a/apps/explorer/test/explorer/importers/receipt_importer_test.exs b/apps/explorer/test/explorer/importers/receipt_importer_test.exs index 0507591cc9..b5115e08af 100644 --- a/apps/explorer/test/explorer/importers/receipt_importer_test.exs +++ b/apps/explorer/test/explorer/importers/receipt_importer_test.exs @@ -1,8 +1,7 @@ defmodule Explorer.ReceiptImporterTest do use Explorer.DataCase - alias Explorer.Receipt - alias Explorer.Log + alias Explorer.Chain.{Log, Receipt} alias Explorer.ReceiptImporter describe "import/1" do diff --git a/apps/explorer/test/explorer/importers/transaction_importer_test.exs b/apps/explorer/test/explorer/importers/transaction_importer_test.exs index c550adf012..d62d9cf6f9 100644 --- a/apps/explorer/test/explorer/importers/transaction_importer_test.exs +++ b/apps/explorer/test/explorer/importers/transaction_importer_test.exs @@ -1,9 +1,7 @@ defmodule Explorer.TransactionImporterTest do use Explorer.DataCase - alias Explorer.Address - alias Explorer.BlockTransaction - alias Explorer.Transaction + alias Explorer.Chain.{Address, BlockTransaction, Transaction} alias Explorer.TransactionImporter @raw_transaction %{ diff --git a/apps/explorer/test/explorer/resource_test.exs b/apps/explorer/test/explorer/resource_test.exs deleted file mode 100644 index e38ab5a2c9..0000000000 --- a/apps/explorer/test/explorer/resource_test.exs +++ /dev/null @@ -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 diff --git a/apps/explorer/test/explorer/servers/chain_statistics_test.exs b/apps/explorer/test/explorer/servers/chain_statistics_test.exs deleted file mode 100644 index a7f3900d1e..0000000000 --- a/apps/explorer/test/explorer/servers/chain_statistics_test.exs +++ /dev/null @@ -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 diff --git a/apps/explorer/test/explorer/services/address_test.exs b/apps/explorer/test/explorer/services/address_test.exs deleted file mode 100644 index bccd2fcd6e..0000000000 --- a/apps/explorer/test/explorer/services/address_test.exs +++ /dev/null @@ -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 diff --git a/apps/explorer/test/explorer/services/transaction_test.exs b/apps/explorer/test/explorer/services/transaction_test.exs deleted file mode 100644 index ba013ac6dd..0000000000 --- a/apps/explorer/test/explorer/services/transaction_test.exs +++ /dev/null @@ -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 diff --git a/apps/explorer/test/explorer/workers/import_balance_test.exs b/apps/explorer/test/explorer/workers/import_balance_test.exs index 7504744966..a62db89f56 100644 --- a/apps/explorer/test/explorer/workers/import_balance_test.exs +++ b/apps/explorer/test/explorer/workers/import_balance_test.exs @@ -1,16 +1,20 @@ defmodule Explorer.Workers.ImportBalanceTest do import Mock + alias Explorer.Chain + alias Explorer.Chain.Address alias Explorer.Workers.ImportBalance - alias Explorer.Address.Service, as: Address use Explorer.DataCase describe "perform/1" do test "imports the balance for an address" do ImportBalance.perform("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") - address = Address.by_hash("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") - assert address.balance == Decimal.new(1_572_374_181_095_000_000) + + expected_balance = Decimal.new(1_572_374_181_095_000_000) + + assert {:ok, %Address{balance: ^expected_balance}} = + Chain.hash_to_address("0x1d12e5716c593b156eb7152ca4360f6224ba3b0a") end end @@ -25,8 +29,11 @@ defmodule Explorer.Workers.ImportBalanceTest do ) end do ImportBalance.perform_later("0xskateboards") - address = Address.by_hash("0xskateboards") - assert address.balance == Decimal.new(66) + + expected_balance = Decimal.new(66) + + assert {:ok, %Address{balance: ^expected_balance}} = + Chain.hash_to_address("0xskateboards") end end end diff --git a/apps/explorer/test/explorer/workers/import_block_test.exs b/apps/explorer/test/explorer/workers/import_block_test.exs index 66ab7b4a69..9a13f42b9b 100644 --- a/apps/explorer/test/explorer/workers/import_block_test.exs +++ b/apps/explorer/test/explorer/workers/import_block_test.exs @@ -1,11 +1,11 @@ defmodule Explorer.Workers.ImportBlockTest do - alias Explorer.Block - alias Explorer.Repo - alias Explorer.Workers.ImportBlock + use Explorer.DataCase import Mock - use Explorer.DataCase + alias Explorer.Chain.Block + alias Explorer.Repo + alias Explorer.Workers.ImportBlock describe "perform/1" do test "imports the requested block number as an integer" do diff --git a/apps/explorer/test/explorer/workers/import_internal_transaction_test.exs b/apps/explorer/test/explorer/workers/import_internal_transaction_test.exs index 3fc510869c..26e920ce0b 100644 --- a/apps/explorer/test/explorer/workers/import_internal_transaction_test.exs +++ b/apps/explorer/test/explorer/workers/import_internal_transaction_test.exs @@ -2,7 +2,7 @@ defmodule Explorer.Workers.ImportInternalTransactionTest do use Explorer.DataCase alias Explorer.Repo - alias Explorer.InternalTransaction + alias Explorer.Chain.InternalTransaction alias Explorer.Workers.ImportInternalTransaction describe "perform/1" do diff --git a/apps/explorer/test/explorer/workers/import_receipt_test.exs b/apps/explorer/test/explorer/workers/import_receipt_test.exs index ddd4043bab..496b2bf819 100644 --- a/apps/explorer/test/explorer/workers/import_receipt_test.exs +++ b/apps/explorer/test/explorer/workers/import_receipt_test.exs @@ -2,7 +2,7 @@ defmodule Explorer.Workers.ImportReceiptTest do use Explorer.DataCase alias Explorer.Repo - alias Explorer.Receipt + alias Explorer.Chain.Receipt alias Explorer.Workers.ImportReceipt describe "perform/1" do diff --git a/apps/explorer/test/explorer/workers/import_skipped_blocks_test.exs b/apps/explorer/test/explorer/workers/import_skipped_blocks_test.exs index 72c2ea24b4..637cda53a7 100644 --- a/apps/explorer/test/explorer/workers/import_skipped_blocks_test.exs +++ b/apps/explorer/test/explorer/workers/import_skipped_blocks_test.exs @@ -1,12 +1,11 @@ defmodule Explorer.Workers.ImportSkippedBlocksTest do - alias Explorer.Block - alias Explorer.Repo - alias Explorer.Workers.ImportBlock - alias Explorer.Workers.ImportSkippedBlocks + use Explorer.DataCase import Mock - use Explorer.DataCase + alias Explorer.Chain.Block + alias Explorer.Repo + alias Explorer.Workers.{ImportBlock, ImportSkippedBlocks} describe "perform/1" do test "imports the requested number of skipped blocks" do diff --git a/apps/explorer/test/explorer/workers/import_transaction_test.exs b/apps/explorer/test/explorer/workers/import_transaction_test.exs index e187891a82..d7203b0b8f 100644 --- a/apps/explorer/test/explorer/workers/import_transaction_test.exs +++ b/apps/explorer/test/explorer/workers/import_transaction_test.exs @@ -3,10 +3,8 @@ defmodule Explorer.Workers.ImportTransactionTest do import Mock - alias Explorer.InternalTransaction - alias Explorer.Receipt + alias Explorer.Chain.{InternalTransaction, Receipt, Transaction} alias Explorer.Repo - alias Explorer.Transaction alias Explorer.Workers.ImportInternalTransaction alias Explorer.Workers.ImportTransaction diff --git a/apps/explorer/test/explorer/workers/refresh_balance_test.exs b/apps/explorer/test/explorer/workers/refresh_balance_test.exs index 3cb35daa8b..50897e92aa 100644 --- a/apps/explorer/test/explorer/workers/refresh_balance_test.exs +++ b/apps/explorer/test/explorer/workers/refresh_balance_test.exs @@ -3,8 +3,7 @@ defmodule Explorer.Workers.RefreshBalanceTest do import Mock - alias Explorer.Credit - alias Explorer.Debit + alias Explorer.Chain.{Credit, Debit} alias Explorer.Workers.RefreshBalance describe "perform/0" do diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index a71aed08b5..3cf7d2a349 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -39,20 +39,4 @@ defmodule Explorer.DataCase do :ok end - - @doc """ - A helper that transform changeset errors to a map of messages. - - assert {:error, changeset} = Accounts.create_user(%{password: "short"}) - assert "password is too short" in errors_on(changeset).password - assert %{password: ["password is too short"]} = errors_on(changeset) - - """ - def errors_on(changeset) do - Ecto.Changeset.traverse_errors(changeset, fn {message, opts} -> - Enum.reduce(opts, message, fn {key, value}, acc -> - String.replace(acc, "%{#{key}}", to_string(value)) - end) - end) - end end diff --git a/apps/explorer/test/support/factories/address_factory.ex b/apps/explorer/test/support/factories/chain/address_factory.ex similarity index 70% rename from apps/explorer/test/support/factories/address_factory.ex rename to apps/explorer/test/support/factories/chain/address_factory.ex index 59e4ac5012..e26810329f 100644 --- a/apps/explorer/test/support/factories/address_factory.ex +++ b/apps/explorer/test/support/factories/chain/address_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.AddressFactory do +defmodule Explorer.Chain.AddressFactory do defmacro __using__(_opts) do quote do def address_factory do - %Explorer.Address{ + %Explorer.Chain.Address{ hash: String.pad_trailing(sequence("0x"), 42, "address") } end diff --git a/apps/explorer/test/support/factories/block_factory.ex b/apps/explorer/test/support/factories/chain/block_factory.ex similarity index 88% rename from apps/explorer/test/support/factories/block_factory.ex rename to apps/explorer/test/support/factories/chain/block_factory.ex index 6da21b4724..9ef458c079 100644 --- a/apps/explorer/test/support/factories/block_factory.ex +++ b/apps/explorer/test/support/factories/chain/block_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.BlockFactory do +defmodule Explorer.Chain.BlockFactory do defmacro __using__(_opts) do quote do def block_factory do - %Explorer.Block{ + %Explorer.Chain.Block{ number: sequence(""), hash: sequence("0x"), parent_hash: sequence("0x"), diff --git a/apps/explorer/test/support/factories/block_transaction_factory.ex b/apps/explorer/test/support/factories/chain/block_transaction_factory.ex similarity index 53% rename from apps/explorer/test/support/factories/block_transaction_factory.ex rename to apps/explorer/test/support/factories/chain/block_transaction_factory.ex index 5d7ca72918..2489b3a6d4 100644 --- a/apps/explorer/test/support/factories/block_transaction_factory.ex +++ b/apps/explorer/test/support/factories/chain/block_transaction_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.BlockTransactionFactory do +defmodule Explorer.Chain.BlockTransactionFactory do defmacro __using__(_opts) do quote do def block_transaction_factory do - %Explorer.BlockTransaction{} + %Explorer.Chain.BlockTransaction{} end end end diff --git a/apps/explorer/test/support/factories/from_address_factory.ex b/apps/explorer/test/support/factories/chain/from_address_factory.ex similarity index 55% rename from apps/explorer/test/support/factories/from_address_factory.ex rename to apps/explorer/test/support/factories/chain/from_address_factory.ex index 4b829b43c6..3631126aec 100644 --- a/apps/explorer/test/support/factories/from_address_factory.ex +++ b/apps/explorer/test/support/factories/chain/from_address_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.FromAddressFactory do +defmodule Explorer.Chain.FromAddressFactory do defmacro __using__(_opts) do quote do def from_address_factory do - %Explorer.FromAddress{} + %Explorer.Chain.FromAddress{} end end end diff --git a/apps/explorer/test/support/factories/internal_transaction_factory.ex b/apps/explorer/test/support/factories/chain/internal_transaction_factory.ex similarity index 85% rename from apps/explorer/test/support/factories/internal_transaction_factory.ex rename to apps/explorer/test/support/factories/chain/internal_transaction_factory.ex index 745c6a0ce4..2edf5129db 100644 --- a/apps/explorer/test/support/factories/internal_transaction_factory.ex +++ b/apps/explorer/test/support/factories/chain/internal_transaction_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.InternalTransactionFactory do +defmodule Explorer.Chain.InternalTransactionFactory do defmacro __using__(_opts) do quote do def internal_transaction_factory do - %Explorer.InternalTransaction{ + %Explorer.Chain.InternalTransaction{ index: Enum.random(0..9), call_type: Enum.random(["call", "creates", "calldelegate"]), trace_address: [Enum.random(0..4), Enum.random(0..4)], diff --git a/apps/explorer/test/support/factories/log_factory.ex b/apps/explorer/test/support/factories/chain/log_factory.ex similarity index 59% rename from apps/explorer/test/support/factories/log_factory.ex rename to apps/explorer/test/support/factories/chain/log_factory.ex index 9f7fb17793..537f59c533 100644 --- a/apps/explorer/test/support/factories/log_factory.ex +++ b/apps/explorer/test/support/factories/chain/log_factory.ex @@ -1,15 +1,16 @@ -defmodule Explorer.LogFactory do +defmodule Explorer.Chain.LogFactory do defmacro __using__(_opts) do quote do def log_factory do - %Explorer.Log{ - index: sequence(""), + %Explorer.Chain.Log{ + address_id: insert(:address).id, data: sequence("0x"), - type: sequence("0x"), first_topic: nil, + fourth_topic: nil, + index: sequence(""), second_topic: nil, third_topic: nil, - fourth_topic: nil + type: sequence("0x") } end end diff --git a/apps/explorer/test/support/factories/receipt_factory.ex b/apps/explorer/test/support/factories/chain/receipt_factory.ex similarity index 79% rename from apps/explorer/test/support/factories/receipt_factory.ex rename to apps/explorer/test/support/factories/chain/receipt_factory.ex index 78ef9b27d6..0e9012c46d 100644 --- a/apps/explorer/test/support/factories/receipt_factory.ex +++ b/apps/explorer/test/support/factories/chain/receipt_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.ReceiptFactory do +defmodule Explorer.Chain.ReceiptFactory do defmacro __using__(_opts) do quote do def receipt_factory do - %Explorer.Receipt{ + %Explorer.Chain.Receipt{ cumulative_gas_used: Enum.random(21_000..100_000), gas_used: Enum.random(21_000..100_000), status: Enum.random(1..2), diff --git a/apps/explorer/test/support/factories/to_address_factory.ex b/apps/explorer/test/support/factories/chain/to_address_factory.ex similarity index 56% rename from apps/explorer/test/support/factories/to_address_factory.ex rename to apps/explorer/test/support/factories/chain/to_address_factory.ex index e9c1b213b6..06643f4132 100644 --- a/apps/explorer/test/support/factories/to_address_factory.ex +++ b/apps/explorer/test/support/factories/chain/to_address_factory.ex @@ -1,8 +1,8 @@ -defmodule Explorer.ToAddressFactory do +defmodule Explorer.Chain.ToAddressFactory do defmacro __using__(_opts) do quote do def to_address_factory do - %Explorer.ToAddress{} + %Explorer.Chain.ToAddress{} end end end diff --git a/apps/explorer/test/support/factories/transaction_factory.ex b/apps/explorer/test/support/factories/chain/transaction_factory.ex similarity index 88% rename from apps/explorer/test/support/factories/transaction_factory.ex rename to apps/explorer/test/support/factories/chain/transaction_factory.ex index ce81d0004f..a10f5c3dc5 100644 --- a/apps/explorer/test/support/factories/transaction_factory.ex +++ b/apps/explorer/test/support/factories/chain/transaction_factory.ex @@ -1,12 +1,11 @@ -defmodule Explorer.TransactionFactory do +defmodule Explorer.Chain.TransactionFactory do defmacro __using__(_opts) do quote do - alias Explorer.Address - alias Explorer.BlockTransaction + alias Explorer.Chain.{Address, BlockTransaction, Transaction} alias Explorer.Repo def transaction_factory do - %Explorer.Transaction{ + %Transaction{ hash: String.pad_trailing(sequence("0x"), 43, "action"), value: Enum.random(1..100_000), gas: Enum.random(21_000..100_000), diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 437ad8cc70..fe715c6d77 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -1,13 +1,13 @@ defmodule Explorer.Factory do @dialyzer {:nowarn_function, fields_for: 1} use ExMachina.Ecto, repo: Explorer.Repo - use Explorer.AddressFactory - use Explorer.BlockFactory - use Explorer.BlockTransactionFactory - use Explorer.FromAddressFactory - use Explorer.InternalTransactionFactory - use Explorer.LogFactory - use Explorer.ToAddressFactory - use Explorer.TransactionFactory - use Explorer.ReceiptFactory + use Explorer.Chain.AddressFactory + use Explorer.Chain.BlockFactory + use Explorer.Chain.BlockTransactionFactory + use Explorer.Chain.FromAddressFactory + use Explorer.Chain.InternalTransactionFactory + use Explorer.Chain.LogFactory + use Explorer.Chain.ReceiptFactory + use Explorer.Chain.ToAddressFactory + use Explorer.Chain.TransactionFactory end diff --git a/apps/explorer_web/lib/explorer_web.ex b/apps/explorer_web/lib/explorer_web.ex index 3b86a63567..ea0eac3e7b 100644 --- a/apps/explorer_web/lib/explorer_web.ex +++ b/apps/explorer_web/lib/explorer_web.ex @@ -20,9 +20,11 @@ defmodule ExplorerWeb do def controller do quote do use Phoenix.Controller, namespace: ExplorerWeb - import Plug.Conn + + import ExplorerWeb.Controller import ExplorerWeb.Router.Helpers import ExplorerWeb.Gettext + import Plug.Conn end end @@ -38,17 +40,16 @@ defmodule ExplorerWeb do # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML - import ExplorerWeb.Router.Helpers - import ExplorerWeb.ErrorHelpers - import ExplorerWeb.Gettext - import Scrivener.HTML + import ExplorerWeb.{ErrorHelpers, Gettext, Router.Helpers} import ReactPhoenix.ClientSide + import Scrivener.HTML end end def router do quote do use Phoenix.Router + import Plug.Conn import Phoenix.Controller end @@ -57,6 +58,7 @@ defmodule ExplorerWeb do def channel do quote do use Phoenix.Channel + import ExplorerWeb.Gettext end end diff --git a/apps/explorer_web/lib/explorer_web/chain.ex b/apps/explorer_web/lib/explorer_web/chain.ex new file mode 100644 index 0000000000..7dfccf1d3b --- /dev/null +++ b/apps/explorer_web/lib/explorer_web/chain.ex @@ -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 diff --git a/apps/explorer_web/lib/explorer_web/controller.ex b/apps/explorer_web/lib/explorer_web/controller.ex new file mode 100644 index 0000000000..d2839afa80 --- /dev/null +++ b/apps/explorer_web/lib/explorer_web/controller.ex @@ -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 diff --git a/apps/explorer_web/lib/explorer_web/controllers/address_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/address_controller.ex index 01f0f0471b..88d413627b 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/address_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/address_controller.ex @@ -1,10 +1,14 @@ defmodule ExplorerWeb.AddressController do use ExplorerWeb, :controller - alias Explorer.Address.Service, as: Address + alias Explorer.Chain - def show(conn, %{"id" => id}) do - address = id |> Address.by_hash() - render(conn, "show.html", address: address) + def show(conn, %{"id" => hash}) do + hash + |> Chain.hash_to_address() + |> case do + {:ok, address} -> render(conn, "show.html", address: address) + {:error, :not_found} -> not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex index c184f7d36d..2421c5ccf7 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/address_transaction_from_controller.ex @@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionFromController do use ExplorerWeb, :controller - alias Explorer.Address.Service, as: Address - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction - alias Explorer.Transaction.Service.Query + alias Explorer.Chain alias ExplorerWeb.TransactionForm - def index(conn, %{"address_id" => address_id} = params) do - address = Address.by_hash(address_id) + def index(conn, %{"address_id" => from_address_hash} = params) do + case Chain.hash_to_address(from_address_hash) do + {:ok, from_address} -> + page = + Chain.from_address_to_transactions( + from_address, + necessity_by_association: %{ + block: :required, + from_address: :optional, + to_address: :optional, + receipt: :required + }, + pagination: params + ) - query = - Transaction - |> Query.from_address(address.id) - |> Query.include_addresses() - |> Query.require_receipt() - |> Query.require_block() - |> Query.chron() + entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) + render(conn, "index.html", transactions: Map.put(page, :entries, entries)) - page = Repo.paginate(query, params) - entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) - render(conn, "index.html", transactions: Map.put(page, :entries, entries)) + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/address_transaction_to_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/address_transaction_to_controller.ex index a3755ae970..5763c051cd 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/address_transaction_to_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/address_transaction_to_controller.ex @@ -5,25 +5,29 @@ defmodule ExplorerWeb.AddressTransactionToController do use ExplorerWeb, :controller - alias Explorer.Address.Service, as: Address - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction - alias Explorer.Transaction.Service.Query + alias Explorer.Chain alias ExplorerWeb.TransactionForm - def index(conn, %{"address_id" => address_id} = params) do - address = Address.by_hash(address_id) + def index(conn, %{"address_id" => to_address_hash} = params) do + case Chain.hash_to_address(to_address_hash) do + {:ok, to_address} -> + page = + Chain.to_address_to_transactions( + to_address, + necessity_by_association: %{ + block: :required, + from_address: :optional, + to_address: :optional, + receipt: :required + }, + pagination: params + ) - query = - Transaction - |> Query.to_address(address.id) - |> Query.include_addresses() - |> Query.require_receipt() - |> Query.require_block() - |> Query.chron() + entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) + render(conn, "index.html", transactions: Map.put(page, :entries, entries)) - page = Repo.paginate(query, params) - entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) - render(conn, "index.html", transactions: Map.put(page, :entries, entries)) + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/block_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/block_controller.ex index 9c8cbb3db2..77a462907e 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/block_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/block_controller.ex @@ -1,31 +1,25 @@ defmodule ExplorerWeb.BlockController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Block - alias Explorer.Repo.NewRelic, as: Repo + alias Explorer.Chain alias ExplorerWeb.BlockForm def index(conn, params) do blocks = - from( - block in Block, - order_by: [desc: block.number], - preload: :transactions - ) + Chain.list_blocks(necessity_by_association: %{transactions: :optional}, pagination: params) - render(conn, "index.html", blocks: Repo.paginate(blocks, params)) + render(conn, "index.html", blocks: blocks) end def show(conn, %{"id" => number}) do - block = - Block - |> where(number: ^number) - |> first - |> Repo.one() - |> BlockForm.build() + case Chain.number_to_block(number) do + {:ok, block} -> + block_form = BlockForm.build(block) + + render(conn, "show.html", block: block_form) - render(conn, "show.html", block: block) + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/block_transaction_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/block_transaction_controller.ex index ed0a8f2edc..ffa3cad5cb 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/block_transaction_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/block_transaction_controller.ex @@ -1,27 +1,34 @@ defmodule ExplorerWeb.BlockTransactionController do use ExplorerWeb, :controller - import Ecto.Query + import ExplorerWeb.Chain, only: [param_to_block_number: 1] - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction + alias Explorer.Chain alias ExplorerWeb.TransactionForm - def index(conn, %{"block_id" => block_number} = params) do - query = - from( - transaction in Transaction, - join: block in assoc(transaction, :block), - join: receipt in assoc(transaction, :receipt), - join: from_address in assoc(transaction, :from_address), - join: to_address in assoc(transaction, :to_address), - preload: [:block, :receipt, :to_address, :from_address], - order_by: [desc: transaction.inserted_at], - where: block.number == ^block_number - ) + def index(conn, %{"block_id" => formatted_block_number} = params) do + with {:ok, block_number} <- param_to_block_number(formatted_block_number), + {:ok, block} <- Chain.number_to_block(block_number) do + page = + Chain.block_to_transactions( + block, + necessity_by_association: %{ + block: :required, + from_address: :required, + to_address: :required, + receipt: :required + }, + pagination: params + ) - page = Repo.paginate(query, params) - entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) - render(conn, "index.html", transactions: Map.put(page, :entries, entries)) + entries = Enum.map(page.entries, &TransactionForm.build_and_merge/1) + render(conn, "index.html", transactions: Map.put(page, :entries, entries)) + else + {:error, :invalid} -> + not_found(conn) + + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex index da212bc728..8d3943730b 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/chain_controller.ex @@ -1,34 +1,35 @@ defmodule ExplorerWeb.ChainController do use ExplorerWeb, :controller - alias Explorer.Servers.ChainStatistics - alias Explorer.Resource + alias Explorer.Chain.{Address, Block, Statistics, Transaction} + alias ExplorerWeb.Chain def show(conn, _params) do - render(conn, "show.html", chain: ChainStatistics.fetch()) + render(conn, "show.html", chain: Statistics.fetch()) end def search(conn, %{"q" => query}) do query |> String.trim() - |> Resource.lookup() + |> Chain.from_param() |> case do - nil -> - conn - |> put_status(:not_found) - |> put_view(ExplorerWeb.ErrorView) - |> render("404.html") - - item -> + {:ok, item} -> redirect_search_results(conn, item) + + {:error, :not_found} -> + not_found(conn) end end - defp redirect_search_results(conn, %Explorer.Block{} = item) do + defp redirect_search_results(conn, %Address{} = item) do + redirect(conn, to: address_path(conn, :show, Gettext.get_locale(), item.hash)) + end + + defp redirect_search_results(conn, %Block{} = item) do redirect(conn, to: block_path(conn, :show, Gettext.get_locale(), item.number)) end - defp redirect_search_results(conn, %Explorer.Transaction{} = item) do + defp redirect_search_results(conn, %Transaction{} = item) do redirect( conn, to: @@ -40,8 +41,4 @@ defmodule ExplorerWeb.ChainController do ) ) end - - defp redirect_search_results(conn, %Explorer.Address{} = item) do - redirect(conn, to: address_path(conn, :show, Gettext.get_locale(), item.hash)) - end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/pending_transaction_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/pending_transaction_controller.ex index 9e06464f99..541ffa903b 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/pending_transaction_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/pending_transaction_controller.ex @@ -1,55 +1,31 @@ defmodule ExplorerWeb.PendingTransactionController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction + alias Explorer.Chain + alias Explorer.Chain.Transaction alias ExplorerWeb.PendingTransactionForm - def index(conn, %{"last_seen" => last_seen} = _) do - query = - from( - transaction in Transaction, - inner_join: to_address in assoc(transaction, :to_address), - inner_join: from_address in assoc(transaction, :from_address), - preload: [to_address: to_address, from_address: from_address], - where: - fragment( - "NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)", - transaction.id - ), - where: transaction.id < ^last_seen, - order_by: [desc: transaction.id], - limit: 10 - ) - - total_query = - from( - transaction in Transaction, - where: - fragment( - "NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)", - transaction.id - ), - order_by: [desc: transaction.id], - limit: 1 + def index(conn, %{"last_seen" => last_seen_id} = _) do + total = Chain.transaction_count(pending: true) + + entries = + last_seen_id + |> Chain.transactions_recently_before_id( + necessity_by_association: %{ + from_address: :optional, + to_address: :optional + }, + pending: true ) + |> Enum.map(&PendingTransactionForm.build/1) - total = - case Repo.one(total_query) do - nil -> 0 - total -> total.id - end - - entries = Repo.all(query) last = List.last(entries) || Transaction.null() render( conn, "index.html", transactions: %{ - entries: entries |> Enum.map(&PendingTransactionForm.build/1), + entries: entries, total_entries: total, last_seen: last.id } @@ -57,21 +33,12 @@ defmodule ExplorerWeb.PendingTransactionController do end def index(conn, params) do - query = - from( - transaction in Transaction, - select: transaction.id, - where: - fragment( - "NOT EXISTS (SELECT true FROM receipts WHERE receipts.transaction_id = ?)", - transaction.id - ), - order_by: [desc: transaction.id], - limit: 1 - ) + last_seen = + [pending: true] + |> Chain.last_transaction_id() + |> Kernel.+(1) + |> Integer.to_string() - first_id = Repo.one(query) || 0 - last_seen = Integer.to_string(first_id + 1) index(conn, Map.put(params, "last_seen", last_seen)) end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex index 3dc157877a..f94ea5b190 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/transaction_controller.ex @@ -1,38 +1,23 @@ defmodule ExplorerWeb.TransactionController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction - alias Explorer.Transaction.Service - alias Explorer.Transaction.Service.Query + alias Explorer.Chain + alias Explorer.Chain.Transaction alias ExplorerWeb.TransactionForm - def index(conn, %{"last_seen" => last_seen}) do - query = - Transaction - |> Query.recently_seen(last_seen) - |> Query.include_addresses() - |> Query.require_receipt() - |> Query.require_block() - - total_query = - from( - transaction in Transaction, - order_by: [desc: transaction.id], - limit: 1 - ) - - total = - case Repo.one(total_query) do - nil -> 0 - total -> total.id - end + def index(conn, %{"last_seen" => last_seen_id}) do + total = Chain.transaction_count() entries = - query - |> Repo.all() + last_seen_id + |> Chain.transactions_recently_before_id( + necessity_by_association: %{ + block: :required, + from_address: :optional, + to_address: :optional, + receipt: :required + } + ) |> Enum.map(&TransactionForm.build_and_merge/1) last = List.last(entries) || Transaction.null() @@ -49,38 +34,42 @@ defmodule ExplorerWeb.TransactionController do end def index(conn, params) do - query = - from( - t in Transaction, - select: t.id, - order_by: [desc: t.id], - limit: 1 - ) + last_seen = + Chain.last_transaction_id() + |> Kernel.+(1) + |> Integer.to_string() - first_id = Repo.one(query) || 0 - last_seen = Integer.to_string(first_id + 1) index(conn, Map.put(params, "last_seen", last_seen)) end def show(conn, params) do - transaction = get_transaction(String.downcase(params["id"])) + case Chain.hash_to_transaction( + params["id"], + necessity_by_association: %{ + block: :optional, + from_address: :optional, + to_address: :optional, + receipt: :optional + } + ) do + {:ok, transaction} -> + internal_transactions = + Chain.transaction_hash_to_internal_transactions( + transaction.hash, + necessity_by_association: %{from_address: :required, to_address: :required} + ) - internal_transactions = Service.internal_transactions(transaction.hash) + transaction_form = TransactionForm.build_and_merge(transaction) - render( - conn, - internal_transactions: internal_transactions, - transaction: transaction - ) - end + render( + conn, + "show.html", + internal_transactions: internal_transactions, + transaction: transaction_form + ) - defp get_transaction(hash) do - Transaction - |> Query.by_hash(hash) - |> Query.include_addresses() - |> Query.include_receipt() - |> Query.include_block() - |> Repo.one() - |> TransactionForm.build_and_merge() + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex b/apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex index ce66cd819d..e7fff8f224 100644 --- a/apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex +++ b/apps/explorer_web/lib/explorer_web/controllers/transaction_log_controller.ex @@ -1,41 +1,28 @@ defmodule ExplorerWeb.TransactionLogController do use ExplorerWeb, :controller - import Ecto.Query - - alias Explorer.Log - alias Explorer.Repo.NewRelic, as: Repo - alias Explorer.Transaction - alias Explorer.Transaction.Service.Query + alias Explorer.Chain alias ExplorerWeb.TransactionForm - def index(conn, %{"transaction_id" => transaction_id}) do - transaction_hash = String.downcase(transaction_id) - transaction = get_transaction(transaction_hash) + def index(conn, %{"transaction_id" => transaction_hash} = params) do + case Chain.hash_to_transaction( + transaction_hash, + necessity_by_association: %{from_address: :required, to_address: :required} + ) do + {:ok, transaction} -> + logs = + Chain.transaction_to_logs( + transaction, + necessity_by_association: %{address: :optional}, + pagination: params + ) - logs = - from( - log in Log, - join: transaction in assoc(log, :transaction), - preload: [:address], - where: fragment("lower(?)", transaction.hash) == ^transaction_hash - ) + transaction_form = TransactionForm.build_and_merge(transaction) - render( - conn, - "index.html", - logs: Repo.paginate(logs), - transaction: transaction - ) - end + render(conn, "index.html", logs: logs, transaction: transaction_form) - defp get_transaction(hash) do - Transaction - |> Query.by_hash(hash) - |> Query.include_addresses() - |> Query.include_receipt() - |> Query.include_block() - |> Repo.one() - |> TransactionForm.build_and_merge() + {:error, :not_found} -> + not_found(conn) + end end end diff --git a/apps/explorer_web/lib/explorer_web/forms/block_form.ex b/apps/explorer_web/lib/explorer_web/forms/block_form.ex index 517a1f1dab..7af7f4cbe8 100644 --- a/apps/explorer_web/lib/explorer_web/forms/block_form.ex +++ b/apps/explorer_web/lib/explorer_web/forms/block_form.ex @@ -1,32 +1,17 @@ defmodule ExplorerWeb.BlockForm do @moduledoc false - alias Explorer.Block - alias Explorer.BlockTransaction - alias Explorer.Repo - import Ecto.Query + + alias Explorer.Chain def build(block) do block |> Map.merge(%{ - transactions_count: block |> get_transactions_count, - age: block |> calculate_age, - formatted_timestamp: block |> format_timestamp + age: calculate_age(block), + formatted_timestamp: format_timestamp(block), + transactions_count: Chain.block_to_transaction_count(block) }) end - def get_transactions_count(block) do - query = - from( - block_transaction in BlockTransaction, - join: block in Block, - where: block.id == block_transaction.block_id, - where: block.id == ^block.id, - select: count(block_transaction.block_id) - ) - - Repo.one(query) - end - def calculate_age(block) do block.timestamp |> Timex.from_now() end diff --git a/apps/explorer_web/lib/explorer_web/forms/pending_transaction_form.ex b/apps/explorer_web/lib/explorer_web/forms/pending_transaction_form.ex index 342e41eed4..808882adba 100644 --- a/apps/explorer_web/lib/explorer_web/forms/pending_transaction_form.ex +++ b/apps/explorer_web/lib/explorer_web/forms/pending_transaction_form.ex @@ -3,30 +3,40 @@ defmodule ExplorerWeb.PendingTransactionForm do import ExplorerWeb.Gettext + alias Explorer.Chain.{Address, Transaction} + + # Functions + def build(transaction) do Map.merge(transaction, %{ - to_address_hash: transaction |> to_address_hash, - from_address_hash: transaction |> from_address_hash, - first_seen: transaction |> first_seen, - last_seen: transaction |> last_seen, + first_seen: first_seen(transaction), + formatted_status: gettext("Pending"), + from_address_hash: from_address_hash(transaction), + last_seen: last_seen(transaction), status: :pending, - formatted_status: gettext("Pending") + to_address_hash: to_address_hash(transaction) }) end - def to_address_hash(transaction) do - (transaction.to_address && transaction.to_address.hash) || nil - end - - def from_address_hash(transaction) do - (transaction.to_address && transaction.from_address.hash) || nil - end - def first_seen(transaction) do transaction.inserted_at |> Timex.from_now() 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 transaction.updated_at |> Timex.from_now() end + + def to_address_hash(%Transaction{to_address: to_address}) do + case to_address do + %Address{hash: hash} -> hash + _ -> nil + end + end end diff --git a/apps/explorer_web/lib/explorer_web/forms/transaction_form.ex b/apps/explorer_web/lib/explorer_web/forms/transaction_form.ex index b7344ffe6e..daf25da8a9 100644 --- a/apps/explorer_web/lib/explorer_web/forms/transaction_form.ex +++ b/apps/explorer_web/lib/explorer_web/forms/transaction_form.ex @@ -1,17 +1,15 @@ defmodule ExplorerWeb.TransactionForm do @moduledoc "Format a Block and a Transaction for display." - import Ecto.Query import ExplorerWeb.Gettext alias Cldr.Number - alias Explorer.Block - alias Explorer.Receipt - alias Explorer.Repo + alias Explorer.Chain + alias Explorer.Chain.{Receipt, Transaction} def build(transaction) do - block = (Ecto.assoc_loaded?(transaction.block) && transaction.block) || nil - receipt = Ecto.assoc_loaded?(transaction.receipt) && transaction.receipt + block = block(transaction) + receipt = receipt(transaction) status = status(transaction, receipt || Receipt.null()) %{ @@ -34,47 +32,38 @@ defmodule ExplorerWeb.TransactionForm do Map.merge(transaction, build(transaction)) end - def block_number(block) do - (block && block.number) || "" + def block(%Transaction{block: block}) do + if Ecto.assoc_loaded?(block) do + block + else + nil + end end def block_age(block) do (block && block.timestamp |> Timex.from_now()) || gettext("Pending") end - def format_age(block) do - (block && "#{block_age(block)} (#{format_timestamp(block)})") || gettext("Pending") + def block_number(block) do + (block && block.number) || "" end - def format_timestamp(block) do - (block && block.timestamp |> Timex.format!("%b-%d-%Y %H:%M:%S %p %Z", :strftime)) || - gettext("Pending") + def confirmations(nil), do: 0 + + def confirmations(block) do + Chain.confirmations(block, max_block_number: Chain.max_block_number()) end def cumulative_gas_used(block) do (block && block.gas_used |> Number.to_string!()) || gettext("Pending") end - def to_address_hash(transaction) do - (transaction.to_address && transaction.to_address.hash) || nil - end - - def from_address_hash(transaction) do - (transaction.to_address && transaction.from_address.hash) || nil - end - - def confirmations(block) do - query = from(block in Block, select: max(block.number)) - (block && Repo.one(query) - block.number) || 0 + def first_seen(transaction) do + transaction.inserted_at |> Timex.from_now() end - def status(transaction, receipt) do - %{ - 0 => %{true => :out_of_gas, false => :failed}, - 1 => %{true => :success, false => :success} - } - |> Map.get(receipt.status, %{true: :pending, false: :pending}) - |> Map.get(receipt.gas_used == transaction.gas) + def format_age(block) do + (block && "#{block_age(block)} (#{format_timestamp(block)})") || gettext("Pending") end def format_status(status) do @@ -87,11 +76,37 @@ defmodule ExplorerWeb.TransactionForm do |> Map.fetch!(status) end - def first_seen(transaction) do - transaction.inserted_at |> Timex.from_now() + def format_timestamp(block) do + (block && block.timestamp |> Timex.format!("%b-%d-%Y %H:%M:%S %p %Z", :strftime)) || + gettext("Pending") + end + + def from_address_hash(transaction) do + (transaction.to_address && transaction.from_address.hash) || nil end def last_seen(transaction) do transaction.updated_at |> Timex.from_now() end + + def receipt(%Transaction{receipt: receipt}) do + if Ecto.assoc_loaded?(receipt) do + receipt + else + nil + end + end + + def status(transaction, receipt) do + %{ + 0 => %{true => :out_of_gas, false => :failed}, + 1 => %{true => :success, false => :success} + } + |> Map.get(receipt.status, %{true: :pending, false: :pending}) + |> Map.get(receipt.gas_used == transaction.gas) + end + + def to_address_hash(transaction) do + (transaction.to_address && transaction.to_address.hash) || nil + end end diff --git a/apps/explorer_web/mix.exs b/apps/explorer_web/mix.exs index be5677f6eb..3b977f193d 100644 --- a/apps/explorer_web/mix.exs +++ b/apps/explorer_web/mix.exs @@ -72,7 +72,7 @@ defmodule ExplorerWeb.Mixfile do defp deps do [ {:cowboy, "~> 1.0"}, - {:credo, "~> 0.8", only: [:dev, :test], runtime: false}, + {:credo, "0.9.1", only: [:dev, :test], runtime: false}, {:crontab, "~> 1.1"}, {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, {:ex_cldr_numbers, "~> 1.0"}, @@ -116,6 +116,7 @@ defmodule ExplorerWeb.Mixfile do # See the documentation for `Mix` for more info on aliases. defp aliases do [ + compile: "compile --warnings-as-errors", "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], test: ["ecto.create --quiet", "ecto.migrate", "test"] diff --git a/apps/explorer_web/test/explorer_web/chain_test.exs b/apps/explorer_web/test/explorer_web/chain_test.exs new file mode 100644 index 0000000000..88a0d14d25 --- /dev/null +++ b/apps/explorer_web/test/explorer_web/chain_test.exs @@ -0,0 +1,42 @@ +defmodule ExplorerWeb.ChainTest do + use Explorer.DataCase + + alias Explorer.Chain.{Address, Block, Transaction} + alias ExplorerWeb.Chain + + describe "from_param/1" do + test "finds a block by block number with a valid block number" do + %Block{number: number} = insert(:block, number: 37) + + assert {:ok, %Block{number: ^number}} = + number + |> to_string() + |> Chain.from_param() + end + + test "finds a transaction by hash" do + %Transaction{hash: hash} = insert(:transaction) + + assert {:ok, %Transaction{hash: ^hash}} = Chain.from_param(hash) + end + + test "finds an address by hash" do + %Address{hash: hash} = insert(:address) + + assert {:ok, %Address{hash: ^hash}} = Chain.from_param(hash) + end + + test "returns {:error, :not_found} when garbage is passed in" do + assert {:error, :not_found} = Chain.from_param("any ol' thing") + end + + test "returns {:error, :not_found} 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 {:error, :not_found} = Chain.from_param("38999") + assert {:error, :not_found} = Chain.from_param(transaction_hash) + assert {:error, :not_found} = Chain.from_param(address_hash) + end + end +end diff --git a/apps/explorer_web/test/explorer_web/controllers/address_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/address_controller_test.exs index 645ca14cc1..c85deddc6c 100644 --- a/apps/explorer_web/test/explorer_web/controllers/address_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/address_controller_test.exs @@ -1,15 +1,22 @@ defmodule ExplorerWeb.AddressControllerTest do use ExplorerWeb.ConnCase - alias Explorer.Credit - alias Explorer.Debit + alias Explorer.Chain.{Credit, Debit} describe "GET show/3" do - test "returns an address", %{conn: conn} do + test "without address returns not found", %{conn: conn} do + conn = get(conn, "/en/addresses/unknown") + + assert html_response(conn, 404) + end + + test "with address returns an address", %{conn: conn} do address = insert(:address, hash: "0x9") Credit.refresh() Debit.refresh() + conn = get(conn, "/en/addresses/0x9") + assert conn.assigns.address.id == address.id end end diff --git a/apps/explorer_web/test/explorer_web/controllers/address_transaction_from_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/address_transaction_from_controller_test.exs index 3ba802c75b..70672c33a3 100644 --- a/apps/explorer_web/test/explorer_web/controllers/address_transaction_from_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/address_transaction_from_controller_test.exs @@ -4,6 +4,12 @@ defmodule ExplorerWeb.AddressTransactionFromControllerTest do import ExplorerWeb.Router.Helpers, only: [address_transaction_from_path: 4] describe "GET index/2" do + test "without address", %{conn: conn} do + conn = get(conn, address_transaction_from_path(conn, :index, :en, "unknown")) + + assert html_response(conn, 404) + end + test "returns transactions from this address", %{conn: conn} do address = insert(:address) transaction = insert(:transaction, hash: "0xsnacks", from_address_id: address.id) diff --git a/apps/explorer_web/test/explorer_web/controllers/address_transaction_to_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/address_transaction_to_controller_test.exs index e0bf7c9657..e66c797631 100644 --- a/apps/explorer_web/test/explorer_web/controllers/address_transaction_to_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/address_transaction_to_controller_test.exs @@ -4,6 +4,12 @@ defmodule ExplorerWeb.AddressTransactionToControllerTest do import ExplorerWeb.Router.Helpers, only: [address_transaction_to_path: 4] describe "GET index/2" do + test "without address", %{conn: conn} do + conn = get(conn, address_transaction_to_path(conn, :index, :en, "unknown")) + + assert html_response(conn, 404) + end + test "returns transactions to this address", %{conn: conn} do address = insert(:address) transaction = insert(:transaction, hash: "0xsnacks", to_address_id: address.id) diff --git a/apps/explorer_web/test/explorer_web/controllers/block_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/block_controller_test.exs index 548a270757..d92f81c120 100644 --- a/apps/explorer_web/test/explorer_web/controllers/block_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/block_controller_test.exs @@ -2,7 +2,13 @@ defmodule ExplorerWeb.BlockControllerTest do use ExplorerWeb.ConnCase describe "GET show/2" do - test "returns a block", %{conn: conn} do + test "without block", %{conn: conn} do + conn = get(conn, "/en/blocks/3") + + assert html_response(conn, 404) + end + + test "with block returns a block", %{conn: conn} do block = insert(:block, number: 3) conn = get(conn, "/en/blocks/3") assert conn.assigns.block.id == block.id diff --git a/apps/explorer_web/test/explorer_web/controllers/block_transaction_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/block_transaction_controller_test.exs index 61e9e20c05..1812309769 100644 --- a/apps/explorer_web/test/explorer_web/controllers/block_transaction_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/block_transaction_controller_test.exs @@ -4,6 +4,18 @@ defmodule ExplorerWeb.BlockTransactionControllerTest do import ExplorerWeb.Router.Helpers, only: [block_transaction_path: 4] describe "GET index/2" do + test "with invalid block number", %{conn: conn} do + conn = get(conn, block_transaction_path(conn, :index, :en, "unknown")) + + assert html_response(conn, 404) + end + + test "with valid block number without block", %{conn: conn} do + conn = get(conn, block_transaction_path(conn, :index, :en, "1")) + + assert html_response(conn, 404) + end + test "returns transactions for the block", %{conn: conn} do transaction = insert(:transaction, hash: "0xsnacks") insert(:receipt, transaction: transaction) diff --git a/apps/explorer_web/test/explorer_web/controllers/transaction_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/transaction_controller_test.exs index 0b7d5e5bf7..d04cfde3d0 100644 --- a/apps/explorer_web/test/explorer_web/controllers/transaction_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/transaction_controller_test.exs @@ -57,6 +57,12 @@ defmodule ExplorerWeb.TransactionControllerTest do end describe "GET show/3" do + test "without transaction", %{conn: conn} do + conn = get(conn, "/en/transactions/0x1") + + assert html_response(conn, 404) + end + test "when there is an associated block, it returns a transaction with block data", %{ conn: conn } do diff --git a/apps/explorer_web/test/explorer_web/controllers/transaction_log_controller_test.exs b/apps/explorer_web/test/explorer_web/controllers/transaction_log_controller_test.exs index ee94254538..f3f19d31a1 100644 --- a/apps/explorer_web/test/explorer_web/controllers/transaction_log_controller_test.exs +++ b/apps/explorer_web/test/explorer_web/controllers/transaction_log_controller_test.exs @@ -4,11 +4,17 @@ defmodule ExplorerWeb.TransactionLogControllerTest do import ExplorerWeb.Router.Helpers, only: [transaction_log_path: 4] describe "GET index/2" do + test "without transaction", %{conn: conn} do + conn = get(conn, transaction_log_path(conn, :index, :en, "unknown")) + + assert html_response(conn, 404) + end + test "returns logs for the transaction", %{conn: conn} do transaction = insert(:transaction) receipt = insert(:receipt, transaction: transaction) address = insert(:address) - insert(:log, receipt: receipt, address: address) + insert(:log, receipt: receipt, address_id: address.id) path = transaction_log_path(ExplorerWeb.Endpoint, :index, :en, transaction.hash) conn = get(conn, path) diff --git a/apps/explorer_web/test/explorer_web/features/contributor_browsing_test.exs b/apps/explorer_web/test/explorer_web/features/contributor_browsing_test.exs index 9f0a23a00c..ecedbe152b 100644 --- a/apps/explorer_web/test/explorer_web/features/contributor_browsing_test.exs +++ b/apps/explorer_web/test/explorer_web/features/contributor_browsing_test.exs @@ -3,8 +3,7 @@ defmodule ExplorerWeb.UserListTest do import Wallaby.Query, only: [css: 1, css: 2, link: 1] - alias Explorer.Credit - alias Explorer.Debit + alias Explorer.Chain.{Credit, Debit} @logo css("img.header__logo") @@ -138,7 +137,7 @@ defmodule ExplorerWeb.UserListTest do insert(:block_transaction, block: block, transaction: transaction) receipt = insert(:receipt, transaction: transaction, status: 1) - insert(:log, address: lincoln, receipt: receipt) + insert(:log, address_id: lincoln.id, receipt: receipt) # From Lincoln to Taft. txn_from_lincoln = diff --git a/mix.exs b/mix.exs index 0ecff97879..a7c7592385 100644 --- a/mix.exs +++ b/mix.exs @@ -1,8 +1,11 @@ defmodule ExplorerUmbrella.Mixfile do use Mix.Project + # Functions + def project do [ + aliases: aliases(), apps_path: "apps", deps: deps(), dialyzer: [ @@ -22,6 +25,14 @@ defmodule ExplorerUmbrella.Mixfile do ] end + ## Private Functions + + defp aliases do + [ + compile: "compile --warnings-as-errors" + ] + end + # Dependencies can be Hex packages: # # {:mydep, "~> 0.3.0"} @@ -36,6 +47,8 @@ defmodule ExplorerUmbrella.Mixfile do # and cannot be accessed from applications inside the apps folder defp deps do [ + # Documentation + {:ex_doc, "~> 0.18.3", only: [:dev]}, # Code coverage {:excoveralls, "~> 0.8.1", only: [:test]} ] diff --git a/mix.lock b/mix.lock index c8464428fd..0638e7bbff 100644 --- a/mix.lock +++ b/mix.lock @@ -7,16 +7,18 @@ "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"}, - "credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "credo": {:hex, :credo, "0.9.1", "f021affa11b32a94dc2e807a6472ce0914289c9132f99644a97fc84432b202a1", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.2", "4784a50987b4a19af07a908f98e8a308b00f9c93efc5a7892155dc10cd8fc7d9", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.4.1", "ad9e501edf7322f122f7fc151cce7c2a0c9ada96f2b0155b8a09a795c2029770", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"}, "ecto": {:hex, :ecto, "2.2.8", "a4463c0928b970f2cee722cd29aaac154e866a15882c5737e0038bbfcf03ec2c", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "ethereumex": {:hex, :ethereumex, "0.3.0", "9d5e25dc2f9ed357f9f7f2a49e09b608b41a56283c20f50e896a11af225cc723", [:mix], [{:httpoison, "~> 1.0.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:poison, "~> 3.1.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ex_cldr": {:hex, :ex_cldr, "1.3.2", "8f4a00c99d1c537b8e8db7e7903f4bd78d82a7289502d080f70365392b13921b", [:mix], [{:abnf2, "~> 0.1", [hex: :abnf2, repo: "hexpm", optional: false]}, {:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.13", [hex: :gettext, repo: "hexpm", optional: true]}, {:poison, "~> 2.1 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "1.2.0", "ef27299922da913ffad1ed296cacf28b6452fc1243b77301dc17c03276c6ee34", [:mix], [{:decimal, "~> 1.4", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 1.3", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:poison, "~> 2.1 or ~> 3.1", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "ex_cldr_units": {:hex, :ex_cldr_units, "1.1.1", "b3c7256709bdeb3740a5f64ce2bce659eb9cf4cc1afb4cf94aba033b4a18bc5f", [:mix], [{:ex_cldr, "~> 1.0", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 1.0", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}], "hexpm"}, + "ex_doc": {:hex, :ex_doc, "0.18.3", "f4b0e4a2ec6f333dccf761838a4b253d75e11f714b85ae271c9ae361367897b7", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}, "ex_jasmine": {:git, "https://github.com/minifast/ex_jasmine.git", "f2c906e36b469a9bf0891c23cd72135ba6a257f1", [branch: "master"]}, "ex_machina": {:hex, :ex_machina, "2.1.0", "4874dc9c78e7cf2d429f24dc3c4005674d4e4da6a08be961ffccc08fb528e28b", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "exactor": {:hex, :exactor, "2.2.4", "5efb4ddeb2c48d9a1d7c9b465a6fffdd82300eb9618ece5d34c3334d5d7245b1", [:mix], [], "hexpm"},