defmodule Tezex.ForgeOperation do @moduledoc """ Prepare an operation or an operation group for injection into the Tezos blockchain. Mostly ported from pytezos@9352c4579e436b92f8070343964af20747255197 > pytezos / MIT License / (c) 2020 Baking Bad / (c) 2018 Arthur Breitman """ alias Tezex.Forge @operation_tags %{ "endorsement" => 0, "endorsement_with_slot" => 10, "proposals" => 5, "ballot" => 6, "seed_nonce_revelation" => 1, "double_endorsement_evidence" => 2, "double_baking_evidence" => 3, "activate_account" => 4, "failing_noop" => 17, "reveal" => 107, "transaction" => 108, "origination" => 109, "delegation" => 110, "register_global_constant" => 111, "transfer_ticket" => 158, "smart_rollup_add_messages" => 201, "smart_rollup_execute_outbox_message" => 206 } @reserved_entrypoints %{ "default" => <<0>>, "root" => <<1>>, "do" => <<2>>, "set_delegate" => <<3>>, "remove_delegate" => <<4>>, "deposit" => <<5>> } # Checks if the content dictionary has parameters that are not the default 'Unit' type for 'default' entrypoint defp has_parameters(content) do case content do %{"parameters" => %{"entrypoint" => "default", "value" => %{"prim" => "Unit"}}} -> false %{"parameters" => _} -> true _ -> false end end def entrypoint(entrypoint) do case Map.get(@reserved_entrypoints, entrypoint) do nil -> <<255>> <> Forge.forge_array(entrypoint, :bytes, 1) code -> code end end defp forge_tag(tag) do <> end @spec operation(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def operation(content) do case content["kind"] do "failing_noop" -> failing_noop(content) "activate_account" -> activate_account(content) "reveal" -> reveal(content) "transaction" -> transaction(content) "origination" -> origination(content) "delegation" -> delegation(content) "endorsement" -> endorsement(content) "endorsement_with_slot" -> endorsement_with_slot(content) "register_global_constant" -> register_global_constant(content) "transfer_ticket" -> transfer_ticket(content) "smart_rollup_add_messages" -> smart_rollup_add_messages(content) "smart_rollup_execute_outbox_message" -> smart_rollup_execute_outbox_message(content) kind -> raise "unknown operation kind: #{kind}" end end @spec operation_group(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def operation_group(operation_group) do with :ok <- validate_required_keys(operation_group, ~w(branch contents)), operations = Enum.map(operation_group["contents"], &operation/1), nil <- Enum.find(operations, &(elem(&1, 0) == :error)) do operations = Enum.map(operations, fn {:ok, operation} -> operation end) content = [ Forge.forge_base58(operation_group["branch"]), Enum.join(operations) ] |> IO.iodata_to_binary() |> Base.encode16(case: :lower) {:ok, content} end end @spec activate_account(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def activate_account(content) do with :ok <- validate_required_keys(content, ~w(kind pkh secret)) do content = [ forge_tag(@operation_tags[content["kind"]]), binary_slice(Forge.forge_address(content["pkh"]), 2..-1//1), Base.decode16!(content["secret"], case: :mixed) ] |> IO.iodata_to_binary() {:ok, content} end end @spec reveal(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def reveal(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit public_key) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_public_key(content["public_key"]) ] |> IO.iodata_to_binary() {:ok, content} end end @spec transaction(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def transaction(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit amount destination) ), :ok <- (has_parameters(content) && validate_required_keys( content, ~w(parameters parameters.entrypoint parameters.value) )) || :ok do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_nat(String.to_integer(content["amount"])), Forge.forge_address(content["destination"]), if has_parameters(content) do params = content["parameters"] [ Forge.forge_bool(true), entrypoint(params["entrypoint"]), Forge.forge_array(Forge.forge_micheline(params["value"])) ] else Forge.forge_bool(false) end ] |> IO.iodata_to_binary() {:ok, content} end end @spec origination(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def origination(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit balance) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_nat(String.to_integer(content["balance"])), case Map.get(content, "delegate") do nil -> Forge.forge_bool(false) delegate -> [Forge.forge_bool(true), Forge.forge_address(delegate, :bytes, true)] end, Forge.forge_script(content["script"]) ] |> IO.iodata_to_binary() {:ok, content} end end @spec delegation(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def delegation(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), case Map.get(content, "delegate") do nil -> Forge.forge_bool(false) delegate -> [Forge.forge_bool(true), Forge.forge_address(delegate, :bytes, true)] end ] |> IO.iodata_to_binary() {:ok, content} end end @spec endorsement(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def endorsement(content) do with :ok <- validate_required_keys(content, ~w(kind level)) do content = [forge_tag(content["kind"]), Forge.forge_int32(String.to_integer(content["level"]))] |> IO.iodata_to_binary() {:ok, content} end end @spec inline_endorsement(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def inline_endorsement(content) do with :ok <- validate_required_keys( content, ~w(branch operations operations.kind operations.level signature) ) do content = [ Forge.forge_base58(content["branch"]), Forge.forge_nat(@operation_tags[content["operations"]["kind"]]), Forge.forge_int32(String.to_integer(content["operations"]["level"])), Forge.forge_base58(content["signature"]) ] |> IO.iodata_to_binary() {:ok, content} end end @spec endorsement_with_slot(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def endorsement_with_slot(content) do with :ok <- validate_required_keys(content, ~w(kind endorsement slot)), {:ok, endorsement} <- inline_endorsement(content["endorsement"]) do content = [ forge_tag(content["kind"]), Forge.forge_array(endorsement), Forge.forge_int16(String.to_integer(content["slot"])) ] |> IO.iodata_to_binary() {:ok, content} end end @spec failing_noop(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def failing_noop(content) do with :ok <- validate_required_keys(content, ~w(kind arbitrary)) do content = [forge_tag(content["kind"]), Forge.forge_array(content["arbitrary"])] |> IO.iodata_to_binary() {:ok, content} end end @spec register_global_constant(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def register_global_constant(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit value) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_array(Forge.forge_micheline(content["value"])) ] |> IO.iodata_to_binary() {:ok, content} end end @spec transfer_ticket(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def transfer_ticket(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit ticket_contents ticket_ty ticket_ticketer ticket_amount destination entrypoint) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_array(Forge.forge_micheline(content["ticket_contents"])), Forge.forge_array(Forge.forge_micheline(content["ticket_ty"])), Forge.forge_address(content["ticket_ticketer"]), Forge.forge_nat(String.to_integer(content["ticket_amount"])), Forge.forge_address(content["destination"]), Forge.forge_array(content["entrypoint"]) ] |> IO.iodata_to_binary() {:ok, content} end end @spec smart_rollup_add_messages(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def smart_rollup_add_messages(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit message) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_array( Enum.join( Enum.map(content["message"], &Forge.forge_array(:binary.decode_hex(&1))), "" ) ) ] |> IO.iodata_to_binary() {:ok, content} end end @spec smart_rollup_execute_outbox_message(map()) :: {:ok, nonempty_binary()} | {:error, nonempty_binary()} def smart_rollup_execute_outbox_message(content) do with :ok <- validate_required_keys( content, ~w(kind source fee counter gas_limit storage_limit rollup cemented_commitment output_proof) ) do content = [ forge_tag(@operation_tags[content["kind"]]), Forge.forge_address(content["source"], :bytes, true), Forge.forge_nat(String.to_integer(content["fee"])), Forge.forge_nat(String.to_integer(content["counter"])), Forge.forge_nat(String.to_integer(content["gas_limit"])), Forge.forge_nat(String.to_integer(content["storage_limit"])), Forge.forge_base58(content["rollup"]), Forge.forge_base58(content["cemented_commitment"]), Forge.forge_array(:binary.decode_hex(content["output_proof"])) ] |> IO.iodata_to_binary() {:ok, content} end end @spec validate_required_keys(map(), list()) :: :ok | {:error, nonempty_binary()} def validate_required_keys(map, required_keys, acc \\ "") when is_map(map) and is_list(required_keys) do required_keys = Enum.group_by(required_keys, &String.contains?(&1, ".")) root = Map.get(required_keys, false, []) missing_keys = root -- Map.keys(map) if Enum.empty?(missing_keys) do next = Map.get(required_keys, true, []) if Enum.empty?(next) do :ok else Enum.group_by( next, fn v -> [a, _] = String.split(v, ".", parts: 2) a end, fn v -> [_, b] = String.split(v, ".", parts: 2) b end ) |> Enum.reduce_while(:ok, fn {key, keys}, _ -> case validate_required_keys(map[key], keys, "#{acc}#{key}.") do :ok -> {:cont, :ok} err -> {:halt, err} end end) end else {:error, "Operation content is missing required keys: #{acc}#{Enum.join(missing_keys, ", ")}"} end end end