diff --git a/.credo.exs b/.credo.exs index 7cae86277c..bafe1de1ef 100644 --- a/.credo.exs +++ b/.credo.exs @@ -76,7 +76,7 @@ # {Credo.Check.Design.AliasUsage, 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}, # For some checks, you can also set other parameters diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index a388a0a07a..7611a86f19 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -16,6 +16,7 @@ defmodule Explorer.Chain do ] alias Ecto.Adapters.SQL + alias Ecto.Multi alias Explorer.Chain.{ Address, @@ -1352,10 +1353,53 @@ defmodule Explorer.Chain do |> Data.to_string() 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 - %SmartContract{} - |> SmartContract.changeset(attrs) - |> Repo.insert() + smart_contract_changeset = SmartContract.changeset(%SmartContract{}, attrs) + + 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 @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() |> Repo.all() 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 diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 0385fca71c..56a9c898de 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Address do use Explorer.Schema 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 @required_attrs ~w(hash)a @@ -23,6 +23,7 @@ defmodule Explorer.Chain.Address do which `fetched_balance` was fetched * `hash` - the hash of the address's public key * `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 * `updated_at` when this address was last updated """ @@ -31,6 +32,7 @@ defmodule Explorer.Chain.Address do fetched_balance_block_number: Block.block_number(), hash: Hash.Address.t(), contract_code: Data.t() | nil, + names: %Ecto.Association.NotLoaded{} | [Address.Name.t()], inserted_at: DateTime.t(), updated_at: DateTime.t() } @@ -50,6 +52,8 @@ defmodule Explorer.Chain.Address do foreign_key: :created_contract_address_hash ) + has_many(:names, Address.Name, foreign_key: :address_hash) + timestamps() end diff --git a/apps/explorer/lib/explorer/chain/address/name.ex b/apps/explorer/lib/explorer/chain/address/name.ex new file mode 100644 index 0000000000..7a22f2bb27 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/address/name.ex @@ -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 diff --git a/apps/explorer/priv/repo/migrations/20180821142139_create_address_names.exs b/apps/explorer/priv/repo/migrations/20180821142139_create_address_names.exs new file mode 100644 index 0000000000..d501bee198 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20180821142139_create_address_names.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 037b9c01cc..6affaaf5d2 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -1133,7 +1133,7 @@ defmodule Explorer.ChainTest do end describe "create_smart_contract/1" do - test "with valid data creates a smart contract" do + setup do smart_contract_bytecode = "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 smart_contract.name == "SimpleStorage" assert smart_contract.compiler_version == "0.4.23" assert smart_contract.optimization == false assert smart_contract.contract_source_code != "" 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 @@ -1727,4 +1748,51 @@ defmodule Explorer.ChainTest do assert expected_tokens == [token.name] 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 diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index b25b642b1d..e646d01830 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -33,6 +33,13 @@ defmodule Explorer.Factory do } end + def address_name_factory do + %Address.Name{ + address: build(:address), + name: "FooContract" + } + end + def unfetched_balance_factory do %Balance{ address_hash: address_hash(), diff --git a/apps/indexer/lib/indexer/token_fetcher.ex b/apps/indexer/lib/indexer/token_fetcher.ex index 82efdb169a..a480f79df6 100644 --- a/apps/indexer/lib/indexer/token_fetcher.ex +++ b/apps/indexer/lib/indexer/token_fetcher.ex @@ -139,7 +139,7 @@ defmodule Indexer.TokenFetcher do 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 end