lib/aptos/clients/rest_client.ex

defmodule Aptos.RestClient do
  @moduledoc """
  JSON endpoints to read data from the network
  """

  @endpoint Aptos.NetworkConfig.node_url()

  use Tesla, docs: false

  plug Tesla.Middleware.BaseUrl, @endpoint
  plug Tesla.Middleware.JSON, engine_opts: [keys: :atoms]
  plug Tesla.Middleware.Timeout, timeout: 15_000

  alias Aptos.Client.Result

  import Aptos.Util

  # Account

  @doc """
  Fetches the authentication key and the sequence number for an account address.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec show_account(binary, keyword()) :: Result.from_tesla()
  def show_account(address, opts \\ []) do
    address = "0x" <> Base.encode16(address, case: :lower)

    get("/accounts/#{address}", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves a collection of account resources for a given account.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  - `{:limit, pos_integer()}`
  - `{:start, String.t()}`
  """
  @spec list_account_resources(binary, keyword()) :: Result.from_tesla()
  def list_account_resources(address, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/resources", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves all account resources for a given account, automatically running through pagination.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec list_all_account_resources(binary, keyword()) :: Result.from_tesla()
  def list_all_account_resources(address, opts \\ []) do
    fetch_all_by_cursor(:list_account_resources, [address], opts)
  end

  @doc """
  Retrieves an individual resource from a given account.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec show_account_resource(binary, String.t(), keyword()) :: Result.from_tesla()
  def show_account_resource(address, resource, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/resource/#{URI.encode(resource)}", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves a collection of account modules' bytecode for a given account.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  - `{:limit, pos_integer()}`
  - `{:start, String.t()}`
  """
  @spec list_account_modules(binary, keyword()) :: Result.from_tesla()
  def list_account_modules(address, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/modules", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves all account modules' bytecode for a given account, automatically running through pagination.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec list_all_account_modules(binary, keyword()) :: Result.from_tesla()
  def list_all_account_modules(address, opts \\ []) do
    fetch_all_by_cursor(:list_account_modules, [address], opts)
  end

  @doc """
  Retrieves an individual module from a given account.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec show_account_module(binary, String.t(), keyword()) :: Result.from_tesla()
  def show_account_module(address, module_name, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/module/#{module_name}", query: [])
    |> Result.from_tesla()
  end

  # Blocks

  @doc """
  Fetches a block by its height.
  """
  @spec show_block_by_height(non_neg_integer(), keyword()) :: Result.from_tesla()
  def show_block_by_height(height, opts \\ []) do
    get("/blocks/by_height/#{height}", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Fetches a block by a version in the block.
  """
  @spec show_block_by_version(non_neg_integer(), keyword()) :: Result.from_tesla()
  def show_block_by_version(version, opts \\ []) do
    get("/blocks/by_version/#{version}", query: opts)
    |> Result.from_tesla()
  end

  # Transactions

  @doc """
  Looks up a transaction by its hash.
  """
  @spec show_tx_by_hash(String.t()) :: Result.from_tesla()
  def show_tx_by_hash(tx_hash) do
    get("/transactions/by_hash/#{tx_hash}")
    |> Result.from_tesla()
  end

  @doc """
  Retrieves a transaction by a given version.
  """
  @spec show_tx_by_version(pos_integer()) :: Result.from_tesla()
  def show_tx_by_version(version_number) do
    get("/transactions/by_version/#{version_number}")
    |> Result.from_tesla()
  end

  @doc """
  Retrieves on-chain committed transactions.

  Available options are:
  - `{:limit, pos_integer()}`
  - `{:start, pos_integer()}`
  """
  @spec list_tx(keyword()) :: Result.from_tesla()
  def list_tx(opts \\ []) do
    get("transactions", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves on-chain committed transactions from an account.

  Available options are:
  - `{:limit, pos_integer()}`
  - `{:start, pos_integer()}`
  """
  @spec list_account_tx(binary, keyword()) :: Result.from_tesla()
  def list_account_tx(address, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/transactions", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Retrieves estimated gas price from node.
  """
  @spec estimate_gas_price() :: Result.from_tesla()
  def estimate_gas_price() do
    get("/estimate_gas_price")
    |> Result.from_tesla()
  end

  # Tables

  @doc """
  Retrieves a table item.

  Available options are:
  - `{:ledger_version, pos_integer()}`
  """
  @spec show_table_item(String.t(), String.t(), String.t(), String.t()) :: Result.from_tesla()
  def show_table_item(key, handle, key_type, value_type, opts \\ []) do
    body = %{
      key_type: key_type,
      value_type: value_type,
      key: key
    }

    post("/tables/#{handle}/item", body, query: opts)
    |> Result.from_tesla()
  end

  # Events

  @doc """
  Fetches events by event handle.

  Available options are:
  - `{:limit, pos_integer()}`
  - `{:start, pos_integer()}`
  """
  @spec list_events_by_handle(binary(), String.t(), String.t(), keyword()) :: Result.from_tesla()
  def list_events_by_handle(address, handle, field, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/events/#{handle}/#{field}", query: opts)
    |> Result.from_tesla()
  end

  @doc """
  Fetches events by creation number.

  Available options are:
  - `{:limit, pos_integer()}`
  - `{:start, pos_integer()}`
  """
  @spec list_events_by_creation_number(binary(), pos_integer(), keyword()) :: Result.from_tesla()
  def list_events_by_creation_number(address, creation_number, opts \\ []) do
    get("/accounts/#{binary_to_hex(address)}/events/#{creation_number}", query: opts)
    |> Result.from_tesla()
  end

  # Helpers

  defp fetch_all_by_cursor(func, args, opts) do
    cleaned_opts = Keyword.drop(opts, [:cursor, :limit])
    fetch_all_by_cursor(func, args, cleaned_opts, nil, [])
  end

  defp fetch_all_by_cursor(func, args, opts, cursor, fetched) do
    merged_opts =
      if cursor do
        Keyword.put(opts, :start, cursor)
      else
        opts
      end

    with {:ok, headers, entries} <-
           apply(__MODULE__, func, args ++ [merged_opts]) do
      case headers[:cursor] do
        nil ->
          {:ok, headers, fetched ++ entries}

        new_cursor ->
          fetch_all_by_cursor(func, args, opts, new_cursor, fetched ++ entries)
      end
    end
  end
end