Add support for named addresses (#590)

pull/595/head
Alex Garibay 6 years ago committed by GitHub
parent 050870f8b4
commit cade255e15
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .credo.exs
  2. 83
      apps/explorer/lib/explorer/chain.ex
  3. 6
      apps/explorer/lib/explorer/chain/address.ex
  4. 42
      apps/explorer/lib/explorer/chain/address/name.ex
  5. 33
      apps/explorer/priv/repo/migrations/20180821142139_create_address_names.exs
  6. 70
      apps/explorer/test/explorer/chain_test.exs
  7. 7
      apps/explorer/test/support/factory.ex
  8. 2
      apps/indexer/lib/indexer/token_fetcher.ex

@ -76,7 +76,7 @@
# #
{Credo.Check.Design.AliasUsage, {Credo.Check.Design.AliasUsage,
excluded_namespaces: ~w(Socket Task), excluded_namespaces: ~w(Socket Task),
excluded_lastnames: ~w(Address DateTime Full Number Repo Time Unit), excluded_lastnames: ~w(Address DateTime Full Name Number Repo Time Unit),
priority: :low}, priority: :low},
# For some checks, you can also set other parameters # For some checks, you can also set other parameters

@ -16,6 +16,7 @@ defmodule Explorer.Chain do
] ]
alias Ecto.Adapters.SQL alias Ecto.Adapters.SQL
alias Ecto.Multi
alias Explorer.Chain.{ alias Explorer.Chain.{
Address, Address,
@ -1352,10 +1353,53 @@ defmodule Explorer.Chain do
|> Data.to_string() |> Data.to_string()
end end
@doc """
Inserts a `t:SmartContract.t/0`.
As part of inserting a new smart contract, an additional record is inserted for
naming the address for reference.
"""
@spec create_smart_contract(map()) :: {:ok, SmartContract.t()} | {:error, Ecto.Changeset.t()}
def create_smart_contract(attrs \\ %{}) do def create_smart_contract(attrs \\ %{}) do
%SmartContract{} smart_contract_changeset = SmartContract.changeset(%SmartContract{}, attrs)
|> SmartContract.changeset(attrs)
|> Repo.insert() insert_result =
Multi.new()
|> Multi.insert(:smart_contract, smart_contract_changeset)
|> Multi.run(:clear_primary_address_names, &clear_primary_address_names/1)
|> Multi.run(:insert_address_name, &create_address_name/1)
|> Repo.transaction()
with {:ok, %{smart_contract: smart_contract}} <- insert_result do
{:ok, smart_contract}
else
{:error, :smart_contract, changeset, _} ->
{:error, changeset}
end
end
defp clear_primary_address_names(%{smart_contract: %SmartContract{address_hash: address_hash}}) do
clear_primary_query =
from(address_name in Address.Name,
where: address_name.address_hash == ^address_hash,
update: [set: [primary: false]]
)
Repo.update_all(clear_primary_query, [])
{:ok, []}
end
defp create_address_name(%{smart_contract: %SmartContract{name: name, address_hash: address_hash}}) do
params = %{
address_hash: address_hash,
name: name,
primary: true
}
%Address.Name{}
|> Address.Name.changeset(params)
|> Repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name])
end end
@spec address_hash_to_smart_contract(%Explorer.Chain.Hash{}) :: %Explorer.Chain.SmartContract{} @spec address_hash_to_smart_contract(%Explorer.Chain.Hash{}) :: %Explorer.Chain.SmartContract{}
@ -1582,4 +1626,37 @@ defmodule Explorer.Chain do
|> Token.with_transfers_by_address() |> Token.with_transfers_by_address()
|> Repo.all() |> Repo.all()
end end
@doc """
Update a new `t:Token.t/0` record.
As part of updating token, an additional record is inserted for
naming the address for reference if a name is provided for a token.
"""
@spec update_token(Token.t(), map()) :: {:ok, Token.t()} | {:error, Ecto.Changeset.t()}
def update_token(%Token{contract_address_hash: address_hash} = token, params \\ %{}) do
token_changeset = Token.changeset(token, params)
address_name_changeset = Address.Name.changeset(%Address.Name{}, Map.put(params, :address_hash, address_hash))
token_opts = [on_conflict: :replace_all, conflict_target: :contract_address_hash]
address_name_opts = [on_conflict: :nothing, conflict_target: [:address_hash, :name]]
insert_result =
Multi.new()
|> Multi.insert(:token, token_changeset, token_opts)
|> Multi.run(
:address_name,
fn _ ->
{:ok, Repo.insert(address_name_changeset, address_name_opts)}
end
)
|> Repo.transaction()
with {:ok, %{token: token}} <- insert_result do
{:ok, token}
else
{:error, :token, changeset, _} ->
{:error, changeset}
end
end
end end

@ -6,7 +6,7 @@ defmodule Explorer.Chain.Address do
use Explorer.Schema use Explorer.Schema
alias Ecto.Changeset alias Ecto.Changeset
alias Explorer.Chain.{Block, Data, Hash, Wei, SmartContract, InternalTransaction, Token} alias Explorer.Chain.{Address, Block, Data, Hash, Wei, SmartContract, InternalTransaction, Token}
@optional_attrs ~w(contract_code fetched_balance fetched_balance_block_number)a @optional_attrs ~w(contract_code fetched_balance fetched_balance_block_number)a
@required_attrs ~w(hash)a @required_attrs ~w(hash)a
@ -23,6 +23,7 @@ defmodule Explorer.Chain.Address do
which `fetched_balance` was fetched which `fetched_balance` was fetched
* `hash` - the hash of the address's public key * `hash` - the hash of the address's public key
* `contract_code` - the code of the contract when an Address is a contract * `contract_code` - the code of the contract when an Address is a contract
* `names` - names known for the address
* `inserted_at` - when this address was inserted * `inserted_at` - when this address was inserted
* `updated_at` when this address was last updated * `updated_at` when this address was last updated
""" """
@ -31,6 +32,7 @@ defmodule Explorer.Chain.Address do
fetched_balance_block_number: Block.block_number(), fetched_balance_block_number: Block.block_number(),
hash: Hash.Address.t(), hash: Hash.Address.t(),
contract_code: Data.t() | nil, contract_code: Data.t() | nil,
names: %Ecto.Association.NotLoaded{} | [Address.Name.t()],
inserted_at: DateTime.t(), inserted_at: DateTime.t(),
updated_at: DateTime.t() updated_at: DateTime.t()
} }
@ -50,6 +52,8 @@ defmodule Explorer.Chain.Address do
foreign_key: :created_contract_address_hash foreign_key: :created_contract_address_hash
) )
has_many(:names, Address.Name, foreign_key: :address_hash)
timestamps() timestamps()
end end

@ -0,0 +1,42 @@
defmodule Explorer.Chain.Address.Name do
@moduledoc """
Represents a name for an Address.
"""
use Explorer.Schema
alias Explorer.Chain.{Address, Hash}
@typedoc """
* `address` - the `t:Explorer.Chain.Address.t/0` with `value` at end of `block_number`.
* `address_hash` - foreign key for `address`.
* `name` - name for the address
* `primary` - flag for if the name is the primary name for the address
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Address.t(),
name: String.t(),
primary: boolean()
}
@primary_key false
schema "address_names" do
field(:name, :string)
field(:primary, :boolean)
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
timestamps()
end
@required_fields ~w(address_hash name)a
@optional_fields ~w(primary)a
@allowed_fields @required_fields ++ @optional_fields
def changeset(%__MODULE__{} = struct, params \\ %{}) do
struct
|> cast(params, @allowed_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:address_hash)
end
end

@ -0,0 +1,33 @@
defmodule Explorer.Repo.Migrations.CreateAddressNames do
use Ecto.Migration
def change do
create table(:address_names, primary_key: false) do
add(:address_hash, :bytea, null: false)
add(:name, :string, null: false)
add(:primary, :boolean, null: false, default: false)
timestamps()
end
# Only 1 primary per address
create(unique_index(:address_names, [:address_hash], where: ~s|"primary" = true|))
# No duplicate names per address
create(unique_index(:address_names, [:address_hash, :name], name: :unique_address_names))
insert_names_from_existing_data_query = """
INSERT INTO address_names (address_hash, name, "primary", inserted_at, updated_at)
(
SELECT address_hash, name, true, NOW(), NOW()
FROM smart_contracts WHERE name IS NOT NULL
UNION
SELECT contract_address_hash, name, false, NOW(), NOW()
FROM tokens WHERE name IS NOT NULL
);
"""
execute(insert_names_from_existing_data_query)
end
end

@ -1133,7 +1133,7 @@ defmodule Explorer.ChainTest do
end end
describe "create_smart_contract/1" do describe "create_smart_contract/1" do
test "with valid data creates a smart contract" do setup do
smart_contract_bytecode = smart_contract_bytecode =
"0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029"
@ -1186,12 +1186,33 @@ defmodule Explorer.ChainTest do
] ]
} }
{:ok, valid_attrs: valid_attrs, address: created_contract_address}
end
test "with valid data creates a smart contract", %{valid_attrs: valid_attrs} do
assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs) assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs)
assert smart_contract.name == "SimpleStorage" assert smart_contract.name == "SimpleStorage"
assert smart_contract.compiler_version == "0.4.23" assert smart_contract.compiler_version == "0.4.23"
assert smart_contract.optimization == false assert smart_contract.optimization == false
assert smart_contract.contract_source_code != "" assert smart_contract.contract_source_code != ""
assert smart_contract.abi != "" assert smart_contract.abi != ""
assert Repo.get_by(Address.Name,
address_hash: smart_contract.address_hash,
name: smart_contract.name,
primary: true
)
end
test "clears an existing primary name and sets the new one", %{valid_attrs: valid_attrs, address: address} do
insert(:address_name, address: address, primary: true)
assert {:ok, %SmartContract{} = smart_contract} = Chain.create_smart_contract(valid_attrs)
assert Repo.get_by(Address.Name,
address_hash: smart_contract.address_hash,
name: smart_contract.name,
primary: true
)
end end
end end
@ -1727,4 +1748,51 @@ defmodule Explorer.ChainTest do
assert expected_tokens == [token.name] assert expected_tokens == [token.name]
end end
end end
describe "update_token/2" do
test "updates a token's values" do
token = insert(:token, name: nil, symbol: nil, total_supply: nil, decimals: nil, cataloged: false)
update_params = %{
name: "Hodl Token",
symbol: "HT",
total_supply: 10,
decimals: 1,
cataloged: true
}
assert {:ok, updated_token} = Chain.update_token(token, update_params)
assert updated_token.name == update_params.name
assert updated_token.symbol == update_params.symbol
assert updated_token.total_supply == Decimal.new(update_params.total_supply)
assert updated_token.decimals == update_params.decimals
assert updated_token.cataloged
end
end
test "inserts an address name record when token has a name in params" do
token = insert(:token, name: nil, symbol: nil, total_supply: nil, decimals: nil, cataloged: false)
update_params = %{
name: "Hodl Token",
symbol: "HT",
total_supply: 10,
decimals: 1,
cataloged: true
}
Chain.update_token(token, update_params)
assert Repo.get_by(Address.Name, name: update_params.name, address_hash: token.contract_address_hash)
end
test "does not insert address name record when token doesn't have name in params" do
token = insert(:token, name: nil, symbol: nil, total_supply: nil, decimals: nil, cataloged: false)
update_params = %{
cataloged: true
}
Chain.update_token(token, update_params)
refute Repo.get_by(Address.Name, address_hash: token.contract_address_hash)
end
end end

@ -33,6 +33,13 @@ defmodule Explorer.Factory do
} }
end end
def address_name_factory do
%Address.Name{
address: build(:address),
name: "FooContract"
}
end
def unfetched_balance_factory do def unfetched_balance_factory do
%Balance{ %Balance{
address_hash: address_hash(), address_hash: address_hash(),

@ -139,7 +139,7 @@ defmodule Indexer.TokenFetcher do
token_params = format_token_params(token, token_contract_results) token_params = format_token_params(token, token_contract_results)
{:ok, %{tokens: [_]}} = Chain.import(%{tokens: %{params: [token_params], on_conflict: :replace_all}}) {:ok, _} = Chain.update_token(token, token_params)
:ok :ok
end end

Loading…
Cancel
Save