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 |
defmodule ExplorerWeb.AddressController do |
||||||
use ExplorerWeb, :controller |
use ExplorerWeb, :controller |
||||||
|
|
||||||
import Ecto.Query |
alias Explorer.Address.Service, as: Address |
||||||
|
|
||||||
alias Explorer.Address |
|
||||||
alias Explorer.AddressForm |
|
||||||
alias Explorer.Repo.NewRelic, as: Repo |
|
||||||
|
|
||||||
def show(conn, %{"id" => id}) do |
def show(conn, %{"id" => id}) do |
||||||
hash = String.downcase(id) |
address = id |> Address.by_hash() |
||||||
|
render(conn, "show.html", address: address) |
||||||
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)) |
|
||||||
end |
end |
||||||
end |
end |
||||||
|
@ -1,4 +1,13 @@ |
|||||||
defmodule ExplorerWeb.AddressView do |
defmodule ExplorerWeb.AddressView do |
||||||
use ExplorerWeb, :view |
use ExplorerWeb, :view |
||||||
@dialyzer :no_match |
@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 |
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