lib/icon/rpc/request/goloop.ex

defmodule Icon.RPC.Request.Goloop do
  @moduledoc """
  This module defines the Goloop API request payloads.
  """
  import Icon.RPC.Identity, only: [has_address: 1]
  import Icon.Schema, only: [enum: 1]

  alias Icon.RPC.{Identity, Request}
  alias Icon.Schema
  alias Icon.Schema.Error

  @typedoc """
  Supported methods.
  """
  @type method ::
          :get_last_block
          | :get_block_by_height
          | :get_block_by_hash
          | :get_balance
          | :call
          | :get_score_api
          | :get_total_supply
          | :get_transaction_result
          | :get_transaction_by_hash
          | :send_transaction
          | :send_transaction_and_wait
          | :wait_transaction_result
          | :estimate_step

  @doc """
  Builds request for getting the lastest block.

  ## Example

  The following builds a request to get the latest block:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_last_block(identity)
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getLastBlock",
      options: ...,
      params: %{}
    }
  }
  ```
  """
  @spec get_last_block(Identity.t()) :: {:ok, Request.t()}
  def get_last_block(identity)

  def get_last_block(%Identity{} = identity) do
    request =
      :get_last_block
      |> method()
      |> Request.build(%{}, identity: identity)

    {:ok, request}
  end

  @doc """
  Builds a request for getting a block by `height`.

  ## Example

  The following builds a request to get a block by height:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_block_by_height(identity, 42)
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getBlockByHeight",
      options: ...,
      params: %{
        height: 42
      }
    }
  }
  ```
  """
  @spec get_block_by_height(Identity.t(), Schema.Types.Integer.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_block_by_height(identity, height)

  def get_block_by_height(%Identity{} = identity, height) do
    schema = %{height: {:pos_integer, required: true}}

    with {:ok, params} <- validate(schema, height: height) do
      request =
        :get_block_by_height
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  @doc """
  Builds a request for getting a block by `hash`.

  ## Example

  The following builds a request to get a block by hash:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_block_by_hash(identity, "0x8e25acc5b5c74375079d51828760821fc6f54283656620b1d5a715edcc0770c6")
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getBlockByHash",
      options: ...,
      params: %{
        hash: "0x8e25acc5b5c74375079d51828760821fc6f54283656620b1d5a715edcc0770c6"
      }
    }
  }
  ```
  """
  @spec get_block_by_hash(Identity.t(), Schema.Types.Hash.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_block_by_hash(identity, hash)

  def get_block_by_hash(%Identity{} = identity, hash) do
    schema = %{hash: {:hash, required: true}}

    with {:ok, params} <- validate(schema, hash: hash) do
      request =
        :get_block_by_hash
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  @doc """
  Builds a request for calling a readonly SCORE `method` with some optional
  `params` and `options` using a valid `identity` with a wallet.

  Options:
  - `schema` - `method`'s schema to validate `params`.

  ## Example

  The following shows how build a call to the method `getBalance` in a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.Goloop.call(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   "getBalance",
  ...>   %{address: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"},
  ...>   %{address: {:address, required: true}}
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_call",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        dataType: "call",
        data: %{
          method: "getBalance",
          params: %{
            address: "hx2e243ad926ac48d15156756fce28314357d49d83"
          }
        }
      }
    }
  }
  ```
  """
  @spec call(Identity.t(), Schema.Types.SCORE.t(), binary()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec call(
          Identity.t(),
          Schema.Types.SCORE.t(),
          binary(),
          nil | map() | keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec call(
          Identity.t(),
          Schema.Types.SCORE.t(),
          binary(),
          nil | map() | keyword(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def call(identity, to, method, params \\ nil, options \\ [])

  def call(
        %Identity{address: from} = identity,
        to,
        method,
        call_params,
        options
      )
      when has_address(identity) do
    call_schema =
      if is_nil(call_params), do: nil, else: options[:schema] || :any

    schema = %{
      from: {:eoa_address, required: true},
      to: {:score_address, required: true},
      dataType: {enum([:call]), default: :call},
      data:
        if call_schema do
          %{
            method: {:string, required: true},
            params: {call_schema, required: true}
          }
        else
          %{method: {:string, required: true}}
        end
    }

    params = %{
      from: from,
      to: to,
      data: %{
        method: method,
        params: call_params
      }
    }

    with {:ok, params} <- validate(schema, params) do
      request =
        :call
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  def call(%Identity{} = _identity, _to, _method, _params, _options) do
    identity_must_have_a_wallet()
  end

  @doc """
  Builds a request for getting the balance of an EOA or SCORE `address`. If the
  address is not provided, it uses the one in the `identity`.

  ## Example

  The following builds a request for getting the balance of the wallet doing the
  request:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.get_balance(identity)
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getBalance",
      options: ...,
      params: %{
        address: "hxbe258ceb872e08851f1f59694dac2558708ece11"
      }
    }
  }
  ```
  """
  @spec get_balance(Identity.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec get_balance(Identity.t(), nil | Schema.Types.Address.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_balance(identity, address \\ nil)

  def get_balance(%Identity{} = identity, address) do
    address = address || identity.address
    schema = %{address: {:address, required: true}}

    with {:ok, params} <- validate(schema, address: address) do
      request =
        :get_balance
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  @doc """
  Builds a request for getting the API of a SCORE given its `address`.

  ## Example

  The following builds a request for getting the API of a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_score_api(identity, "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32")
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getScoreApi",
      options: ...,
      params: %{
        address: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
      }
    }
  }
  ```
  """
  @spec get_score_api(Identity.t(), Schema.Types.SCORE.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_score_api(identity, address)

  def get_score_api(%Identity{} = identity, address) do
    schema = %{address: {:score_address, required: true}}

    with {:ok, params} <- validate(schema, address: address) do
      request =
        :get_score_api
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  @doc """
  Builds a request for geting the total ICX supply.

  ## Example

  The following builds a request for getting the ICX total supply:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_total_supply(identity)
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getTotalSupply",
      options: ...,
      params: %{}
    }
  }
  ```
  """
  @spec get_total_supply(Identity.t()) :: {:ok, Request.t()}
  def get_total_supply(identity)

  def get_total_supply(%Identity{} = identity) do
    request =
      :get_total_supply
      |> method()
      |> Request.build(%{}, identity: identity)

    {:ok, request}
  end

  @doc """
  Builds a request for getting the transaction result given its `tx_hash` and
  some optional `options`.

  Options:
  - `timeout` - Timeout in milliseconds for waiting for the result of the
    transaction.

  ## Example

  The following builds a request for getting a transaction result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_transaction_result(identity, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b")
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getTransactionResult",
      options: ...,
      params: %{
        txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"
      }
    }
  }
  ```
  """
  @spec get_transaction_result(Identity.t(), Schema.Types.Hash.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec get_transaction_result(Identity.t(), Schema.Types.Hash.t(), keyword()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_transaction_result(identity, tx_hash, options \\ [])

  def get_transaction_result(%Identity{} = identity, tx_hash, options) do
    schema = %{txHash: {:hash, required: true}}

    with {:ok, params} <- validate(schema, txHash: tx_hash) do
      timeout = options[:timeout] || 0

      method =
        if timeout > 0,
          do: :wait_transaction_result,
          else: :get_transaction_result

      options =
        if timeout > 0,
          do: [schema: schema, identity: identity, timeout: timeout],
          else: [schema: schema, identity: identity]

      request =
        method
        |> method()
        |> Request.build(params, options)

      {:ok, request}
    end
  end

  @doc """
  Builds a request for getting a transaction by `tx_hash`.

  ## Example

  The following builds a request for getting a transaction:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.RPC.Request.get_transaction_by_hash(identity, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b")
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_getTransactionByHash",
      options: ...,
      params: %{
        txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"
      }
    }
  }
  ```
  """
  @spec get_transaction_by_hash(Identity.t(), Schema.Types.Hash.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def get_transaction_by_hash(identity, tx_hash)

  def get_transaction_by_hash(%Identity{} = identity, tx_hash) do
    schema = %{txHash: {:hash, required: true}}

    with {:ok, params} <- validate(schema, txHash: tx_hash) do
      request =
        :get_transaction_by_hash
        |> method()
        |> Request.build(params, schema: schema, identity: identity)

      {:ok, request}
    end
  end

  @doc """
  Builds an ICX transfer transaction given a `recipient` address and the
  `amount` of ICX in loop (1 ICX = 10¹⁸ loop).

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.

  ### Example

  The following builds a request for sending 1 ICX to another wallet:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.transfer(
  ...>   identity,
  ...>   "hx2e243ad926ac48d15156756fce28314357d49d83",
  ...>   1_000_000_000_000_000_000
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "hx2e243ad926ac48d15156756fce28314357d49d83",
        value: 1_000_000_000_000_000_000,
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3
      }
    }
  }
  ```
  """
  @spec transfer(
          Identity.t(),
          Schema.Types.Address.t(),
          Schema.Types.Loop.t()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec transfer(
          Identity.t(),
          Schema.Types.Address.t(),
          Schema.Types.Loop.t(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def transfer(identity, recipient, amount, options \\ [])

  def transfer(%Identity{} = identity, to, value, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(:value, value)

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:address, required: true},
        value: {:loop, required: true}
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds an `message` transfer to a given `recipient`.

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.

  ### Example

  The following builds a request for sending a message to another address:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.send_message(
  ...>   identity,
  ...>   "hx2e243ad926ac48d15156756fce28314357d49d83",
  ...>   "Hello world!"
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "hx2e243ad926ac48d15156756fce28314357d49d83",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :message,
        data: "Hello world!"
      }
    }
  }
  ```
  """
  @spec send_message(Identity.t(), Schema.Types.Address.t(), binary()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec send_message(
          Identity.t(),
          Schema.Types.Address.t(),
          binary(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def send_message(identity, recipient, message, options \\ [])

  def send_message(%Identity{} = identity, to, message, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(:data, message)

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:address, required: true},
        dataType: {enum([:message]), default: :message},
        data: {:binary_data, required: true}
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a transaction for calling a SCORE `method`.

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.
  - `schema` - Method parameters schema.

  ### Example

  The following builds a request for calling the method `transfer` in a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.transaction_call(
  ...>   identity,
  ...>   "cx2e243ad926ac48d15156756fce28314357d49d83",
  ...>   "transfer",
  ...>   %{
  ...>     address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
  ...>     value: 1_000_000_000_000_000_000
  ...>   },
  ...>   schema: %{
  ...>     address: {:address, required: true},
  ...>     value: {:loop, required: true}
  ...>   }
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cx2e243ad926ac48d15156756fce28314357d49d83",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :call,
        data: %{
          method: "transfer",
          params: %{
            address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
            value: 1_000_000_000_000_000_000
          }
        }
      }
    }
  }
  ```
  """
  @spec transaction_call(Identity.t(), Schema.Types.SCORE.t(), binary()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec transaction_call(
          Identity.t(),
          Schema.Types.SCORE.t(),
          binary(),
          nil | map() | keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec transaction_call(
          Identity.t(),
          Schema.Types.SCORE.t(),
          binary(),
          nil | map() | keyword(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def transaction_call(identity, score, method, params \\ nil, options \\ [])

  def transaction_call(%Identity{} = identity, to, method, call_params, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(:data, %{
        method: method,
        params: call_params
      })

    call_schema =
      if is_nil(call_params), do: nil, else: options[:schema] || :any

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:score_address, required: true},
        dataType: {enum([:call]), default: :call},
        data:
          if call_schema do
            {%{
               method: {:string, required: true},
               params: {call_schema, required: true}
             }, required: true}
          else
            {%{method: {:string, required: true}}, required: true}
          end
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a request for deploying a new SCORE given its `content`.

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.
  - `content_type` - MIME type of the SCORE contents. Defaults to
    `application/zip`.
  - `on_install_params` - Parameters for the function `on_install/0`.
  - `on_install_schema` - Schema for the parameters of the function
    `on_install/0`.

  ### Example

  The following builds a request for deploying a SCORE:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.install_score(
  ...>   identity,
  ...>   content,
  ...>   on_install_params: %{
  ...>     address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
  ...>   },
  ...>   on_install_schema: %{
  ...>     address: {:address, required: true}
  ...>   }
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cx0000000000000000000000000000000000000000",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deploy,
        data: %{
          content_type: "application/zip",
          content: <<70, 79, 82, 49, 0, ...>>,
          params: %{
            address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
          }
        }
      }
    }
  }
  ```
  """
  @spec install_score(Identity.t(), Schema.Types.BinaryData.t()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec install_score(Identity.t(), Schema.Types.BinaryData.t(), keyword()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def install_score(identity, content, options \\ [])

  def install_score(%Identity{} = identity, content, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, "cx0000000000000000000000000000000000000000")
      |> Map.put(:data, %{
        contentType: options[:content_type] || "application/zip",
        content: content,
        params: options[:on_install_params]
      })

    on_install_schema = options[:on_install_schema]

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:score_address, required: true},
        dataType: {enum([:deploy]), default: :deploy},
        data:
          if on_install_schema do
            {%{
               contentType: {:string, required: true},
               content: {:binary_data, required: true},
               params: {on_install_schema, required: true}
             }, required: true}
          else
            {%{
               contentType: {:string, required: true},
               content: {:binary_data, required: true}
             }, required: true}
          end
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a request for updating an existent SCORE given its `address` and
  `content`.

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.
  - `content_type` - MIME type of the SCORE contents. Defaults to
    `application/zip`.
  - `on_update_params` - Parameters for the function `on_update/0`.
  - `on_update_schema` - Schema for the parameters of the function
    `on_update/0`.

  ### Example

  The following builds a request for updating a SCORE:

  ```elixir
  iex> {:ok, content} = File.read("./my-updated-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.update_score(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   content,
  ...>   on_update_params: %{
  ...>     address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
  ...>   },
  ...>   on_update_schema: %{
  ...>     address: {:address, required: true}
  ...>   }
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deploy,
        data: %{
          content_type: "application/zip",
          content: <<70, 79, 82, 49, 0, ...>>,
          params: %{
            address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
          }
        }
      }
    }
  }
  ```
  """
  @spec update_score(
          Identity.t(),
          Schema.Types.SCORE.t(),
          Schema.Types.BinaryData.t()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec update_score(
          Identity.t(),
          Schema.Types.SCORE.t(),
          Schema.Types.BinaryData.t(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def update_score(identity, score_address, content, options \\ [])

  def update_score(%Identity{} = identity, to, content, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(:data, %{
        contentType: options[:content_type] || "application/zip",
        content: content,
        params: options[:on_update_params]
      })

    on_update_schema = options[:on_update_schema]

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:score_address, required: true},
        dataType: {enum([:deploy]), default: :deploy},
        data:
          if on_update_schema do
            {%{
               contentType: {:string, required: true},
               content: {:binary_data, required: true},
               params: {on_update_schema, required: true}
             }, required: true}
          else
            {%{
               contentType: {:string, required: true},
               content: {:binary_data, required: true}
             }, required: true}
          end
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a request for depositing ICX in loop (10¹⁸ loop = 1 ICX) into a SCORE
  for paying other user's fees when transacting with it (fee sharing).

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.

  ### Example

  The following builds a request for depositing ICX into a SCORE for fee
  sharing:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.deposit_shared_fee(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   1_000_000_000_000_000_000
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
        value: 1_000_000_000_000_000_000,
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deposit,
        data: %{
          action: :add
        }
      }
    }
  }
  ```
  """
  @spec deposit_shared_fee(
          Identity.t(),
          Schema.Types.SCORE.t(),
          Schema.Types.Loop.t()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec deposit_shared_fee(
          Identity.t(),
          Schema.Types.SCORE.t(),
          Schema.Types.Loop.t(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def deposit_shared_fee(identity, score_address, amount, options \\ [])

  def deposit_shared_fee(%Identity{} = identity, to, amount, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(:value, amount)

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:score_address, required: true},
        value: {:loop, required: true},
        dataType: {enum([:deposit]), default: :deposit},
        data:
          {%{
             action: {enum([:add]), default: :add}
           }, default: %{action: "add"}}
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a request for withdrawing, partially or totally, the shared fee
  deposited ICX in loop (10¹⁸ loop = 1 ICX) from a SCORE.

  There are three types of withdrawals:

  - Withdrawing a deposit by its hash.
  - Withdrawing a loop amount from the SCORE.
  - Withdrawing the whole deposit.

  Options:
  - `timeout` - Time in milliseconds to wait for the transaction result.
  - `params` - Extra transaction parameters for overriding the defaults.

  ### Examples

  The following builds a request for withdrawing the whole deposit from the
  SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.withdraw_shared_fee(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deposit,
        data: %{
          action: :withdraw
        }
      }
    }
  }
  ```

  The following builds a request for withdrawing 1 ICX from the shared fee
  deposit:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.withdraw_shared_fee(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   1_000_000_000_000_000_000
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deposit,
        data: %{
          action: :withdraw,
          amount: 1_000_000_000_000_000_000
        }
      }
    }
  }
  ```

  The following builds a request for withdrawing a specific deposit by hash:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.RPC.Request.withdraw_shared_fee(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   "0xc71303ef8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"
  ...> )
  {
    :ok,
    %Icon.RPC.Request{
      method: "icx_sendTransaction",
      options: ...,
      params: %{
        from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
        to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
        nid: 1,
        nonce: 1641487595040282,
        timestamp: ~U[2022-01-06 16:46:35.042078Z],
        version: 3,
        dataType: :deposit,
        data: %{
          action: :withdraw,
          id: "0xc71303ef8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"
        }
      }
    }
  }
  ```
  """
  @spec withdraw_shared_fee(
          Identity.t(),
          Schema.Types.SCORE.t()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec withdraw_shared_fee(
          Identity.t(),
          Schema.Types.SCORE.t(),
          nil | Schema.Types.Loop.t() | Schema.Types.Hash.t()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  @spec withdraw_shared_fee(
          Identity.t(),
          Schema.Types.SCORE.t(),
          nil | Schema.Types.Loop.t() | Schema.Types.Hash.t(),
          keyword()
        ) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  def withdraw_shared_fee(
        identity,
        score_address,
        hash_or_amount \\ nil,
        options \\ []
      )

  def withdraw_shared_fee(%Identity{} = identity, to, hash_or_amount, options) do
    params =
      options
      |> Keyword.get(:params, %{})
      |> Map.put(:to, to)
      |> Map.put(
        :data,
        case hash_or_amount do
          nil -> %{}
          amount when is_integer(amount) -> %{amount: amount}
          id -> %{id: id}
        end
      )

    schema =
      base_transaction_schema()
      |> Map.merge(%{
        to: {:score_address, required: true},
        dataType: {enum([:deposit]), default: :deposit},
        data:
          case hash_or_amount do
            nil ->
              {%{
                 action: {enum([:withdraw]), default: :withdraw}
               }, default: %{action: "withdraw"}}

            amount when is_integer(amount) ->
              {%{
                 action: {enum([:withdraw]), default: :withdraw},
                 amount: {:loop, required: true}
               }, default: %{action: "withdraw"}}

            _ ->
              {%{
                 action: {enum([:withdraw]), default: :withdraw},
                 id: {:hash, required: true}
               }, default: %{action: "withdraw"}}
          end
      })

    build_transaction(identity, params, schema, options)
  end

  @doc """
  Builds a request for requesting the step limit estimation of a transaction.
  """
  @spec estimate_step(Identity.t(), map(), Schema.t()) :: {:ok, Request.t()}
  def estimate_step(identity, params, schema)

  def estimate_step(%Identity{} = identity, params, schema) do
    identity = %{identity | debug: true}

    request =
      :estimate_step
      |> method()
      |> Request.build(params, schema: schema, identity: identity)

    {:ok, request}
  end

  #########
  # Helpers

  @spec validate(Schema.t(), map() | keyword()) ::
          {:ok, map()}
          | {:error, Error.t()}
  defp validate(schema, params) do
    schema
    |> Schema.generate()
    |> Schema.new(params)
    |> Schema.load()
    |> Schema.apply()
  end

  @spec method(method :: method()) :: binary()
  defp method(:get_last_block), do: "icx_getLastBlock"
  defp method(:get_block_by_height), do: "icx_getBlockByHeight"
  defp method(:get_block_by_hash), do: "icx_getBlockByHash"
  defp method(:call), do: "icx_call"
  defp method(:get_balance), do: "icx_getBalance"
  defp method(:get_score_api), do: "icx_getScoreApi"
  defp method(:get_total_supply), do: "icx_getTotalSupply"
  defp method(:get_transaction_result), do: "icx_getTransactionResult"
  defp method(:get_transaction_by_hash), do: "icx_getTransactionByHash"
  defp method(:send_transaction), do: "icx_sendTransaction"
  defp method(:send_transaction_and_wait), do: "icx_sendTransactionAndWait"
  defp method(:wait_transaction_result), do: "icx_waitTransactionResult"
  defp method(:estimate_step), do: "debug_estimateStep"

  @spec add_identity(Identity.t(), keyword() | map()) ::
          {:ok, map()}
          | {:error, Error.t()}
  defp add_identity(identity, params)

  defp add_identity(%Identity{address: "hx" <> _} = identity, params)
       when is_map(params) do
    params =
      params
      |> Map.put(:nid, identity.network_id)
      |> Map.put(:from, identity.address)

    {:ok, params}
  end

  @spec build_transaction(Identity.t(), map(), Schema.t(), keyword()) ::
          {:ok, Request.t()}
          | {:error, Error.t()}
  defp build_transaction(identity, params, schema, options)

  defp build_transaction(%Identity{} = identity, params, schema, options)
       when has_address(identity) do
    with {:ok, params} <- add_identity(identity, params),
         {:ok, params} <- validate(schema, params) do
      timeout = options[:timeout] || 0

      method =
        if timeout > 0,
          do: :send_transaction_and_wait,
          else: :send_transaction

      request =
        method
        |> method()
        |> Request.build(params,
          schema: schema,
          identity: identity,
          timeout: timeout
        )

      {:ok, request}
    end
  end

  defp build_transaction(%Identity{} = _identity, _params, _schema, _options) do
    identity_must_have_a_wallet()
  end

  @spec identity_must_have_a_wallet() :: {:error, Error.t()}
  defp identity_must_have_a_wallet do
    reason =
      Error.new(
        reason: :invalid_request,
        message: "identity must have a wallet"
      )

    {:error, reason}
  end

  # Base transaction schema
  @spec base_transaction_schema() :: Schema.t()
  defp base_transaction_schema do
    %{
      version: {:integer, required: true, default: 3},
      from: {:eoa_address, required: true},
      stepLimit: :integer,
      nonce: {:integer, default: &default_nonce/1},
      timestamp: {:timestamp, required: true, default: &default_timestamp/1},
      nid: {:integer, required: true},
      signature: :signature
    }
  end

  @spec default_nonce(Schema.state()) :: non_neg_integer()
  defp default_nonce(%Schema{} = _state) do
    :erlang.system_time(:microsecond)
  end

  @spec default_timestamp(Schema.state()) :: DateTime.t()
  defp default_timestamp(%Schema{} = _state) do
    DateTime.utc_now()
  end
end