From 5d08f69dd78cf6857235c2e24c45a005becc8b59 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Mon, 7 Apr 2025 15:16:33 -0400 Subject: [PATCH 1/4] Extract and return tx id from accepted submissions --- lib/transaction/hash.ex | 41 +++++++++++++++++++++++++++ lib/transaction/response.ex | 21 +++++++------- lib/xander/transaction.ex | 32 +++++++++++++++++---- mix.exs | 2 ++ mix.lock | 3 ++ run_submit_tx.exs | 8 ++++-- test/test_helper.exs | 2 +- test/xander/transaction/hash_test.exs | 5 ++++ 8 files changed, 94 insertions(+), 20 deletions(-) create mode 100644 lib/transaction/hash.ex create mode 100644 test/xander/transaction/hash_test.exs diff --git a/lib/transaction/hash.ex b/lib/transaction/hash.ex new file mode 100644 index 0000000..1561d6c --- /dev/null +++ b/lib/transaction/hash.ex @@ -0,0 +1,41 @@ +defmodule Xander.Transaction.Hash do + @moduledoc """ + Extracts a transaction ID from a transaction CBOR hex. + """ + + require Logger + + @doc """ + Extracts a transaction ID from a transaction CBOR hex + Returns the transaction ID as a string, or nil if there's an error. + + ## Examples + + # Valid transaction hash + iex> Xander.Transaction.Hash.get_id("84a500d901028182582015e6ff205f08b2a29371e97b876e62ef942e5b8b74331d0e1cc735b5945d32bc02018282583900f9a2fa2e3fb59bbf8cfda38437c391fc3047e56a3b7c37e5f7a5fbd12d2915e78d60e700f34154074b6ec1cd365531f196276748ba498f251a00b71b0082583900f9a2fa2e3fb59bbf8cfda38437c391fc3047e56a3b7c37e5f7a5fbd12d2915e78d60e700f34154074b6ec1cd365531f196276748ba498f251a246d92ef021a000292dd031a0481bdd00800a100d9010281825820439c9a6aa2b4533d7a8309326f644998b8e8b42f22b8850fa858a93fc7e2888e584089bc8da32942758efdba2779daa6252e00c06bfc76b5a1b144d33e5d01b3f5c75ca45e784005ce73f839cf8c6bf0851552daabe13b94ad95997de91c66ae5b06f5f6") + "dc2d59c0188d55a94fc4780b930276d49303b558534dedbdfe7aca1c995cf463" + + # Invalid transaction hash + iex> Xander.Transaction.Hash.get_id("84a500d9123") + nil + """ + @spec get_id(String.t()) :: String.t() | nil + def get_id(cbor_hash) do + try do + cbor_hash = String.upcase(cbor_hash) + {:ok, transaction} = Base.decode16(cbor_hash) + {:ok, [tx_body | _], ""} = CBOR.decode(transaction) + + tx_body + |> CBOR.encode() + # Blake2b-256 is currently not supported by the built-in :crypto module + # so we must use the :blake2 dependency. + |> Blake2.hash2b(32) + |> Base.encode16(case: :lower) + rescue + error -> + Logger.warning("Error extracting tx id: #{inspect(error)}") + nil + end + end +end diff --git a/lib/transaction/response.ex b/lib/transaction/response.ex index cbedf6e..579bf1a 100644 --- a/lib/transaction/response.ex +++ b/lib/transaction/response.ex @@ -5,6 +5,16 @@ defmodule Xander.Transaction.Response do require Logger + @type error_reason :: + :cannot_decode_non_binary_values + | :cbor_function_clause_error + | :cbor_match_error + | :invalid_format + | :invalid_input + | :disconnected + + @type t :: {:ok, :accepted} | {:rejected, any()} | {:error, error_reason} + @doc """ Parses the response from a transaction. Accepts CBOR encoded responses. @@ -34,16 +44,7 @@ defmodule Xander.Transaction.Response do iex> Xander.Transaction.Response.parse_response(nil) {:error, :invalid_input} """ - @spec parse_response(binary() | nil) :: - {:error, - :cannot_decode_non_binary_values - | :cbor_function_clause_error - | :cbor_match_error - | :invalid_format - | :invalid_input - | :disconnected} - | {:rejected, any()} - | {:ok, :accepted} + @spec parse_response(binary() | nil) :: t() def parse_response(cbor_response) do case Xander.Util.plex(cbor_response) do {:ok, %{payload: cbor_response_payload}} -> diff --git a/lib/xander/transaction.ex b/lib/xander/transaction.ex index 8048159..a325c0d 100644 --- a/lib/xander/transaction.ex +++ b/lib/xander/transaction.ex @@ -7,6 +7,7 @@ defmodule Xander.Transaction do alias Xander.Handshake alias Xander.Messages + alias Xander.Transaction.Hash alias Xander.Transaction.Response require Logger @@ -27,6 +28,7 @@ defmodule Xander.Transaction do network: any(), queue: :queue.queue() } + @type tx_id :: binary() ############## # Public API # @@ -41,7 +43,8 @@ defmodule Xander.Transaction do Xander.Transaction.send(tx_hash) ``` """ - @spec send(atom() | pid(), binary()) :: :ok | {:error, atom()} + @spec send(atom() | pid(), binary()) :: + {:accepted, tx_id()} | {:rejected, binary()} | {:error, binary()} def send(pid \\ __MODULE__, tx_hash) do :gen_statem.call(pid, {:request, :send_tx, tx_hash}) end @@ -191,7 +194,8 @@ defmodule Xander.Transaction do ) do # Add new request to queue so that it can be processed # after the current transaction is complete. - new_queue = :queue.in({from, tx_hash}, queue) + tx_id = Hash.get_id(tx_hash) + new_queue = :queue.in({from, tx_hash, tx_id}, queue) new_data = %__MODULE__{data | queue: new_queue} {:keep_state, new_data} end @@ -202,7 +206,7 @@ defmodule Xander.Transaction do %__MODULE__{client: client, socket: socket, queue: queue} = data ) do case :queue.peek(queue) do - {:value, {_from, tx_hash}} -> + {:value, {_from, tx_hash, _tx_id}} -> :inet.setopts(socket, active: :once) :ok = client.send(socket, Messages.transaction(tx_hash)) @@ -267,7 +271,12 @@ defmodule Xander.Transaction do # Track the caller and transaction in our queue. A queue is kept to handle # transactions that are sent from the dependent process while the current # transaction is being processed. - data = update_in(data.queue, &:queue.in({from, tx_hash}, &1)) + + # Extract the transaction ID from the transaction hash. + # This value will be returned to the caller when the transaction is accepted. + tx_id = Hash.get_id(tx_hash) + + data = update_in(data.queue, &:queue.in({from, tx_hash, tx_id}, &1)) {:next_state, :busy, data, [{:next_event, :internal, :send_tx}]} end @@ -279,8 +288,19 @@ defmodule Xander.Transaction do end defp process_queue_item(data, result) do - {{:value, {caller, _tx_hash}}, new_data} = get_and_update_in(data.queue, &:queue.out/1) - actions = [{:reply, caller, result}] + {{:value, {caller, _tx_hash, tx_id}}, new_data} = + get_and_update_in(data.queue, &:queue.out/1) + + submission_result = + case result do + # If transaction was accepted, then include the tx id + # in the result tuple for the client + {:ok, :accepted} -> {:accepted, tx_id} + # If not accepted, then return the result as is + _ -> result + end + + actions = [{:reply, caller, submission_result}] # Check if there are more items in the queue. # These are from calls that came in while the current transaction was being processed. diff --git a/mix.exs b/mix.exs index 7ef8ae6..bfcf52b 100644 --- a/mix.exs +++ b/mix.exs @@ -29,7 +29,9 @@ defmodule Xander.MixProject do defp deps do [ + {:blake2, "~> 1.0"}, {:cbor, "~> 1.0"}, + {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:ex_doc, "~> 0.29", only: :dev, runtime: false} ] end diff --git a/mix.lock b/mix.lock index 4fbc8d6..8909e4d 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,9 @@ %{ + "blake2": {:hex, :blake2, "1.0.4", "8263c69a191142922bc2510f1ffc0de0ae96e8c3bd5e2ad3fac7e87aed94c8b1", [:mix], [], "hexpm", "e9f4120d163ba14d86304195e50745fa18483e6ad2be94c864ae449bbdd6a189"}, "cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "ex_doc": {:hex, :ex_doc, "0.36.1", "4197d034f93e0b89ec79fac56e226107824adcce8d2dd0a26f5ed3a95efc36b1", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d7d26a7cf965dacadcd48f9fa7b5953d7d0cfa3b44fa7a65514427da44eafd89"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, diff --git a/run_submit_tx.exs b/run_submit_tx.exs index 5a8c3bc..fbac9c0 100644 --- a/run_submit_tx.exs +++ b/run_submit_tx.exs @@ -1,7 +1,8 @@ # Install Xander from local path Mix.install([ {:xander, path: Path.expand(".")}, - {:cbor, "~> 1.0.0"} + {:cbor, "~> 1.0.0"}, + {:blake2, "~> 1.0.0"} ]) socket_path = System.get_env("CARDANO_NODE_SOCKET_PATH", "/tmp/cardano-node-preview.socket") @@ -21,10 +22,11 @@ case Transaction.start_link(config) do {:ok, pid} -> IO.puts("Successfully connected to Cardano node 🎉\n") - tx_hex = "" + tx_hex = + "" case Transaction.send(pid, tx_hex) do - {:ok, :accepted} -> IO.puts("Transaction submitted successfully") + {:accepted, tx_id} -> IO.puts("Transaction submitted successfully: #{tx_id}") _ -> IO.puts("Error submitting transaction") end diff --git a/test/test_helper.exs b/test/test_helper.exs index 48a0509..ae6b1f8 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1,4 +1,4 @@ -Application.stop(:xander) +Logger.configure(level: :error) # Configure ExUnit to exclude integration tests by default ExUnit.configure(exclude: [:integration]) diff --git a/test/xander/transaction/hash_test.exs b/test/xander/transaction/hash_test.exs new file mode 100644 index 0000000..34a4fa3 --- /dev/null +++ b/test/xander/transaction/hash_test.exs @@ -0,0 +1,5 @@ +defmodule Xander.Transaction.HashTest do + use ExUnit.Case + + doctest Xander.Transaction.Hash +end From 2801b8c146bad9404f33806e36f57a8303001a8b Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Thu, 10 Apr 2025 17:35:34 -0400 Subject: [PATCH 2/4] Include missing deps on scripts. --- run.exs | 1 + run_submit_tx.exs | 10 ++++------ run_with_demeter.exs | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/run.exs b/run.exs index c8cbf36..9b3ffa7 100644 --- a/run.exs +++ b/run.exs @@ -1,5 +1,6 @@ # Install Xander from local path Mix.install([ + {:blake2, "~> 1.0"}, {:xander, path: Path.expand(".")} ]) diff --git a/run_submit_tx.exs b/run_submit_tx.exs index fbac9c0..edd42ce 100644 --- a/run_submit_tx.exs +++ b/run_submit_tx.exs @@ -1,8 +1,7 @@ # Install Xander from local path Mix.install([ - {:xander, path: Path.expand(".")}, - {:cbor, "~> 1.0.0"}, - {:blake2, "~> 1.0.0"} + {:blake2, "~> 1.0.0"}, + {:xander, path: Path.expand(".")} ]) socket_path = System.get_env("CARDANO_NODE_SOCKET_PATH", "/tmp/cardano-node-preview.socket") @@ -22,11 +21,10 @@ case Transaction.start_link(config) do {:ok, pid} -> IO.puts("Successfully connected to Cardano node 🎉\n") - tx_hex = - "" + tx_hex = "" case Transaction.send(pid, tx_hex) do - {:accepted, tx_id} -> IO.puts("Transaction submitted successfully: #{tx_id}") + {:accepted, tx_id} -> IO.puts("\nTransaction submitted successfully ✅\nTx ID: #{tx_id}") _ -> IO.puts("Error submitting transaction") end diff --git a/run_with_demeter.exs b/run_with_demeter.exs index 82ff0c2..ce7211b 100644 --- a/run_with_demeter.exs +++ b/run_with_demeter.exs @@ -1,5 +1,6 @@ # Install Xander from local path Mix.install([ + {:blake2, "~> 1.0"}, {:xander, path: Path.expand(".")} ]) From c8d54282a9f05b841350e3fecf482e26cd2d1833 Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Tue, 29 Apr 2025 15:20:42 -0400 Subject: [PATCH 3/4] Extract tx_id in last step. Removes unnecessary duplicated calls. --- lib/xander/transaction.ex | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/lib/xander/transaction.ex b/lib/xander/transaction.ex index a325c0d..1965c7e 100644 --- a/lib/xander/transaction.ex +++ b/lib/xander/transaction.ex @@ -194,8 +194,7 @@ defmodule Xander.Transaction do ) do # Add new request to queue so that it can be processed # after the current transaction is complete. - tx_id = Hash.get_id(tx_hash) - new_queue = :queue.in({from, tx_hash, tx_id}, queue) + new_queue = :queue.in({from, tx_hash}, queue) new_data = %__MODULE__{data | queue: new_queue} {:keep_state, new_data} end @@ -206,7 +205,7 @@ defmodule Xander.Transaction do %__MODULE__{client: client, socket: socket, queue: queue} = data ) do case :queue.peek(queue) do - {:value, {_from, tx_hash, _tx_id}} -> + {:value, {_from, tx_hash}} -> :inet.setopts(socket, active: :once) :ok = client.send(socket, Messages.transaction(tx_hash)) @@ -272,11 +271,7 @@ defmodule Xander.Transaction do # transactions that are sent from the dependent process while the current # transaction is being processed. - # Extract the transaction ID from the transaction hash. - # This value will be returned to the caller when the transaction is accepted. - tx_id = Hash.get_id(tx_hash) - - data = update_in(data.queue, &:queue.in({from, tx_hash, tx_id}, &1)) + data = update_in(data.queue, &:queue.in({from, tx_hash}, &1)) {:next_state, :busy, data, [{:next_event, :internal, :send_tx}]} end @@ -288,14 +283,14 @@ defmodule Xander.Transaction do end defp process_queue_item(data, result) do - {{:value, {caller, _tx_hash, tx_id}}, new_data} = + {{:value, {caller, tx_hash}}, new_data} = get_and_update_in(data.queue, &:queue.out/1) submission_result = case result do # If transaction was accepted, then include the tx id # in the result tuple for the client - {:ok, :accepted} -> {:accepted, tx_id} + {:ok, :accepted} -> {:accepted, Hash.get_id(tx_hash)} # If not accepted, then return the result as is _ -> result end From 8979559e66af6d4218a19e2d4325eacd97f3cc5f Mon Sep 17 00:00:00 2001 From: Carlos Souza Date: Tue, 29 Apr 2025 15:28:32 -0400 Subject: [PATCH 4/4] Assert on tx_id --- test/integration/transaction_test.exs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/integration/transaction_test.exs b/test/integration/transaction_test.exs index efb2da4..788fb4d 100644 --- a/test/integration/transaction_test.exs +++ b/test/integration/transaction_test.exs @@ -291,12 +291,10 @@ defmodule Xander.Integration.TransactionTest do Enum.map(signed_transactions, fn {index, tx_cbor, _tx_signed} -> Task.async(fn -> case Xander.Transaction.send(tx_cbor) do - {:ok, response} -> - IO.puts( - "Received successful response for transaction #{index}: #{inspect(response)}" - ) + {:accepted, tx_id} -> + IO.puts("Received successful response for transaction #{index}: #{inspect(tx_id)}") - assert response == :accepted + assert tx_id == Xander.Transaction.Hash.get_id(tx_cbor) {index, :ok} {:rejected, reason} ->