diff --git a/lib/archethic/contracts/interpreter/condition.ex b/lib/archethic/contracts/interpreter/condition.ex index 5b97fa891..54f9b5ebd 100644 --- a/lib/archethic/contracts/interpreter/condition.ex +++ b/lib/archethic/contracts/interpreter/condition.ex @@ -263,6 +263,14 @@ defmodule Archethic.Contracts.ConditionInterpreter do {node, acc} end + # Whitelist the get_first_transaction_address/0 function in condition + defp prewalk( + node = {{:atom, "get_first_transaction_address"}, _, []}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + # Whitelist the get_genesis_public_key/0 function in condition defp prewalk( node = {{:atom, "get_genesis_public_key"}, _, []}, diff --git a/lib/archethic/contracts/interpreter/library.ex b/lib/archethic/contracts/interpreter/library.ex index 0e0aee28c..916a711fe 100644 --- a/lib/archethic/contracts/interpreter/library.ex +++ b/lib/archethic/contracts/interpreter/library.ex @@ -1,13 +1,13 @@ defmodule Archethic.Contracts.Interpreter.Library do @moduledoc false - alias Archethic.Election - - alias Archethic.P2P - alias Archethic.P2P.Message.GetGenesisAddress - alias Archethic.P2P.Message.GetFirstPublicKey - alias Archethic.P2P.Message.GenesisAddress - alias Archethic.P2P.Message.FirstPublicKey + alias Archethic.{ + Election, + P2P, + P2P.Message.GetFirstPublicKey, + P2P.Message.FirstPublicKey, + TransactionChain + } @doc """ Match a regex expression @@ -181,19 +181,6 @@ defmodule Archethic.Contracts.Interpreter.Library do def size(list) when is_list(list), do: length(list) def size(map) when is_map(map), do: map_size(map) - @doc """ - Get the genesis address of the chain - - """ - @spec get_genesis_address(binary()) :: - binary() - def get_genesis_address(address) do - bin_address = decode_binary(address) - nodes = Election.chain_storage_nodes(bin_address, P2P.authorized_and_available_nodes()) - {:ok, address} = download_first_address(nodes, bin_address) - Base.encode16(address) - end - @doc """ Get the genesis public key """ @@ -215,22 +202,13 @@ defmodule Archethic.Contracts.Interpreter.Library do defp download_first_public_key([], _address), do: {:error, :network_issue} - defp download_first_address([node | rest], address) do - case P2P.send_message(node, %GetGenesisAddress{address: address}) do - {:ok, %GenesisAddress{address: address}} -> {:ok, address} - {:error, _} -> download_first_address(rest, address) - end - end - - defp download_first_address([], _address), do: {:error, :network_issue} - @doc """ Return the current UNIX timestamp """ @spec timestamp() :: non_neg_integer() def timestamp, do: DateTime.utc_now() |> DateTime.to_unix() - defp decode_binary(bin) do + def decode_binary(bin) do if String.printable?(bin) do case Base.decode16(bin, case: :mixed) do {:ok, hex} -> @@ -243,4 +221,34 @@ defmodule Archethic.Contracts.Interpreter.Library do bin end end + + @doc """ + Get the genesis address of the chain + """ + @spec get_genesis_address(binary()) :: + binary() + def get_genesis_address(address) do + addr_bin = decode_binary(address) + nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes()) + + case TransactionChain.fetch_genesis_address_remotely(addr_bin, nodes) do + {:ok, genesis_address} -> Base.encode16(genesis_address) + {:error, reason} -> raise "[get_genesis_address] #{inspect(reason)}" + end + end + + @doc """ + Get the First transaction address of the transaction chain for the given address + """ + @spec get_first_transaction_address(address :: binary()) :: + binary() + def get_first_transaction_address(address) do + addr_bin = decode_binary(address) + nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes()) + + case TransactionChain.fetch_first_transaction_address_remotely(addr_bin, nodes) do + {:ok, first_transaction_address} -> Base.encode16(first_transaction_address) + {:error, reason} -> raise "[get_first_transaction_address] #{inspect(reason)}" + end + end end diff --git a/lib/archethic/contracts/interpreter/utils.ex b/lib/archethic/contracts/interpreter/utils.ex index 8541fe5d6..6b54aadf1 100644 --- a/lib/archethic/contracts/interpreter/utils.ex +++ b/lib/archethic/contracts/interpreter/utils.ex @@ -268,6 +268,15 @@ defmodule Archethic.Contracts.Interpreter.Utils do {node, acc} end + # Whitelist the get_first_address/1 function + def prewalk( + node = {{:atom, "get_first_transaction_address"}, _, [_address]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root do + {node, acc} + end + # Whitelist the get_genesis_public_key/1 function def prewalk( node = {{:atom, "get_genesis_public_key"}, _, [_address]}, diff --git a/lib/archethic/p2p/message.ex b/lib/archethic/p2p/message.ex index 969d6b946..07db86820 100644 --- a/lib/archethic/p2p/message.ex +++ b/lib/archethic/p2p/message.ex @@ -43,6 +43,8 @@ defmodule Archethic.P2P.Message do GetTransactionChainLength, GetTransactionInputs, GetUnspentOutputs, + GetFirstTransactionAddress, + FirstTransactionAddress, LastTransactionAddress, ListNodes, NewBeaconSlot, @@ -100,6 +102,8 @@ defmodule Archethic.P2P.Message do | GetBalance.t() | GetTransactionInputs.t() | GetTransactionChainLength.t() + | GetFirstTransactionAddress.t() + | FirstTransactionAddress.t() | NotifyEndOfNodeSync.t() | GetLastTransactionAddress.t() | NotifyLastTransactionAddress.t() @@ -200,12 +204,9 @@ defmodule Archethic.P2P.Message do @doc """ Serialize a message into binary - ## Examples - iex> Message.encode(%Ok{}) <<254>> - iex> %Message.GetTransaction{ ...> address: <<0, 40, 71, 99, 6, 218, 243, 156, 193, 63, 176, 168, 22, 226, 31, 170, 119, 122, ...> 13, 188, 75, 49, 171, 219, 222, 133, 86, 132, 188, 206, 233, 66, 7>> diff --git a/lib/archethic/p2p/message/first_transaction_address.ex b/lib/archethic/p2p/message/first_transaction_address.ex new file mode 100644 index 000000000..9264b1661 --- /dev/null +++ b/lib/archethic/p2p/message/first_transaction_address.ex @@ -0,0 +1,45 @@ +defmodule Archethic.P2P.Message.FirstTransactionAddress do + @moduledoc false + alias Archethic.Utils + @enforce_keys [:address] + defstruct [:address] + + @type t() :: %__MODULE__{ + address: binary() + } + + @doc """ + Serialize FirstTransactionAddress Struct + + iex> %FirstTransactionAddress{ + ...> address: <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + ...> 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + ...> } |> FirstTransactionAddress.serialize() + #address + <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + """ + def serialize(%__MODULE__{address: address}) do + <> + end + + @doc """ + DeSerialize FirstTransactionAddress Struct + + iex> # First address + ...> <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + ...> 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + ...> |> FirstTransactionAddress.deserialize() + { + %FirstTransactionAddress{ + address: <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + }, ""} + + """ + def deserialize(bin) do + {address, <>} = Utils.deserialize_address(bin) + + {%__MODULE__{address: address}, rest} + end +end diff --git a/lib/archethic/p2p/message/get_first_transaction_address.ex b/lib/archethic/p2p/message/get_first_transaction_address.ex new file mode 100644 index 000000000..33186ce93 --- /dev/null +++ b/lib/archethic/p2p/message/get_first_transaction_address.ex @@ -0,0 +1,65 @@ +defmodule Archethic.P2P.Message.GetFirstTransactionAddress do + @moduledoc """ + Represents a message to request the first address from a transaction chain. + Genesis address != first transaction address + Hash of current index public key gives current index address + Hash of genesis public key gives genesis address + Hash of first public key gives first transaction address + """ + alias Archethic.Utils + alias Archethic.P2P.Message.FirstTransactionAddress + alias Archethic.TransactionChain + alias Archethic.P2P.Message.NotFound + + @enforce_keys [:address] + defstruct [:address] + + @type t() :: %__MODULE__{ + address: binary() + } + + @doc """ + Serialize GetFirstTransactionAddress Struct + + iex> %GetFirstTransactionAddress{ + ...> address: <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + ...> 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + ...> } |> GetFirstTransactionAddress.serialize() + #address + <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + """ + def serialize(%__MODULE__{address: address}) do + <> + end + + @doc """ + DeSerialize GetFirstTransactionAddress Struct + + iex> # First address + ...> <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + ...> 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + ...> |> GetFirstTransactionAddress.deserialize() + { + %GetFirstTransactionAddress{ + address: <<0, 0, 94, 5, 249, 103, 126, 31, 43, 57, 25, 14, 187, 133, 59, 234, 201, 172, + 3, 195, 43, 81, 81, 146, 164, 202, 147, 218, 207, 204, 31, 185, 73, 251>> + }, ""} + + """ + def deserialize(bin) do + {address, <>} = Utils.deserialize_address(bin) + + {%__MODULE__{address: address}, rest} + end + + def process(%__MODULE__{address: address}) do + case TransactionChain.get_first_transaction_address(address) do + {:error, :transaction_not_exists} -> + %NotFound{} + + {:ok, first_address} -> + %FirstTransactionAddress{address: first_address} + end + end +end diff --git a/lib/archethic/p2p/message/message_id.ex b/lib/archethic/p2p/message/message_id.ex index 53eef839b..98ca7aa81 100644 --- a/lib/archethic/p2p/message/message_id.ex +++ b/lib/archethic/p2p/message/message_id.ex @@ -25,6 +25,8 @@ defmodule Archethic.P2P.MessageId do GetTransactionChainLength, GetP2PView, GetFirstPublicKey, + GetFirstTransactionAddress, + FirstTransactionAddress, NotifyLastTransactionAddress, GetTransactionSummary, Ping, @@ -102,7 +104,7 @@ defmodule Archethic.P2P.MessageId do GetLastTransactionAddress => 21, NotifyLastTransactionAddress => 22, GetTransactionSummary => 23, - # id 24 is available for a new message + GetFirstTransactionAddress => 24, Ping => 25, GetBeaconSummary => 26, NewBeaconSlot => 27, @@ -119,6 +121,7 @@ defmodule Archethic.P2P.MessageId do NotifyReplicationValidation => 38, # Responses + FirstTransactionAddress => 228, AddressList => 229, ShardRepair => 230, SummaryAggregate => 231, diff --git a/lib/archethic/transaction_chain.ex b/lib/archethic/transaction_chain.ex index 37bdc1699..4765ab209 100644 --- a/lib/archethic/transaction_chain.ex +++ b/lib/archethic/transaction_chain.ex @@ -30,7 +30,9 @@ defmodule Archethic.TransactionChain do TransactionChainLength, TransactionInputList, TransactionList, - UnspentOutputList + UnspentOutputList, + GetFirstTransactionAddress, + FirstTransactionAddress } alias __MODULE__.MemTables.KOLedger @@ -262,6 +264,24 @@ defmodule Archethic.TransactionChain do get_transaction(address, fields) end + @doc """ + Get the first transaction Address from a genesis/chain address + """ + @spec get_first_transaction_address(address :: binary()) :: + {:ok, address :: binary()} | {:error, :transaction_not_exists} + def get_first_transaction_address(address) when is_binary(address) do + address = + address + |> get_genesis_address() + |> list_chain_addresses() + |> Enum.at(0) + + case address do + nil -> {:error, :transaction_not_exists} + {address, _datetime} -> {:ok, address} + end + end + @doc """ Get the genesis address from a given chain address """ @@ -981,7 +1001,7 @@ defmodule Archethic.TransactionChain do @doc """ Retrieve the genesis address for a chain from P2P Quorom - It queries the the network for genesis address + It queries the the network for genesis address. """ @spec fetch_genesis_address_remotely(address :: binary(), list(Node.t())) :: {:ok, binary()} | {:error, :network_issue} @@ -994,4 +1014,23 @@ defmodule Archethic.TransactionChain do {:error, :network_issue} end end + + @doc """ + Retrieve the First transaction address for a chain from P2P Quorom + """ + @spec fetch_first_transaction_address_remotely(address :: binary(), nodes :: list(Node.t())) :: + {:ok, binary()} | {:error, :network_issue} | {:error, :does_not_exist} + def fetch_first_transaction_address_remotely(address, nodes) + when is_binary(address) and is_list(nodes) do + case P2P.quorum_read(nodes, %GetFirstTransactionAddress{address: address}) do + {:ok, %NotFound{}} -> + {:error, :does_not_exist} + + {:ok, %FirstTransactionAddress{address: first_address}} -> + {:ok, first_address} + + _ -> + {:error, :network_issue} + end + end end diff --git a/test/archethic/contracts/interpreter/action_test.exs b/test/archethic/contracts/interpreter/action_test.exs index 87d7bb03a..f1aaa027d 100644 --- a/test/archethic/contracts/interpreter/action_test.exs +++ b/test/archethic/contracts/interpreter/action_test.exs @@ -3,10 +3,13 @@ defmodule Archethic.Contracts.ActionInterpreterTest do alias Archethic.Contracts.ActionInterpreter alias Archethic.Contracts.Interpreter + alias Archethic.Crypto alias Archethic.P2P alias Archethic.P2P.Node alias Archethic.P2P.Message.GenesisAddress + alias Archethic.P2P.Message.GetFirstTransactionAddress + alias Archethic.P2P.Message.FirstTransactionAddress alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -383,4 +386,55 @@ defmodule Archethic.Contracts.ActionInterpreterTest do |> ActionInterpreter.execute() end end + + test "Use get_first_transaction_address/1 in actions" do + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: "key1", + last_public_key: "key1", + network_patch: "AAA", + geo_patch: "AAA", + available?: true, + authorized?: true, + authorization_date: DateTime.utc_now() + }) + + first_address_bin = + "some random seed" |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address() + + second_address_bin = + "some random seed" |> Crypto.derive_keypair(1) |> elem(0) |> Crypto.derive_address() + + first_addr = first_address_bin |> Base.encode16() + second_addr = second_address_bin |> Base.encode16() + + MockClient + |> stub(:send_message, fn + _, %GetFirstTransactionAddress{address: ^first_address_bin}, _ -> + {:ok, %FirstTransactionAddress{address: second_address_bin}} + + _, _, _ -> + {:error, :network_error} + end) + + contract_code = ~s""" + actions triggered_by: transaction do + address = get_first_transaction_address("#{first_addr}") + if address == "#{second_addr}" do + set_content "first_address_acquired" + else + set_content "not_acquired" + end + end + """ + + assert %Transaction{data: %TransactionData{content: "first_address_acquired"}} = + contract_code + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + |> elem(2) + |> ActionInterpreter.execute() + end end diff --git a/test/archethic/contracts/interpreter/condition_test.exs b/test/archethic/contracts/interpreter/condition_test.exs index 826f53feb..6fa8f8015 100644 --- a/test/archethic/contracts/interpreter/condition_test.exs +++ b/test/archethic/contracts/interpreter/condition_test.exs @@ -9,6 +9,7 @@ defmodule Archethic.Contracts.ConditionInterpreterTest do alias Archethic.P2P.Node alias Archethic.P2P.Message.FirstPublicKey alias Archethic.P2P.Message.GenesisAddress + alias Archethic.P2P.Message.FirstTransactionAddress doctest ConditionInterpreter @@ -199,7 +200,7 @@ defmodule Archethic.Contracts.ConditionInterpreterTest do }) end - test "shall get the first address of the chain in the conditions" do + test "shall get the genesis address of the chain in the conditions" do key = <<0::16, :crypto.strong_rand_bytes(32)::binary>> P2P.add_and_connect_node(%Node{ @@ -237,6 +238,44 @@ defmodule Archethic.Contracts.ConditionInterpreterTest do }) end + test "should get first tx address of the chain in the conditions" do + key = <<0::16, :crypto.strong_rand_bytes(32)::binary>> + + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: key, + last_public_key: key, + available?: true, + geo_patch: "AAA", + network_patch: "AAA", + authorized?: true, + authorization_date: DateTime.utc_now() + }) + + address = "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + b_address = Base.decode16!(address) + + MockClient + |> expect(:send_message, 1, fn _, _, _ -> + {:ok, %FirstTransactionAddress{address: b_address}} + end) + + assert true = + ~s""" + condition transaction: [ + address: get_first_transaction_address() == "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + |> elem(2) + |> ConditionInterpreter.valid_conditions?(%{ + "transaction" => %{"address" => :crypto.strong_rand_bytes(32)} + }) + end + test "shall get the first public of the chain in the conditions" do key = <<0::16, :crypto.strong_rand_bytes(32)::binary>> diff --git a/test/archethic/contracts/interpreter/library_test.exs b/test/archethic/contracts/interpreter/library_test.exs index e13936c1d..c6cbb3a4a 100644 --- a/test/archethic/contracts/interpreter/library_test.exs +++ b/test/archethic/contracts/interpreter/library_test.exs @@ -1,7 +1,36 @@ defmodule Archethic.Contracts.Interpreter.LibraryTest do use ArchethicCase - alias Archethic.Contracts.Interpreter.Library + alias Archethic.{Contracts.Interpreter.Library, P2P, P2P.Node} + + alias P2P.Message.{ + GetFirstTransactionAddress, + FirstTransactionAddress + } doctest Library + + import Mox + + test "get_first_transaction_address/1" do + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: "key1", + last_public_key: "key1", + network_patch: "AAA", + geo_patch: "AAA", + available?: true, + authorized?: true, + authorization_date: DateTime.utc_now() + }) + + MockClient + |> expect(:send_message, fn + _, %GetFirstTransactionAddress{address: "addr2"}, _ -> + {:ok, %FirstTransactionAddress{address: "addr1"}} + end) + + assert "addr1" == Library.get_first_transaction_address("addr2") |> Library.decode_binary() + end end diff --git a/test/archethic/p2p/message/first_transaction_address_test.exs b/test/archethic/p2p/message/first_transaction_address_test.exs new file mode 100644 index 000000000..29da6933d --- /dev/null +++ b/test/archethic/p2p/message/first_transaction_address_test.exs @@ -0,0 +1,19 @@ +defmodule Archethic.P2P.Message.FirstTransactionAddressTest do + @moduledoc false + use ExUnit.Case + + alias Archethic.P2P.Message.FirstTransactionAddress + alias Archethic.P2P.Message + + doctest FirstTransactionAddress + + test "encode decode" do + msg2 = %FirstTransactionAddress{address: <<0::272>>} + + assert msg2 == + msg2 + |> Message.encode() + |> Message.decode() + |> elem(0) + end +end diff --git a/test/archethic/p2p/message/get_first_transaction_address_test.exs b/test/archethic/p2p/message/get_first_transaction_address_test.exs new file mode 100644 index 000000000..1ad7e7632 --- /dev/null +++ b/test/archethic/p2p/message/get_first_transaction_address_test.exs @@ -0,0 +1,49 @@ +defmodule Archethic.P2P.Message.GetFirstTransactionAddressTest do + @moduledoc false + use ExUnit.Case + use ArchethicCase + + alias Archethic.P2P.Message.GetFirstTransactionAddress + alias Archethic.P2P.Message.FirstTransactionAddress + alias Archethic.P2P.Message + doctest GetFirstTransactionAddress + + import Mox + + test "Process" do + MockDB + |> stub(:get_genesis_address, fn + "not_existing_address" -> + "not_existing_address" + + "address10" -> + "address0" + end) + |> stub(:list_chain_addresses, fn + "not_existing_address" -> + [] + + "address0" -> + [ + {"address1", DateTime.utc_now() |> DateTime.add(-2000)}, + {"addr2", DateTime.utc_now() |> DateTime.add(-1000)}, + {"addr3", DateTime.utc_now() |> DateTime.add(-500)} + ] + end) + + assert %FirstTransactionAddress{address: "address1"} == + GetFirstTransactionAddress.process(%GetFirstTransactionAddress{ + address: "address10" + }) + end + + test "encode decode" do + msg = %GetFirstTransactionAddress{address: <<0::272>>} + + assert msg == + msg + |> Message.encode() + |> Message.decode() + |> elem(0) + end +end diff --git a/test/archethic/transaction_chain_test.exs b/test/archethic/transaction_chain_test.exs index f130c4c15..d0ce95b95 100644 --- a/test/archethic/transaction_chain_test.exs +++ b/test/archethic/transaction_chain_test.exs @@ -17,6 +17,9 @@ defmodule Archethic.TransactionChainTest do alias Archethic.P2P.Message.TransactionInputList alias Archethic.P2P.Message.UnspentOutputList alias Archethic.P2P.Node + alias Archethic.P2P.Message.GetFirstTransactionAddress + alias Archethic.P2P.Message.FirstTransactionAddress + alias Archethic.P2P.Message.NotFound alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction @@ -676,4 +679,163 @@ defmodule Archethic.TransactionChainTest do assert {:ok, 2} = TransactionChain.fetch_size_remotely("Alice1", nodes) end end + + describe "fetch_first_address_remotely" do + test "when first txn exists" do + nodes = [ + %Node{ + first_public_key: "node1", + last_public_key: "node1", + ip: {127, 0, 0, 1}, + port: 3000, + available?: true, + availability_history: <<1::1>>, + authorized?: true, + geo_patch: "AAA", + authorization_date: DateTime.utc_now() + }, + %Node{ + first_public_key: "node2", + last_public_key: "node2", + ip: {127, 0, 0, 1}, + port: 3001, + availability_history: <<1::1>>, + geo_patch: "AAA", + authorized?: true, + authorization_date: DateTime.utc_now() + }, + %Node{ + first_public_key: "node3", + last_public_key: "node3", + geo_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3002, + availability_history: <<1::1>>, + authorized?: true, + authorization_date: DateTime.utc_now() + } + ] + + Enum.each(nodes, &P2P.add_and_connect_node/1) + + MockClient + |> stub(:send_message, fn + _, %GetFirstTransactionAddress{address: "addr2"}, _ -> + {:ok, %FirstTransactionAddress{address: "addr1"}} + end) + + assert {:ok, "addr1"} = + TransactionChain.fetch_first_transaction_address_remotely("addr2", nodes) + end + + test "when asked from genesis address" do + nodes = [ + %Node{ + first_public_key: "node1", + last_public_key: "node1", + ip: {127, 0, 0, 1}, + port: 3000, + available?: true, + availability_history: <<1::1>>, + authorized?: true, + geo_patch: "AAA", + authorization_date: DateTime.utc_now() + }, + %Node{ + first_public_key: "node2", + last_public_key: "node2", + ip: {127, 0, 0, 1}, + port: 3001, + availability_history: <<1::1>>, + geo_patch: "AAA", + authorized?: true, + authorization_date: DateTime.utc_now() + }, + %Node{ + first_public_key: "node3", + last_public_key: "node3", + geo_patch: "AAA", + ip: {127, 0, 0, 1}, + port: 3002, + availability_history: <<1::1>>, + authorized?: true, + authorization_date: DateTime.utc_now() + } + ] + + Enum.each(nodes, &P2P.add_and_connect_node/1) + + MockClient + |> stub(:send_message, fn + %Node{ + first_public_key: "node1" + }, + %GetFirstTransactionAddress{address: "addr0"}, + _ -> + {:ok, %FirstTransactionAddress{address: "addr0"}} + + %Node{ + first_public_key: "node2" + }, + %GetFirstTransactionAddress{address: "addr0"}, + _ -> + %Archethic.P2P.Message.NotFound{} + + %Node{ + first_public_key: "node3" + }, + %GetFirstTransactionAddress{address: "addr0"}, + _ -> + {:ok, %FirstTransactionAddress{address: "addr0"}} + + _, _, _ -> + {:ok, %NotFound{}} + end) + + assert {:ok, "addr0"} = + TransactionChain.fetch_first_transaction_address_remotely("addr0", nodes) + + assert {:error, :does_not_exist} = + TransactionChain.fetch_first_transaction_address_remotely( + "not_existing_address", + nodes + ) + end + end + + describe "First Tx" do + setup do + MockDB + |> stub(:get_genesis_address, fn + "not_existing_address" -> + "not_existing_address" + + "addr10" -> + "addr0" + end) + |> stub(:list_chain_addresses, fn + "not_existing_address" -> + [] + + "addr0" -> + [ + {"addr1", DateTime.utc_now() |> DateTime.add(-2000)}, + {"addr2", DateTime.utc_now() |> DateTime.add(-1000)}, + {"addr3", DateTime.utc_now() |> DateTime.add(-500)} + ] + end) + |> stub(:get_transaction, fn "addr1", _ -> + {:ok, %Transaction{address: "addr1"}} + end) + + :ok + end + + test "get_first_transaction_address/2" do + assert {:ok, "addr1"} = TransactionChain.get_first_transaction_address("addr10") + + assert {:error, :transaction_not_exists} = + TransactionChain.get_first_transaction_address("not_existing_address") + end + end end