lib/micheline.ex

defmodule Tezex.Micheline do
  @moduledoc """
  Decode Micheline code.
  """
  alias Tezex.Micheline.Zarith

  @kw ~w(
      parameter storage code False Elt Left None Pair Right Some True Unit PACK UNPACK
      BLAKE2B SHA256 SHA512 ABS ADD AMOUNT AND BALANCE CAR CDR CHECK_SIGNATURE COMPARE CONCAT
      CONS CREATE_ACCOUNT CREATE_CONTRACT IMPLICIT_ACCOUNT DIP DROP DUP EDIV EMPTY_MAP EMPTY_SET
      EQ EXEC FAILWITH GE GET GT HASH_KEY IF IF_CONS IF_LEFT IF_NONE INT LAMBDA LE LEFT
      LOOP LSL LSR LT MAP MEM MUL NEG NEQ NIL NONE NOT NOW OR PAIR PUSH RIGHT SIZE
      SOME SOURCE SENDER SELF STEPS_TO_QUOTA SUB SWAP TRANSFER_TOKENS SET_DELEGATE UNIT UPDATE XOR
      ITER LOOP_LEFT ADDRESS CONTRACT ISNAT CAST RENAME bool contract int key key_hash lambda
      list map big_map nat option or pair set signature string bytes mutez timestamp unit
      operation address SLICE DIG DUG EMPTY_BIG_MAP APPLY chain_id CHAIN_ID LEVEL SELF_ADDRESS
      never NEVER UNPAIR VOTING_POWER TOTAL_VOTING_POWER KECCAK SHA3 PAIRING_CHECK bls12_381_g1
      bls12_381_g2 bls12_381_fr sapling_state sapling_transaction SAPLING_EMPTY_STATE SAPLING_VERIFY_UPDATE
      ticket TICKET READ_TICKET SPLIT_TICKET JOIN_TICKETS GET_AND_UPDATE chest chest_key OPEN_CHEST
      VIEW view constant
    )

  @doc """
  Parse a single message from a Micheline packed message.

  ## Examples
      iex> Tezex.Micheline.read_packed("02000000210061010000000574657a6f730100000000010000000b63727970746f6e6f6d6963")
      %{int: 33}

      iex> Tezex.Micheline.read_packed("0200e1d22c")
      %{int: -365_729}
  """
  @spec read_packed(binary) :: any
  def read_packed(<<_::binary-size(2), rest::binary>>) do
    {val, _consumed} = hex_to_micheline(rest)
    val
  end

  @doc """
  Parse a Micheline hex string, return a tuple `{result, consumed}` containing
  a list of Micheline objects as maps, and the number of bytes that was consumed in the process.

  ## Examples
      iex> Tezex.Micheline.hex_to_micheline("02000000210061010000000574657a6f730100000000010000000b63727970746f6e6f6d6963")
      {[%{int: -33}, %{string: "tezos"}, %{string: ""}, %{string: "cryptonomic"}], 76}

      iex> Tezex.Micheline.hex_to_micheline("00e1d22c")
      {%{int: -365729}, 8}
  """
  @spec hex_to_micheline(binary) :: {any, pos_integer}
  # literal int or nat
  def hex_to_micheline("00" <> rest) do
    {result, consumed} = Zarith.consume(rest)
    {result, consumed + 2}
  end

  # literal string
  def hex_to_micheline("01" <> rest) do
    {hex_string, length} = micheline_hex_to_string(rest)

    {%{string: :binary.decode_hex(hex_string)}, length + 2}
  end

  # array
  def hex_to_micheline(<<"02", length::binary-size(8), rest::binary>>) do
    length = hex_to_dec(length) * 2

    {array, consumed} =
      Stream.cycle([nil])
      |> Enum.reduce_while({[], rest, 0}, fn _, {acc, part, consumed} ->
        if consumed < length do
          {content, length} = hex_to_micheline(part)
          <<_consumed::binary-size(length), part::binary>> = part
          {:cont, {acc ++ [content], part, consumed + length}}
        else
          {:halt, {acc, consumed}}
        end
      end)

    {array, consumed + 8 + 2}
  end

  # primitive / no arg / no annot
  def hex_to_micheline("03" <> rest) do
    {kw, consumed} = code_to_kw(rest)
    {%{prim: kw}, 2 + consumed}
  end

  # primitive / no arg / annot
  def hex_to_micheline("04" <> rest) do
    tot_consumed = 2
    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {annot, consumed} = micheline_hex_to_string(rest)
    annot = :binary.decode_hex(annot)

    {%{prim: kw, annot: annot}, consumed + tot_consumed}
  end

  # primitive / 1 arg / no annot
  def hex_to_micheline("05" <> rest) do
    tot_consumed = 2

    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed

    {%{prim: kw, args: [arg]}, tot_consumed}
  end

  # primitive / 1 arg / annot
  def hex_to_micheline("06" <> rest) do
    tot_consumed = 2

    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {annot, consumed} = micheline_hex_to_string(rest)
    tot_consumed = consumed + tot_consumed

    {%{prim: kw, args: [arg], annots: decode_annotations(annot)}, tot_consumed}
  end

  # primitive / 2 arg / no annot
  def hex_to_micheline("07" <> rest) do
    tot_consumed = 2

    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg1, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg2, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed

    {%{prim: kw, args: [arg1, arg2]}, tot_consumed}
  end

  # primitive / 2 arg / annot
  def hex_to_micheline("08" <> rest) do
    tot_consumed = 2

    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg1, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {arg2, consumed} = hex_to_micheline(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {annot, consumed} = micheline_hex_to_string(rest)
    tot_consumed = consumed + tot_consumed

    {%{prim: kw, args: [arg1, arg2], annots: decode_annotations(annot)}, tot_consumed}
  end

  # primitive / N arg / maybe annot
  def hex_to_micheline("09" <> rest) do
    tot_consumed = 2

    {kw, consumed} = code_to_kw(rest)
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    {args, consumed} = hex_to_micheline("02" <> rest)
    # -2 to factor out the "02" we just added
    consumed = consumed - 2
    tot_consumed = consumed + tot_consumed
    <<_consumed::binary-size(consumed), rest::binary>> = rest

    case rest do
      # no annotation to parse
      <<"00000000", _rest::binary>> ->
        {%{prim: kw, args: args}, tot_consumed + 8}

      _ ->
        {annot, consumed} = micheline_hex_to_string(rest)
        tot_consumed = consumed + tot_consumed
        {%{prim: kw, args: args, annots: decode_annotations(annot)}, tot_consumed}
    end
  end

  # raw bytes
  def hex_to_micheline(<<"0a", rest::binary>>) do
    {bytes, length} = micheline_hex_to_string(rest)
    {%{bytes: bytes}, length + 2}
  end

  @spec micheline_hex_to_string(binary) :: {binary, pos_integer}
  def micheline_hex_to_string(<<length::binary-size(8), rest::binary>>) do
    length = hex_to_dec(length) * 2
    <<text::binary-size(length), _rest::binary>> = rest
    {text, length + 8}
  end

  defp code_to_kw(code) when is_integer(code) do
    {Enum.at(@kw, code), 2}
  end

  defp code_to_kw(<<code::binary-size(2), _rest::binary>>) when is_binary(code) do
    {d, _} = Integer.parse(code, 16)
    code_to_kw(d)
  end

  defp decode_annotations(annots_hex) do
    annots_hex
    |> :binary.decode_hex()
    |> String.split(" ")
  end

  defp hex_to_dec(hex) do
    {d, ""} = Integer.parse(hex, 16)
    d
  end
end