lib/micheline.ex

defmodule Tezex.Micheline do
  @moduledoc """
  Serialize/deserialize data to/from Tezos Micheline optimized binary representation.
  """
  alias Tezex.Forge
  alias Tezex.Zarith

  @type pack_types :: nil | :address | :bytes | :int | :key_hash | :nat | :string

  @doc """
  Serialize a piece of data to its optimized binary representation.

  ## Examples
      iex> pack(-6407, :int)
      "0500c764"
      iex> pack(%{"prim" => "Pair", "args" => [%{"int" => "-33"}, %{"int" => "71"}]})
      "0507070061008701"
  """
  @spec pack(binary() | integer() | map(), pack_types()) :: nonempty_binary()
  @spec pack(binary() | integer() | map()) :: nonempty_binary()
  def pack(value, type \\ nil) do
    case type do
      :int ->
        "0500" <> Zarith.encode(value)

      :nat ->
        "0500" <> Zarith.encode(value)

      :string ->
        hex_bytes = Base.encode16(value, case: :lower)
        "0501" <> encode_byte_size(hex_bytes) <> hex_bytes

      :bytes ->
        hex_bytes = Base.encode16(value, case: :lower)
        "050a#{encode_byte_size(hex_bytes)}#{hex_bytes}"

      :key_hash ->
        address = Forge.forge_address(value, :hex)
        "050a#{encode_byte_size(address)}#{address}"

      :address ->
        address = Forge.forge_address(value, :hex)
        "050a#{encode_byte_size(address)}#{address}"

      nil ->
        "05" <> Forge.forge_micheline(value, :hex)
    end
  end

  @doc """
  Deserialize a piece of data from its optimized binary representation.

  ## Examples
      iex> unpack("050a0000001601e67bac124dff100a57644de0cf26d341ebf9492600", :address)
      "KT1VbT8n6YbrzPSjdAscKfJGDDNafB5yHn1H"
      iex> unpack("0507070001000c")
      %{"prim" => "Pair", "args" => [%{"int" => "1"}, %{"int" => "12"}]}
  """
  @type unpack_result :: binary() | integer() | map() | list(unpack_result())
  @spec unpack(binary(), pack_types()) :: unpack_result()
  @spec unpack(binary()) :: unpack_result()
  def unpack(hex_value, type \\ nil) do
    case type do
      :int ->
        hex_value
        |> binary_slice(4..-1//1)
        |> Forge.unforge_int(:hex)
        |> elem(0)

      :nat ->
        hex_value
        |> binary_slice(4..-1//1)
        |> Forge.unforge_int(:hex)
        |> elem(0)

      :string ->
        hex_value
        |> binary_slice(12..-1//1)
        |> :binary.decode_hex()

      :bytes ->
        hex_value
        |> binary_slice(12..-1//1)

      :key_hash ->
        hex_value
        |> binary_slice(12..-1//1)
        |> Forge.unforge_address(:hex)

      :address ->
        hex_value
        |> binary_slice(12..-1//1)
        |> Forge.unforge_address(:hex)

      nil ->
        hex_value
        |> binary_slice(2..-1//1)
        |> Forge.unforge_micheline(:hex)
    end
  end

  defp encode_byte_size(bytes) do
    (byte_size(bytes) / 2)
    |> trunc()
    |> Integer.to_string(16)
    |> String.pad_leading(8, "0")
  end
end