lib/solana/spl/token/mint.ex

defmodule Solana.SPL.Token.Mint do
  @moduledoc """
  Functions for interacting with the mint accounts of Solana's [Token
  Program](https://spl.solana.com/token).
  """
  alias Solana.{Instruction, Account, SPL.Token, SystemProgram}
  import Solana.Helpers

  @typedoc "Token Program mint account metadata."
  @type t :: %__MODULE__{
          authority: Solana.key() | nil,
          supply: non_neg_integer,
          decimals: byte,
          initialized?: boolean,
          freeze_authority: Solana.key() | nil
        }

  defstruct [
    :authority,
    :supply,
    :freeze_authority,
    decimals: 0,
    initialized?: false
  ]

  @doc """
  The size of a serialized token mint account.
  """
  @spec byte_size() :: pos_integer
  def byte_size(), do: 82

  @doc """
  Translates the result of a `Solana.RPC.Request.get_account_info/2` into a
  `t:Solana.SPL.Token.Mint.t/0`.
  """
  @spec from_account_info(info :: map) :: t | :error
  def from_account_info(info)

  def from_account_info(%{"data" => %{"parsed" => %{"type" => "mint", "info" => info}}}) do
    case {from_mint_account_info(info), info["freezeAuthority"]} do
      {:error, _} -> :error
      {mint, nil} -> mint
      {mint, authority} -> %{mint | freeze_authority: B58.decode58!(authority)}
    end
  end

  def from_account_info(_), do: :error

  defp from_mint_account_info(%{
         "supply" => supply,
         "isInitialized" => initialized?,
         "mintAuthority" => authority,
         "decimals" => decimals
       }) do
    %__MODULE__{
      decimals: decimals,
      authority: B58.decode58!(authority),
      initialized?: initialized?,
      supply: String.to_integer(supply)
    }
  end

  defp from_mint_account_info(_), do: :error

  @init_schema [
    payer: [
      type: {:custom, Solana.Key, :check, []},
      required: true,
      doc: "The account that will pay for the mint creation"
    ],
    balance: [
      type: :non_neg_integer,
      required: true,
      doc: "The lamport balance the mint account should have"
    ],
    decimals: [
      type: {:in, 0..255},
      required: true,
      doc: "decimals for the new mint"
    ],
    authority: [
      type: {:custom, Solana.Key, :check, []},
      required: true,
      doc: "authority for the new mint"
    ],
    freeze_authority: [
      type: {:custom, Solana.Key, :check, []},
      doc: "freeze authority for the new mint"
    ],
    new: [
      type: {:custom, Solana.Key, :check, []},
      required: true,
      doc: "public key for the new mint"
    ]
  ]
  @doc """
  Genereates the instructions to initialize a mint account.

  ## Options

  #{NimbleOptions.docs(@init_schema)}
  """
  def init(opts) do
    case validate(opts, @init_schema) do
      {:ok, params} ->
        [
          SystemProgram.create_account(
            lamports: params.balance,
            space: byte_size(),
            from: params.payer,
            new: params.new,
            program_id: Token.id()
          ),
          initialize_ix(params)
        ]

      error ->
        error
    end
  end

  defp initialize_ix(params) do
    %Instruction{
      program: Token.id(),
      accounts: [
        %Account{key: params.new, writable?: true},
        %Account{key: Solana.rent()}
      ],
      data:
        Instruction.encode_data([
          0,
          params.decimals,
          params.authority
          | add_freeze_authority(params)
        ])
    }
  end

  defp add_freeze_authority(%{freeze_authority: freeze_authority}) do
    [1, freeze_authority]
  end

  defp add_freeze_authority(_params), do: [0, <<0::32*8>>]
end