lib/solana/rpc/request.ex

defmodule Solana.RPC.Request do
  @moduledoc """
  Functions for creating Solana JSON-RPC API requests.

  This client only implements the most common methods (see the function
  documentation below). If you need a method that's on the [full
  list](https://docs.solana.com/developing/clients/jsonrpc-api#json-rpc-api-reference)
  but is not implemented here, please open an issue or contact the maintainers.
  """

  @typedoc "JSON-RPC API request (pre-encoding)"
  @type t :: {String.t(), [String.t() | map]}

  @typedoc "JSON-RPC API request (JSON encoding)"
  @type json :: %{
          jsonrpc: String.t(),
          id: term,
          method: String.t(),
          params: list
        }

  @doc """
  Encodes a `t:Solana.RPC.Request.t/0` (or a list of them) in the [required
  format](https://docs.solana.com/developing/clients/jsonrpc-api#request-formatting).
  """
  @spec encode(requests :: [t]) :: [json]
  def encode(requests) when is_list(requests) do
    requests
    |> Enum.with_index()
    |> Enum.map(&to_json_rpc/1)
  end

  @spec encode(request :: t) :: json
  def encode(request), do: to_json_rpc({request, 0})

  defp to_json_rpc({{method, []}, id}) do
    %{jsonrpc: "2.0", id: id, method: method}
  end

  defp to_json_rpc({{method, params}, id}) do
    %{jsonrpc: "2.0", id: id, method: method, params: check_params(params)}
  end

  defp check_params([]), do: []
  defp check_params([map = %{} | rest]) when map_size(map) == 0, do: check_params(rest)
  defp check_params([elem | rest]), do: [elem | check_params(rest)]

  @doc """
  Returns all information associated with the account of the provided Pubkey.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getaccountinfo).
  """
  @spec get_account_info(account :: Solana.key(), opts :: keyword) :: t
  def get_account_info(account, opts \\ []) do
    {"getAccountInfo", [B58.encode58(account), encode_opts(opts, %{"encoding" => "base64"})]}
  end

  @doc """
  Returns the balance of the provided pubkey's account.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getbalance).
  """
  @spec get_balance(account :: Solana.key(), opts :: keyword) :: t
  def get_balance(account, opts \\ []) do
    {"getBalance", [B58.encode58(account), encode_opts(opts)]}
  end

  @doc """
  Returns identity and transaction information about a confirmed block in the
  ledger.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getblock).
  """
  @spec get_block(start_slot :: non_neg_integer, opts :: keyword) :: t
  def get_block(start_slot, opts \\ []) do
    {"getBlock", [start_slot, encode_opts(opts)]}
  end

  @doc """
  Returns a recent block hash from the ledger, and a fee schedule that can be
  used to compute the cost of submitting a transaction using it.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getrecentblockhash).
  """
  @spec get_recent_blockhash(opts :: keyword) :: t
  def get_recent_blockhash(opts \\ []) do
    {"getRecentBlockhash", [encode_opts(opts)]}
  end

  @doc """
  Returns minimum balance required to make an account rent exempt.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getminimumbalanceforrentexemption).
  """
  @spec get_minimum_balance_for_rent_exemption(length :: non_neg_integer, opts :: keyword) :: t
  def get_minimum_balance_for_rent_exemption(length, opts \\ []) do
    {"getMinimumBalanceForRentExemption", [length, encode_opts(opts)]}
  end

  @doc """
  Submits a signed transaction to the cluster for processing.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#sendtransaction).
  """
  @spec send_transaction(transaction :: Solana.Transaction.t(), opts :: keyword) :: t
  def send_transaction(tx = %Solana.Transaction{}, opts \\ []) do
    {:ok, tx_bin} = Solana.Transaction.to_binary(tx)
    opts = opts |> fix_tx_opts() |> encode_opts(%{"encoding" => "base64"})
    {"sendTransaction", [Base.encode64(tx_bin), opts]}
  end

  defp fix_tx_opts(opts) do
    opts
    |> Enum.map(fn
      {:commitment, commitment} -> {:preflight_commitment, commitment}
      other -> other
    end)
    |> Enum.into([])
  end

  @doc """
  Requests an airdrop of lamports to an account.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#requestairdrop).
  """
  @spec request_airdrop(account :: Solana.key(), sol :: pos_integer, opts :: keyword) :: t
  def request_airdrop(account, sol, opts \\ []) do
    {"requestAirdrop",
     [B58.encode58(account), sol * Solana.lamports_per_sol(), encode_opts(opts)]}
  end

  @doc """
  Returns confirmed signatures for transactions involving an address backwards
  in time from the provided signature or most recent confirmed block.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getsignaturesforaddress).
  """
  @spec get_signatures_for_address(account :: Solana.key(), opts :: keyword) :: t
  def get_signatures_for_address(account, opts \\ []) do
    {"getSignaturesForAddress", [B58.encode58(account), encode_opts(opts)]}
  end

  @doc """
  Returns the statuses of a list of signatures.

  Unless the `searchTransactionHistory` configuration parameter is included,
  this method only searches the recent status cache of signatures, which retains
  statuses for all active slots plus `MAX_RECENT_BLOCKHASHES` rooted slots.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getsignaturestatuses).
  """
  @spec get_signature_statuses(signatures :: [Solana.key()], opts :: keyword) :: t
  def get_signature_statuses(signatures, opts \\ []) when is_list(signatures) do
    {"getSignatureStatuses", [Enum.map(signatures, &B58.encode58/1), encode_opts(opts)]}
  end

  @doc """
  Returns transaction details for a confirmed transaction.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#gettransaction).
  """
  @spec get_transaction(signature :: Solana.key(), opts :: keyword) :: t
  def get_transaction(signature, opts \\ []) do
    {"getTransaction", [B58.encode58(signature), encode_opts(opts)]}
  end

  @doc """
  Returns the total supply of an SPL Token.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#gettokensupply).
  """
  @spec get_token_supply(mint :: Solana.key(), opts :: keyword) :: t
  def get_token_supply(mint, opts \\ []) do
    {"getTokenSupply", [B58.encode58(mint), encode_opts(opts)]}
  end

  @doc """
  Returns the 20 largest accounts of a particular SPL Token type.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#gettokenlargestaccounts).
  """
  @spec get_token_largest_accounts(mint :: Solana.key(), opts :: keyword) :: t
  def get_token_largest_accounts(mint, opts \\ []) do
    {"getTokenLargestAccounts", [B58.encode58(mint), encode_opts(opts)]}
  end

  @doc """
  Returns the account information for a list of pubkeys.

  For more information, see [the Solana
  docs](https://docs.solana.com/developing/clients/jsonrpc-api#getmultipleaccounts).
  """
  @spec get_multiple_accounts(accounts :: [Solana.key()], opts :: keyword) :: t
  def get_multiple_accounts(accounts, opts \\ []) when is_list(accounts) do
    {"getMultipleAccounts",
     [Enum.map(accounts, &B58.encode58/1), encode_opts(opts, %{"encoding" => "base64"})]}
  end

  defp encode_opts(opts, defaults \\ %{}) do
    Enum.into(opts, defaults, fn {k, v} -> {camelize(k), encode_value(v)} end)
  end

  defp camelize(word) do
    case Regex.split(~r/(?:^|[-_])|(?=[A-Z])/, to_string(word)) do
      words ->
        words
        |> Enum.filter(&(&1 != ""))
        |> camelize_list(:lower)
        |> Enum.join()
    end
  end

  defp camelize_list([], _), do: []

  defp camelize_list([h | tail], :lower) do
    [String.downcase(h)] ++ camelize_list(tail, :upper)
  end

  defp camelize_list([h | tail], :upper) do
    [String.capitalize(h)] ++ camelize_list(tail, :upper)
  end

  defp encode_value(v) do
    cond do
      :ok == elem(Solana.Key.check(v), 0) -> B58.encode58(v)
      :ok == elem(Solana.Transaction.check(v), 0) -> B58.encode58(v)
      true -> v
    end
  end
end