> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbitflare.com/llms.txt
> Use this file to discover all available pages before exploring further.

# EVM SDK

> Rust SDK for OrbitFlare's EVM chains, the orbitflare-evm-sdk crate.

One Rust client for OrbitFlare's EVM chains - Polygon, BNB Smart Chain, and Robinhood Chain - plus any EVM chain you define yourself. Built on [alloy](https://github.com/alloy-rs/alloy) types throughout (`Address`, `U256`, `B256`, `Filter`, `TransactionRequest`, typed blocks, transactions, receipts, and logs), with OrbitFlare's own transport: endpoint failover, retry with backoff, and self-healing WebSocket subscriptions.

The clients are generic over the chain (`RpcClient<C>`, `WsClient<C>`); the aliases below are convenience shorthands. gRPC is currently Polygon only (Bor).

## Supported chains

| Chain           | Chain ID | Alias                                     |
| --------------- | -------- | ----------------------------------------- |
| Polygon         | 137      | `PolygonRpcClient`, `PolygonWsClient`     |
| BNB Smart Chain | 56       | `BnbRpcClient`, `BnbWsClient`             |
| Robinhood Chain | 4663     | `RobinhoodRpcClient`, `RobinhoodWsClient` |
| Your own        | any      | `RpcClient<C>` where `impl Chain for C`   |

<Note>
  Migrating from `orbitflare-robinhood-sdk`? Robinhood Chain is now covered here via `RobinhoodRpcClient` / `RobinhoodWsClient`. The RPC and WebSocket surface is the same; the standalone crate is deprecated.
</Note>

## Install

```bash theme={null}
cargo add orbitflare-evm-sdk
```

Only the RPC client is enabled by default. Enable what you need:

```bash theme={null}
cargo add orbitflare-evm-sdk --features ws
cargo add orbitflare-evm-sdk --features grpc
cargo add orbitflare-evm-sdk --features all
```

## RPC

<CodeGroup>
  ```rust Example theme={null}
  let client = PolygonRpcClient::builder()
      .url("https://ams.poly.rpc.orbitflare.com")
      .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
      .build()?;

  let block = client.get_block_number().await?;
  let gas_price = client.get_gas_price().await?;
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::{primitives::address, PolygonRpcClient, Result};

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = PolygonRpcClient::builder()
          .url("https://ams.poly.rpc.orbitflare.com")
          .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
          .build()?;

      let block = client.get_block_number().await?;
      let gas_price = client.get_gas_price().await?;

      let wallet = address!("d8dA6BF26964aF9D7eEd9e03E53415D37aA96045");
      let balance = client.get_balance(wallet).await?;

      println!("block {block}, gas {gas_price} wei, balance {balance} wei");
      Ok(())
  }
  ```
</CodeGroup>

There are no baked-in default endpoints: set the URL with `.url()` or via an environment variable (see [Endpoints](#endpoints)), and the API key with `.api_key()` or `ORBITFLARE_LICENSE_KEY`.

### Builder methods

**`.url(url)`** - The primary endpoint. Resolution order: `.url()` on the builder, then the `ORBITFLARE_RPC_URL` environment variable.

**`.urls(&[...])`** - Set the primary and all fallbacks in one call. The first element is the primary, the rest are fallbacks.

**`.fallback_url(url)` / `.fallback_urls(&[...])`** - Add failover endpoints. Failing endpoints are quarantined with exponential cooldown (10s, 20s, 40s, max 60s) and automatically retried once the cooldown expires; healthy endpoints are always preferred.

**`.api_key(key)`** - Your OrbitFlare license key. If not set, the SDK checks `ORBITFLARE_LICENSE_KEY`. The key is appended to the endpoint URL as `?api_key=<key>` at request time.

**`.block_tag(tag)`** - Default block tag used by state queries (`get_balance`, `call`, `get_code`, ...). Defaults to `Latest`.

**`.retry(policy)`** - Controls retry on transient errors (5xx, 429, connection resets, JSON-RPC error code -32005) with exponential backoff before failing over to the next endpoint.

**`.timeout(duration)`** - HTTP timeout for each individual request.

### Available methods

| Method                                                       | Returns                                      |
| ------------------------------------------------------------ | -------------------------------------------- |
| `get_block_number()`                                         | `u64`                                        |
| `get_chain_id()`                                             | `u64`                                        |
| `get_balance(Address)`                                       | `U256` wei                                   |
| `get_transaction_count(Address)`                             | `u64` nonce                                  |
| `get_gas_price()`                                            | `u128` wei                                   |
| `max_priority_fee_per_gas()`                                 | `u128` wei                                   |
| `get_block_by_number(impl Into<BlockNumberOrTag>, full_txs)` | `Option<Block>`                              |
| `get_block_by_hash(B256, full_txs)`                          | `Option<Block>`                              |
| `get_transaction_by_hash(B256)`                              | `Option<Transaction>`                        |
| `get_transaction_receipt(B256)`                              | `Option<TransactionReceipt>`                 |
| `get_logs(&Filter)`                                          | `Vec<Log>`                                   |
| `get_code(Address)`                                          | `Bytes`                                      |
| `get_storage_at(Address, B256)`                              | `B256`                                       |
| `call(&TransactionRequest)`                                  | `Bytes`                                      |
| `estimate_gas(&TransactionRequest)`                          | `u64`                                        |
| `send_raw_transaction(&[u8])`                                | `B256` tx hash                               |
| `fee_history(blocks, newest, percentiles)`                   | `FeeHistory`                                 |
| `request(method, params)`                                    | Any RPC method by name (`serde_json::Value`) |
| `request_raw(body)`                                          | Raw JSON-RPC body string                     |

### Log filters

`get_logs` takes alloy's `Filter` directly:

<CodeGroup>
  ```rust Example theme={null}
  let filter = Filter::new()
      .from_block(60_000_000u64)
      .to_block(BlockNumberOrTag::Latest)
      .address(address!("0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"))
      .event_signature(transfer_topic);

  let logs = client.get_logs(&filter).await?;
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::{primitives::{address, b256}, BlockNumberOrTag, Filter, PolygonRpcClient, Result};

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = PolygonRpcClient::builder()
          .url("https://ams.poly.rpc.orbitflare.com")
          .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
          .build()?;

      let transfer_topic =
          b256!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef");

      let filter = Filter::new()
          .from_block(60_000_000u64)
          .to_block(BlockNumberOrTag::Latest)
          .address(address!("0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270"))
          .event_signature(transfer_topic);

      let logs = client.get_logs(&filter).await?;
      println!("{} logs", logs.len());
      Ok(())
  }
  ```
</CodeGroup>

### Contract calls and transactions

`call` and `estimate_gas` take alloy's `TransactionRequest`. To send a transaction, build and sign it with alloy (`alloy-signer`, `alloy-network`), then broadcast through the SDK:

```rust theme={null}
let hash = client.send_raw_transaction(&signed_tx_rlp).await?;
let receipt = client.get_transaction_receipt(hash).await?;
```

### Arbitrary methods

`request` calls any RPC method by name - the SDK builds the JSON-RPC envelope, handles retry and failover, and returns the `result` field. `request_raw` sends a raw JSON-RPC body string.

```rust theme={null}
use serde_json::json;

let syncing = client.request("eth_syncing", json!([])).await?;
```

## WebSocket

Enable the `ws` feature. `.build()` is async - it establishes the connection before returning.

<CodeGroup>
  ```rust Example theme={null}
  let client = PolygonWsClient::builder()
      .url("wss://ams.poly.rpc.orbitflare.com")
      .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
      .build()
      .await?;

  let mut heads = client.new_heads_subscribe().await?;
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::{PolygonWsClient, Result};

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = PolygonWsClient::builder()
          .url("wss://ams.poly.rpc.orbitflare.com")
          .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
          .build()
          .await?;

      let mut heads = client.new_heads_subscribe().await?;

      while let Some(head) = heads.next().await {
          println!("block {} (gas used {})", head.number, head.gas_used);
      }
      Ok(())
  }
  ```
</CodeGroup>

The builder shares `.urls()`, `.fallback_url(s)`, `.api_key()`, and `.retry()` with the RPC builder; the WebSocket-specific options are `.ping_interval_secs(n)` (default 10) and `.max_missed_pongs(n)` (default 3).

### Subscriptions

Subscriptions are typed - each yields the corresponding alloy type instead of raw JSON:

| Method                                 | Yields                    |
| -------------------------------------- | ------------------------- |
| `new_heads_subscribe()`                | `Header` per new block    |
| `logs_subscribe(&Filter)`              | `Log` matching the filter |
| `new_pending_transactions_subscribe()` | `B256` transaction hash   |

All subscriptions return a `WsSubscription<T>`. Call `.next()` for the next typed event (`None` means the subscription was closed), or `.next_raw()` for the untyped `serde_json::Value` payload. `sub.unsubscribe().await` removes one explicitly; dropping a subscription also works.

### Reconnection

If the connection drops, the background task reconnects with exponential backoff and re-subscribes every active subscription automatically. Your `.next()` calls just keep working. Dead connections are detected via active ping/pong.

## Polygon gRPC

Polygon exposes a Bor gRPC interface for low-overhead block, header, and receipt access. Enable the `grpc` feature. gRPC runs over plaintext HTTP/2; authenticate with a token via `.api_key()` (sent as `x-token`) or use IP whitelisting.

<CodeGroup>
  ```rust Example theme={null}
  let client = PolygonGrpcClient::builder()
      .url("http://your-bor-grpc-endpoint:3131")
      .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
      .build()?;

  let header = client.header_by_number(BlockNumber::Latest).await?;
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::grpc::{BlockNumber, PolygonGrpcClient};
  use orbitflare_evm_sdk::Result;

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = PolygonGrpcClient::builder()
          .url("http://your-bor-grpc-endpoint:3131")
          .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
          .build()?;

      let header = client.header_by_number(BlockNumber::Latest).await?;
      let author = client.author(header.number).await?;

      println!("block {} authored by {author}", header.number);
      Ok(())
  }
  ```
</CodeGroup>

| Method                                     | Returns                |
| ------------------------------------------ | ---------------------- |
| `header_by_number(impl Into<BlockNumber>)` | `Header`               |
| `block_by_number(impl Into<BlockNumber>)`  | `Block`                |
| `transaction_receipt(B256)`                | `Receipt`              |
| `bor_block_receipt(B256)`                  | `Receipt`              |
| `author(impl Into<BlockNumber>)`           | `Address`              |
| `td_by_hash(B256)` / `td_by_number(...)`   | `u64` total difficulty |
| `root_hash(start, end)`                    | `String`               |
| `block_info_in_batch(start, end)`          | `Vec<BlockInfo>`       |

H160/H256 values convert to alloy `Address`/`B256` with the `ToAlloy` trait.

## Custom chains

Any EVM chain works: implement `Chain` for a marker type and point a generic `RpcClient<C>` at its URL. `CHAIN_ID` is metadata (available as `RpcClient::<C>::chain_id_const()`); it does not gate requests.

<CodeGroup>
  ```rust Example theme={null}
  struct Base;

  impl Chain for Base {
      const CHAIN_ID: u64 = 8453;
  }

  let client = RpcClient::<Base>::builder()
      .url("https://mainnet.base.org")
      .build()?;
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::{Chain, RpcClient, Result};

  struct Base;

  impl Chain for Base {
      const CHAIN_ID: u64 = 8453;
  }

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = RpcClient::<Base>::builder()
          .url("https://mainnet.base.org")
          .build()?;

      println!("chain id: {}", client.get_chain_id().await?);
      println!("block:    {}", client.get_block_number().await?);
      Ok(())
  }
  ```
</CodeGroup>

## Arbitrum and Nitro block fields

Robinhood Chain is Arbitrum Nitro, so its blocks carry extra fields (`l1BlockNumber`, `sendRoot`, `sendCount`) that standard EVM block types drop. The SDK preserves them and exposes typed accessors via `NitroBlockExt`. Any other non-standard field is still available on `block.other`.

<CodeGroup>
  ```rust Example theme={null}
  let block = client
      .get_block_by_number(client.block_tag(), false)
      .await?
      .expect("block");

  let l1 = block.l1_block_number();
  let send_root = block.send_root();
  ```

  ```rust Full Example theme={null}
  use orbitflare_evm_sdk::{NitroBlockExt, RobinhoodRpcClient, Result};

  #[tokio::main]
  async fn main() -> Result<()> {
      let client = RobinhoodRpcClient::builder()
          .url("https://robinhood.rpc.orbitflare.com")
          .api_key("ORBIT-XXXXXX-NNNNNN-NNNNNN")
          .build()?;

      let block = client
          .get_block_by_number(client.block_tag(), false)
          .await?
          .expect("block");

      println!("l1 block:   {:?}", block.l1_block_number());
      println!("send root:  {:?}", block.send_root());
      println!("send count: {:?}", block.send_count());
      Ok(())
  }
  ```
</CodeGroup>

Arbitrum precompiles (ArbSys, ArbGasInfo, ...) are reachable through `call()` like any contract.

## Endpoints

There are no default endpoints. Set the URL per client with `.url()`, or via an environment variable. Resolution order: `.url()` on the builder, then the environment variable.

| Chain           | RPC                                    | WebSocket                            |
| --------------- | -------------------------------------- | ------------------------------------ |
| Polygon         | `https://ams.poly.rpc.orbitflare.com`  | `wss://ams.poly.rpc.orbitflare.com`  |
| BNB Smart Chain | `https://bsc.rpc.orbitflare.com`       | `wss://bsc.rpc.orbitflare.com`       |
| Robinhood Chain | `https://robinhood.rpc.orbitflare.com` | `wss://robinhood.rpc.orbitflare.com` |

Use the exact endpoints from your OrbitFlare dashboard.

## Environment variables

| Variable                 | Used by              | Purpose                                                |
| ------------------------ | -------------------- | ------------------------------------------------------ |
| `ORBITFLARE_LICENSE_KEY` | RPC, WebSocket, gRPC | API key appended to endpoint URLs (`x-token` for gRPC) |
| `ORBITFLARE_RPC_URL`     | RPC                  | Endpoint used if `.url()` is not called                |
| `ORBITFLARE_WS_URL`      | WebSocket            | Endpoint used if `.url()` is not called                |
| `ORBITFLARE_GRPC_URL`    | gRPC                 | Endpoint used if `.url()` is not called                |

## Source

The SDK is open source: [github.com/orbitflare/orbitflare-evm-sdk-rs](https://github.com/orbitflare/orbitflare-evm-sdk-rs)
