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 @type error_reason() :: {:missing_keys, [String.t()]} @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(kind) do <> end @spec operation(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} 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 @keys ~w(branch contents) @spec operation_group(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def operation_group(operation_group) do with :ok <- validate_required_keys(operation_group, @keys), 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 @keys ~w(kind pkh secret) @spec activate_account(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def activate_account(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit public_key) @spec reveal(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def reveal(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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"]), case Map.get(content, "proof") do nil -> Forge.forge_bool(false) proof -> [Forge.forge_bool(true), Forge.forge_array(Forge.forge_base58(proof))] end ] |> IO.iodata_to_binary() {:ok, content} end end @keys ~w(kind source fee counter gas_limit storage_limit amount destination) @params ~w(parameters parameters.entrypoint parameters.value) @spec transaction(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def transaction(content) do with :ok <- validate_required_keys(content, @keys), :ok <- if(has_parameters(content), do: validate_required_keys(content, @params), else: :ok ) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit balance) @spec origination(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def origination(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit) @spec delegation(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def delegation(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind level) @spec endorsement(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def endorsement(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), Forge.forge_int32(String.to_integer(content["level"])) ] |> IO.iodata_to_binary() {:ok, content} end end @keys ~w(branch operations operations.kind operations.level signature) @spec inline_endorsement(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def inline_endorsement(content) do with :ok <- validate_required_keys(content, @keys) 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 @keys ~w(kind endorsement slot) @spec endorsement_with_slot(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def endorsement_with_slot(content) do with :ok <- validate_required_keys(content, @keys), {: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 @keys ~w(kind arbitrary) @spec failing_noop(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def failing_noop(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(content["kind"]), Forge.forge_array(content["arbitrary"]) ] |> IO.iodata_to_binary() {:ok, content} end end @keys ~w(kind source fee counter gas_limit storage_limit value) @spec register_global_constant(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def register_global_constant(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit ticket_contents ticket_ty ticket_ticketer ticket_amount destination entrypoint) @spec transfer_ticket(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def transfer_ticket(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit message) @spec smart_rollup_add_messages(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def smart_rollup_add_messages(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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 @keys ~w(kind source fee counter gas_limit storage_limit rollup cemented_commitment output_proof) @spec smart_rollup_execute_outbox_message(map()) :: {:ok, nonempty_binary()} | {:error, error_reason()} def smart_rollup_execute_outbox_message(content) do with :ok <- validate_required_keys(content, @keys) do content = [ forge_tag(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, error_reason()} 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, {:missing_keys, Enum.map(missing_keys, &(acc <> &1))}} end end end