lib/sui/rpc.ex

defmodule Web3SuiEx.Sui.RPC do
  @moduledoc """
    Api Docs: https://docs.sui.io/sui-jsonrpc#sui_getObject
  """
  alias Web3SuiEx.Sui.Bcs.IntentMessage

  require Logger

  defstruct [:endpoint, :client]

  @endpoint %{
    devnet: "https://fullnode.devnet.sui.io:443"
  }

  @faucet_endpoint %{
    devnet: "https://faucet.devnet.sui.io/gas"
  }
  defmodule ExecuteTransactionRequestType do
    def wait_for_local_execution, do: "WaitForLocalExecution"
    def wait_for_effects_cert, do: "WaitForEffectsCert"
  end

  def connect() do
    connect(@endpoint.devnet)
  end

  def connect(:devnet) do
    connect(@endpoint.devnet)
  end

  def connect(:faucet) do
    client =
      Tesla.client([
        {Tesla.Middleware.BaseUrl, @faucet_endpoint.devnet},
        # {Tesla.Middleware.Headers, [{"content-type", "application/json"}]},
        {Tesla.Middleware.JSON, engine_opts: [keys: :atoms]}
      ])

    {:ok, %__MODULE__{client: client, endpoint: @faucet_endpoint.devnet}}
  end

  def connect(endpoint) do
    client =
      Tesla.client([
        # TODO: convert input/output type
        {Tesla.Middleware.BaseUrl, endpoint},
        {Tesla.Middleware.Headers, [{"content-type", "application/json"}]},
        {Tesla.Middleware.JSON, engine_opts: [keys: :atoms]}
      ])

    {:ok, %__MODULE__{client: client, endpoint: endpoint}}
  end

  def get_faucet(client, address_hex) do
    body =
      Jason.encode!(%{
        "FixedAmountRequest" => %{
          "recipient" => address_hex
        }
      })

    post(client, body)
  end

  def get_object(client, object_id, options \\ :default) do
    body = build_body(:get_obj, object_id, options)
    post(client, body)
  end

  def suix_getReferenceGasPrice(client) do
    {:ok, v} = client |> call("suix_getReferenceGasPrice", [])
    String.to_integer(v)
  end

  def sui_executeTransactionBlock(
        client,
        signer,
        %IntentMessage{intent: _intent, data: value} = intent_msg
      ) do
    bcs_bytes_to_sign = Bcs.encode(intent_msg)
    {:ok, signatures} = sign(signer, bcs_bytes_to_sign)
    tx_bytes = Bcs.encode(value, Web3SuiEx.Sui.Bcs.TransactionData)

    sui_executeTransactionBlock(
      client,
      :base64.encode(tx_bytes),
      signatures,
      ExecuteTransactionRequestType.wait_for_local_execution(),
      :default
    )
  end

  def sui_executeTransactionBlock(client, tx_bytes, signatures, request_type, options \\ :default) do
    Logger.debug(
      "tx_bytes = #{inspect(tx_bytes)}, signatures=#{inspect(signatures)}, request_type=#{request_type}"
    )

    call(client, "sui_executeTransactionBlock", [
      tx_bytes,
      signatures,
      transaction_option(options),
      request_type
    ])
  end

  def unsafe_moveCall(
        client,
        signer,
        package_object_id,
        module,
        function,
        type_arguments,
        arguments,
        gas,
        gas_budget
      ) do
    call(client, "unsafe_moveCall", [
      signer,
      package_object_id,
      module,
      function,
      type_arguments,
      arguments,
      gas,
      gas_budget
    ])
  end

  def unsafe_transferObject(client, signer, object_id, gas, gas_budget, recipient) do
    call(client, "unsafe_transferObject", [signer, object_id, gas, gas_budget, recipient])
  end

  def unsafe_call(
        client,
        %Web3SuiEx.Sui.Account{sui_address_hex: sui_address_hex} = account,
        method,
        params
      ) do
    {:ok, %{txBytes: tx_bytes}} = client |> call(method, [sui_address_hex | params])
    flag = Bcs.encode(Web3SuiEx.Sui.Bcs.IntentMessage.Intent.default())
    {:ok, signatures} = sign(account, flag <> :base64.decode(tx_bytes))

    case client
    |> sui_executeTransactionBlock(
      tx_bytes,
      signatures,
      Web3SuiEx.Sui.RPC.ExecuteTransactionRequestType.wait_for_local_execution()
    ) do
      {:ok, %{effects: %{status: %{status: "success"}}}} = res->
      res
      {:ok, %{effects: %{status: %{status: "failure", error: error}}}}->
        {:error, error}
      other ->
      other
      end
  end

  def transaction_option(:default) do
    %{
      "showInput" => true,
      "showRawInput" => true,
      "showEffects" => true,
      "showEvents" => true,
      "showObjectChanges" => true,
      "showBalanceChanges" => true
    }
  end

  def transaction_option(options) do
    options
  end

  def build_body(:get_obj, object_id, :default) do
    options = %{
      "showType" => true,
      "showOwner" => true,
      "showPreviousTransaction" => true,
      "showDisplay" => false,
      "showContent" => true,
      "showBcs" => false,
      "showStorageRebate" => true
    }

    build_body(:get_obj, object_id, options)
  end

  def build_body(:get_obj, object_id, options) do
    %{
      jsonrpc: "2.0",
      method: "sui_getObject",
      params: [
        object_id,
        options
      ],
      id: 1
    }
  end

  def call(client \\ nil, method, params) do
    client
    |> post(
      Jason.encode!(%{
        :jsonrpc => "2.0",
        :id => :erlang.system_time(1000),
        :method => method,
        :params => params
      })
    )
  end

  @spec sign(Web3SuiEx.Sui.Account, binary()) :: {:ok, list()} | :error
  def sign(%Web3SuiEx.Sui.Account{priv_key_base64: priv_key_base64}, tx_bytes) do
    :sui_nif.sign(tx_bytes, priv_key_base64)
  end

  defp post(client, body, options \\ [])

  defp post(nil, body, options) do
    {:ok, client} = connect()
    post(client, body, options)
  end

  defp post(%{client: client}, body, options) do
    with {:ok, %{body: resp_body}} <- Tesla.post(client, "", reset_req(body), options) do
      case resp_body do
        %{error: %{code: _, message: message}} -> {:error, message}
        %{result: res} -> {:ok, res}
      end
    else
      {:error, error} -> {:error, error}
    end
  end

  defp reset_req(body) when is_binary(body) do
    body
  end

  defp reset_req(body), do: Jason.encode!(body)
end