diff --git a/lib/archethic/contracts/interpreter/action.ex b/lib/archethic/contracts/interpreter/action.ex index 9b7015732..06c35553e 100644 --- a/lib/archethic/contracts/interpreter/action.ex +++ b/lib/archethic/contracts/interpreter/action.ex @@ -146,132 +146,109 @@ defmodule Archethic.Contracts.ActionInterpreter do {node, acc} end - # Whitelist the add_uco_transfer function parameters + # Blacklist the add_uco_transfer argument list defp prewalk( node = {{:atom, "to"}, address}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} ) - when is_binary(address) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "to"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} - ) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_uco_transfer arguments", node}) end defp prewalk( node = {{:atom, "amount"}, amount}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} ) - when is_integer(amount) and amount > 0 do - {node, acc} + when (not is_tuple(amount) and not is_integer(amount)) or amount <= 0 do + throw({:error, "invalid add_uco_transfer arguments", node}) end defp prewalk( - node = {{:atom, "amount"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + ) + when atom != "to" and atom != "amount" do + throw({:error, "invalid add_uco_transfer arguments", node}) end - # Whitelist the add_token_transfer argument list + # Blacklist the add_token_transfer argument list defp prewalk( node = {{:atom, "to"}, address}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_binary(address) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "to"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( node = {{:atom, "amount"}, amount}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_integer(amount) and amount > 0 do - {node, acc} - end - - defp prewalk( - node = {{:atom, "amount"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} + when (not is_tuple(amount) and not is_integer(amount)) or amount <= 0 do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "token_address"}, token_address}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + node = {{:atom, "token_address"}, address}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_binary(token_address) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "token_address"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "token_id"}, token_id}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + node = {{:atom, "token_id"}, id}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_integer(token_id) and token_id >= 0 do - {node, acc} + when (not is_tuple(id) and not is_integer(id)) or id < 0 do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "token_id"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + ) + when atom != "to" and atom != "amount" and atom != "token_address" and atom != "token_id" do + throw({:error, "invalid add_token_transfer arguments", node}) end - # Whitelist the add_ownership argument list + # Blacklist the add_ownership argument list defp prewalk( node = {{:atom, "secret"}, secret}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} ) - when is_binary(secret) do - {node, acc} + when not is_tuple(secret) and not is_binary(secret) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "secret"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, "secret_key"}, secret_key}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + ) + when not is_tuple(secret_key) and not is_binary(secret_key) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "secret_key"}, _secret_key}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, "authorized_public_keys"}, authorized_public_keys}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + ) + when not is_tuple(authorized_public_keys) and not is_list(authorized_public_keys) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "authorized_public_keys"}, authorized_public_keys}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} ) - when is_list(authorized_public_keys) do - {node, acc} + when atom != "secret" and atom != "secret_key" and atom != "authorized_public_keys" do + throw({:error, "invalid add_ownership arguments", node}) end + # Whitelist the keywords defp prewalk( - node = {{:atom, "authorized_public_keys"}, {{:atom, _, _}}}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + node = {{:atom, _}, _}, + acc = {:ok, _} ) do {node, acc} end diff --git a/lib/archethic/contracts/interpreter/transaction_statements.ex b/lib/archethic/contracts/interpreter/transaction_statements.ex index bfd06a205..0db8449e4 100644 --- a/lib/archethic/contracts/interpreter/transaction_statements.ex +++ b/lib/archethic/contracts/interpreter/transaction_statements.ex @@ -216,4 +216,146 @@ defmodule Archethic.Contracts.Interpreter.TransactionStatements do &[recipient_address | &1] ) end + + @doc """ + Add multiple recipients + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> TransactionStatements.add_recipients(%Transaction{data: %TransactionData{recipients: [address1]}}, [address2]) + %Transaction{ + data: %TransactionData{ + recipients: [address2, address1] + } + } + """ + @spec add_recipients(Transaction.t(), list(binary())) :: Transaction.t() + def add_recipients(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_recipient(&2, &1)) + end + + @doc """ + Add multiple ownerships + + ## Examples + + iex> {pub_key1, _} = Archethic.Crypto.generate_deterministic_keypair("seed") + iex> {pub_key2, _} = Archethic.Crypto.generate_deterministic_keypair("seed2") + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ownerships: [ + ...> %Ownership{ + ...> authorized_keys: %{ + ...> ^pub_key2 => _ + ...> }, + ...> secret: "ENCODED_SECRET2" + ...> }, + ...> %Ownership{ + ...> authorized_keys: %{ + ...> ^pub_key1 => _ + ...> }, + ...> secret: "ENCODED_SECRET1" + ...> } + ...> ] + ...> } + ...> } = TransactionStatements.add_ownerships(%Transaction{data: %TransactionData{}}, [[ + ...> {"secret", "ENCODED_SECRET1"}, + ...> {"secret_key", :crypto.strong_rand_bytes(32)}, + ...> {"authorized_public_keys", [pub_key1]} + ...> ], + ...> [ + ...> {"secret", "ENCODED_SECRET2"}, + ...> {"secret_key", :crypto.strong_rand_bytes(32)}, + ...> {"authorized_public_keys", [pub_key2]} + ...> ] + ...> ]) + """ + @spec add_ownerships(Transaction.t(), list(list())) :: Transaction.t() + def add_ownerships(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_ownership(&2, &1)) + end + + @doc """ + Add multiple token transfers + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address3 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address4 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ledger: %Ledger{ + ...> token: %TokenLedger{ + ...> transfers: [ + ...> %TokenTransfer{ + ...> to: ^address3, + ...> amount: 3, + ...> token_address: ^address4, + ...> token_id: 4 + ...> }, + ...> %TokenTransfer{ + ...> to: ^address1, + ...> amount: 1, + ...> token_address: ^address2, + ...> token_id: 2 + ...> } + ...> ] + ...> } + ...> } + ...> } + ...> } = TransactionStatements.add_token_transfers(%Transaction{data: %TransactionData{}}, [[ + ...> {"to", address1}, + ...> {"amount", 1}, + ...> {"token_address", address2}, + ...> {"token_id", 2} + ...> ], + ...> [ + ...> {"to", address3}, + ...> {"amount", 3}, + ...> {"token_address", address4}, + ...> {"token_id", 4} + ...> ]]) + """ + @spec add_token_transfers(Transaction.t(), list(list())) :: Transaction.t() + def add_token_transfers(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_token_transfer(&2, &1)) + end + + @doc """ + Add multiple UCO transfers + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ledger: %Ledger{ + ...> uco: %UCOLedger{ + ...> transfers: [ + ...> %UCOTransfer{ + ...> to: ^address2, + ...> amount: 2 + ...> }, + ...> %UCOTransfer{ + ...> to: ^address1, + ...> amount: 1 + ...> } + ...> ] + ...> } + ...> } + ...> } + ...> } = TransactionStatements.add_uco_transfers(%Transaction{data: %TransactionData{}}, [ + ...> [{"to", address1}, {"amount", 1}], + ...> [{"to", address2}, {"amount", 2}] + ...> ]) + """ + @spec add_uco_transfers(Transaction.t(), list(list())) :: Transaction.t() + def add_uco_transfers(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_uco_transfer(&2, &1)) + end end diff --git a/test/archethic/contracts/interpreter/action_test.exs b/test/archethic/contracts/interpreter/action_test.exs index 2c59b167c..5e962d373 100644 --- a/test/archethic/contracts/interpreter/action_test.exs +++ b/test/archethic/contracts/interpreter/action_test.exs @@ -524,4 +524,139 @@ defmodule Archethic.Contracts.ActionInterpreterTest do } }) end + + describe "blacklist" do + test "should parse when arguments are allowed" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "ABC123", amount: 64 + add_token_transfer to: "ABC123", amount: 64, token_id: 0, token_address: "012" + add_ownership secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are variables" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + address = "ABC123" + add_uco_transfer to: address, amount: 64 + add_token_transfer to: address, amount: 64, token_id: 0, token_address: "012" + add_ownership secret: address, secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are fields" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: transaction.address, amount: 64 + add_token_transfer to: transaction.address, amount: 64, token_id: 0, token_address: "012" + add_ownership secret: transaction.address, secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are functions" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: regex_extract("@addr", ".*"), amount: 64 + add_token_transfer to: regex_extract("@addr", ".*"), amount: 64, token_id: 0, token_address: "012" + add_ownership secret: regex_extract("@addr", ".*"), secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are string interpolation" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + name = "sophia" + add_uco_transfer to: "hello #{name}", amount: 64 + add_token_transfer to: "hello #{name}", amount: 64, token_id: 0, token_address: "012" + add_ownership secret: "hello #{name}", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when building a keyword list" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + uco_transfer = [to: "ABC123", amount: 33] + add_uco_transfer uco_transfer + + token_transfer = [to: "ABC123", amount: 64, token_id: 0, token_address: "012"] + add_token_transfer token_transfer + + ownership = [secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"]] + add_ownership ownership + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should not parse when arguments are not allowed" do + assert {:error, "invalid add_uco_transfer arguments - amount"} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "abc123", amount: 0 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_uco_transfer arguments - hello"} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "abc123", amount: 31, hello: 1 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_token_transfer arguments - amount"} = + ~S""" + actions triggered_by: transaction do + add_token_transfer to: "abc123", amount: "thirty one" + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_ownership arguments - authorized_public_keys"} = + ~S""" + actions triggered_by: transaction do + add_ownership secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: 42 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + end end