defmodule Solana.RPC do @moduledoc """ Functions for dealing with Solana's [JSON-RPC API](https://docs.solana.com/developing/clients/jsonrpc-api). """ import Solana.Helpers alias Solana.RPC require Logger @typedoc "Solana JSON-RPC API client." @type client :: Tesla.Client.t() @client_schema [ adapter: [ type: :any, default: Tesla.Adapter.Httpc, doc: "Which `Tesla` adapter to use." ], network: [ type: {:custom, __MODULE__, :cluster_url, []}, required: true, doc: "Which [Solana cluster](https://docs.solana.com/clusters) to connect to." ], retry_options: [ type: :keyword_list, default: [], doc: "Options to pass to `Tesla.Middleware.Retry`." ], headers: [ type: {:list, {:tuple, [:string, :string]}}, default: [], doc: "API headers" ] ] @doc """ Creates an API client used to interact with Solana's [JSON-RPC API](https://docs.solana.com/developing/clients/jsonrpc-api). ## Example iex> key = Solana.keypair() |> Solana.pubkey!() iex> client = Solana.RPC.client(network: "localhost") iex> {:ok, signature} = Solana.RPC.send_request(client, Solana.RPC.Request.request_airdrop(key, 1)) iex> is_binary(signature) true ## Options #{NimbleOptions.docs(@client_schema)} """ @spec client(keyword) :: client def client(opts) do case validate(opts, @client_schema) do {:ok, config} -> middleware = [ {Tesla.Middleware.BaseUrl, config.network}, {Tesla.Middleware.Headers, config.headers}, RPC.Middleware, Tesla.Middleware.JSON, {Tesla.Middleware.Retry, retry_opts(config)} ] Tesla.client(middleware, config.adapter) error -> error end end @doc """ Sends the provided requests to the configured Solana RPC endpoint. """ @spec send_request(client, [term()] | term) :: {:ok, term()} | {:error, term()} def send_request(client, requests) do case Tesla.post(client, "/", Solana.RPC.Request.encode(requests)) do {:ok, %{body: body}} -> {:ok, body} error -> error end end @doc """ Sends the provided transactions to the configured RPC endpoint, then confirms them. Returns a tuple containing all the transactions in the order they were confirmed, OR an error tuple containing the list of all the transactions that were confirmed before the error occurred. """ @spec send_and_confirm(client, pid, [Solana.Transaction.t()] | Solana.Transaction.t(), keyword) :: {:ok, [binary]} | {:error, :timeout, [binary]} def send_and_confirm(client, tracker, txs, opts \\ []) do timeout = Keyword.get(opts, :timeout, 5_000) request_opts = Keyword.take(opts, [:commitment]) requests = Enum.map(List.wrap(txs), &RPC.Request.send_transaction(&1, request_opts)) client |> RPC.send_request(requests) |> Enum.flat_map(fn {:ok, signature} -> [signature] {:error, %{"data" => %{"logs" => logs}, "message" => message}} -> [message | logs] |> Enum.join("\n") |> Logger.error() [] {:error, error} -> Logger.error("error sending transaction: #{inspect(error)}") [] end) |> case do [] -> :error signatures -> :ok = RPC.Tracker.start_tracking(tracker, signatures, request_opts) await_confirmations(signatures, timeout, []) end end defp await_confirmations([], _, confirmed), do: {:ok, confirmed} defp await_confirmations(signatures, timeout, done) do receive do {:ok, confirmed} -> signatures |> MapSet.new() |> MapSet.difference(MapSet.new(confirmed)) |> MapSet.to_list() |> await_confirmations(timeout, List.flatten([done, confirmed])) after timeout -> {:error, :timeout, done} end end @doc false def cluster_url(network) when network in ["devnet", "mainnet-beta", "testnet"] do {:ok, "https://api.#{network}.solana.com"} end def cluster_url("localhost"), do: {:ok, "http://127.0.0.1:8899"} def cluster_url(other) when is_binary(other) do case URI.parse(other) do %{scheme: nil, host: nil} -> {:error, "invalid cluster"} _ -> {:ok, other} end end def cluster_url(_), do: {:error, "invalid cluster"} defp retry_opts(%{retry_options: retry_options}) do Keyword.merge(retry_defaults(), retry_options) end defp retry_defaults do [max_retries: 10, max_delay: 4_000, should_retry: &should_retry?/1] end defp should_retry?({:ok, %{status: status}}) when status in 500..599, do: true defp should_retry?({:ok, _}), do: false defp should_retry?({:error, _}), do: true end