diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index 4597684b2..e523c770a 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -51,9 +51,7 @@ defmodule Archethic.Contracts do nil | Transaction.t(), nil | Recipient.t(), Keyword.t() - ) :: - {:ok, nil | Transaction.t()} - | {:error, :contract_failure | :invalid_triggers_execution} + ) :: {:ok, nil | Transaction.t()} | {:error, String.t()} defdelegate execute_trigger( trigger_type, contract, diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 97834a39e..657490df6 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -126,7 +126,7 @@ defmodule Archethic.Contracts.Interpreter do execute_opts() ) :: {:ok, nil | Transaction.t()} - | {:error, :contract_failure | :invalid_triggers_execution} + | {:error, String.t()} def execute_trigger( trigger_key, %Contract{ @@ -141,7 +141,7 @@ defmodule Archethic.Contracts.Interpreter do ) do case Map.get(triggers, trigger_key) do nil -> - {:error, :invalid_triggers_execution} + {:error, "Trigger not found on the contract"} %{args: args, ast: trigger_code} -> timestamp_now = @@ -183,9 +183,7 @@ defmodule Archethic.Contracts.Interpreter do end rescue err -> - Logger.error(Exception.format(:error, err, __STACKTRACE__)) - # it's ok to loose the error because it's user-code - {:error, :contract_failure} + {:error, error_to_string(err, __STACKTRACE__)} end @doc """ @@ -503,4 +501,20 @@ defmodule Archethic.Contracts.Interpreter do end end end + + defp error_to_string(err, stacktrace) do + case Enum.find_value(stacktrace, fn + {_, _, _, [file: 'nofile', line: line]} -> + line + + _ -> + false + end) do + line when is_integer(line) -> + Exception.message(err) <> " - L#{line}" + + _ -> + Exception.message(err) + end + end end diff --git a/lib/archethic/contracts/interpreter/common_interpreter.ex b/lib/archethic/contracts/interpreter/common_interpreter.ex index 6a010467f..efda02909 100644 --- a/lib/archethic/contracts/interpreter/common_interpreter.ex +++ b/lib/archethic/contracts/interpreter/common_interpreter.ex @@ -152,11 +152,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # whitelist assignation & write them to scope # this is done in the prewalk because it must be done before the "variable are read from scope" step def prewalk( - _node = {:=, _, [{{:atom, var_name}, _, nil}, value]}, + _node = {:=, meta, [{{:atom, var_name}, _, nil}, value]}, acc ) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Scope.write_cascade(unquote(var_name), unquote(value)) end @@ -167,9 +167,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do end # Dot access non-nested (x.y) - def prewalk(_node = {{:., _, [{{:atom, map_name}, _, nil}, {:atom, key_name}]}, _, _}, acc) do + def prewalk( + _node = {{:., meta, [{{:atom, map_name}, _, nil}, {:atom, key_name}]}, _, _}, + acc + ) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Scope.read(unquote(map_name), unquote(key_name)) end @@ -178,9 +181,9 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # Dot access nested (x.y.z) # or Module.function().z - def prewalk({{:., _, [first_arg, {:atom, key_name}]}, _, []}, acc) do + def prewalk({{:., meta, [first_arg, {:atom, key_name}]}, _, []}, acc) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Map.get(unquote(first_arg), unquote(key_name)) end @@ -189,12 +192,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # Map access non-nested (x[y]) def prewalk( - _node = {{:., _, [Access, :get]}, _, [{{:atom, map_name}, _, nil}, accessor]}, + _node = {{:., meta, [Access, :get]}, _, [{{:atom, map_name}, _, nil}, accessor]}, acc ) do # accessor can be a variable, a function call, a dot access, a string new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Scope.read(unquote(map_name), unquote(accessor)) end @@ -203,11 +206,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # Map access nested (x[y][z]) def prewalk( - _node = {{:., _, [Access, :get]}, _, [first_arg, accessor]}, + _node = {{:., meta, [Access, :get]}, _, [first_arg, accessor]}, acc ) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Map.get(unquote(first_arg), unquote(accessor)) end @@ -244,8 +247,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # log (not documented, only useful for developer debugging) # TODO: should be implemented in a module Logger (only available if config allows it) # will soon be updated to log into the playground console - def prewalk(_node = {{:atom, "log"}, _, [data]}, acc) do - new_node = quote do: apply(IO, :inspect, [unquote(data)]) + def prewalk(_node = {{:atom, "log"}, meta, [data]}, acc) do + new_node = + quote line: Keyword.fetch!(meta, :line) do + apply(IO, :inspect, [unquote(data)]) + end + {new_node, acc} end @@ -274,20 +281,21 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # ---------------------------------------------------------------------- # exit block == set parent scope # we need to return user's last expression and not the result of Scope.leave_scope() + # ps: there is no meta in a :__block__ def postwalk( - _node = {:__block__, meta, expressions}, + _node = {:__block__, [], expressions}, acc ) do {last_expression, expressions} = List.pop_at(expressions, -1) - {:__block__, _meta, new_expressions} = + {:__block__, [], new_expressions} = quote do result = unquote(last_expression) Scope.leave_scope() result end - {{:__block__, meta, expressions ++ new_expressions}, acc} + {{:__block__, [], expressions ++ new_expressions}, acc} end # Module function call @@ -307,7 +315,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do new_node = if Library.function_tagged_with?(module_name, function_name, :write_contract) do - quote do + quote line: Keyword.fetch!(meta, :line) do # mark the next_tx as dirty Scope.update_global([:next_transaction_changed], fn _ -> true end) @@ -328,11 +336,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # variable are read from scope def postwalk( - _node = {{:atom, var_name}, _, nil}, + _node = {{:atom, var_name}, meta, nil}, acc ) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Scope.read(unquote(var_name)) end @@ -342,7 +350,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # for var in list def postwalk( _node = - {{:atom, "for"}, _, + {{:atom, "for"}, meta, [ {:%{}, _, [{var_name, list}]}, [do: block] @@ -356,7 +364,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # transform the for-loop into Enum.each # and create a variable in the scope new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Enum.each(unquote(list), fn x -> Scope.write_at(unquote(var_name), x) @@ -367,9 +375,9 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do {new_node, acc} end - def postwalk({{:atom, function_name}, _, args}, acc) when is_list(args) do + def postwalk({{:atom, function_name}, meta, args}, acc) when is_list(args) do new_node = - quote do + quote line: Keyword.fetch!(meta, :line) do Scope.execute_function_ast(unquote(function_name), unquote(args)) end @@ -377,7 +385,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do end # BigInt mathematics to avoid floating point issues - def postwalk(_node = {ast, meta, [lhs, rhs]}, acc) when ast in [:*, :/, :+, :-] do + def postwalk(_node = {ast, meta, [lhs, rhs]}, acc) + when ast in [:*, :/, :+, :-] do new_node = quote line: Keyword.fetch!(meta, :line) do AST.decimal_arithmetic(unquote(ast), unquote(lhs), unquote(rhs)) diff --git a/lib/archethic/contracts/interpreter/library/common/http_impl.ex b/lib/archethic/contracts/interpreter/library/common/http_impl.ex index c4ca8acdf..6c3d4e46e 100644 --- a/lib/archethic/contracts/interpreter/library/common/http_impl.ex +++ b/lib/archethic/contracts/interpreter/library/common/http_impl.ex @@ -38,6 +38,9 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do {:ok, {:error, :threshold_reached}} -> raise Library.Error, message: "Http.fetch/1 response is bigger than threshold" + {:ok, {:error, :not_https}} -> + raise Library.Error, message: "Http.fetch/1 was called with a non https url" + {:ok, {:error, _}} -> # Mint.HTTP.connect error # Mint.HTTP.stream error @@ -49,7 +52,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do nil -> # Task.shutdown - raise Library.Error, message: "Http.fetch/1 timed out for url: #{uri}" + raise Library.Error, message: "Http.fetch/1 timed out" end end @@ -87,6 +90,10 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do raise Library.Error, message: "Http.fetch_many/1 response is bigger than threshold for url: #{uri}" + {{:ok, {:error, :not_https}}, uri}, _ -> + raise Library.Error, + message: "Http.fetch_many/1 was called with a non https url: #{uri}" + {{:ok, {:error, _}}, uri}, _ -> # Mint.HTTP.connect error # Mint.HTTP.stream error diff --git a/lib/archethic_web/api/jsonrpc/error.ex b/lib/archethic_web/api/jsonrpc/error.ex index f15ca3347..4bc24fde9 100644 --- a/lib/archethic_web/api/jsonrpc/error.ex +++ b/lib/archethic_web/api/jsonrpc/error.ex @@ -37,7 +37,6 @@ defmodule ArchethicWeb.API.JsonRPC.Error do # Smart Contract context defp get_custom_code(:contract_failure), do: 203 defp get_custom_code(:no_recipients), do: 204 - defp get_custom_code(:invalid_triggers_execution), do: 205 defp get_custom_code(:invalid_transaction_constraints), do: 206 defp get_custom_code(:invalid_inherit_constraints), do: 207 defp get_custom_code(:parsing_contract), do: 208 diff --git a/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex b/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex index 4e17cb319..1d30306f9 100644 --- a/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex +++ b/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex @@ -78,14 +78,34 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do ) do with {:ok, contract_tx} <- Archethic.get_last_transaction(recipient_address), - {:ok, contract} <- Contracts.from_transaction(contract_tx), + {:ok, contract} <- validate_and_parse_contract_tx(contract_tx), trigger <- Contract.get_trigger_for_recipient(recipient), :ok <- validate_contract_condition(trigger, contract, trigger_tx, recipient, timestamp), - {:ok, next_tx} <- Contracts.execute_trigger(trigger, contract, trigger_tx, recipient) do + {:ok, next_tx} <- validate_and_execute_trigger(trigger, contract, trigger_tx, recipient) do validate_contract_condition(:inherit, contract, next_tx, nil, timestamp) end end + defp validate_and_parse_contract_tx(tx) do + case Contracts.from_transaction(tx) do + {:ok, contract} -> + {:ok, contract} + + {:error, reason} -> + {:error, {:parsing_error, reason}} + end + end + + def validate_and_execute_trigger(trigger, contract, trigger_tx, recipient) do + case Contracts.execute_trigger(trigger, contract, trigger_tx, recipient) do + {:ok, nil_or_next_tex} -> + {:ok, nil_or_next_tex} + + {:error, reason} -> + {:error, {:execute_error, reason}} + end + end + defp create_valid_response(%Recipient{address: recipient_address}) do %{"recipient_address" => Base.encode16(recipient_address), "valid" => true} end @@ -107,14 +127,6 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do defp format_reason(:network_issue), do: {:internal_error, "Cannot fetch contract transaction"} - defp format_reason(:contract_failure), - do: {:custom_error, :contract_failure, "Contract execution encountered an error"} - - defp format_reason(:invalid_triggers_execution), - do: - {:custom_error, :invalid_triggers_execution, - "Contract does not contain a trigger transaction"} - defp format_reason(:invalid_transaction_constraints), do: {:custom_error, :invalid_transaction_constraints, @@ -128,9 +140,12 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do defp format_reason(:timeout), do: {:internal_error, "Timeout while simulating contract execution"} - defp format_reason(reason) when is_binary(reason), + defp format_reason({:parsing_error, reason}) when is_binary(reason), do: {:custom_error, :parsing_contract, "Error while parsing contract", reason} + defp format_reason({:execute_error, reason}) when is_binary(reason), + do: {:custom_error, :contract_failure, "Error while executing contract", reason} + defp format_reason(_), do: {:internal_error, "Unknown error"} defp validate_contract_condition(condition_type, contract, tx, recipient, timestamp) do diff --git a/lib/archethic_web/api/rest/controllers/transaction_controller.ex b/lib/archethic_web/api/rest/controllers/transaction_controller.ex index 451ee5af9..5547ce4aa 100644 --- a/lib/archethic_web/api/rest/controllers/transaction_controller.ex +++ b/lib/archethic_web/api/rest/controllers/transaction_controller.ex @@ -248,22 +248,6 @@ defmodule ArchethicWeb.API.REST.TransactionController do {:error, "Network issue, please try again later."} # execute_contract errors - {:error, :contract_failure} -> - {:error, "Contract execution produced an error."} - - {:error, :invalid_triggers_execution} -> - {:error, "Contract does not have a `actions triggered_by: transaction` block."} - - {:error, :invalid_transaction_constraints} -> - {:error, - "Contract refused incoming transaction. Check the `condition transaction` block."} - - {:error, :invalid_oracle_constraints} -> - {:error, "Contract refused incoming transaction. Check the `condition oracle` block."} - - {:error, :invalid_inherit_constraints} -> - {:error, "Contract refused outcoming transaction. Check the `condition inherit` block."} - # parse_contract errors {:error, reason} when is_binary(reason) -> {:error, reason} diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index c7efdad8b..0389d922b 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -656,6 +656,32 @@ defmodule Archethic.Contracts.InterpreterTest do ) end + test "should return the proper line in case of error" do + code = """ + @version 1 + condition triggered_by: transaction, as: [] + actions triggered_by: transaction do + div_by_zero() + end + + export fun div_by_zero() do + 1 / 0 + end + """ + + contract_tx = ContractFactory.create_valid_contract_tx(code) + + incoming_tx = TransactionFactory.create_valid_transaction([]) + + assert {:error, "division_by_zero - L8"} = + Interpreter.execute_trigger( + {:transaction, nil, nil}, + Contract.from_transaction!(contract_tx), + incoming_tx, + nil + ) + end + test "should return nil when the contract is correct but no Contract.* call" do code = """ @version 1 @@ -680,7 +706,7 @@ defmodule Archethic.Contracts.InterpreterTest do ) end - test "should return contract_failure if contract code crash" do + test "should return an error if contract code crash" do code = """ @version 1 condition triggered_by: transaction, as: [] @@ -696,7 +722,7 @@ defmodule Archethic.Contracts.InterpreterTest do incoming_tx = TransactionFactory.create_valid_transaction([]) assert match?( - {:error, :contract_failure}, + {:error, "division_by_zero - L5"}, Interpreter.execute_trigger( {:transaction, nil, nil}, Contract.from_transaction!(contract_tx), @@ -717,7 +743,7 @@ defmodule Archethic.Contracts.InterpreterTest do contract_tx = ContractFactory.create_valid_contract_tx(code) assert match?( - {:error, :contract_failure}, + {:error, "Contract used add_uco_transfer with an invalid amount - L5"}, Interpreter.execute_trigger( {:transaction, nil, nil}, Contract.from_transaction!(contract_tx), diff --git a/test/archethic_web/api/jsonrpc/error_test.exs b/test/archethic_web/api/jsonrpc/error_test.exs index 3c34be614..b97c72039 100644 --- a/test/archethic_web/api/jsonrpc/error_test.exs +++ b/test/archethic_web/api/jsonrpc/error_test.exs @@ -32,7 +32,6 @@ defmodule ArchethicWeb.API.JsonRPC.ErrorTest do test "should return custom error code for smart contract context" do assert %{"code" => 203} = Error.get_error({:custom_error, :contract_failure, ""}) assert %{"code" => 204} = Error.get_error({:custom_error, :no_recipients, ""}) - assert %{"code" => 205} = Error.get_error({:custom_error, :invalid_triggers_execution, ""}) assert %{"code" => 206} = Error.get_error({:custom_error, :invalid_transaction_constraints, ""})