lib/icon.ex

defmodule Icon do
  @moduledoc """
  `Icon` is a library for interacting with the interoperable decentralized
  aggregator network [ICON 2.0](https://icon.foundation).
  """
  alias Icon.RPC.{Identity, Request}
  alias Icon.Schema

  alias Icon.Schema.{
    Error,
    Types.Address,
    Types.BinaryData,
    Types.Block,
    Types.Hash,
    Types.Loop,
    Types.SCORE,
    Types.Transaction
  }

  @doc """
  Gets block by `hash_or_height`. If hash or height are not provided, it will
  retrieve the latest block.

  ## Example

  - Get latest block:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_block(identity)
  {
    :ok,
    %Icon.Schema.Types.Block{
      block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      confirmed_transaction_list: [
        %Icon.Schema.Types.Transaction{
          data: %{
            result: %{
              coveredByFee: 0,
              coveredByOverIssuedICX: 12_000,
              issue: 0
            }
          },
          dataType: :base,
          timestamp: ~U[2022-01-22 11:06:21.258886Z],
          txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
          version: 3
        }
      ],
      height: 3_153_751,
      merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
      peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
      prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
      signature: nil,
      time_stamp: ~U[2022-01-22 11:06:21.258886Z],
      version: "2.0"
    }
  }
  ```

  - Get block by height:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_block(identity, 3_153_751)
  {
    :ok,
    %Icon.Schema.Types.Block{
      block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      confirmed_transaction_list: [
        %Icon.Schema.Types.Transaction{
          data: %{
            result: %{
              coveredByFee: 0,
              coveredByOverIssuedICX: 12_000,
              issue: 0
            }
          },
          dataType: :base,
          timestamp: ~U[2022-01-22 11:06:21.258886Z],
          txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
          version: 3
        }
      ],
      height: 3_153_751,
      merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
      peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
      prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
      signature: nil,
      time_stamp: ~U[2022-01-22 11:06:21.258886Z],
      version: "2.0"
    }
  }
  ```

  - Get block by hash:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_block(identity, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b")
  {
    :ok,
    %Icon.Schema.Types.Block{
      block_hash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      confirmed_transaction_list: [
        %Icon.Schema.Types.Transaction{
          data: %{
            result: %{
              coveredByFee: 0,
              coveredByOverIssuedICX: 12_000,
              issue: 0
            }
          },
          dataType: :base,
          timestamp: ~U[2022-01-22 11:06:21.258886Z],
          txHash: "0x75e553dcd57853e6c96428c4fede49209a3055fc905db757baa470c1e94f736d",
          version: 3
        }
      ],
      height: 3_153_751,
      merkle_tree_root_hash: "0xce5aa42a762ee88a32fc2a792dfb5975858a71a8abf4ec51fb1218e3b827aa01",
      peer_id: "hxb97c82a5577a0a436f51a41421ad2d3b28da3f25",
      prev_block_hash: "0xfe8138afd24512cc0e9f4da8df350300a759a480f15c8a00b04b2d753ea62ac3",
      signature: nil,
      time_stamp: ~U[2022-01-22 11:06:21.258886Z],
      version: "2.0"
    }
  }
  ```
  """
  @spec get_block(Identity.t()) ::
          {:ok, Block.t()}
          | {:error, Error.t()}
  @spec get_block(Identity.t(), nil | pos_integer() | Hash.t()) ::
          {:ok, Block.t()}
          | {:error, Error.t()}
  def get_block(identity, height_or_hash \\ nil)

  def get_block(identity, nil) do
    with {:ok, request} <- Request.Goloop.get_last_block(identity),
         {:ok, response} <- Request.send(request) do
      load_block(response)
    end
  end

  def get_block(identity, height) when is_integer(height) and height > 0 do
    with {:ok, request} <- Request.Goloop.get_block_by_height(identity, height),
         {:ok, response} <- Request.send(request) do
      load_block(response)
    end
  end

  def get_block(identity, hash) do
    with {:ok, request} <- Request.Goloop.get_block_by_hash(identity, hash),
         {:ok, response} <- Request.send(request) do
      load_block(response)
    end
  end

  @doc """
  Calls a readonly SCORE `method` (no transaction).

  The `identity` should be created using a valid `private_key`, otherwise the
  call cannot be executed.

  Options:
  - `call_schema` - Schema to validate the `params`. When no schema is provided,
    the default schema `:any` will be used instead.
  - `response_schema` - Schema for transforming the incoming values. When no
    schema is provided, the default schema `:any` will be used instead.

  ## Example

  - Calling the method `getBalance` without parameters:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.call(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   "getBalance",
  ...> )
  {:ok, "0x2a"}
  ```

  - Calling the method `getBalance` with an address as parameter without type
    conversion:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.call(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   "getBalance",
  ...>   %{address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"},
  ...> )
  {:ok, "0x2a"}
  ```

  - Calling the method `getBalance` with an address as parameter with type
    conversion using a `call_schema` (recommended):

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.call(
  ...>   identity,
  ...>   "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
  ...>   "getBalance",
  ...>   %{address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"},
  ...>   call_schema: %{address: {:address, required: true}},
  ...>   response_schema: :loop
  ...> )
  {:ok, 42}
  ```
  """
  @spec call(Identity.t(), SCORE.t(), binary()) ::
          {:ok, any()}
          | {:error, Error.t()}
  @spec call(Identity.t(), SCORE.t(), binary(), nil | map() | keyword()) ::
          {:ok, any()}
          | {:error, Error.t()}
  @spec call(
          Identity.t(),
          SCORE.t(),
          binary(),
          nil | map() | keyword(),
          keyword()
        ) ::
          {:ok, any()}
          | {:error, Error.t()}
  def call(identity, score_address, method, params \\ nil, options \\ [])

  def call(%Identity{} = identity, to, method, params, options) do
    options =
      case options[:call_schema] do
        nil ->
          options

        value ->
          options
          |> Keyword.delete(:call_schema)
          |> Keyword.put(:schema, value)
      end

    with {:ok, request} <-
           Request.Goloop.call(identity, to, method, params, options),
         {:ok, response} <- Request.send(request) do
      load_call_response(response, options)
    end
  end

  @doc """
  Gets the balance of an EOA or SCORE `address`. If the `address` is not provided,
  it uses the one in the `identity`. The balance is returned in loop
  (1 ICX = 10¹⁸ loop).

  ## Examples

  - Requesting the balance of the loaded identity:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.get_balance(identity)
  {:ok, 2_045_995_000_000_000_000_000}
  ```

  - Requesting the balance of a wallet:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_balance(identity, "hxbe258ceb872e08851f1f59694dac2558708ece11")
  {:ok, 0}
  ```
  """
  @spec get_balance(Identity.t()) :: {:ok, Loop.t()} | {:error, Error.t()}
  @spec get_balance(Identity.t(), nil | Address.t()) ::
          {:ok, Loop.t()}
          | {:error, Error.t()}
  def get_balance(identity, address \\ nil)

  def get_balance(%Identity{} = identity, address) do
    with {:ok, request} <- Request.Goloop.get_balance(identity, address),
         {:ok, response} <- Request.send(request),
         :error <- Loop.load(response) do
      reason =
        Error.new(
          reason: :server_error,
          message: "cannot cast balance to loop"
        )

      {:error, reason}
    end
  end

  @doc """
  Gets SCORE API.

  ## Example

  - Gets the API of a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_score_api(identity, "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32")
  {
    :ok,
    [
      %{
        "type" => "function",
        "name" => "balanceOf",
        "inputs" => [
          %{
            "name" => "_owner",
            "type" => "Address"
          }
        ],
        "outputs" => [
          %{
            "type" => "int"
          }
        ],
        "readonly" => "0x1"
      },
      ...
    ]
  }
  ```

  ## API Entries

  Each member of the list will have the following keys:

  Key        | Description
  :--------- | :----------
  `type`     | Either `function`, `fallback` or `eventlog`.
  `name`     | Name of the function or the event log.
  `inputs`   | A list of parameters the function or the event receives.
  `outputs`  | A list of values a function returns.
  `readonly` | Whether the function call can be done without a transaction or not.
  `payable`  | Whether the function can be paid or not.

  > Note: Both `readonly` and `payable` will be returned in the ICON 2.0
  > representation or a boolean value e.g. `0x1` for `true`.

  Each input will have the following keys:

  Key       | Description
  :-------- | :----------
  `name`    | Parameter name.
  `type`    | Parameter type. Either `int`, `str`, `bytes`, `bool` or `Address`.
  `indexed` | (Only for event logs) if the parameter is indexed or not.

  > Note: `indexed` will be returned in the ICON 2.0 representation of a boolean
  > e.g. `0x1` for `true`.

  Each output will have the following keys:

  Key    | Description
  :----- | :----------
  `type` | Result type. Either `int`, `str`, `bytes`, `bool`, `Address`, `dict` or `list`.
  """
  @spec get_score_api(Identity.t(), SCORE.t()) ::
          {:ok, list()}
          | {:error, Error.t()}
  def get_score_api(identity, score_address)

  def get_score_api(%Identity{} = identity, score_address) do
    with {:ok, request} <- Request.Goloop.get_score_api(identity, score_address) do
      Request.send(request)
    end
  end

  @doc """
  Gets the ICX's total supply in loop (1 ICX = 10¹⁸ loop).

  ## Examples

  - Requesting ICX's total supply:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.total_supply(identity)
  {:ok, 1_300_163_572_018_865_530_968_203_250}
  ```
  """
  @spec get_total_supply(Identity.t()) :: {:ok, Loop.t()} | {:error, Error.t()}
  def get_total_supply(identity)

  def get_total_supply(%Identity{} = identity) do
    with {:ok, request} <- Request.Goloop.get_total_supply(identity),
         {:ok, response} <- Request.send(request),
         :error <- Loop.load(response) do
      reason =
        Error.new(
          reason: :server_error,
          message: "cannot cast total supply to loop"
        )

      {:error, reason}
    end
  end

  @doc """
  Gets a transaction result by `hash`.

  Options:
  - `timeout` - Timeout in milliseconds for waiting for the result of the
  transaction in case it's pending.

  ## Example

  - Requesting a successful transaction result by `hash`:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_transaction_result(identity, ""0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25"")
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
      blockHeight: 45162694,
      cumulativeStepUsed: 0,
      eventLogs: [
        %Icon.Schema.Types.EventLog{
          data: ["{\\"method\\": \\"_swap\\", \\"params\\": {\\"toToken\\": \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\", \\"minimumReceive\\": \\"1000020000000000000000\\", \\"path\\": [\\"cx2609b924e33ef00b648a409245c7ea394c467824\\", \\"cxf61cd5a45dc9f91c15aa65831a30a90d59a09619\\", \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\"]}}"],
          header: "Transfer(Address,Address,int,bytes)",
          indexed: [
            "hx948b9727f426ae7789741da8c796807f78ba137f",
            "cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
            1000020000000000000000
          ],
          name: "Transfer",
          score_address: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb"
        },
        ...
      ]
      failure: nil,
      logsBloom: <<0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, 0, 0, 16, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, 1, 0, 1, 0, 0, 16, 0, 128,
        ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12500000000,
      stepUsed: 13816635,
      to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
      txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
      txIndex: 2
    }
  }
  ```
  """
  @spec get_transaction_result(Identity.t(), Hash.t()) ::
          {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec get_transaction_result(Identity.t(), Hash.t(), keyword()) ::
          {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def get_transaction_result(identity, tx_hash, options \\ [])

  def get_transaction_result(%Identity{} = identity, hash, options) do
    with {:ok, request} <-
           Request.Goloop.get_transaction_result(identity, hash, options),
         {:ok, response} <- Request.send(request) do
      load_transaction_result(response)
    end
  end

  @doc """
  Gets a transaction by `hash`.

  ## Example

  - Requesting a successful transaction by `hash`:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> Icon.get_transaction_by_hash(identity, "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25")
  {
    :ok,
    %Icon.Schema.Types.Transaction{
      blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
      blockHeight: 45162694,
      data: %{
        method: "transfer",
        params: %{
          "_data" => "0x7b226d6574686f64223a20225f73776170222c2022706172616d73223a207b22746f546f6b656e223a2022637838386664376466376464666638326637636337333563383731646335313938333863623233356262222c20226d696e696d756d52656365697665223a202231303030303230303030303030303030303030303030222c202270617468223a205b22637832363039623932346533336566303062363438613430393234356337656133393463343637383234222c2022637866363163643561343564633966393163313561613635383331613330613930643539613039363139222c2022637838386664376466376464666638326637636337333563383731646335313938333863623233356262225d7d7d",
          "_to" => "cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
          "_value" => "0x363610bbaabe220000"
        }
      },
      dataType: :call,
      from: "hx948b9727f426ae7789741da8c796807f78ba137f",
      nid: 1,
      nonce: 223,
      signature: "TTfXvXZ3NG53R2tx9D69fMvmHW8mIIWWEDZnNfOgGG1BOeGYSYzV37PWCi7ryXKKAc7e80ue937yrull8hoZxgE=",
      stepLimit: 50000000,
      timestamp: ~U[2022-01-22 06:48:51.250054Z],
      to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
      txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
      txIndex: 2,
      value: 0,
      version: 3
    }
  }
  ```

  The `params` key cannot be decoded beforehand, so we need to use a schema to
  retrieve the Elixir values. Using the previous example, we would do something
  like this:

  ```elixir
  iex> identity = Icon.RPC.Identity.new()
  iex> {:ok, tx} = Icon.get_transaction_by_hash(identity, "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25")
  iex> schema = %{
  ...>   _data: :binary_data,
  ...>   _to: :address,
  ...>   _value: :loop
  ...> }
  iex> {:ok, decoded_params} = (
  ...>   schema
  ...>   |> Icon.Schema.generate()
  ...>   |> Icon.Schema.new(tx.data.params)
  ...>   |> Icon.Schema.load()
  ...>   |> Icon.Schema.apply()
  ...> )
  iex> put_in(tx, [:data, :params], decoded_params)
  %Icon.Schema.Types.Transaction{
    blockHash: "0xd6e8ed8035b38a5c09de59df101c7e6258e6d7e0690d3c6c6093045a5550bb83",
    blockHeight: 45162694,
    data: %{
      method: "transfer",
      params: %{
        _data: "{\\"method\\": \\"_swap\\", \\"params\\": {\\"toToken\\": \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\", \\"minimumReceive\\": \\"1000020000000000000000\\", \\"path\\": [\\"cx2609b924e33ef00b648a409245c7ea394c467824\\", \\"cxf61cd5a45dc9f91c15aa65831a30a90d59a09619\\", \\"cx88fd7df7ddff82f7cc735c871dc519838cb235bb\\"]}}",
        _to: "cx21e94c08c03daee80c25d8ee3ea22a20786ec231",
        _value: 1000020000000000000000
      }
    },
    dataType: :call,
    from: "hx948b9727f426ae7789741da8c796807f78ba137f",
    nid: 1,
    nonce: 223,
    signature: "TTfXvXZ3NG53R2tx9D69fMvmHW8mIIWWEDZnNfOgGG1BOeGYSYzV37PWCi7ryXKKAc7e80ue937yrull8hoZxgE=",
    stepLimit: 50000000,
    timestamp: ~U[2022-01-22 06:48:51.250054Z],
    to: "cx88fd7df7ddff82f7cc735c871dc519838cb235bb",
    txHash: "0x917def9734385cbb0c1f3e9d6fc0e46706f51348ab9cea1d7e1bf44e1ed51b25",
    txIndex: 2,
    value: 0,
    version: 3
  }
  ```

  For more information about schemas see `Icon.Schema` module.
  """
  @spec get_transaction_by_hash(Identity.t(), Hash.t()) ::
          {:ok, Transaction.t()}
          | {:error, Error.t()}
  def get_transaction_by_hash(identity, hash)

  def get_transaction_by_hash(%Identity{} = identity, hash) do
    with {:ok, request} <-
           Request.Goloop.get_transaction_by_hash(identity, hash),
         {:ok, response} <- Request.send(request) do
      load_transaction(response)
    end
  end

  @doc """
  Transfers an ICX `amount` to a `recipient`.

  The `identity` should be created using a valid `private_key`, otherwise the
  transfer cannot be executed.

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

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Transfer `1.00` ICX to another wallet:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
  iex> amount = 1_000_000_000_000_000_000 # 1 ICX in loop
  iex> Icon.transfer(identity, recipient, amount)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Transfer `1.00` ICX to another wallet and wait 5 seconds for the result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
  iex> amount = 1_000_000_000_000_000_000 # 1 ICX in loop
  iex> Icon.transfer(identity, recipient, amount, timeout: 5_000)
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "hx2e243ad926ac48d15156756fce28314357d49d83",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```
  """
  @spec transfer(Identity.t(), Address.t(), Loop.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec transfer(Identity.t(), Address.t(), Loop.t(), keyword()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def transfer(identity, recipient, amount, options \\ [])

  def transfer(%Identity{} = identity, to, value, options) do
    with {:ok, request} <-
           Request.Goloop.transfer(identity, to, value, options) do
      send_transaction(request)
    end
  end

  @doc """
  Send a signed `message` to a `recipient`.

  The `identity` should be created using a valid `private_key`, otherwise the
  message cannot be sent.

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

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Send a message to another wallet:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
  iex> Icon.send_message(identity, recipient, "Hello!")
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Send a message to another wallet and wait 5 seconds for the result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> recipient = "hx2e243ad926ac48d15156756fce28314357d49d83"
  iex> Icon.send_message(identity, recipient, "Hello!", timeout: 5_000)
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "hx2e243ad926ac48d15156756fce28314357d49d83",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```
  """
  @spec send_message(Identity.t(), Schema.Types.Address.t(), binary()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec send_message(
          Identity.t(),
          Schema.Types.Address.t(),
          binary(),
          keyword()
        ) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def send_message(identity, recipient, message, options \\ [])

  def send_message(%Identity{} = identity, to, message, options) do
    with {:ok, request} <-
           Request.Goloop.send_message(identity, to, message, options) do
      send_transaction(request)
    end
  end

  @doc """
  Calls a `method` in a `score` in a signed transaction.

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

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Call Schema

  The `call_schema` option gives defines the types of the `call_params`. This is
  required for `method` calls with parameters, because ICON has a different
  type representation than Elixir e.g. let's say we want to call the method
  `transfer(from: Address, to: Address, amount: int)` for transferring an
  `amount` of tokens `from` one EOA address `to` another. Then the schema would
  look like this:

  ```elixir
  %{
    from: {:eoa_address, required: true},
    to: {:eoa_address, required: true},
    amount: {:loop, required: true}
  }
  ```

  Then, while doing the actual transaction call, the schema will help with the
  conversion of the `call_params`. So, something like the following:

  ```elixir
  %{
    from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
    to: "hx2e243ad926ac48d15156756fce28314357d49d83",
    amount: 1_000_000_000_000_000_000
  }
  ```

  will be converted to the following when communicating with the node:

  ```json
  {
    "from": "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
    "to": "hx2e243ad926ac48d15156756fce28314357d49d83",
    "amount": "0xde0b6b3a7640000"
  }
  ```

  ## Examples

  Given the example method used in the previous section, we can call it as
  follows:

  - Transfer tokens from one wallet to another:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.transaction_call(
  ...>   identity,
  ...>   "cx2e243ad926ac48d15156756fce28314357d49d83",
  ...>   "transfer",
  ...>   %{
  ...>     from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
  ...>     to: "hx2e243ad926ac48d15156756fce28314357d49d83",
  ...>     amount: 1_000_000_000_000_000_000
  ...>   },
  ...>   call_schema: %{
  ...>     from: {:eoa_address, required: true},
  ...>     to: {:eoa_address, required: true},
  ...>     amount: {:loop, required: true}
  ...>   }
  ...> )
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Transfer token from one wallet to another and wait 5 second for the result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.transaction_call(
  ...>   identity,
  ...>   "cx2e243ad926ac48d15156756fce28314357d49d83",
  ...>   "transfer",
  ...>   %{
  ...>     from: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57",
  ...>     to: "hx2e243ad926ac48d15156756fce28314357d49d83",
  ...>     amount: 1_000_000_000_000_000_000
  ...>   },
  ...>   call_schema: %{
  ...>     from: {:eoa_address, required: true},
  ...>     to: {:eoa_address, required: true},
  ...>     amount: {:loop, required: true}
  ...>   },
  ...>   timeout: 5_000
  ...> )
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "hx2e243ad926ac48d15156756fce28314357d49d83",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```
  """
  @spec transaction_call(Identity.t(), SCORE.t(), binary()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec transaction_call(
          Identity.t(),
          SCORE.t(),
          binary(),
          nil | map() | keyword()
        ) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec transaction_call(
          Identity.t(),
          SCORE.t(),
          binary(),
          nil | map() | keyword(),
          keyword()
        ) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def transaction_call(identity, score, method, params \\ nil, options \\ [])

  def transaction_call(%Identity{} = identity, to, method, call_params, options) do
    options =
      case options[:call_schema] do
        nil ->
          options

        value ->
          options
          |> Keyword.delete(:call_schema)
          |> Keyword.put(:schema, value)
      end

    with {:ok, request} <-
           Request.Goloop.transaction_call(
             identity,
             to,
             method,
             call_params,
             options
           ) do
      send_transaction(request)
    end
  end

  @doc """
  Creates a new SCORE.

  The `identity` should be created using a valid `private_key`, otherwise the
  message cannot be sent.

  Options:
  - `timeout` - Time in milliseconds to wait for the transfer 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`.

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Creates a new contract:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.install_score(identity, content)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Creates a new contract and waits 5 seconds for the result:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.install_score(identity, content, timeout: 5_000)
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "cx0000000000000000000000000000000000000000",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```

  - Creates a new contract and passes paremeters to the `on_install` function:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> Icon.install_score(identity, content,
  ...>   on_install_params: %{
  ...>     address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
  ...>   },
  ...>   on_install_schema: %{
  ...>     address: {:address, required: true}
  ...>   }
  ...> )
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```
  """
  @spec install_score(Identity.t(), BinaryData.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec install_score(Identity.t(), BinaryData.t(), keyword()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def install_score(identity, content, options \\ [])

  def install_score(%Identity{} = identity, content, options) do
    with {:ok, request} <-
           Request.Goloop.install_score(identity, content, options) do
      send_transaction(request)
    end
  end

  @doc """
  Updates a SCORE.

  The `identity` should be created using a valid `private_key`, otherwise the
  message cannot be sent.

  Options:
  - `timeout` - Time in milliseconds to wait for the transfer 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`.

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Updates a contract:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.update_score(identity, score_address, content)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Updates a contract and waits 5 seconds for the result:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.update_score(identity, score_address, content, timeout: 5_000)
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```

  - Updates a contract and passes paremeters to the `on_update` function:

  ```elixir
  iex> {:ok, content} = File.read("./my-contract.javac")
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.update_score(identity, score_address, content,
  ...>   on_update_params: %{
  ...>     address: "hxfd7e4560ba363f5aabd32caac7317feeee70ea57"
  ...>   },
  ...>   on_update_schema: %{
  ...>     address: {:address, required: true}
  ...>   }
  ...> )
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```
  """
  @spec update_score(Identity.t(), SCORE.t(), BinaryData.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec update_score(Identity.t(), SCORE.t(), BinaryData.t(), keyword()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def update_score(identity, score_address, content, options \\ [])

  def update_score(%Identity{} = identity, to, content, options) do
    with {:ok, request} <-
           Request.Goloop.update_score(identity, to, content, options) do
      send_transaction(request)
    end
  end

  @doc """
  Deposits ICX in loop (1 ICX = 10¹⁸ loop) into a SCORE for paying user's fees
  when they transact with the contract (fee sharing).

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

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Deposits `1.00` ICX in a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.deposit_shared_fee(identity, score_address, 1_000_000_000_000_000_000)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Deposit `1.00` ICX in a SCORE and wait 5 seconds for the result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.deposit_shared_fee(identity, score_address, 1_000_000_000_000_000_000,
  ...>   timeout: 5_000
  ...> )
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```
  """
  @spec deposit_shared_fee(Identity.t(), SCORE.t(), Loop.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec deposit_shared_fee(Identity.t(), SCORE.t(), Loop.t(), keyword()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  def deposit_shared_fee(identity, score_address, amount, options \\ [])

  def deposit_shared_fee(%Identity{} = identity, to, amount, options) do
    with {:ok, request} <-
           Request.Goloop.deposit_shared_fee(identity, to, amount, options) do
      send_transaction(request)
    end
  end

  @doc """
  Withdraws ICX from a SCORE that was destined for paying user's fees when they
  transact with the contract (fee sharing).

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

  While technically any parameter can be overriden with the `params` option, not
  all of them make sense to do so. The following are some of the most usuful
  parameters to modify via this option:

  - `nonce` - An arbitrary number used to prevent transaction hash collision.
  - `timestamp` - Transaction creation time. Timestamp is in microsecond.
  - `stepLimit` - Maximum step allowance that can be used by the transaction.

  ## Examples

  - Withdraws `1.00` ICX from a SCORE:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.withdraw_shared_fee(identity, score_address, 1_000_000_000_000_000_000)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Withdraws ICX using the deposit hash:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> hash = "0xc71303ef8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"
  iex> Icon.withdraw_shared_fee(identity, score_address, hash)
  {:ok, "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b"}
  ```

  - Withdraw the whole deposit from a SCORE and wait 5 seconds for the result:

  ```elixir
  iex> identity = Icon.RPC.Identity.new(private_key: "8ad9...")
  iex> score_address = "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32"
  iex> Icon.withdraw_shared_fee(identity, score_address, timeout: 5_000)
  {
    :ok,
    %Icon.Schema.Types.Transaction.Result{
      blockHash: "0x52bab965acf6fa11f7e7450a87947d944ad8a7f88915e27579f21244f68c6285",
      blockHeight: 2_427_717,
      cumulativeStepUsed: 0,
      failure: nil,
      logsBloom: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>,
      scoreAddress: nil,
      status: :success,
      stepPrice: 12_500_000_000,
      stepUsed: 100_000,
      to: "cxb0776ee37f5b45bfaea8cff1d8232fbb6122ec32",
      txHash: "0xd579ce6162019928d874da9bd1dbf7cced2359a5614e8aa0bf7cf75f3770504b",
      txIndex: 1
    }
  }
  ```
  """
  @spec withdraw_shared_fee(Identity.t(), SCORE.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec withdraw_shared_fee(Identity.t(), SCORE.t(), nil | Loop.t() | Hash.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  @spec withdraw_shared_fee(
          Identity.t(),
          SCORE.t(),
          nil | Loop.t() | Hash.t(),
          keyword()
        ) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.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
    with {:ok, request} <-
           Request.Goloop.withdraw_shared_fee(
             identity,
             to,
             hash_or_amount,
             options
           ) do
      send_transaction(request)
    end
  end

  #########
  # Helpers

  @spec load_call_response(any(), keyword()) ::
          {:ok, any()} | {:error, Error.t()}
  defp load_call_response(response, options)

  defp load_call_response(response, options) do
    response_schema = options[:response_schema] || :any

    response_schema
    |> Schema.generate()
    |> Schema.new(response)
    |> Schema.load()
    |> apply_call_response(response_schema)
    |> case do
      {:ok, _} = ok ->
        ok

      {:error, _} ->
        reason =
          Error.new(
            reason: :server_error,
            message: "cannot cast call response"
          )

        {:error, reason}
    end
  end

  @spec apply_call_response(Schema.state(), Schema.t()) ::
          {:ok, any()}
          | {:error, Error.t()}
  defp apply_call_response(state, schema) do
    if is_atom(schema) and function_exported?(schema, :__schema__, 0) do
      Schema.apply(state, into: schema)
    else
      Schema.apply(state)
    end
  end

  @spec send_transaction(Request.t()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  defp send_transaction(request)

  defp send_transaction(%Request{} = request) do
    with {:ok, request} <- Request.add_step_limit(request),
         {:ok, request} <- Request.sign(request),
         {:ok, response} <- Request.send(request) do
      load_transaction_response(response)
    end
  end

  @spec load_transaction_response(any()) ::
          {:ok, Hash.t()}
          | {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  defp load_transaction_response(data)

  defp load_transaction_response("0x" <> _ = data) do
    {:ok, data}
  end

  defp load_transaction_response(data) do
    load_transaction_result(data)
  end

  @spec load_transaction_result(any()) ::
          {:ok, Transaction.Result.t()}
          | {:error, Error.t()}
  defp load_transaction_result(data)

  defp load_transaction_result(data) when is_map(data) do
    Transaction.Result
    |> Schema.generate()
    |> Schema.new(data)
    |> Schema.load()
    |> Schema.apply(into: Transaction.Result)
  end

  defp load_transaction_result(_) do
    reason =
      Error.new(
        reason: :server_error,
        message: "cannot cast transaction result"
      )

    {:error, reason}
  end

  @spec load_block(any()) ::
          {:ok, Block.t()}
          | {:error, Error.t()}
  defp load_block(data)

  defp load_block(data) when is_map(data) do
    Block
    |> Schema.generate()
    |> Schema.new(data)
    |> Schema.load()
    |> Schema.apply(into: Block)
  end

  defp load_block(_) do
    reason =
      Error.new(
        reason: :server_error,
        message: "cannot cast block"
      )

    {:error, reason}
  end

  @spec load_transaction(any()) ::
          {:ok, Transaction.t()}
          | {:error, Error.t()}
  defp load_transaction(data)

  defp load_transaction(data) when is_map(data) do
    Transaction
    |> Schema.generate()
    |> Schema.new(data)
    |> Schema.load()
    |> Schema.apply(into: Transaction)
  end

  defp load_transaction(_) do
    reason =
      Error.new(
        reason: :server_error,
        message: "cannot cast transaction"
      )

    {:error, reason}
  end
end