Exth.Rpc (Exth v0.2.2)

View Source

Core module for making JSON-RPC requests to EVM-compatible blockchain nodes.

This module provides a comprehensive interface for interacting with JSON-RPC endpoints, offering flexible request building, multiple transport options, and robust error handling.

Key Features

  • Client-based and standalone request building

  • Batch request support for better performance

  • Configurable transport layers (HTTP, custom implementations)

  • Automatic request ID generation and version management

  • Comprehensive error handling

Quick Start

# Create a client
client = Rpc.new_client(:http, rpc_url: "https://eth-mainnet.example.com")

# Make a simple request
request = Rpc.request("eth_blockNumber", [])
{:ok, response} = Rpc.send(client, request)

Request Patterns

Client-based Requests

# Create client once and reuse
client = Rpc.new_client(:http, rpc_url: "https://eth-mainnet.example.com")

# Make requests with client
request = Rpc.request(client, "eth_getBalance", ["0x742d...", "latest"])
{:ok, response} = Rpc.send(client, request)

Standalone Requests

# Build requests without client
request = Rpc.request("eth_blockNumber", [])
{:ok, response} = Rpc.send(client, request)

Batch Requests

# Send multiple requests in one call
requests = [
  Rpc.request("eth_blockNumber", []),
  Rpc.request("eth_gasPrice", [])
]
{:ok, [block_response, gas_response]} = Rpc.send(client, requests)

Transport Configuration

The module supports different transport layers:

  • :http - Standard HTTP/HTTPS transport with configurable headers and timeouts
  • :custom - Custom transport implementations for special needs

Error Handling

case Rpc.send(client, request) do
  {:ok, response} -> 
    # Handle successful response
    handle_success(response.result)

  {:error, %{code: code, message: msg}} -> 
    # Handle RPC-level errors
    handle_rpc_error(code, msg)

  {:error, %Exception{} = e} -> 
    # Handle transport-level errors
    handle_transport_error(e)
end

See the individual function documentation for more detailed usage examples and options.

Summary

Functions

Returns the JSON-RPC protocol version used by the client.

Creates a new JSON-RPC client with the specified transport type and options.

Builds a new JSON-RPC request without requiring a client instance.

Builds a new JSON-RPC request using a client for the given method and parameters.

Sends one or more JSON-RPC requests using the client's configured transport.

Types

id()

@type id() :: pos_integer()

jsonrpc()

@type jsonrpc() :: String.t()

method()

@type method() :: atom() | String.t()

params()

@type params() :: [binary()]

Functions

jsonrpc_version()

@spec jsonrpc_version() :: jsonrpc()

Returns the JSON-RPC protocol version used by the client.

new_client(type, opts)

@spec new_client(
  Exth.Transport.type(),
  keyword()
) :: Exth.Rpc.Client.t()

Creates a new JSON-RPC client with the specified transport type and options.

Transport Types

  • :http - HTTP/HTTPS transport
  • :custom - Custom transport implementation

Options

  • :rpc_url - (Required) The endpoint URL
  • :headers - Additional HTTP headers (HTTP transport only)
  • :timeout - Request timeout in ms (HTTP transport only)

Examples

client = Rpc.new_client(:http, rpc_url: "https://eth-mainnet.example.com")

request(method, params)

@spec request(method(), params()) :: Exth.Rpc.Request.t()

Builds a new JSON-RPC request without requiring a client instance.

This is a convenience function for creating requests that can be later used with a client. Useful when building multiple requests before sending them.

When creating a standalone request, it will:

  • Set the protocol version to "2.0"
  • Leave the request ID as nil (it will be set when used with a client)
  • Format parameters according to the JSON-RPC spec

The ID-less request allows for flexibility, as the actual ID will be assigned by the client when the request is sent, ensuring proper ID sequencing within the client's context.

Examples

# Create standalone requests - no IDs yet
request1 = Rpc.request("eth_blockNumber", [])
# => %Request{id: nil, method: "eth_blockNumber", params: []}

request2 = Rpc.request("eth_getBalance", ["0x742d...", "latest"])
# => %Request{id: nil, method: "eth_getBalance", params: ["0x742d...", "latest"]}

# When sent with a client, IDs will be assigned
{:ok, response} = Rpc.send(client, request1)
# request1 now has id: 1

# In batch requests, each request gets a sequential ID
{:ok, responses} = Rpc.send(client, [request1, request2])
# request1 has id: 2, request2 has id: 3

request(client, method, params)

@spec request(Exth.Rpc.Client.t(), method(), params()) :: Exth.Rpc.Request.t()

Builds a new JSON-RPC request using a client for the given method and parameters.

When using a client, the request will automatically:

  • Set the protocol version to "2.0"
  • Generate a unique request ID based on the client's ID sequence
  • Format parameters according to the JSON-RPC spec

The client-based request ensures that each request gets a unique ID within the client's context, which is important for correlating responses in batch requests or concurrent operations.

Examples

# Simple request with no parameters - includes client-generated ID
request = Rpc.request(client, "eth_blockNumber", [])
# => %Request{id: 1, method: "eth_blockNumber", params: []}

# Request with multiple parameters - next ID in sequence
request = Rpc.request(client, "eth_getBalance", ["0x742d...", "latest"])
# => %Request{id: 2, method: "eth_getBalance", params: ["0x742d...", "latest"]}

# The request can be later used with Rpc.send/2
{:ok, response} = Rpc.send(client, request)

send(arg1, arg2)

Sends one or more JSON-RPC requests using the client's configured transport.

This function is the main interface for executing requests. It handles both single and batch requests automatically, with proper error handling and response decoding.

Request Types

  • Single request: Pass a single Request.t()
  • Batch request: Pass a list of Request.t()

Returns

  • {:ok, response} - Successful single request with decoded response
  • {:ok, responses} - Successful batch request with list of decoded responses
  • {:error, reason} - Request failed with error details (Exception.t() or map())

Examples

# Single request
{:ok, response} = Rpc.send(client, request)
block_number = response.result

# Batch request for better performance
requests = [
  Rpc.request("eth_blockNumber", []),
  Rpc.request("eth_gasPrice", [])
]
{:ok, [block_response, gas_response]} = Rpc.send(client, requests)

# You can also invert the arguments
Rpc.request("eth_blockNumber", [])
|> Rpc.send(client)

# Error handling
case Rpc.send(client, request) do
  {:ok, response} -> handle_success(response)
  {:error, %{code: code, message: msg}} -> handle_rpc_error(code, msg)
  {:error, %Exception{} = e} -> handle_transport_error(e)
end