parent
c82d7ea79a
commit
18e0d9afde
@ -0,0 +1,19 @@ |
||||
defmodule Explorer.Ethereum do |
||||
@client Application.get_env(:explorer, :ethereum)[:backend] |
||||
|
||||
defmodule API do |
||||
@moduledoc false |
||||
@callback download_balance(String.t()) :: String.t() |
||||
end |
||||
|
||||
defdelegate download_balance(hash), to: @client |
||||
|
||||
def decode_integer_field(hex) do |
||||
{"0x", base_16} = String.split_at(hex, 2) |
||||
String.to_integer(base_16, 16) |
||||
end |
||||
|
||||
def decode_time_field(field) do |
||||
field |> decode_integer_field() |> Timex.from_unix() |
||||
end |
||||
end |
@ -0,0 +1,14 @@ |
||||
defmodule Explorer.Ethereum.Live do |
||||
@moduledoc """ |
||||
An implementation for Ethereum that uses the actual node. |
||||
""" |
||||
|
||||
@behaviour Explorer.Ethereum.API |
||||
|
||||
import Ethereumex.HttpClient, only: [eth_get_balance: 1] |
||||
|
||||
def download_balance(hash) do |
||||
{:ok, result} = eth_get_balance(hash) |
||||
result |
||||
end |
||||
end |
@ -0,0 +1,9 @@ |
||||
defmodule Explorer.Ethereum.Test do |
||||
@moduledoc """ |
||||
An interface for the Ethereum node that does not hit the network |
||||
""" |
||||
@behaviour Explorer.Ethereum.API |
||||
def download_balance(_hash) do |
||||
"0x15d231fca629c7c0" |
||||
end |
||||
end |
@ -1,13 +0,0 @@ |
||||
defmodule Explorer.AddressForm do |
||||
@moduledoc false |
||||
|
||||
alias Explorer.Credit |
||||
alias Explorer.Debit |
||||
|
||||
def build(address) do |
||||
credit = address.credit || Credit.null() |
||||
debit = address.debit || Debit.null() |
||||
balance = Decimal.sub(credit.value, debit.value) |
||||
Map.put(address, :balance, balance) |
||||
end |
||||
end |
@ -0,0 +1,18 @@ |
||||
defmodule Explorer.BalanceImporter do |
||||
@moduledoc "Imports a balance for a given address." |
||||
|
||||
alias Explorer.Address.Service, as: Address |
||||
alias Explorer.Ethereum |
||||
|
||||
def import(hash) do |
||||
hash |
||||
|> Ethereum.download_balance() |
||||
|> persist_balance(hash) |
||||
end |
||||
|
||||
defp persist_balance(balance, hash) do |
||||
balance |
||||
|> Ethereum.decode_integer_field() |
||||
|> Address.update_balance(hash) |
||||
end |
||||
end |
@ -0,0 +1,58 @@ |
||||
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 |
@ -0,0 +1,13 @@ |
||||
defmodule Explorer.Workers.ImportBalance do |
||||
@moduledoc "A worker that imports the balance for a given address." |
||||
|
||||
alias Explorer.BalanceImporter |
||||
|
||||
def perform(hash) do |
||||
BalanceImporter.import(hash) |
||||
end |
||||
|
||||
def perform_later(hash) do |
||||
Exq.enqueue(Exq.Enqueuer, "balances", __MODULE__, [hash]) |
||||
end |
||||
end |
@ -1,24 +1,10 @@ |
||||
defmodule ExplorerWeb.AddressController do |
||||
use ExplorerWeb, :controller |
||||
|
||||
import Ecto.Query |
||||
|
||||
alias Explorer.Address |
||||
alias Explorer.AddressForm |
||||
alias Explorer.Repo.NewRelic, as: Repo |
||||
alias Explorer.Address.Service, as: Address |
||||
|
||||
def show(conn, %{"id" => id}) do |
||||
hash = String.downcase(id) |
||||
|
||||
query = |
||||
from( |
||||
address in Address, |
||||
where: fragment("lower(?)", address.hash) == ^hash, |
||||
preload: [:credit, :debit], |
||||
limit: 1 |
||||
) |
||||
|
||||
address = Repo.one(query) |
||||
render(conn, "show.html", address: AddressForm.build(address)) |
||||
address = id |> Address.by_hash() |
||||
render(conn, "show.html", address: address) |
||||
end |
||||
end |
||||
|
@ -1,4 +1,13 @@ |
||||
defmodule ExplorerWeb.AddressView do |
||||
use ExplorerWeb, :view |
||||
@dialyzer :no_match |
||||
|
||||
def format_balance(nil), do: "0" |
||||
|
||||
def format_balance(balance) do |
||||
balance |
||||
|> Decimal.new() |
||||
|> Decimal.div(Decimal.new(1_000_000_000_000_000_000)) |
||||
|> Decimal.to_string(:normal) |
||||
end |
||||
end |
||||
|
@ -0,0 +1,10 @@ |
||||
defmodule Explorer.Repo.Migrations.AddBalanceAndBalanceUpdatedAtToAddress do |
||||
use Ecto.Migration |
||||
|
||||
def change do |
||||
alter table(:addresses) do |
||||
add :balance, :numeric, precision: 100 |
||||
add :balance_updated_at, :utc_datetime |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,18 @@ |
||||
defmodule Explorer.EthereumTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Ethereum |
||||
|
||||
describe "decode_integer_field/1" do |
||||
test "returns the integer value of a hex value" do |
||||
assert(Ethereum.decode_integer_field("0x7f2fb") == 520_955) |
||||
end |
||||
end |
||||
|
||||
describe "decode_time_field/1" do |
||||
test "returns the date value of a hex value" do |
||||
the_seventies = Timex.parse!("1970-01-01T00:00:18-00:00", "{ISO:Extended}") |
||||
assert(Ethereum.decode_time_field("0x12") == the_seventies) |
||||
end |
||||
end |
||||
end |
@ -1,31 +0,0 @@ |
||||
defmodule Explorer.AddressFormTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.AddressForm |
||||
alias Explorer.Credit |
||||
alias Explorer.Debit |
||||
|
||||
describe "build/1" do |
||||
test "returns a balance" do |
||||
recipient = insert(:address) |
||||
transaction = insert(:transaction, value: 10, to_address_id: recipient.id) |
||||
block = insert(:block) |
||||
insert(:block_transaction, block: block, transaction: transaction) |
||||
insert(:receipt, transaction: transaction, status: 1) |
||||
|
||||
Credit.refresh() |
||||
Debit.refresh() |
||||
|
||||
assert AddressForm.build(Repo.preload(recipient, [:debit, :credit])).balance == |
||||
Decimal.new(10) |
||||
end |
||||
|
||||
test "returns a zero balance when the address does not have balances" do |
||||
address = insert(:address, %{hash: "bert"}) |
||||
insert(:transaction, value: 5, to_address_id: address.id) |
||||
insert(:transaction, value: 5, to_address_id: address.id) |
||||
|
||||
assert AddressForm.build(Repo.preload(address, [:debit, :credit])).balance == Decimal.new(0) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,32 @@ |
||||
defmodule Explorer.BalanceImporterTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Explorer.Address.Service, as: Address |
||||
alias Explorer.BalanceImporter |
||||
|
||||
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) |
||||
end |
||||
|
||||
test "it updates the balance update time for an address" do |
||||
insert( |
||||
:address, |
||||
hash: "0x5cc18cc34175d358ff8e19b7f98566263c4106a0", |
||||
balance_updated_at: nil |
||||
) |
||||
|
||||
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") |
||||
address = Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") |
||||
refute is_nil(address.balance_updated_at) |
||||
end |
||||
|
||||
test "it creates an address if one does not exist" do |
||||
BalanceImporter.import("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") |
||||
assert Address.by_hash("0x5cc18cc34175d358ff8e19b7f98566263c4106a0") |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,56 @@ |
||||
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 |
@ -0,0 +1,33 @@ |
||||
defmodule Explorer.Workers.ImportBalanceTest do |
||||
import Mock |
||||
|
||||
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) |
||||
end |
||||
end |
||||
|
||||
describe "perform_later/1" do |
||||
test "delays the import of the balance for an address" do |
||||
with_mock Exq, |
||||
enqueue: fn _, _, _, _ -> |
||||
insert( |
||||
:address, |
||||
hash: "0xskateboards", |
||||
balance: 66 |
||||
) |
||||
end do |
||||
ImportBalance.perform_later("0xskateboards") |
||||
address = Address.by_hash("0xskateboards") |
||||
assert address.balance == Decimal.new(66) |
||||
end |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue