diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index 7be94c43c..71063c73e 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -258,27 +258,35 @@ defmodule Archethic.Contracts do defp get_condition_constants( :inherit, - %Contract{constants: %Constants{contract: contract_constant}}, + %Contract{ + constants: %Constants{contract: contract_constant}, + functions: functions + }, transaction, datetime ) do %{ "previous" => contract_constant, "next" => Constants.from_transaction(transaction), - "_time_now" => DateTime.to_unix(datetime) + "_time_now" => DateTime.to_unix(datetime), + "functions" => functions } end defp get_condition_constants( _, - %Contract{constants: %Constants{contract: contract_constant}}, + %Contract{ + constants: %Constants{contract: contract_constant}, + functions: functions + }, transaction, datetime ) do %{ "transaction" => Constants.from_transaction(transaction), "contract" => contract_constant, - "_time_now" => DateTime.to_unix(datetime) + "_time_now" => DateTime.to_unix(datetime), + "functions" => functions } end end diff --git a/lib/archethic/contracts/contract.ex b/lib/archethic/contracts/contract.ex index f73ed963e..99c83b9bf 100644 --- a/lib/archethic/contracts/contract.ex +++ b/lib/archethic/contracts/contract.ex @@ -14,6 +14,7 @@ defmodule Archethic.Contracts.Contract do alias Archethic.TransactionChain.TransactionData defstruct triggers: %{}, + functions: %{}, version: 0, conditions: %{}, constants: %Constants{}, @@ -94,4 +95,28 @@ defmodule Archethic.Contracts.Contract do ) do Map.update!(contract, :conditions, &Map.put(&1, condition_name, conditions)) end + + @doc """ + Add a public or private function to the contract + """ + @spec add_function( + contract :: t(), + function_name :: binary(), + ast :: any(), + args :: list(), + visibility :: atom() + ) :: t() + def add_function( + contract = %__MODULE__{}, + function_name, + ast, + args, + visibility + ) do + Map.update!( + contract, + :functions, + &Map.put(&1, {function_name, length(args)}, %{args: args, ast: ast, visibility: visibility}) + ) + end end diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 64ad3163d..2119fa525 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -6,6 +6,8 @@ defmodule Archethic.Contracts.Interpreter do alias __MODULE__.Legacy alias __MODULE__.ActionInterpreter alias __MODULE__.ConditionInterpreter + alias __MODULE__.FunctionInterpreter + alias __MODULE__.ConditionValidator alias Archethic.Contracts.Contract @@ -19,6 +21,7 @@ defmodule Archethic.Contracts.Interpreter do @type version() :: integer() @type execute_opts :: [time_now: DateTime.t()] + @type function_key() :: {String.t(), integer()} @doc """ Dispatch through the correct interpreter. @@ -117,7 +120,8 @@ defmodule Archethic.Contracts.Interpreter do %Contract{ version: version, triggers: triggers, - constants: %Constants{contract: contract_constants} + constants: %Constants{contract: contract_constants}, + functions: functions }, maybe_trigger_tx, opts \\ [] @@ -150,7 +154,8 @@ defmodule Archethic.Contracts.Interpreter do Constants.from_transaction(trigger_tx) end, "contract" => contract_constants, - "_time_now" => timestamp_now + "_time_now" => timestamp_now, + "functions" => functions } result = @@ -291,7 +296,9 @@ defmodule Archethic.Contracts.Interpreter do end defp parse_contract(1, ast) do - case parse_ast_block(ast, %Contract{}) do + functions_keys = get_function_keys(ast) + + case parse_ast_block(ast, %Contract{}, functions_keys) do {:ok, contract} -> {:ok, %{contract | version: 1}} @@ -304,20 +311,20 @@ defmodule Archethic.Contracts.Interpreter do {:error, "@version not supported"} end - defp parse_ast_block([ast | rest], contract) do - case parse_ast(ast, contract) do + defp parse_ast_block([ast | rest], contract, functions_keys) do + case parse_ast(ast, contract, functions_keys) do {:ok, contract} -> - parse_ast_block(rest, contract) + parse_ast_block(rest, contract, functions_keys) {:error, _, _} = e -> e end end - defp parse_ast_block([], contract), do: {:ok, contract} + defp parse_ast_block([], contract, _), do: {:ok, contract} - defp parse_ast(ast = {{:atom, "condition"}, _, _}, contract) do - case ConditionInterpreter.parse(ast) do + defp parse_ast(ast = {{:atom, "condition"}, _, _}, contract, functions_keys) do + case ConditionInterpreter.parse(ast, functions_keys) do {:ok, condition_type, condition} -> {:ok, Contract.add_condition(contract, condition_type, condition)} @@ -326,8 +333,8 @@ defmodule Archethic.Contracts.Interpreter do end end - defp parse_ast(ast = {{:atom, "actions"}, _, _}, contract) do - case ActionInterpreter.parse(ast) do + defp parse_ast(ast = {{:atom, "actions"}, _, _}, contract, functions_keys) do + case ActionInterpreter.parse(ast, functions_keys) do {:ok, trigger_type, actions} -> {:ok, Contract.add_trigger(contract, trigger_type, actions)} @@ -336,7 +343,31 @@ defmodule Archethic.Contracts.Interpreter do end end - defp parse_ast(ast, _), do: {:error, ast, "unexpected term"} + defp parse_ast( + ast = {{:atom, "export"}, _, [{{:atom, "fun"}, _, _} | _]}, + contract, + functions_keys + ) do + case FunctionInterpreter.parse(ast, functions_keys) do + {:ok, function_name, args, ast} -> + {:ok, Contract.add_function(contract, function_name, ast, args, :public)} + + {:error, _, _} = e -> + e + end + end + + defp parse_ast(ast = {{:atom, "fun"}, _, _}, contract, functions_keys) do + case FunctionInterpreter.parse(ast, functions_keys) do + {:ok, function_name, args, ast} -> + {:ok, Contract.add_function(contract, function_name, ast, args, :private)} + + {:error, _, _} = e -> + e + end + end + + defp parse_ast(ast, _, _), do: {:error, ast, "unexpected term"} defp time_now(:transaction, %Transaction{ validation_stamp: %ValidationStamp{timestamp: timestamp} @@ -358,6 +389,21 @@ defmodule Archethic.Contracts.Interpreter do Utils.get_current_time_for_interval(interval) end + defp get_function_keys([{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | rest]) do + [{function_name, length(args)} | get_function_keys(rest)] + end + + defp get_function_keys([ + {{:atom, "export"}, _, + [{{:atom, "fun"}, _, [{{:atom, function_name}, _, args} | _]} | _]} + | rest + ]) do + [{function_name, length(args)} | get_function_keys(rest)] + end + + defp get_function_keys([_ | rest]), do: get_function_keys(rest) + defp get_function_keys([]), do: [] + # ----------------------------------------- # contract validation # ----------------------------------------- diff --git a/lib/archethic/contracts/interpreter/action_interpreter.ex b/lib/archethic/contracts/interpreter/action_interpreter.ex index 0d72722aa..29df0a456 100644 --- a/lib/archethic/contracts/interpreter/action_interpreter.ex +++ b/lib/archethic/contracts/interpreter/action_interpreter.ex @@ -5,6 +5,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do alias Archethic.TransactionChain.TransactionData alias Archethic.Contracts.ContractConstants, as: Constants + alias Archethic.Contracts.Interpreter alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter.CommonInterpreter alias Archethic.Contracts.Interpreter.Library @@ -13,13 +14,14 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do @doc """ Parse the given node and return the trigger and the actions block. """ - @spec parse(any()) :: {:ok, atom(), any()} | {:error, any(), String.t()} - def parse({{:atom, "actions"}, _, [keyword, [do: block]]}) do + @spec parse(any(), list(Interpreter.function_key())) :: + {:ok, atom(), any()} | {:error, any(), String.t()} + def parse({{:atom, "actions"}, _, [keyword, [do: block]]}, functions_keys) do trigger_type = extract_trigger(keyword) # We only parse the do..end block with the macro.traverse # this help us keep a clean accumulator that is used only for scoping. - actions_ast = parse_block(AST.wrap_in_block(block)) + actions_ast = parse_block(AST.wrap_in_block(block), functions_keys) {:ok, trigger_type, actions_ast} catch @@ -30,7 +32,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do {:error, node, reason} end - def parse(node) do + def parse(node, _) do {:error, node, "unexpected term"} end @@ -127,7 +129,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do throw({:error, node, "Invalid trigger"}) end - defp parse_block(ast) do + defp parse_block(ast, functions_keys) do # here the accumulator is an list of parent scopes & current scope # where we can access variables from all of them # `acc = [ref1]` means read variable from scope.ref1 or scope @@ -142,7 +144,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do prewalk(node, acc) end, fn node, acc -> - postwalk(node, acc) + postwalk(node, acc, functions_keys) end ) @@ -187,7 +189,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do defp postwalk( node = {{:., _meta, [{:__aliases__, _, [atom: "Contract"]}, {:atom, function_name}]}, _, args}, - acc + acc, + _ ) do absolute_module_atom = Archethic.Contracts.Interpreter.Library.Contract @@ -204,7 +207,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do function_atom = String.to_existing_atom(function_name) - # check the type of the args + # check the type of the args, and allow custom function call as args unless absolute_module_atom.check_types(function_atom, args) do throw({:error, node, "invalid function arguments"}) end @@ -226,8 +229,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do end # --------------- catch all ------------------- - defp postwalk(node, acc) do - CommonInterpreter.postwalk(node, acc) + defp postwalk(node, acc, functions_keys) do + CommonInterpreter.postwalk(node, acc, functions_keys) end # keep only the transaction fields we are interested in diff --git a/lib/archethic/contracts/interpreter/common_interpreter.ex b/lib/archethic/contracts/interpreter/common_interpreter.ex index bc0184c11..5a0f3a282 100644 --- a/lib/archethic/contracts/interpreter/common_interpreter.ex +++ b/lib/archethic/contracts/interpreter/common_interpreter.ex @@ -253,6 +253,9 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do {ast, acc} end + # function call, should be placed after "for" prewalk + def prewalk(node = {{:atom, _}, _, args}, acc) when is_list(args), do: {node, acc} + # log (not documented, only useful for developer debugging) # will soon be updated to log into the playground console def prewalk(_node = {{:atom, "log"}, _, [data]}, acc) do @@ -274,7 +277,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # exit block == set parent scope def postwalk( node = {:__block__, _, _}, - acc + acc, + _ ) do {node, List.delete_at(acc, -1)} end @@ -283,7 +287,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do def postwalk( node = {{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args}, - acc + acc, + _ ) when module_name in @modules_whitelisted do absolute_module_atom = @@ -307,6 +312,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do function_atom = String.to_existing_atom(function_name) # check the type of the args + unless absolute_module_atom.check_types(function_atom, args) do throw({:error, node, "invalid function arguments"}) end @@ -322,7 +328,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do # variable are read from scope def postwalk( _node = {{:atom, var_name}, _, nil}, - acc + acc, + _ ) do new_node = quote do @@ -340,7 +347,8 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do {:%{}, _, [{var_name, list}]}, [do: block] ]}, - acc + acc, + _ ) do # FIXME: here acc is already the parent acc, it is not the acc of the do block # FIXME: this means that our `var_name` will live in the parent scope @@ -360,8 +368,23 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do {new_node, acc} end + def postwalk(node = {{:atom, function_name}, _, args}, acc, function_keys) when is_list(args) do + if Enum.member?(function_keys, {function_name, length(args)}) do + new_node = + quote do + Scope.execute_function_ast(unquote(function_name), unquote(args)) + end + + {new_node, acc} + else + reason = "The function #{function_name}/#{Integer.to_string(length(args))} does not exist" + + throw({:error, node, reason}) + end + 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)) @@ -371,7 +394,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do end # whitelist rest - def postwalk(node, acc), do: {node, acc} + def postwalk(node, acc, _), do: {node, acc} # ---------------------------------------------------------------------- # _ _ diff --git a/lib/archethic/contracts/interpreter/condition_interpreter.ex b/lib/archethic/contracts/interpreter/condition_interpreter.ex index 1d3e0078c..a62e5d7fc 100644 --- a/lib/archethic/contracts/interpreter/condition_interpreter.ex +++ b/lib/archethic/contracts/interpreter/condition_interpreter.ex @@ -6,6 +6,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do alias Archethic.Contracts.Interpreter.Scope alias Archethic.Contracts.ContractConditions, as: Conditions alias Archethic.Contracts.Interpreter.ASTHelper, as: AST + alias Archethic.Contracts.Interpreter @modules_whitelisted Library.list_common_modules() @condition_fields Conditions.__struct__() @@ -18,9 +19,12 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do @doc """ Parse the given node and return the trigger and the actions block. """ - @spec parse(any()) :: + @spec parse(any(), list(Interpreter.function_key())) :: {:ok, condition_type(), Conditions.t()} | {:error, any(), String.t()} - def parse(node = {{:atom, "condition"}, _, [[{{:atom, condition_name}, keyword}]]}) do + def parse( + node = {{:atom, "condition"}, _, [[{{:atom, condition_name}, keyword}]]}, + functions_keys + ) do {condition_type, global_variable} = case condition_name do "transaction" -> {:transaction, "transaction"} @@ -39,7 +43,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do throw({:error, node, "invalid condition field: #{key}"}) end - new_value = to_boolean_expression([global_variable, key], value) + new_value = to_boolean_expression([global_variable, key], value, functions_keys) Map.put(acc, String.to_existing_atom(key), new_value) end) @@ -52,7 +56,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do {:error, node, reason} end - def parse(node) do + def parse(node, _) do {:error, node, "unexpected term"} end @@ -64,7 +68,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do # | .__/|_| |_| \_/ \__,_|\__\___| # |_| # ---------------------------------------------------------------------- - defp to_boolean_expression(_subject, bool) when is_boolean(bool) do + defp to_boolean_expression(_subject, bool, _) when is_boolean(bool) do bool end @@ -79,14 +83,14 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do # - `subject == ["next", "content"]` # - `value == "ciao"` # - defp to_boolean_expression(subject, value) + defp to_boolean_expression(subject, value, _) when is_binary(value) or is_integer(value) or is_float(value) do quote do unquote(value) == Scope.read_global(unquote(subject)) end end - defp to_boolean_expression(subject, ast) do + defp to_boolean_expression(subject, ast, functions_keys) do # here the accumulator is an list of parent scopes & current scope # where we can access variables from all of them # `acc = [ref1]` means read variable from scope.ref1 or scope @@ -101,7 +105,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do prewalk(subject, node, acc) end, fn node, acc -> - postwalk(subject, node, acc) + postwalk(subject, node, acc, functions_keys) end ) @@ -134,7 +138,8 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do subject, node = {{:., meta, [{:__aliases__, _, [atom: module_name]}, {:atom, function_name}]}, _, args}, - acc + acc, + function_keys ) when module_name in @modules_whitelisted do # if function exist with arity => node @@ -149,7 +154,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do cond do # check function is available with given arity Library.function_exists?(absolute_module_atom, function_name, arity) -> - {new_node, _} = CommonInterpreter.postwalk(node, acc) + {new_node, _} = CommonInterpreter.postwalk(node, acc, function_keys) new_node # if function exist with arity+1 => prepend the key to args @@ -164,7 +169,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do {{:., meta, [{:__aliases__, meta, [atom: module_name]}, {:atom, function_name}]}, meta, [ast | args]} - {new_node, _} = CommonInterpreter.postwalk(node_with_key_appended, acc) + {new_node, _} = CommonInterpreter.postwalk(node_with_key_appended, acc, function_keys) new_node # check function exists @@ -178,7 +183,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do {new_node, acc} end - defp postwalk(_subject, node, acc) do - CommonInterpreter.postwalk(node, acc) + defp postwalk(_subject, node, acc, functions_keys) do + CommonInterpreter.postwalk(node, acc, functions_keys) end end diff --git a/lib/archethic/contracts/interpreter/function_interpreter.ex b/lib/archethic/contracts/interpreter/function_interpreter.ex new file mode 100644 index 000000000..021693cf0 --- /dev/null +++ b/lib/archethic/contracts/interpreter/function_interpreter.ex @@ -0,0 +1,131 @@ +defmodule Archethic.Contracts.Interpreter.FunctionInterpreter do + @moduledoc false + alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.ASTHelper, as: AST + alias Archethic.Contracts.Interpreter.Scope + alias Archethic.Contracts.Interpreter.CommonInterpreter + require Logger + + @doc """ + Parse the given node and return the function name it's args as strings and the AST block. + """ + @spec parse(ast :: any(), function_keys :: list(Interpreter.function_key())) :: + {:ok, function_name :: binary(), args :: list(), function_ast :: any()} + | {:error, node :: any(), reason :: binary()} + def parse({{:atom, "fun"}, _, [{{:atom, function_name}, _, args}, [do: block]]}, functions_keys) do + ast = parse_block(AST.wrap_in_block(block), functions_keys) + args = parse_args(args) + {:ok, function_name, args, ast} + catch + {:error, node} -> + {:error, node, "unexpected term"} + + {:error, node, reason} -> + {:error, node, reason} + end + + def parse( + {{:atom, "export"}, _, + [{{:atom, "fun"}, _, [{{:atom, function_name}, _, args}]}, [do: block]]}, + functions_keys + ) do + ast = parse_block(AST.wrap_in_block(block), functions_keys) + args = parse_args(args) + + {:ok, function_name, args, ast} + catch + {:error, node} -> + {:error, node, "unexpected term"} + + {:error, node, reason} -> + {:error, node, reason} + end + + def parse(node, _) do + {:error, node, "unexpected term"} + end + + @doc """ + Execute function code and returns the result + """ + @spec execute(ast :: any(), constants :: map()) :: result :: any() + def execute(ast, constants) do + Scope.init(constants) + {result, _} = Code.eval_quoted(ast) + result + end + + # ---------------------------------------------------------------------- + # _ _ + # _ __ _ __(___ ____ _| |_ ___ + # | '_ \| '__| \ \ / / _` | __/ _ \ + # | |_) | | | |\ V | (_| | || __/ + # | .__/|_| |_| \_/ \__,_|\__\___| + # |_| + # ---------------------------------------------------------------------- + defp parse_block(ast, functions_keys) do + # here the accumulator is an list of parent scopes & current scope + # where we can access variables from all of them + # `acc = [ref1]` means read variable from scope.ref1 or scope + # `acc = [ref1, ref2]` means read variable from scope.ref1.ref2 or scope.ref1 or scope + # function's args are added to the acc by the interpreter + acc = [] + + {new_ast, _} = + Macro.traverse( + ast, + acc, + fn node, acc -> + prewalk(node, acc) + end, + fn node, acc -> + postwalk(node, acc, functions_keys) + end + ) + + new_ast + end + + defp parse_args(nil), do: [] + + defp parse_args(args) do + Enum.map(args, fn {{:atom, arg}, _, _} -> arg end) + end + + # ---------------------------------------------------------------------- + # _ _ + # _ __ _ __ _____ ____ _| | | __ + # | '_ \| '__/ _ \ \ /\ / / _` | | |/ / + # | |_) | | | __/\ V V | (_| | | < + # | .__/|_| \___| \_/\_/ \__,_|_|_|\_\ + # |_| + # ---------------------------------------------------------------------- + + # Ban access to Contract module + defp prewalk( + node = {:__aliases__, _, [atom: "Contract"]}, + _ + ) do + throw({:error, node, "Contract is not allowed in function"}) + end + + defp prewalk( + node, + acc + ) do + CommonInterpreter.prewalk(node, acc) + end + + # ---------------------------------------------------------------------- + # _ _ _ + # _ __ ___ ___| |___ ____ _| | | __ + # | '_ \ / _ \/ __| __\ \ /\ / / _` | | |/ / + # | |_) | (_) \__ | |_ \ V V | (_| | | < + # | .__/ \___/|___/\__| \_/\_/ \__,_|_|_|\_\ + # |_| + # ---------------------------------------------------------------------- + # --------------- catch all ------------------- + defp postwalk(node, acc, function_keys) do + CommonInterpreter.postwalk(node, acc, function_keys) + end +end diff --git a/lib/archethic/contracts/interpreter/scope.ex b/lib/archethic/contracts/interpreter/scope.ex index 59cb2dfa2..82542b84b 100644 --- a/lib/archethic/contracts/interpreter/scope.ex +++ b/lib/archethic/contracts/interpreter/scope.ex @@ -114,6 +114,20 @@ defmodule Archethic.Contracts.Interpreter.Scope do ) end + defp get_function_ast(function_name, args) do + get_in(Process.get(:scope), ["functions", {function_name, length(args)}, :ast]) + end + + @doc """ + Execute a function AST + """ + @spec execute_function_ast(String.t(), list(any())) :: any() + def execute_function_ast(function_name, args) do + get_function_ast(function_name, args) + |> Code.eval_quoted() + |> elem(0) + end + # Return the path where to assign/read a variable. # It will recurse from the deepest path to the root path until it finds a match. # If no match it will return the current path. diff --git a/test/archethic/contracts/interpreter/action_interpreter_test.exs b/test/archethic/contracts/interpreter/action_interpreter_test.exs index 3688f4a5c..26916e3b9 100644 --- a/test/archethic/contracts/interpreter/action_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/action_interpreter_test.exs @@ -33,7 +33,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use whitelisted module existing function" do @@ -47,7 +47,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to have comments" do @@ -62,7 +62,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should return the correct trigger type" do @@ -75,7 +75,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) code = ~S""" actions triggered_by: interval, at: "* * * * *" do @@ -86,7 +86,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) code = ~S""" actions triggered_by: datetime, at: 1676282760 do @@ -97,7 +97,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not parse if datetime is not rounded" do @@ -110,7 +110,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should return a proper error message if trigger is invalid" do @@ -123,7 +123,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use whitelisted module non existing function" do @@ -137,7 +137,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) code = ~S""" actions triggered_by: transaction do @@ -149,7 +149,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to create variables" do @@ -163,7 +163,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to create lists" do @@ -177,7 +177,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to create keywords" do @@ -191,7 +191,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use common functions" do @@ -206,7 +206,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use non existing functions" do @@ -221,7 +221,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) code = ~S""" actions triggered_by: transaction do @@ -234,7 +234,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use wrong types in common functions" do @@ -249,7 +249,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use the result of a function call as a parameter" do @@ -263,7 +263,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use a function call as a parameter to a lib function" do @@ -279,7 +279,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use wrong types in contract functions" do @@ -293,7 +293,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use if as an expression" do @@ -311,7 +311,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should not be able to use for as an expression" do @@ -327,7 +327,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use nested ." do @@ -344,7 +344,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) code = ~S""" actions triggered_by: transaction do @@ -358,7 +358,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use [] access with a string" do @@ -374,7 +374,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use [] access with a variable" do @@ -391,7 +391,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use [] access with a dot access" do @@ -408,7 +408,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use [] access with a fn call" do @@ -424,7 +424,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use nested [] access" do @@ -440,7 +440,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use loop" do @@ -460,7 +460,7 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) end test "should be able to use ranges" do @@ -474,7 +474,36 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ActionInterpreter.parse() + |> ActionInterpreter.parse([]) + end + + test "should not be able to use non existing function" do + code = ~S""" + actions triggered_by: transaction do + hello() + end + """ + + assert {:error, _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse([]) + end + + test "should be able to use existing function" do + code = ~S""" + actions triggered_by: transaction do + hello() + end + """ + + assert {:ok, _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + # mark as existing + |> ActionInterpreter.parse([{"hello", 0}]) end end diff --git a/test/archethic/contracts/interpreter/condition_interpreter_test.exs b/test/archethic/contracts/interpreter/condition_interpreter_test.exs index 403d416bb..7de24d811 100644 --- a/test/archethic/contracts/interpreter/condition_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/condition_interpreter_test.exs @@ -5,6 +5,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.Contracts.Interpreter alias Archethic.Contracts.Interpreter.ConditionInterpreter + alias Archethic.Contracts.Interpreter.ConditionValidator alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -26,7 +27,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "parse a condition oracle" do @@ -38,7 +39,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "parse a condition transaction" do @@ -50,7 +51,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "does not parse anything else" do @@ -62,7 +63,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end end @@ -76,7 +77,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "parse strict value" do @@ -90,7 +91,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) assert is_tuple(ast) && :ok == Macro.validate(ast) end @@ -106,7 +107,24 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) + + assert is_tuple(ast) && :ok == Macro.validate(ast) + end + + test "parse custom functions" do + code = ~s""" + condition transaction: [ + uco_transfers: get_uco_transfers() > 0 + ] + """ + + assert {:ok, :transaction, %Conditions{uco_transfers: ast}} = + code + |> Interpreter.sanitize_code() + |> elem(1) + # mark function as existing + |> ConditionInterpreter.parse([{"get_uco_transfers", 0}]) assert is_tuple(ast) && :ok == Macro.validate(ast) end @@ -122,7 +140,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "parse false" do @@ -136,7 +154,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end test "parse AST" do @@ -150,7 +168,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) end end @@ -170,7 +188,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -190,7 +208,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -214,7 +232,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -234,7 +252,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -253,7 +271,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do refute code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -287,7 +305,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -305,7 +323,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do refute code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -355,7 +373,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -386,7 +404,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -401,7 +419,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -416,7 +434,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do refute code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -431,7 +449,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "transaction" => Constants.from_transaction(tx) @@ -451,7 +469,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -470,7 +488,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do refute code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -491,7 +509,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -510,7 +528,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -529,7 +547,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do refute code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -553,7 +571,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), @@ -598,7 +616,7 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do assert code |> Interpreter.sanitize_code() |> elem(1) - |> ConditionInterpreter.parse() + |> ConditionInterpreter.parse([]) |> elem(2) |> ConditionValidator.valid_conditions?(%{ "previous" => Constants.from_transaction(previous_tx), diff --git a/test/archethic/contracts/interpreter/function_interpreter_test.exs b/test/archethic/contracts/interpreter/function_interpreter_test.exs new file mode 100644 index 000000000..185705f24 --- /dev/null +++ b/test/archethic/contracts/interpreter/function_interpreter_test.exs @@ -0,0 +1,160 @@ +defmodule Archethic.Contracts.Interpreter.FunctionInterpreterTest do + @moduledoc false + + use ArchethicCase + use ExUnitProperties + + alias Archethic.Contracts.Interpreter.FunctionInterpreter + alias Archethic.Contracts.Interpreter + + # ---------------------------------------------- + # parse/2 + # ---------------------------------------------- + describe "parse/2" do + test "should be able to parse a private function" do + code = ~S""" + fun test_private do + + end + """ + + assert {:ok, "test_private", _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should be able to parse a public function" do + code = ~S""" + export fun test_public do + + end + """ + + assert {:ok, "test_public", _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should be able to parse a private function with arguments" do + code = ~S""" + fun test_private(arg1, arg2) do + + end + """ + + assert {:ok, "test_private", ["arg1", "arg2"], _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should be able to parse a public function with arguments" do + code = ~S""" + export fun test_public(arg1, arg2) do + + end + """ + + assert {:ok, "test_public", ["arg1", "arg2"], _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should not be able to use non-whitelisted modules" do + code = ~S""" + fun test_private do + Contract.set_content "hello" + end + """ + + assert {:error, _, "Contract is not allowed in function"} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should be able to parse when there is whitelisted module" do + code = ~S""" + fun test do + Json.to_string "[1,2,3]" + end + """ + + assert {:ok, "test", [], _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should not be able to call non declared function" do + code = ~S""" + fun test do + hello() + end + """ + + assert {:error, _, "The function hello/0 does not exist"} = + code + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + end + + test "should be able to call declared function" do + code = ~S""" + fun test do + hello() + end + """ + + assert {:ok, "test", _, _} = + code + |> Interpreter.sanitize_code() + |> elem(1) + # mark function as declared + |> FunctionInterpreter.parse([{"hello", 0}]) + end + end + + describe "execute/2" do + test "should be able to execute function without args" do + fun1 = ~S""" + export fun hello do + 1 + 3 + end + """ + + {:ok, "hello", [], ast_hello} = + fun1 + |> Interpreter.sanitize_code() + |> elem(1) + |> FunctionInterpreter.parse([]) + + fun2 = ~S""" + fun test() do + hello() + end + """ + + {:ok, "test", [], ast_test} = + fun2 + |> Interpreter.sanitize_code() + |> elem(1) + # pass allowed function + |> FunctionInterpreter.parse([{"hello", 0}]) + + function_constant = %{"functions" => %{{"hello", 0} => %{args: [], ast: ast_hello}}} + + assert 4.0 = FunctionInterpreter.execute(ast_test, function_constant) + end + end +end diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 88d782517..742f34372 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -93,6 +93,89 @@ defmodule Archethic.Contracts.InterpreterTest do |> Interpreter.parse() end + test "should be able to use custom functions" do + assert {:ok, _} = + """ + @version 1 + + fun hello_world() do + "hello world" + end + + condition transaction: [] + actions triggered_by: transaction do + x = hello_world() + x + end + + """ + |> Interpreter.parse() + end + + test "should be able to use custom functions no matter the declaration order" do + assert {:ok, _} = + """ + @version 1 + + fun hello() do + "hello world" + end + + condition transaction: [] + actions triggered_by: transaction do + hello_world() + end + + fun hey() do + hello() + end + + fun hello_world() do + hey() + end + + """ + |> Interpreter.parse() + end + + test "should return an human readable error if custom function does not exist" do + assert {:error, "The function hello_world/0 does not exist - hello_world - L9"} = + """ + @version 1 + + fun hello() do + "hello world" + end + + condition transaction: [] + actions triggered_by: transaction do + x = hello_world() + x + end + + """ + |> Interpreter.parse() + end + + test "should return an human readable error if custom fn is called with bad arity" do + assert {:error, "The function hello_world/1 does not exist - hello_world - L9"} = + """ + @version 1 + + fun hello_world() do + "hello world" + end + + condition transaction: [] + actions triggered_by: transaction do + x = hello_world(1) + x + end + + """ + |> Interpreter.parse() + end + test "should return an human readable error if lib fn is called with bad arity" do assert {:error, "invalid function arity - List.empty?([1], \"foobar\") - L4"} = """ @@ -216,6 +299,193 @@ defmodule Archethic.Contracts.InterpreterTest do Contract.from_transaction!(contract_tx), incoming_tx ) + + code = """ + @version 1 + + condition transaction: [] + actions triggered_by: transaction do + Contract.set_content "hello" + end + + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{}, + validation_stamp: ValidationStamp.generate_dummy() + } + + assert {:ok, + %Transaction{ + data: %TransactionData{ + content: "hello" + } + }} = + Interpreter.execute_trigger( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + end + + test "should be able to use a custom function call as parameter" do + code = """ + @version 1 + + fun hello_world() do + "hello world" + end + + condition transaction: [] + actions triggered_by: transaction do + Contract.set_content hello_world() + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{}, + validation_stamp: ValidationStamp.generate_dummy() + } + + assert {:ok, + %Transaction{ + data: %TransactionData{ + content: "hello world" + } + }} = + Interpreter.execute_trigger( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + end + + test "Should not be able to use out of scope variables" do + code = """ + @version 1 + condition transaction: [] + actions triggered_by: transaction do + my_var = "toto" + Contract.set_content my_func() + end + + fun my_func() do + my_var + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{}, + validation_stamp: ValidationStamp.generate_dummy() + } + + assert {:error, _} = + Interpreter.execute_trigger( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + + code = """ + @version 1 + condition transaction: [] + actions triggered_by: transaction do + temp = func1() + Contract.set_content func2() + end + + export fun func1() do + my_var = "content" + end + + fun func2() do + my_var + end + + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{}, + validation_stamp: ValidationStamp.generate_dummy() + } + + assert {:error, _} = + Interpreter.execute_trigger( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + end + + test "Should be able to use variables from scope in functions" do + code = """ + @version 1 + condition transaction: [] + actions triggered_by: transaction do + Contract.set_content my_func() + end + + fun my_func() do + my_var = "" + if true do + my_var = "toto" + end + my_var + end + + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{}, + validation_stamp: ValidationStamp.generate_dummy() + } + + assert {:ok, _} = + Interpreter.execute_trigger( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) end test "should return nil when the contract is correct but no Contract.* call" do diff --git a/test/support/template.ex b/test/support/template.ex index 7c3dedc52..8502206c9 100644 --- a/test/support/template.ex +++ b/test/support/template.ex @@ -260,9 +260,9 @@ defmodule ArchethicCase do expect(mock, function_name, 0, function) end - def sanitize_parse_execute(code, constants \\ %{}) do + def sanitize_parse_execute(code, constants \\ %{}, functions \\ []) do with {:ok, sanitized_code} <- Interpreter.sanitize_code(code), - {:ok, _, action_ast} <- ActionInterpreter.parse(sanitized_code) do + {:ok, _, action_ast} <- ActionInterpreter.parse(sanitized_code, functions) do ActionInterpreter.execute( action_ast, constants |> ContractFactory.append_contract_constant(code)