# Introduction

<figure><img src="/files/dgy5kV9AlNC8MD7cZdiX" alt=""><figcaption></figcaption></figure>

Somnia is a high-performance, cost-efficient EVM-compatible Layer 1 blockchain capable of processing over 1,000,000 transactions per second (TPS) with sub-second finality. It is suitable for serving millions of users and building real-time mass-consumer applications like games, social applications, metaverses, and more, all fully on-chain.

{% hint style="success" %}
Somnia Mainnet is LIVE. Visit the [Network Information Page](/developer/network-info#network-details) for Network details.
{% endhint %}

{% hint style="success" %}
**Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT):**\
\
Please join the [Discord](https://discord.com/invite/somnia). Go to the `#dev-chat` channel, tag the Somnia DevRel, `@emreyeth` and request Test Tokens.\
\
You can also join the [Somnia Developer Telegram](https://t.me/+s_oRMnGpOyQ3ODQ0) or use [Faucet](https://testnet.somnia.network/).\
\
You can also email `developers@somnia.foundation` with a brief description of what you are building and your GitHub profile.
{% endhint %}

Somnia is supported by [Improbable](https://www.improbable.io/) and [MSquared](https://msquared.io/). Improbable will develop some of the key technical components of Somnia, including the Blockchain, but the project will require a large and active community to fulfill its vision.

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-cover data-type="image">Cover image</th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Learn more about Somnia</td><td></td><td></td><td><a href="/files/CE8JfY9ZOHjTCWkHgv7f">/files/CE8JfY9ZOHjTCWkHgv7f</a></td><td><a href="/spaces/hEUf5l6MCgn0iJsSVgmd/pages/FKOgbxLw2NhwK4XlxSf2">/spaces/hEUf5l6MCgn0iJsSVgmd/pages/FKOgbxLw2NhwK4XlxSf2</a></td></tr><tr><td>Start developing on Somnia</td><td></td><td></td><td><a href="/files/526bRNiZWR0tnuZi1H7n">/files/526bRNiZWR0tnuZi1H7n</a></td><td><a href="/pages/4fa9JM2h1uNNHKeqdaIP">/pages/4fa9JM2h1uNNHKeqdaIP</a></td></tr><tr><td>Connect To Mainnet</td><td></td><td></td><td><a href="/files/Yhizjf6iPnpNEoI67TxM">/files/Yhizjf6iPnpNEoI67TxM</a></td><td><a href="/pages/g3kML4YwJXPHEnZfkGiS">/pages/g3kML4YwJXPHEnZfkGiS</a></td></tr></tbody></table>

<br>


# Getting Started for Mainnet

## Get SOMI Tokens

Developers and Non-Developers can purchase SOMI Tokens for interacting on Mainnet from the list of exchanges below:

| [Binance](https://www.binance.com/en/trade/SOMI_USDT) | [Bybit](https://www.bybit.com/en/trade/spot/SOMI/USDT)            |
| ----------------------------------------------------- | ----------------------------------------------------------------- |
| [Gate.io](https://www.gate.com/price/somnia-somi)     | [BitMart](https://www.bitmart.com/en-US/trade/SOMI_USDT?r=PjkAFy) |
| [Bitget](https://www.bitgetapp.com/price/somnia)      | [BingX](https://bingx.com/en/spot/SOMIUSDT?ch=cgk_organic)        |

## Bridge to Somnia

To have SOMI directly on the mainnet:\
**Relay:** <https://relay.link/bridge/somnia>\
\
To bridge stablecoins from other Networks to Somnia:\
**LayerZero's STARGATE:** <https://stargate.finance/bridge>\
**Orbiter Finance:** [https://www.orbiter.finance/trade/Somnia](https://www.orbiter.finance/trade/Somnia?from=0x28bec7e30e6faee657a03e19bf1128aad7632a00)\
\
Welcome to Somnia Mainnet. Below is a checklist for you to confirm the migration from Testnet to Mainnet.

## For Non-Developers

* [x] Ensure that you have added the Somnia Mainnet Network to your Wallet. You can use [ChainList](https://chainlist.org/?search=somnia).
* [x] Get/bridge SOMI from a list of Exchanges/Bridges.

## For Developers

Conduct the same checks as non-developers, and in addition, the following.

* [x] Add Somnia to the list of Networks in your configuration files for your Smart Contracts
* [x] Using Hardhat:

```javascript
module.exports = {
  // ...
  networks: {
    somnia: {
      url: "https://api.infra.mainnet.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};
```

* [x] Using Forge Deployment:

```bash
forge create --rpc-url https://api.infra.mainnet.somnia.network --private-key PRIVATE_KEY src/Example.sol:Example
```

* [x] See the Page for the list of infrastructure and Dev Tooling available to you on Somnia.


# Connect Your Wallet To Mainnet

{% hint style="success" %}
Somnia Mainnet is LIVE. Visit the [Network Information Page](/developer/network-info#network-details) for Network details.
{% endhint %}

1. You can create your [MetaMask Wallet ](https://support.metamask.io/getting-started/getting-started-with-metamask/)from here. If you already have a wallet you may skip this step
2. Visit [Chainlist](https://chainlist.org/?search=somnia) and Click on "Connect Wallet":
3. You may use any desired wallet or Metamask (preferred)

<div align="left"><figure><img src="/files/6cc5piUYrjxAzFAi3vW9" alt="" width="369"><figcaption></figcaption></figure></div>

4. Click on "Add to Metamask" to add Somnia Mainnet to your wallet.<br>

<figure><img src="/files/IdlwuRXudDqR8A7j7kH3" alt="" width="375"><figcaption></figcaption></figure>


# Bridging Info

Somnia supports cross-chain asset transfers through two official bridge partners: Relay and Stargate Finance. These bridges enable you to move tokens between Somnia and other blockchain networks while maintaining security and minimizing fees. Whether you're bridging stablecoins, ETH, or other supported assets, this guide will walk you through the complete process.

## Prerequisites

* **Wallet Setup**: MetaMask, WalletConnect-compatible wallet, or hardware wallet
* **Network Configuration**: Somnia network added to your wallet
* **Funded Wallet**: Source chain tokens to cover bridge fees and gas costs
* **Basic Understanding**: Familiarity with blockchain transactions and gas fees

## Supported Bridges

### Relay Bridge

Relay is a multichain payments network that has served 5M+ users and processed $5B+ in volume across 85+ networks. It offers instant cross-chain transactions with 99.9% uptime and payments-grade reliability.

**Key Features**:

* Instant Execution: Cross-chain transfers in 1-10 seconds
* 75+ Networks: Extensive blockchain support including Somnia
* Predictable Fees: Transparent fee structure with no hidden costs
* Enterprise Grade: 99.9% uptime with automatic redundancy systems

**Supported Assets**: ETH, USDC, USDT, and other major tokens\
**Website**: [relay.link/bridge](https://relay.link/bridge)

### Stargate Finance

Stargate is a fully composable cross-chain bridge built on LayerZero that connects 50+ blockchains. It's the first bridge to solve the "bridging trilemma" by providing instant guaranteed finality, native assets, and unified liquidity.

**Key Features**:

* Native Assets: Transfer native tokens without wrapped intermediates
* Instant Finality: Guaranteed transaction completion
* Unified Liquidity: Shared liquidity pools across all chains
* LayerZero Powered: Built on robust omnichain infrastructure

**Supported Assets**: USDC, USDT, ETH, BTC, and LayerZero OFTs\
**Website**: [stargate.finance](https://stargate.finance/)

## Using Relay Bridge

{% stepper %}
{% step %}
**Access Relay Bridge**

Navigate to [relay.link/bridge](https://relay.link/bridge) in your web browser.
{% endstep %}

{% step %}
**Connect Your Wallet**

Click "Connect Wallet" and select your preferred wallet provider. Approve the connection when prompted.
{% endstep %}

{% step %}
**Configure Bridge Transaction**

* Source Chain: Select the blockchain you're bridging FROM
* Destination Chain: Select Somnia (or your target chain)
* Token: Choose the asset you want to bridge
* Amount: Enter the amount to transfer

<figure><img src="https://427512505-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F44oX5XEgjsIITRWz2fbP%2Fuploads%2FyIjtTD8dvQhEhYEbFPqp%2Fimage.png?alt=media&#x26;token=f2c31757-9ecf-459f-a371-6b063fbbc3b3" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
**Review Transaction Details**

Carefully review:

* Bridge Fee: Relay's service fee
* Gas Fee: Network transaction cost
* Estimated Time: Usually 1-10 seconds
* Recipient Address: Verify destination address(Your address or if you want to bridge to a different wallet.

<figure><img src="https://427512505-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F44oX5XEgjsIITRWz2fbP%2Fuploads%2FEfq6xft1t9ubbmoPPLuJ%2Fimage.png?alt=media&#x26;token=aa572d53-89ff-4d29-8343-0f316d5d1527" alt=""><figcaption></figcaption></figure>
{% endstep %}

{% step %}
**Execute Bridge Transaction**

Click "Bridge" and confirm the transaction in your wallet. The process typically involves:

* Approval Transaction: Authorize Relay to spend your tokens (if required)
* Bridge Transaction: Execute the cross-chain transfer
* Confirmation: Receive tokens on destination chain
  {% endstep %}

{% step %}
**Verify Completion**

Check your wallet balance on the destination chain. Relay provides real-time status updates during the bridging process.
{% endstep %}
{% endstepper %}

## Using Stargate Finance

{% stepper %}
{% step %}
**Access Stargate Bridge**

Visit [stargate.finance](https://stargate.finance/) and click "Bridge" or "Transfer".
{% endstep %}

{% step %}
**Connect Wallet & Select Networks**

Connect your wallet and configure:

* From: Source blockchain
* To: Somnia (or destination chain)
* Asset: Select supported token (USDC, USDT, ETH, etc.)\
  ![](https://427512505-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F44oX5XEgjsIITRWz2fbP%2Fuploads%2FKvmRGu0fGzpMlWzyPlGN%2Fimage.png?alt=media\&token=66a534cd-1453-4b19-ac43-5076c86b1ec9)
  {% endstep %}

{% step %}
**Enter Transfer Details**

* Amount: Specify transfer amount
* Recipient: Destination wallet address (defaults to your connected wallet)

\
Only applicable if you are bridging to another wallet address.\\

<figure><img src="https://427512505-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F44oX5XEgjsIITRWz2fbP%2Fuploads%2FIYzm4EewTNtH3zUaNNOp%2Fimage.png?alt=media&#x26;token=625f507b-5583-4a90-bc24-2382ec10c39e" alt=""><figcaption></figcaption></figure>

* Slippage: Set acceptable slippage tolerance (usually 0.1-0.5%)
  {% endstep %}

{% step %}
**Review Pool Information**

Stargate displays:

* Available Liquidity: Pool depth on destination chain
* Bridge Fee: Protocol fee structure
* Gas Estimate: Transaction costs
* Route: Cross-chain path details
  {% endstep %}

{% step %}
**Execute Transfer**

Confirm transaction details and sign with your wallet. 3 Stargate's ΔBridge technology ensures native asset delivery without wrapped tokens.
{% endstep %}

{% step %}
**Monitor Transaction**

Track your transfer through:

* Stargate Interface: Real-time status updates
* LayerZero Scan: Cross-chain transaction [explorer](https://layerzeroscan.com/)
* Destination Explorer: Confirm arrival on target chain
  {% endstep %}
  {% endstepper %}

## Troubleshooting Common Issues

#### Transaction Stuck or Pending

**Symptoms**: Bridge transaction shows "pending" for extended period

{% stepper %}
{% step %}
Check Network Status: Verify source and destination chain health
{% endstep %}

{% step %}
Gas Price Issues: Increase gas price if transaction is stuck
{% endstep %}

{% step %}
Bridge Congestion: Wait for network congestion to clear
{% endstep %}

{% step %}
Contact Support: Reach out to bridge support with transaction hash
{% endstep %}
{% endstepper %}

#### Insufficient Liquidity

**Symptoms**: Bridge shows "insufficient liquidity" error

{% stepper %}
{% step %}
Reduce Amount: Try bridging smaller amounts
{% endstep %}

{% step %}
Wait for Rebalancing: Liquidity pools rebalance automatically
{% endstep %}

{% step %}
Alternative Routes: Use different bridge or route
{% endstep %}

{% step %}
Split Transactions: Break large transfers into smaller ones
{% endstep %}
{% endstepper %}

#### Wrong Network or Address

**Symptoms**: Tokens didn't arrive at expected destination

{% stepper %}
{% step %}
Verify Network: Ensure correct destination network selected
{% endstep %}

{% step %}
Check Address: Confirm recipient address accuracy
{% endstep %}

{% step %}
Network Switch: Switch wallet to destination network
{% endstep %}

{% step %}
Recovery Process: Contact bridge support for recovery options
{% endstep %}
{% endstepper %}

#### High Fees or Slippage

**Symptoms**: Unexpected high costs or poor exchange rates

{% stepper %}
{% step %}
Timing: Bridge during low network congestion periods
{% endstep %}

{% step %}
Route Optimization: Compare different bridge options
{% endstep %}

{% step %}
Amount Adjustment: Larger amounts often have better rates
{% endstep %}

{% step %}
Alternative Bridges: Consider other bridge providers
{% endstep %}
{% endstepper %}

## Verification And Testing

#### Test Bridge Functionality

{% stepper %}
{% step %}
Connect wallet to both source and destination networks
{% endstep %}

{% step %}
Bridge small test amount
{% endstep %}

{% step %}
Verify tokens arrive within expected timeframe
{% endstep %}

{% step %}
Check token balances on both chains
{% endstep %}

{% step %}
Test bridge interface responsiveness
{% endstep %}
{% endstepper %}

#### Confirm Successful Bridge

{% stepper %}
{% step %}
Source Chain: Confirm tokens debited from source wallet
{% endstep %}

{% step %}
Bridge Status: Check bridge interface for completion status
{% endstep %}

{% step %}
Destination Chain: Verify tokens credited to destination wallet
{% endstep %}

{% step %}
Balance Check: Ensure correct amounts received
{% endstep %}
{% endstepper %}

You've successfully learned how to bridge assets to and from Somnia using both Relay and Stargate Finance. These official bridge partners provide secure, fast, and reliable cross-chain transfers with different strengths:

* **Relay**: Best for fast payments and swaps across 75+ networks
* **Stargate**: Ideal for DeFi composability with native asset transfers

Both bridges support Somnia's high-performance infrastructure, enabling seamless integration with the broader multi-chain ecosystem.

*For technical support, contact the respective bridge providers or Somnia community channels.*


# Removing the Somnia Devnet Network

If you used the Somnia Devnet you may get some issues adding the testnet to your wallet. You will first need to remove the Devnet.

To do this open the networks on metamask:<br>

<figure><img src="/files/pl0tl9tjUDPzUv8lF6vO" alt="" width="515"><figcaption><p>Drop down button on the top left of Metamask</p></figcaption></figure>

Then click the three dots next to the network and click delete:

<figure><img src="/files/KztQme1d1TZyx9D6ODQz" alt=""><figcaption></figcaption></figure>

This will remove the Devnet from you networks. You now should be able to add the Testnet!


# Update the block explorer in metamask

You may find that metamask has defaulted to using an incorrect block explorer. You will notice this when you click on view on block explorer in metamask you get a page like this:<br>

<figure><img src="/files/jVCI8GBavtcmZibPYeza" alt=""><figcaption></figcaption></figure>

To fix this you need to update the block explorer in your metamask config for the Somnia devnet. First open metamask.\ <br>

<figure><img src="/files/2P6fnwoeSpwM8WqcMhV3" alt=""><figcaption></figcaption></figure>

Now click on the network in the top right this will bring down the select network windoScroll to the Somnia Testnet and click the 3 dots to open the config.

<figure><img src="/files/7n0xShSLlILfKq47yYlv" alt=""><figcaption></figcaption></figure>

Here click on the down arrow on the block explorer URL and click add block explorer URL<br>

<figure><img src="/files/v3FwSHm2KKCySQkEMfUH" alt="" width="480"><figcaption></figcaption></figure>

In the URL box type: <https://shannon-explorer.somnia.network/>

It will now default to using the somnia-devnet block explorer. You can check this by viewing a transaction on the block explorer

<figure><img src="/files/bx7v6zY2bgt2MueaAD3m" alt=""><figcaption></figcaption></figure>

It should look like this:

<figure><img src="/files/hAog7KlwVE1rDS2Bw0Dh" alt=""><figcaption></figcaption></figure>

Have fun exploring the Testnet!


# Network Info

{% hint style="success" %}
**The Somnia Mainnet is LIVE**
{% endhint %}

{% hint style="info" %}
**Developers who deploy Smart Contracts on Mainnet require Somnia Tokens, SOMI.** It is a real-world utility token that can be purchased on a list of CEXs and DEXs [here](/get-started/getting-started-for-mainnet#get-somi-tokens).
{% endhint %}

{% hint style="success" %}
**Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT):**\
\
Please join the [Discord](https://discord.com/invite/somnia). Go to the `#dev-chat` channel, tag the Somnia DevRel, `@emreyeth` and request Test Tokens.\
\
You can also join the [Somnia Developer Telegram](https://t.me/+s_oRMnGpOyQ3ODQ0) or use [Faucet](https://testnet.somnia.network/).\
\
You can also email `developers@somnia.foundation` with a brief description of what you are building and your GitHub profile.
{% endhint %}

<table><thead><tr><th width="304.841796875">Network</th><th>Mainnet</th><th>Testnet</th></tr></thead><tbody><tr><td>Chain ID</td><td>5031</td><td>50312</td></tr><tr><td>Block Explorer</td><td><a href="https://explorer.somnia.network">https://explorer.somnia.network</a></td><td><a href="https://shannon-explorer.somnia.network/">https://shannon-explorer.somnia.network/</a></td></tr><tr><td>Symbol</td><td>SOMI</td><td>STT</td></tr><tr><td>RPC</td><td><a href="https://api.infra.mainnet.somnia.network/">https://api.infra.mainnet.somnia.network/</a><br><br>wss://api.infra.mainnet.somnia.network/ws</td><td><a href="https://api.infra.testnet.somnia.network/">https://api.infra.testnet.somnia.network/</a><br><br>wss://api.infra.testnet.somnia.network/ws</td></tr><tr><td>MultiCallV3</td><td><a href="https://explorer.somnia.network/address/0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11">0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11</a></td><td><a href="https://shannon-explorer.somnia.network/address/0x841b8199E6d3Db3C6f264f6C2bd8848b3cA64223">0x841b8199E6d3Db3C6f264f6C2bd8848b3cA64223</a></td></tr><tr><td>EntryPoint v0.7</td><td></td><td><a href="https://shannon-explorer.somnia.network/address/0x0000000071727De22E5E9d8BAf0edAc6f37da032">0x0000000071727De22E5E9d8BAf0edAc6f37da032</a></td></tr><tr><td>Factory Address</td><td></td><td><a href="https://shannon-explorer.somnia.network/address/0x4bE0ddfebcA9A5A4a617dee4DeCe99E7c862dceb">0x4be0ddfebca9a5a4a617dee4dece99e7c862dceb</a></td></tr><tr><td>Alternative Testnet Block Explorer</td><td></td><td><a href="https://somnia-testnet.socialscan.io/">https://somnia-testnet.socialscan.io/</a></td></tr><tr><td>Testnet Faucet</td><td></td><td><a href="https://testnet.somnia.network/">https://testnet.somnia.network/</a></td></tr><tr><td>Stakely Mainnet Faucet</td><td><a href="https://stakely.io/faucet/somnia-somi">https://stakely.io/faucet/somnia-somi</a></td><td></td></tr><tr><td>CreateX</td><td>0xD13C575ED5378fd18B100Bd87D5765d9A747358B</td><td>0x535822d4b86b2372FBE4fd9d1468318F04A2A640</td></tr></tbody></table>

## RPC Providers

<table><thead><tr><th width="215.216796875">RPCs</th><th></th></tr></thead><tbody><tr><td>Ankr</td><td><a href="https://www.ankr.com/rpc/somnia/">https://www.ankr.com/rpc/somnia</a></td></tr><tr><td>Public Node</td><td><a href="https://somnia.publicnode.com/">https://somnia.publicnode.com</a></td></tr><tr><td>Stakely</td><td><a href="https://somnia-json-rpc.stakely.io">https://somnia-json-rpc.stakely.io</a></td></tr><tr><td>Validation Cloud</td><td><a href="https://www.validationcloud.io/somnia">https://www.validationcloud.io/somnia</a></td></tr></tbody></table>

## Faucet Providers

<table><thead><tr><th width="215.439453125"></th><th></th></tr></thead><tbody><tr><td>Google Cloud Faucet</td><td><a href="https://cloud.google.com/application/web3/faucet/somnia/shannon">https://cloud.google.com/application/web3/faucet/somnia/shannon</a></td></tr><tr><td>Stakely</td><td><a href="https://stakely.io/faucet/somnia-testnet-stt">https://stakely.io/faucet/somnia-testnet-stt</a></td></tr><tr><td>Thirdweb Faucet</td><td><a href="https://thirdweb.com/somnia-shannon-testnet">https://thirdweb.com/somnia-shannon-testnet</a></td></tr></tbody></table>

## Community

* [Join us on Discord](https://discord.com/invite/Somnia) to chat with devs and raise issues
* [Follow us on Twitter](https://twitter.com/Somnia_Network) for updates

## Solidity Resources

* [Ethereum Developer ](https://learnweb3.io/degrees/ethereum-developer-degree/)by Learn Web3
* [Solidity by Example](https://solidity-by-example.org/)
* [CryptoZombies](https://cryptozombies.io/en/course)
* [Cookbook.dev](https://www.cookbook.dev/search?q=cookbook\&categories=Contracts\&sort=popular\&filter=\&page=1)
* [Alchemy Unversity](https://www.alchemy.com/university/courses/solidity)

## Toolkit

* [Hardhat Toolkit](https://hardhat.org/docs) - Solidity development framework paired with a JavaScript testing framework
* [Foundry Toolkit](https://book.getfoundry.sh/) - Solidity framework for both development and testing.
* [Remix](https://remix.ethereum.org/#lang=en\&optimize=false\&runs=200\&evmVersion=null) - Solidity IDE with interactive features


# Network Overview (Mainnet / Testnet)

Somnia provides two distinct environments for developers and users: **Mainnet** and **Testnet (Shannon)**. Both serve different purposes in the ecosystem, and knowing when to use which is essential for building and deploying applications effectively.

***

### Somnia Mainnet

The **Mainnet** is the official production blockchain of Somnia. All transactions on this chain are **final and irreversible** and require **SOMI tokens** as gas.

#### Key Characteristics

* Real-value environment secured by Somnia’s validator set.
* Integrated with wallets, explorers, bridges, and infrastructure providers.
* Permanent and immutable transaction history.
* Designed for live dApps, end-users, and production-ready deployments.

#### When to Use Mainnet

* Deploying **audited and tested smart contracts**.
* Running dApps with **real users and assets**.
* Managing liquidity, staking, governance, or NFT projects.
* Partner integrations requiring **security and finality**.

#### Example

{% code title="Deploy to Somnia Mainnet" %}

```bash
# Deploying a contract to Somnia Mainnet
npx hardhat run scripts/deploy.js --network somnia_mainnet
```

{% endcode %}

***

### Somnia Testnet (Shannon)

The **Testnet** is a sandbox environment that mirrors mainnet behavior but uses **STT test tokens** with no real-world value. It allows safe experimentation and rapid iteration without financial risk.

#### Key Characteristics

* Transactions use **STT tokens**, available via the faucet.
* Close-to-mainnet parameters for realistic testing.
* Safe for prototyping, debugging, and QA.
* Commonly used in workshops, hackathons, and developer onboarding.

#### When to Use Testnet

* Learning how to connect and deploy on Somnia.
* Prototyping features or building MVPs.
* Debugging smart contracts or dApp flows.
* Preparing for audits and production deployment.

#### Example

{% code title="Deploy to Somnia Testnet (Shannon)" %}

```bash
# Deploying a contract to Somnia Testnet
npx hardhat run scripts/deploy.js --network somnia_testnet
```

{% endcode %}

***

### Quick Comparison

| Feature      | Mainnet (Production)           | Testnet (Shannon)           |
| ------------ | ------------------------------ | --------------------------- |
| Currency     | SOMI (real value)              | STT (valueless, faucet)     |
| Purpose      | Production deployments         | Development & testing       |
| Transactions | Permanent and irreversible     | Experimental and disposable |
| Typical Use  | Live dApps, DeFi, staking, NFT | Prototyping, QA, education  |
| Risk         | Financial impact possible      | No financial risk           |

***

### Best Practices

{% hint style="info" %}

* Start on Testnet: Validate contracts and flows on Shannon before mainnet.
* Audit before launch: Ensure contracts are reviewed and secure.
* Use separate configs: Keep `.env` files distinct for testnet and mainnet.
* Stay updated: Follow official announcements for upgrades and changes.
  {% endhint %}

{% hint style="success" %}
Tip: Treat **Testnet** as your safe playground and **Mainnet** as your production stage. Every project should pass through Testnet before moving to Mainnet.
{% endhint %}


# SOMI coin

SOMI is the native coin of the Somnia Network. It is a currency used to pay for transactions, similar to Ether (ETH) on Ethereum and other EVM networks.

SOMI is denominated in Wei, the base unit and smallest value that can be expressed in the network. Below is a table showing different denomination and comparison to falimiar terms:

<table><thead><tr><th width="161.42578125">Unit</th><th width="250.30078125" align="right">Wei Value</th><th width="75.859375" align="right">Exp</th><th>Ethereum synonym</th></tr></thead><tbody><tr><td>Somi (SOMI)</td><td align="right">1,000,000,000,000,000,000</td><td align="right">1e18</td><td>Ether (ETH)</td></tr><tr><td>milliSomi</td><td align="right">1,000,000,000,000,000,</td><td align="right">1e15</td><td></td></tr><tr><td>microSomi</td><td align="right">1,000,000,000,000</td><td align="right">1e12</td><td></td></tr><tr><td>nanoSomi</td><td align="right">1,000,000,000</td><td align="right">1e9</td><td>gWei</td></tr><tr><td>Wei</td><td align="right">1</td><td align="right">1</td><td>Wei</td></tr></tbody></table>


# Somnia Mainnet Releases

### somnia-c2ebe4eae665079-release - Jig Hard Fork

**Activated**: 6th August 2026, 11:00 UTC

The Jig hard fork is a network upgrade that introduces:

* **Sustained gas discounts:** rewards sustained usage with a discounted base gas price. See [Sustained Use Gas Discounts](/developer/deployment-and-production/sustained-use-gas-discounts).
* **Blake2f fix:** corrects an edge case in the Blake2f precompile
* **`GASPRICE` semantics:** the `GASPRICE` opcode (`tx.gasprice`) now returns the price actually charged for the transaction - the sender's discounted base price plus their priority fee - rather than the block's base gas price. This applies to all transactions, calls and simulations, and makes `GASPRICE` sender-dependent. Contracts that use `tx.gasprice` for gas reimbursement or fee floors should review that logic.
* **Block history:** gas limits reported for historical blocks are now capped consistently

This release also improves RPC compatibility, consensus performance and stability:

* \#1762 Implement eth\_sendRawTransactionSync
* \#1751 Add accessList field in RPC responses
* \#1734 Add `removed` field to eth\_getLogs log objects
* \#1765 Harden RPC storage key-type validation
* \#1742 Aggregate BLS public keys for message verification
* \#1743 #1744 Parallelise and cache the PBFT leader's BLS verifications
* \#1737 Parallelise backend validation by transaction
* \#1749 Replace validator shards with a single MPMC queue
* \#1724 Reduce storage database stalls when pruning
* \#1730 Retire obsolete storage data types

Reclaiming disk space:

After upgrading, node operators can reclaim significant disk space (hundreds of gigabytes on long-running nodes) by deleting the storage data types retired in #1730. With the node **stopped**, run:

```bash
somnia delete-obsolete-storage --storage-database-path <storage path>
```

***

### somnia-369f69e34c9778c-release - RPC compatibility

**Released**: 3rd July 2026

This release is for RPC servers only and fixes compatibility issues:

* \#1717 Report gas limits in line with wallets logic (namely Rabby)
* \#1721 Better handle bursty loads of historical state requests

***

### somnia-f03d9d276649877-release - Bug fixes and stability

{% hint style="info" %}
This release matches somnia-e07ba4d147406c9-release on the testnet.
{% endhint %}

**Released:** 3rd June 2026

Improves system performance and stability, fixes minor bugs:

* \#1695 Fix simulation override on an existing contract (eth\_call)
* \#1686 Fixes for HSDB rollback
* \#1459 Speed up Ice DB compaction
* \#1685 Session transaction cancellation and timeout
* \#1679 Force PBFT\_PERSISTENT\_STATE pruning
* \#1678 Add metrics for reactivity

***

### somnia-f444d2cf8f428d6-release - Hotfix

**Released:** 11th May 2026

A hotfix release addressing Cuthbert (shadow verification subsystem) issue that could result in false positive divergence when malicious transaction is executed.

***

### somnia-f6e406d2fb3ef90-release - Hotfix

**Released:** 22nd April 2026

A hotfix release addressing RPC issues discovered after the Ingot hard fork.

***

### somnia-c1a0de06c6bcdae-release - Ingot Hard Fork

**Activated:** 15th April 2026, 11:30 AM UTC

The Ingot hard fork is a major network upgrade that introduces:

* **Reactivity:** enables on-chain events
* **Session transactions:** allows users to pre-authorize a sequence of transactions, reducing friction for interactive applications
* **EIP-7702:** a standard that lets regular accounts temporarily act like smart contracts, enabling more flexible transaction flows
* **Agentic L1:** expands Somnia to become a hyper-performant L1 for agents and applications

***

### somnia-0e939b9869cab33-release - Hotfix

**Released:** 13th January 2026

A hotfix release addressing storage issues and improving storage efficency.

***

### somnia-3e094e96eab9d76-release - Genesis

**Launched:** 1st September 2025

The first mainnet release of the Somnia blockchain - an ultra-high-speed EVM-compatible L1 built for scale.


# JSON-RPC API

Somnia exposes an RPC API compatible with the Ethereum JSON-RPC specification. The mainnet public RPC endpoint is `https://api.infra.mainnet.somnia.network` (chain ID `0x13a7` / 5031).

## Methods by Name

| Method                                                                                                               | Compatibility                                                                                                    | [Requires Node Ready](#requires-node-ready) | WebSocket Only |
| -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | -------------- |
| [debug\_traceBlockByHash](#debug_traceblockbyhash)                                                                   | [Geth debug](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbyhash)            | Yes                                         |                |
| [debug\_traceBlockByNumber](#debug_traceblockbynumber)                                                               | [Geth debug](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbynumber)          | Yes                                         |                |
| [debug\_traceCall](#debug_tracecall)                                                                                 | [Geth debug](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracecall)                   | Yes                                         |                |
| [debug\_traceTransaction](#debug_tracetransaction)                                                                   | [Geth debug](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracetransaction)            | Yes                                         |                |
| [eth\_accounts](#eth_accounts)                                                                                       | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_accounts)                            |                                             |                |
| [eth\_blockNumber](#eth_blocknumber)                                                                                 | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_blocknumber)                         |                                             |                |
| [eth\_call](#eth_call)                                                                                               | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_call)                                | Yes                                         |                |
| [eth\_chainId](#eth_chainid)                                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_chainid)                             |                                             |                |
| [eth\_createAccessList](#eth_createaccesslist)                                                                       | [Execution APIs](https://ethereum.github.io/execution-apis/api/methods/eth_createAccessList)                     | Yes                                         |                |
| [eth\_estimateGas](#eth_estimategas)                                                                                 | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_estimategas)                         | Yes                                         |                |
| [eth\_feeHistory](#eth_feehistory)                                                                                   | [Execution APIs](https://ethereum.github.io/execution-apis/api/methods/eth_feeHistory)                           |                                             |                |
| [eth\_gasPrice](#eth_gasprice)                                                                                       | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gasprice)                            |                                             |                |
| [eth\_getBalance](#eth_getbalance)                                                                                   | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getbalance)                          |                                             |                |
| [eth\_getBlockByHash](#eth_getblockbyhash)                                                                           | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblockbyhash)                      |                                             |                |
| [eth\_getBlockByNumber](#eth_getblockbynumber)                                                                       | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblockbynumber)                    |                                             |                |
| [eth\_getBlockReceipts](#eth_getblockreceipts)                                                                       | [Execution APIs](https://ethereum.github.io/execution-apis/api/methods/eth_getBlockReceipts)                     |                                             |                |
| [eth\_getBlockTransactionCountByHash](#eth_getblocktransactioncountbyhash)                                           | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblocktransactioncountbyhash)      |                                             |                |
| [eth\_getBlockTransactionCountByNumber](#eth_getblocktransactioncountbynumber)                                       | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblocktransactioncountbynumber)    |                                             |                |
| [eth\_getCode](#eth_getcode)                                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getcode)                             | Yes                                         |                |
| [eth\_getFilterChanges](#eth_getfilterchanges)                                                                       | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getfilterchanges)                    |                                             |                |
| [eth\_getFilterLogs](#eth_getfilterlogs)                                                                             | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getfilterlogs)                       |                                             |                |
| [eth\_getHeaderByHash](#eth_getheaderbyhash)                                                                         | [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#ethgetheaderbyhash)                       |                                             |                |
| [eth\_getHeaderByNumber](#eth_getheaderbynumber)                                                                     | [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#ethgetheaderbynumber)                     |                                             |                |
| [eth\_getLogs](#eth_getlogs)                                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getlogs)                             |                                             |                |
| [eth\_getStorageAt](#eth_getstorageat)                                                                               | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getstorageat)                        | Yes                                         |                |
| [eth\_getTransactionByBlockHashAndIndex](#eth_gettransactionbyblockhashandindex)                                     | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyblockhashandindex)   |                                             |                |
| [eth\_getTransactionByBlockNumberAndIndex](#eth_gettransactionbyblocknumberandindex)                                 | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyblocknumberandindex) |                                             |                |
| [eth\_getTransactionByHash](#eth_gettransactionbyhash)                                                               | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyhash)                |                                             |                |
| [eth\_getTransactionCount](#eth_gettransactioncount)                                                                 | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactioncount)                 | Yes                                         |                |
| [eth\_getTransactionReceipt](#eth_gettransactionreceipt)                                                             | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionreceipt)               |                                             |                |
| [eth\_maxPriorityFeePerGas](#eth_maxpriorityfeepergas)                                                               | [Execution APIs](https://ethereum.github.io/execution-apis/api/methods/eth_maxPriorityFeePerGas)                 |                                             |                |
| [eth\_newBlockFilter](#eth_newblockfilter)                                                                           | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newblockfilter)                      |                                             |                |
| [eth\_newFilter](#eth_newfilter)                                                                                     | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newfilter)                           |                                             |                |
| [eth\_newPendingTransactionFilter](#eth_newpendingtransactionfilter)                                                 | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newpendingtransactionfilter)         |                                             |                |
| [eth\_sendRawTransaction](#eth_sendrawtransaction)                                                                   | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_sendrawtransaction)                  | Yes                                         |                |
| [eth\_subscribe](#eth_subscribe)                                                                                     | [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#eth-subscribe-unsubscribe)                |                                             | Yes            |
| [eth\_syncing](#eth_syncing)                                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_syncing)                             |                                             |                |
| [eth\_uninstallFilter](#eth_uninstallfilter)                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#eth_uninstallfilter)                     |                                             |                |
| [eth\_unsubscribe](#eth_unsubscribe)                                                                                 | [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#eth-subscribe-unsubscribe)                |                                             |                |
| [net\_listening](#net_listening)                                                                                     | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#net_listening)                           |                                             |                |
| [net\_version](#net_version)                                                                                         | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#net_version)                             |                                             |                |
| [realtime\_sendRawTransaction](#realtime_sendrawtransaction)                                                         | Somnia-specific                                                                                                  | Yes                                         |                |
| [somnia\_getBlockByHash](#somnia_getblockbyhash)                                                                     | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_getBlockByNumber](#somnia_getblockbynumber)                                                                 | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_getPrivilegedTransactionReceiptsForBlockByHash](#somnia_getprivilegedtransactionreceiptsforblockbyhash)     | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_getPrivilegedTransactionReceiptsForBlockByNumber](#somnia_getprivilegedtransactionreceiptsforblockbynumber) | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_getSessionAddress](#somnia_getsessionaddress)                                                               | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_getStatistics](#somnia_getstatistics)                                                                       | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_isReady](#somnia_isready)                                                                                   | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_isReadyWithErrorCode](#somnia_isreadywitherrorcode)                                                         | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_nodePublicKeys](#somnia_nodepublickeys)                                                                     | Somnia-specific                                                                                                  |                                             |                |
| [somnia\_reactivityGetSubscriptionInfo](#somnia_reactivitygetsubscriptioninfo)                                       | Somnia-specific, testnet only                                                                                    |                                             |                |
| [somnia\_reactivityGetSubscriptions](#somnia_reactivitygetsubscriptions)                                             | Somnia-specific, testnet only                                                                                    |                                             |                |
| [somnia\_sendSessionTransaction](#somnia_sendsessiontransaction)                                                     | Somnia-specific                                                                                                  |                                             |                |
| [web3\_clientVersion](#web3_clientversion)                                                                           | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#web3_clientversion)                      |                                             |                |
| [web3\_sha3](#web3_sha3)                                                                                             | [Ethereum standard](https://ethereum.org/developers/docs/apis/json-rpc/#web3_sha3)                               |                                             |                |

## Methods by Category

### Network / Web3

#### `net_version`

Returns the network ID.

* **Parameters:** *(none)*
* **Returns:** `quantity` (uint256)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#net_version)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"net_version","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x13a7"}
```

> **Note:** This is currently non-conformant with the [Ethereum JSON-RPC spec](https://ethereum.github.io/execution-apis/api/methods/net_version/), which specifies that the return value is a decimal string. In future Somnia will switch to this form:<br>
>
> ```
> {"jsonrpc":"2.0","id":1,"result":"5031"}
> ```

***

#### `net_listening`

Returns `true` if the node is listening for connections.

* **Parameters:** *(none)*
* **Returns:** `bool`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#net_listening)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"net_listening","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":true}
```

***

#### `web3_clientVersion`

Returns the Somnia client version string.

* **Parameters:** *(none)*
* **Returns:** `string`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#web3_clientversion)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"web3_clientVersion","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"somnia-92fe2faf8cbcd24-release"}
```

***

#### `web3_sha3`

Returns the Keccak-256 hash of the given data.

* **Parameters:** `data` (hex)
* **Returns:** `hash`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#web3_sha3)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"web3_sha3","params":["0x68656c6c6f"],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8"}
```

***

<br>

### Chain State

#### `eth_chainId`

Returns the chain ID.

* **Parameters:** *(none)*
* **Returns:** `quantity` (uint256)
* **Compatibility:** [EIP-695](https://eips.ethereum.org/EIPS/eip-695), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_chainid)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x13a7"}
```

***

#### `eth_syncing`

Returns `true` while the node is still syncing, `false` once ready. Unlike standard Ethereum clients, Somnia returns bool only -- it does not return a sync-status object.

* **Parameters:** *(none)*
* **Returns:** `bool`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_syncing)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_syncing","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":false}
```

***

#### `eth_blockNumber`

Returns the latest block number.

* **Parameters:** *(none)*
* **Returns:** `quantity` (block number)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_blocknumber)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0xff7809e"}
```

***

#### `eth_accounts`

Returns the list of accounts owned by the node (empty for public RPC endpoints).

* **Parameters:** *(none)*
* **Returns:** `address[]`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_accounts)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_accounts","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":[]}
```

***

<br>

### Account State

#### `eth_getBalance`

Returns the balance of an account in wei.

* **Parameters:** `address`, `blockLabel` (`"latest"`, `"earliest"`, `"pending"`, or hex block number)
* **Returns:** `quantity` (uint256)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getbalance)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBalance",
    "params":["0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6", "0xfebbe5e"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x5a957142cf2e47706c"}
```

***

#### `eth_getTransactionCount`

Returns the nonce (number of transactions sent) for an account.

* **Parameters:** `address`, `blockLabel`
* **Returns:** `quantity` (uint64)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactioncount)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getTransactionCount",
    "params":["0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66", "latest"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x12"}
```

***

#### `eth_getCode`

Returns the bytecode at an address (`"0x"` for addresses without code).

* **Parameters:** `address`, `blockLabel`
* **Returns:** `bytes` (hex)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getcode)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getCode",
    "params":["0x4e59b44847b379578588920cA78FbF26c0B4956C", "0xfebbe5e"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081602082378035828234f58015156039578182fd5b8082525050506014600cf3"}
```

***

#### `eth_getStorageAt`

Returns the value in a contract's storage slot.

* **Parameters:** `address`, `slot` (hex uint256), `blockLabel`
* **Returns:** `bytes` (hex)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getstorageat)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getStorageAt",
    "params":["0x28bec7e30e6faee657a03e19bf1128aad7632a00", "0x0", "0x107c14af"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000d6b03d71e401479cc976fc94a41a37576404e7bd"}
```

***

<br>

### Transaction Submission

#### `eth_sendRawTransaction`

Submits a signed transaction. Returns the transaction hash.

* **Parameters:** `signedTransactionData` (hex-encoded signed tx bytes)
* **Returns:** `hash` (tx id)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_sendrawtransaction)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_sendRawTransaction",
    "params":["0x02f8...signed_tx_bytes..."],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x...transaction_hash..."}
```

> **Note:** The example above is a template. You must provide a valid signed transaction. An invalid transaction returns: `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"invalid transaction","data":null}}`

***

#### `realtime_sendRawTransaction`

Submits a signed transaction and **waits for the receipt** before returning. Same parameters as [`eth_sendRawTransaction`](#eth_sendrawtransaction), but the response is a full transaction receipt instead of just the hash.

* **Parameters:** `signedTransactionData` (hex-encoded signed tx bytes)
* **Returns:** [`TransactionReceipt`](#transactionreceipt)
* **Compatibility:** **Somnia-specific** -- no Ethereum equivalent
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"realtime_sendRawTransaction",
    "params":["0x02f8...signed_tx_bytes..."],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":{"transactionHash":"0x...","blockNumber":"0x...","status":"0x1",...}}
```

***

<br>

### Transaction Retrieval

#### `eth_getTransactionByHash`

Returns transaction details by hash, or `null` if not found.

* **Parameters:** `txHash`
* **Returns:** [`Transaction`](#transaction) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyhash)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getTransactionByHash",
    "params":["0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "blockHash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
    "blockNumber": "0xfebbe5e",
    "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
    "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
    "gas": "0x5208",
    "gasPrice": "0x165a0bc00",
    "hash": "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
    "input": "0x",
    "nonce": "0x11",
    "transactionIndex": "0x0",
    "value": "0x29a0cc50fa726000",
    "type": "0x2",
    "chainId": "0x13a7",
    "v": "0x0",
    "r": "0xf2d4bdc57a1bab8df77f9fd311390f3ec9dacb5bd61fdb56b32bd0c3003136bf",
    "s": "0x10283bed1b67d23c87f8d64f309f401b802e5596788b3ce10dc47ee0e4c21fa4",
    "maxPriorityFeePerGas": "0x165a0bc00",
    "maxFeePerGas": "0x165a0bc00"
  }
}
```

***

#### `eth_getTransactionByBlockHashAndIndex`

Returns a transaction by block hash and index within the block.

* **Parameters:** `blockHash`, `index` (hex)
* **Returns:** [`Transaction`](#transaction) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyblockhashandindex)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getTransactionByBlockHashAndIndex",
    "params":["0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98", "0x0"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "blockHash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
    "blockNumber": "0xfebbe5e",
    "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
    "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
    "gas": "0x5208",
    "gasPrice": "0x165a0bc00",
    "hash": "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
    "input": "0x",
    "nonce": "0x11",
    "transactionIndex": "0x0",
    "value": "0x29a0cc50fa726000",
    "type": "0x2",
    "chainId": "0x13a7",
    "v": "0x0",
    "r": "0xf2d4bdc57a1bab8df77f9fd311390f3ec9dacb5bd61fdb56b32bd0c3003136bf",
    "s": "0x10283bed1b67d23c87f8d64f309f401b802e5596788b3ce10dc47ee0e4c21fa4",
    "maxPriorityFeePerGas": "0x165a0bc00",
    "maxFeePerGas": "0x165a0bc00"
  }
}
```

***

#### `eth_getTransactionByBlockNumberAndIndex`

Returns a transaction by block number and index within the block.

* **Parameters:** `blockNumber` (hex or label), `index` (hex)
* **Returns:** [`Transaction`](#transaction) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionbyblocknumberandindex)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getTransactionByBlockNumberAndIndex",
    "params":["0xfebbe5e", "0x0"],
    "id":1
  }'
```

Response format is identical to [`eth_getTransactionByBlockHashAndIndex`](#eth_gettransactionbyblockhashandindex) above.

***

#### `eth_getTransactionReceipt`

Returns the receipt for a mined transaction.

* **Parameters:** `txHash`
* **Returns:** [`TransactionReceipt`](#transactionreceipt) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gettransactionreceipt)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getTransactionReceipt",
    "params":["0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "transactionHash": "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
    "transactionIndex": "0x0",
    "blockHash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
    "blockNumber": "0xfebbe5e",
    "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
    "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
    "cumulativeGasUsed": "0x0",
    "gasUsed": "0x5208",
    "contractAddress": null,
    "type": "0x2",
    "status": "0x1",
    "logsBloom": "0x00000000...00000000",
    "logs": [],
    "effectiveGasPrice": "0x165a0bc00"
  }
}
```

***

#### `eth_getBlockReceipts`

Returns all transaction receipts for a block.

* **Parameters:** `blockNumberOrHash` (hex block number, block hash, or label like `"latest"`)
* **Returns:** [`TransactionReceipt`](#transactionreceipt)`[]`
* **Compatibility:** [execution-apis](https://ethereum.github.io/execution-apis/api/methods/eth_getBlockReceipts)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBlockReceipts",
    "params":["0xfebbe5e"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "transactionHash": "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
      "transactionIndex": "0x0",
      "blockHash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
      "blockNumber": "0xfebbe5e",
      "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
      "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
      "cumulativeGasUsed": "0x0",
      "gasUsed": "0x5208",
      "contractAddress": null,
      "type": "0x2",
      "status": "0x1",
      "logsBloom": "0x00000000...00000000",
      "logs": [],
      "effectiveGasPrice": "0x165a0bc00"
    }
  ]
}
```

***

<br>

### Block Retrieval

#### `eth_getBlockByNumber`

Returns block data by number.

* **Parameters:** `blockNumber` (hex or `"latest"`, `"earliest"`, `"pending"`), `fullTransactions` (bool -- if true, returns full tx objects; if false, returns tx hashes only)
* **Returns:** [`Block`](#block) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblockbynumber)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBlockByNumber",
    "params":["0xfebbe5e", false],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "hash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
    "parentHash": "0x73af1211b4b3ccc9c35db241d9aaf360375a10e2c85bd967c301411208404b45",
    "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
    "miner": "0x0000000000000000000000000000000000000000",
    "stateRoot": "0xc076805ead803fdd1626309ade65a9da117e8dd0292c3b83a3f83b2c275e324c",
    "transactionsRoot": "0xb3704a7f49e846d089b574444a520315c852300c0e8a0b475d2614d71c14e031",
    "receiptsRoot": "0x38cb8db2babffab04de59d22a5ce68fb8cc5fcb00e71df0f9f03e3ce4d93a798",
    "logsBloom": "0x00000000...00000000",
    "difficulty": "0x0",
    "number": "0xfebbe5e",
    "gasLimit": "0x5208",
    "gasUsed": "0x5208",
    "baseFeePerGas": "0x165a0bc00",
    "timestamp": "0x69c925c6",
    "size": "0x222",
    "extraData": "0x0c73c07da3a7bbb6124544f41657d75a6a5ee033adce9d8eff74bfad616567a2",
    "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "nonce": "0x0000000000000000",
    "transactions": [
      "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53"
    ]
  }
}
```

***

#### `eth_getBlockByHash`

Returns block data by hash. Same response format as [`eth_getBlockByNumber`](#eth_getblockbynumber).

* **Parameters:** `blockHash`, `fullTransactions` (bool)
* **Returns:** [`Block`](#block) `| null`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblockbyhash)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBlockByHash",
    "params":["0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98", false],
    "id":1
  }'
```

Response format is identical to [`eth_getBlockByNumber`](#eth_getblockbynumber) above.

***

#### `eth_getHeaderByNumber`

Returns only the block header (no transactions). Same response shape as [`eth_getBlockByNumber`](#eth_getblockbynumber) but with the `transactions` field omitted entirely.

* **Parameters:** `blockLabel` (hex or label)
* **Returns:** [`Block`](#block) `| null` (header only)
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#ethgetheaderbynumber)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getHeaderByNumber",
    "params":["0xfebbe5e"],
    "id":1
  }'
```

> **Note:** The `transactions` field is omitted from the response (not present as an empty array).

***

#### `eth_getHeaderByHash`

Returns only the block header by hash. Same as [`eth_getHeaderByNumber`](#eth_getheaderbynumber) but looks up by block hash.

* **Parameters:** `blockHash`
* **Returns:** [`Block`](#block) `| null` (header only)
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#ethgetheaderbyhash)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getHeaderByHash",
    "params":["0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98"],
    "id":1
  }'
```

> **Note:** The `transactions` field is omitted from the response (not present as an empty array).

***

#### `eth_getBlockTransactionCountByNumber`

Returns the number of transactions in a block.

* **Parameters:** `blockLabel` (hex or label)
* **Returns:** `quantity` (uint64)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblocktransactioncountbynumber)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBlockTransactionCountByNumber",
    "params":["0xfebbe5e"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x1"}
```

***

#### `eth_getBlockTransactionCountByHash`

Returns the number of transactions in a block by hash.

* **Parameters:** `blockHash`
* **Returns:** `quantity` (uint64)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getblocktransactioncountbyhash)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getBlockTransactionCountByHash",
    "params":["0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x1"}
```

***

<br>

### Logs & Filters

#### `eth_getLogs`

Returns logs matching a filter. The block range must not exceed 1000 blocks.

* **Parameters:** filter object with optional fields `fromBlock`, `toBlock`, `address`, `topics`, `blockHash`
* **Returns:** [`Log`](#log)`[]`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getlogs)

| Field       | Type                     | Notes                                                                                                     |
| ----------- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `fromBlock` | `blockLabel` or `null`   | Start of block range                                                                                      |
| `toBlock`   | `blockLabel` or `null`   | End of block range                                                                                        |
| `blockHash` | `hash` or `null`         | Specific block hash                                                                                       |
| `address`   | `address` or `address[]` | Match logs from one or more contracts                                                                     |
| `topics`    | `array` or `null`        | Topic-position filters; each position may be a single hash, an OR-list of hashes, or `null` as a wildcard |

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_getLogs",
    "params":[{
      "fromBlock": "0xfebbe5e",
      "toBlock": "0xfebbe5e"
    }],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "address": "0x2a48aa2986e4bdabfae1c7366c72168cf4772f56",
      "topics": [
        "0xb33ea778dc37bc968fc39ed9c7bb9d2d265850bfb8dcaaaea7987640fe0fa956",
        "0x000000000000000000000000000000000000000000000000000000000000002e",
        "0x000000000000000000000000db1b9390cfae98af059d6c38c987dedc86081da1"
      ],
      "data": "0x0000000000000000000000000000000000000000000000000000000000008477",
      "blockNumber": "0x147df45d",
      "transactionHash": "0x659acd064a5f391d779dd8d778f4eae10cf6d9caa8948e17f8065c12ef6f7b5d",
      "transactionIndex": "0x0",
      "blockHash": "0xdbcabc2fc754288ec00dcd5b69c65033142f202c8cad21b563f778f88c9f7526",
      "logIndex": "0x0"
    }
  ]
}
```

***

#### `eth_newFilter`

Creates a log filter. Returns a filter ID. The block range must not exceed 1000 blocks.

* **Parameters:** filter object with optional fields `fromBlock`, `toBlock`, `address`, `topics`
* **Returns:** `quantity` (filter id)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newfilter)

| Field       | Type                     | Notes                                                                                                     |
| ----------- | ------------------------ | --------------------------------------------------------------------------------------------------------- |
| `fromBlock` | `blockLabel` or `null`   | Start of block range                                                                                      |
| `toBlock`   | `blockLabel` or `null`   | End of block range                                                                                        |
| `address`   | `address` or `address[]` | Match logs from one or more contracts                                                                     |
| `topics`    | `array` or `null`        | Topic-position filters; each position may be a single hash, an OR-list of hashes, or `null` as a wildcard |

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_newFilter",
    "params":[{"fromBlock":"latest","toBlock":"latest"}],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x1517573b"}
```

***

#### `eth_newBlockFilter`

Creates a filter that returns new block hashes.

* **Parameters:** *(none)*
* **Returns:** `quantity` (filter id)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newblockfilter)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_newBlockFilter","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x151730d4"}
```

***

#### `eth_newPendingTransactionFilter`

Creates a filter that returns new pending transaction hashes.

* **Parameters:** *(none)*
* **Returns:** `quantity` (filter id)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_newpendingtransactionfilter)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_newPendingTransactionFilter","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x151730f3"}
```

***

#### `eth_getFilterChanges`

Polls a filter for new results since the last poll. Response format depends on filter type: block hashes for block filters, tx hashes for pending tx filters, log objects for log filters.

* **Parameters:** `filterId` (hex)
* **Returns:** `json` (varies by filter type)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getfilterchanges)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_getFilterChanges","params":["0x151730d4"],"id":1}'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    "0x96fe2ad8067123d10ab4e618fd18316b95990e98c3cf914e835878d9d998167a",
    "0x27b347f204beeb9d06d9ae96483070f609cd863b37a34b3bc7b6e5589b4e93bc",
    "0xfafb5f1ecf15e169878fe83adb56bcf6041dc41fa186396e3f5a9f332c0956ea"
  ]
}
```

***

#### `eth_getFilterLogs`

Returns all logs matching the filter criteria (for log filters only).

* **Parameters:** `filterId` (hex)
* **Returns:** [`Log`](#log)`[]`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_getfilterlogs)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_getFilterLogs","params":["0x1517573b"],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":[]}
```

***

#### `eth_uninstallFilter`

Removes a filter. Returns `true` if the filter was found and removed.

* **Parameters:** `filterId` (hex)
* **Returns:** `bool`
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_uninstallfilter)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_uninstallFilter","params":["0x1517573b"],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":true}
```

> **Note:** Filter IDs are connection-scoped. On load-balanced public endpoints, the filter may be on a different backend than the uninstall request, causing `false` returns. For reliable filter usage, use a persistent WebSocket connection or a dedicated RPC endpoint.

***

<br>

### Subscriptions

#### `eth_subscribe`

Creates a subscription. Supported subscription types:

* `"newHeads"` -- new block headers
* `"logs"` -- log events matching a filter
* `"somnia_finishedTransactions"` -- completed transaction receipts (Somnia-specific)
* `"somnia_finishedBlocks"` -- completed blocks (Somnia-specific)
* `"somnia_watch"` -- log events with watch semantics (Somnia-specific)

**Properties:**

* **Parameters:** `subscriptionType`, optional params object (see [Subscription Schemas](#subscription-schemas))
* **Returns:** `quantity` (subscription id)
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#eth-subscribe-unsubscribe)
* WebSocket only
  * This method emits events, so requires a **WebSocket** connection. Over HTTP it returns `events_not_supported`. [`eth_unsubscribe`](#eth_unsubscribe) works over both HTTP and WebSocket.

```
wscat -c wss://api.infra.testnet.somnia.network/ws
> {"jsonrpc":"2.0","method":"eth_subscribe","params":["newHeads"],"id":1}
< {"jsonrpc":"2.0","id":1,"result":"0x1"}
< {"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x1","result":{...block header...}}}
```

Over HTTP, this returns:

```json
{"jsonrpc":"2.0","id":1,"error":{"code":-1,"message":"events_not_supported","data":null}}
```

***

#### `eth_unsubscribe`

Cancels a subscription. Returns `true` if the subscription was found and removed, `false` otherwise.

Subscription IDs are connection-scoped, so this must be called from the same connection that created the subscription.

* **Parameters:** `subscriptionId` (hex)
* **Returns:** `bool`
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-eth#eth-subscribe-unsubscribe)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_unsubscribe","params":["0x1"],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":false}
```

***

<br>

### Gas & Fees

#### `eth_gasPrice`

Returns the current gas price in wei.

* **Parameters:** *(none)*
* **Returns:** `quantity` (uint256)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_gasprice), [execution-apis](https://ethereum.github.io/execution-apis/api/methods/eth_gasPrice)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x165a0bc00"}
```

***

#### `eth_maxPriorityFeePerGas`

Returns the current max priority fee per gas (tip) in wei.

* **Parameters:** *(none)*
* **Returns:** `quantity` (uint256)
* **Compatibility:** [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559), [execution-apis](https://ethereum.github.io/execution-apis/api/methods/eth_maxPriorityFeePerGas)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"eth_maxPriorityFeePerGas","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x0"}
```

***

#### `eth_feeHistory`

Returns historical gas fee data.

* **Parameters:** `blockCount` (hex), `newestBlock` (hex or label), `rewardPercentiles` (array of floats)
* **Returns:** json object
* **Compatibility:** [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559), [execution-apis](https://ethereum.github.io/execution-apis/api/methods/eth_feeHistory)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_feeHistory",
    "params":["0x4", "latest", [25, 75]],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "oldestBlock": "0xff78371",
    "baseFeePerGas": ["0x165a0bc00", "0x165a0bc00", "0x165a0bc00", "0x165a0bc00"],
    "baseFeePerBlobGas": ["0x0", "0x0", "0x0", "0x0"],
    "gasUsedRatio": [0, 0, 0, 0],
    "blobGasUsedRatio": [0, 0, 0, 0],
    "reward": [["0x0", "0x0"], ["0x0", "0x0"], ["0x0", "0x0"], ["0x0", "0x0"]]
  }
}
```

***

#### `eth_call`

Executes a call without creating a transaction (read-only simulation).

* **Parameters:** [`TransactionRequest`](#transactionrequest), `blockLabelOrHash`, optional [`StateOverrideSet`](#stateoverrideset) (see [Request Schemas](#request-schemas))
* **Returns:** `bytes` (hex)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_call)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_call",
    "params":[{
      "to": "0x28bec7e30e6faee657a03e19bf1128aad7632a00",
      "data": "0x18160ddd"
    }, "0x107c14af"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x000000000000000000000000000000000000000000000000000000c11b9b1648"}
```

***

#### `eth_estimateGas`

Estimates the gas required for a transaction.

* **Parameters:** [`TransactionRequest`](#transactionrequest), optional `blockLabel`, optional [`StateOverrideSet`](#stateoverrideset) (see [Request Schemas](#request-schemas))
* **Returns:** `quantity` (uint64)
* **Compatibility:** [EIP-1474](https://eips.ethereum.org/EIPS/eip-1474), [ethereum.org](https://ethereum.org/developers/docs/apis/json-rpc/#eth_estimategas)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_estimateGas",
    "params":[{
      "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
      "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
      "value": "0x0"
    }],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0x5208"}
```

***

#### `eth_createAccessList`

Returns an empty EIP-2930 access list plus the estimated gas for a transaction. (Somnia doesn't support access lists.)

* **Parameters:** [`TransactionRequest`](#transactionrequest), optional `blockLabel` (see [Request Schemas](#request-schemas))
* **Returns:** `[accessList, gasUsed]` (JSON array)
* **Compatibility:** [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930), [execution-APIs](https://ethereum.github.io/execution-apis/api/methods/eth_createAccessList)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"eth_createAccessList",
    "params":[{
      "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
      "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
      "value": "0x0"
    }],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":[[],"0x7b0c"]}
```

***

<br>

### Debug / Tracing

#### `debug_traceCall`

Traces a call execution with a specified tracer.

* **Parameters:** [`TransactionRequest`](#transactionrequest), `blockLabelOrHash`, trace config (`tracer`, `tracerConfig`, `stateOverrides`) (see [Request Schemas](#request-schemas))
* **Returns:** trace output
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracecall)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"debug_traceCall",
    "params":[{
      "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
      "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
      "value": "0x1"
    }, "latest", {"tracer": "callTracer"}],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "type": "CALL",
    "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
    "gas": "0x3b9a77f8",
    "gasUsed": "0x5208",
    "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
    "input": "0x",
    "output": "0x",
    "error": "INSUFFICIENT_BALANCE",
    "revertReason": null,
    "value": "0x1",
    "logs": [],
    "calls": []
  }
}
```

***

#### `debug_traceTransaction`

Traces an already-mined transaction.

* **Parameters:** `txHash`, tracer config (`tracer`, `tracerConfig`)
* **Returns:** trace output
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtracetransaction)
* Requires node to be [ready](#requires-node-ready)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"debug_traceTransaction",
    "params":[
      "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
      {"tracer": "callTracer"}
    ],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "type": "CALL",
    "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
    "gas": "0x0",
    "gasUsed": "0x5208",
    "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
    "input": "0x",
    "output": "0x",
    "error": null,
    "revertReason": null,
    "value": "0x29a0cc50fa726000",
    "logs": [],
    "calls": []
  }
}
```

***

#### `debug_traceBlockByNumber`

Traces all transactions in a block by block number.

* **Parameters:** `blockLabel` (hex or label), tracer config
* **Returns:** `{txHash, result | null, error | null}[]`
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbynumber)
* Requires node to be [ready](#requires-node-ready)

> **Supported tracers:** `"callTracer"` (with optional `{"onlyTopCall": true}`), `"prestateTracer"` (with optional `{"diffMode": true}`)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"debug_traceBlockByNumber",
    "params":["0xfebbe5e", {"tracer": "callTracer"}],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "txHash": "0xe37a5053ed983747d65a782a4924e2a894729a0cbcbbf012ea6267870a99df53",
      "result": {
        "type": "CALL",
        "from": "0xc04ea345ac1aded81ea4b3bd7ada1f7638e63e66",
        "gas": "0x0",
        "gasUsed": "0x5208",
        "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
        "input": "0x",
        "output": "0x",
        "error": null,
        "revertReason": null,
        "value": "0x29a0cc50fa726000",
        "logs": [],
        "calls": []
      },
      "error": null
    }
  ]
}
```

***

#### `debug_traceBlockByHash`

Traces all transactions in a block by block hash.

* **Parameters:** `blockHash`, tracer config
* **Returns:** `{txHash, result | null, error | null}[]`
* **Compatibility:** [Geth](https://geth.ethereum.org/docs/interacting-with-geth/rpc/ns-debug#debugtraceblockbyhash)
* Requires node to be [ready](#requires-node-ready)

> **Supported tracers:** `"callTracer"` (with optional `{"onlyTopCall": true}`), `"prestateTracer"` (with optional `{"diffMode": true}`)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"debug_traceBlockByHash",
    "params":[
      "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98",
      {"tracer": "callTracer"}
    ],
    "id":1
  }'
```

Response format is identical to [`debug_traceBlockByHash`](#debug_traceblockbyhash) above.

***

<br>

### Somnia-Specific

#### `somnia_isReady`

Returns `true` if the node is synced and ready to process requests.

* **Parameters:** *(none)*
* **Returns:** `bool`
* **Compatibility:** **Somnia-specific**

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"somnia_isReady","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":true}
```

***

#### `somnia_isReadyWithErrorCode`

Like [`somnia_isReady`](#somnia_isready), but returns an RPC error (code -32603) instead of `false` when the node is not ready.

* **Parameters:** *(none)*
* **Returns:** `bool` (error code on failure)
* **Compatibility:** **Somnia-specific**

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"somnia_isReadyWithErrorCode","params":[],"id":1}'
```

```json
{"jsonrpc":"2.0","id":1,"result":true}
```

***

#### `somnia_nodePublicKeys`

Returns the node's public keys for the current epoch.

* **Parameters:** *(none)*
* **Returns:** json object
* **Compatibility:** **Somnia-specific**

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"somnia_nodePublicKeys","params":[],"id":1}'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "address": "0x53ee336e74e41250b1420d102fa35d3ac49c8928",
    "ecdsa_public_key": "0x024683678905b871ce02faf8af763033fea59dfc70f52df8ecb979b4e373abb638",
    "bls_public_key": "0x135dc21ad1326f464b458078408574a7adf3a570697715cbf68b28e817d69380...",
    "bls_proof_of_possession": "0xa14f7c8b...",
    "proof_of_address": "0x91c4c5be..."
  }
}
```

***

#### `somnia_getBlockByNumber`

Returns Somnia's native block representation (consensus + execution blocks), which contains more detail than the Ethereum-compatible [`eth_getBlockByNumber`](#eth_getblockbynumber).

* **Parameters:** `blockLabel` (hex or label)
* **Returns:** [`SomniaLedgerBlock`](#somnialedgerblock) `| null`
* **Compatibility:** **Somnia-specific** -- native block format (cf. [`eth_getBlockByNumber`](#eth_getblockbynumber))

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getBlockByNumber",
    "params":["0xfebbe5e"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "consensus_block": {
      "block_number": "0xfebbe5e",
      "timestamp": "0x19d39bb90f5",
      "data_chain_blocks": ["0x0798136c..."],
      "delayed_ledger_block_number": "0xfebbe4e",
      "delayed_ledger_block_hash": "0x03b01a71...",
      "parent_consensus_block_hash": "0xf9f64c1d...",
      "proposer_address": "0x012f6f8ded4e1f977953cd8dbfff31ebb902549c",
      "block_resources": {
        "validation_gas": "0x9c40",
        "compressed_bytes": "0x7c",
        "uncompressed_bytes": "0x7d"
      },
      "consensus_block_hash": "0xb83203e7..."
    },
    "execution_block": {
      "transaction_ids_hash": "0xb3704a7f...",
      "receipts_hash": "0x38cb8db2...",
      "execution_gas_limit": "0x5208",
      "execution_gas_used": "0x5208",
      "execution_state_snapshot": "0xc076805e...",
      "state_snapshot_block_number": "0xfebb2e7",
      "operation_sequence_hash": "0xb4e658ec67dbe4de"
    },
    "parent_ledger_block_hash": "0x2fe94d90...",
    "ledger_block_hash": "0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98"
  }
}
```

***

#### `somnia_getBlockByHash`

Same as [`somnia_getBlockByNumber`](#somnia_getblockbynumber) but looks up by block hash.

* **Parameters:** `blockHash`
* **Returns:** [`SomniaLedgerBlock`](#somnialedgerblock) `| null`
* **Compatibility:** **Somnia-specific** -- native block format (cf. [`eth_getBlockByHash`](#eth_getblockbyhash))

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getBlockByHash",
    "params":["0x761fdbb98eb904196b6be98870b222fc2f06d2fdac4bae1200f177092ce6ec98"],
    "id":1
  }'
```

Response format is identical to [`somnia_getBlockByNumber`](#somnia_getblockbynumber) above.

***

#### `somnia_getStatistics`

Returns chain statistics over a block range.

* **Parameters:** `fromBlock` (hex or label), `toBlock` (hex or label)
* **Returns:** json object
* **Compatibility:** **Somnia-specific**

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getStatistics",
    "params":["0xfebbe5e", "0xfebbe60"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "numSuccessfulTransactions": "0x1",
    "numRevertedTransactions": "0x0",
    "numContractsAdded": "0x0",
    "numAccountsAdded": "0x0",
    "numNewUsedEoaAccounts": "0x0",
    "numNativeTransferTransactions": "0x1",
    "numGasUnitsSpent": "0x5208",
    "gasFeeSpent": "0x7298a93de000"
  }
}
```

***

#### `somnia_getPrivilegedTransactionReceiptsForBlockByNumber`

Returns receipts for privileged (system) transactions in a block.

* **Parameters:** `blockNumber` (hex or label)
* **Returns:** [`TransactionReceipt`](#transactionreceipt)`[]`
* **Compatibility:** **Somnia-specific** -- system/privileged tx receipts

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getPrivilegedTransactionReceiptsForBlockByNumber",
    "params":["0xfef96ff"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": [
    {
      "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
      "transactionIndex": "0x0",
      "blockHash": "0xf3ef3de9a584326c0252b44f111aa7d342d6698678d310a5b1233622aab94a5e",
      "blockNumber": "0xfef96ff",
      "from": "0x7b8b1bb68c6f0e29f3addcb45a6c0bb8e8e331c7",
      "to": "0x7b8b1bb68c6f0e29f3addcb45a6c0bb8e8e331c7",
      "cumulativeGasUsed": "0x0",
      "gasUsed": "0x42a5ca",
      "contractAddress": null,
      "type": "0x18",
      "status": "0x1",
      "logsBloom": "0x00000000...00000000",
      "logs": [
        {
          "address": "0x7b8b1bb68c6f0e29f3addcb45a6c0bb8e8e331c7",
          "topics": ["0x6debf9c0b8bd7ecda40db89a2641f61251d80a576b5c5e5f06de7f1c2a65850a"],
          "data": "0x00000000000000000000000000000000000000000000000000000000000000330000000000000000000000000000000000000000000000000000000069c988e4",
          "blockNumber": "0xfef96ff",
          "transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
          "transactionIndex": "0x0",
          "blockHash": "0xf3ef3de9a584326c0252b44f111aa7d342d6698678d310a5b1233622aab94a5e",
          "logIndex": "0x0"
        }
      ],
      "effectiveGasPrice": "0x0"
    },
    ...
  ]
}
```

***

#### `somnia_getPrivilegedTransactionReceiptsForBlockByHash`

Returns receipts for privileged (system) transactions in a block. Same as [`somnia_getPrivilegedTransactionReceiptsForBlockByNumber`](#somnia_getprivilegedtransactionreceiptsforblockbynumber) but looks up by block hash.

* **Parameters:** `blockHash`
* **Returns:** [`TransactionReceipt`](#transactionreceipt)`[]`
* **Compatibility:** **Somnia-specific** -- system/privileged tx receipts

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getPrivilegedTransactionReceiptsForBlockByHash",
    "params":["0xf3ef3de9a584326c0252b44f111aa7d342d6698678d310a5b1233622aab94a5e"],
    "id":1
  }'
```

Response format is identical to [`somnia_getPrivilegedTransactionReceiptsForBlockByNumber`](#somnia_getprivilegedtransactionreceiptsforblockbynumber) above.

***

#### `somnia_getSessionAddress`

Derives a session key address from a seed.

* **Parameters:** `seed` (32-byte hash)
* **Returns:** `address`
* **Compatibility:** **Somnia-specific** -- session key derivation

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_getSessionAddress",
    "params":["0x0000000000000000000000000000000000000000000000000000000000000001"],
    "id":1
  }'
```

```json
{"jsonrpc":"2.0","id":1,"result":"0xf2d469b1fb798a5e9ced20e22daa7a911f35d697"}
```

***

#### `somnia_sendSessionTransaction`

Sends a transaction using a session key (derived from a seed). The session account must have sufficient funds.

* **Parameters:** object with `seed` (hash), `gas` (hex), `to` (address, optional), `value` (hex, optional), `data` (hex, optional)
* **Returns:** [`TransactionReceipt`](#transactionreceipt) `| null`
* **Compatibility:** **Somnia-specific** -- session key transactions

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_sendSessionTransaction",
    "params":[{
      "seed": "0x0000000000000000000000000000000000000000000000000000000000000001",
      "gas": "0x5208",
      "to": "0x1909cdb9524d065e5751d2fe79e2b9ad54d822e6",
      "value": "0x0"
    }],
    "id":1
  }'
```

Response format is identical to [`eth_getTransactionReceipt`](#eth_gettransactionreceipt) above.

***

#### `somnia_reactivityGetSubscriptionInfo`

Returns details of a [Somnia Reactivity Protocol](https://docs.somnia.network/developer/reactivity) subscription by ID.

* **Parameters:** `subscriptionId` (hex-encoded subscription ID)
* **Returns:** [`ReactivitySubscription`](#reactivitysubscription)
* **Compatibility:** **Somnia-specific** -- [Somnia Reactivity Protocol](https://docs.somnia.network/developer/reactivity)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_reactivityGetSubscriptionInfo",
    "params":["0x1"],
    "id":1
  }'
```

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "id": "0x1",
    "topics": [
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
      "0x0000000000000000000000000000000000000000000000000000000000000000",
      "0x0000000000000000000000000000000000000000000000000000000000000000",
      "0x0000000000000000000000000000000000000000000000000000000000000000"
    ],
    "origin": "0x0000000000000000000000000000000000000000",
    "caller": "0x0000000000000000000000000000000000000000",
    "emitter": "0x107256636b98e705fade330bbcb903580b8b227d",
    "owner": "0x12a76e09bae1934265a1aea2812e0c772f372f8d",
    "handler_contract_address": "0xf84f7633bd403967999b06911f978a74326f547e",
    "handler_function_selector": "0x53edf33d",
    "gas_limit": "0x4c4b40",
    "priority_fee_per_gas": "0x77359400",
    "max_fee_per_gas": "0x2540be400"
  }
}
```

***

#### `somnia_reactivityGetSubscriptions`

Returns all [Somnia Reactivity Protocol](https://docs.somnia.network/developer/reactivity) subscriptions owned by an address.

* **Parameters:** `address` (owner address)
* **Returns:** [`ReactivitySubscription[]`](#reactivitysubscription)
* **Compatibility:** **Somnia-specific** -- [Somnia Reactivity Protocol](https://docs.somnia.network/developer/reactivity)

```bash
curl -s -X POST $RPC_URL \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0",
    "method":"somnia_reactivityGetSubscriptions",
    "params":["0x12a76e09bae1934265a1aea2812e0c772f372f8d"],
    "id":1
  }'
```

Response is a JSON array of [`ReactivitySubscription`](#reactivitysubscription) objects.

***

<br>

## Notes

### General Usage

#### Endpoints

| Network                                     | HTTP                                       | WebSocket                                   |
| ------------------------------------------- | ------------------------------------------ | ------------------------------------------- |
| Mainnet (chain ID `0x13a7` / 5031)          | `https://api.infra.mainnet.somnia.network` | `wss://api.infra.mainnet.somnia.network/ws` |
| Testnet Shannon (chain ID `0xc488` / 50312) | `https://api.infra.testnet.somnia.network` | `wss://api.infra.testnet.somnia.network/ws` |

#### Request Format

All requests use the [JSON-RPC 2.0](https://www.jsonrpc.org/specification) protocol over HTTP POST or WebSocket. The request body is:

```json
{"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": 1}
```

**Batch requests** are supported: send a JSON array of request objects and receive a JSON array of responses in the same order.

```json
[
  {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1},
  {"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":2}
]
```

#### Hex Encoding

All numeric values in requests and responses are hex-encoded with a `0x` prefix, following the [Ethereum JSON-RPC convention](https://ethereum.org/developers/docs/apis/json-rpc/#conventions):

* **Quantities** (block numbers, gas, balances, nonces): hex without leading zeros — `"0x1"`, `"0x5208"`, `"0x165a0bc00"`
* **Data** (hashes, addresses, bytecode, input data): hex with even-length zero-padding — `"0x0000...0000"`, `"0x"`

#### Block Labels

Many methods accept a **block label** parameter. This can be:

* A hex-encoded block number (e.g. `"0xfebbe5e"`)
* `"latest"` — the most recent confirmed block
* `"earliest"` — the genesis block (block 0)
* `"finalized"`, `"pending"`, `"safe"` — currently treated the same as `"latest"`

#### Transaction Types

Somnia supports the following Ethereum transaction types via [`eth_sendRawTransaction`](#eth_sendrawtransaction):

| Type  | Name        | EIP                                                 | Notes                                       |
| ----- | ----------- | --------------------------------------------------- | ------------------------------------------- |
| `0x0` | Legacy      | pre-EIP-2718                                        | `gasPrice` field                            |
| `0x1` | Access list | [EIP-2930](https://eips.ethereum.org/EIPS/eip-2930) | `gasPrice` + `accessList`                   |
| `0x2` | Dynamic fee | [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559) | `maxFeePerGas` + `maxPriorityFeePerGas`     |
| `0x3` | Blob        | [EIP-4844](https://eips.ethereum.org/EIPS/eip-4844) | **Not supported**                           |
| `0x4` | Set code    | [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) | `authorizationList` for EOA code delegation |

#### Error Codes

Error responses follow the JSON-RPC 2.0 format:

```json
{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"insufficient balance","data":null}}
```

| Code     | Meaning              | Common causes                                               |
| -------- | -------------------- | ----------------------------------------------------------- |
| `-32700` | Parse error          | Malformed JSON                                              |
| `-32600` | Invalid request      | Missing `jsonrpc`, `method`, or `id` fields                 |
| `-32601` | Method not found     | Typo in method name, or method not available on this node   |
| `-32602` | Invalid parameters   | Wrong number or types of params                             |
| `-32603` | Internal error       | Node not ready, or unexpected server error                  |
| `-32604` | Too many requests    | Rate limit exceeded                                         |
| `-32004` | Method not supported | Method exists but is disabled on this node                  |
| `-32000` | Invalid input        | Transaction-specific errors (see below)                     |
| `-1`     | Application error    | Handler-specific errors (e.g. `"block range exceeds 1000"`) |

Common `-32000` transaction errors from [`eth_sendRawTransaction`](#eth_sendrawtransaction):

| Message                      | Meaning                                                    |
| ---------------------------- | ---------------------------------------------------------- |
| `"invalid transaction"`      | RLP decoding failed or transaction is malformed            |
| `"invalid signature"`        | ECDSA signature verification failed                        |
| `"nonce too low"`            | Transaction nonce is behind the account's current nonce    |
| `"nonce too high"`           | Transaction nonce is too far ahead                         |
| `"insufficient balance"`     | Account cannot cover value + gas                           |
| `"gas price below base fee"` | `gasPrice` or `maxFeePerGas` is below the current base fee |
| `"mempool full"`             | Node's transaction pool is at capacity                     |

### Special Cases

#### Requires Node Ready

Some methods are marked **Requires Node Ready** in the index table. These methods will return an error if the node has not finished syncing and is not yet ready to serve requests.

You can check whether a node is ready before making calls by using [`somnia_isReady`](#somnia_isready).

#### Log Query Limits

[`eth_getLogs`](#eth_getlogs), [`eth_newFilter`](#eth_newfilter), and [`eth_getFilterLogs`](#eth_getfilterlogs) enforce a maximum block range (currently 1000 blocks). Queries exceeding this range will return an error. To scan larger ranges, issue multiple queries with non-overlapping block ranges.

#### Filter and Subscription Statefulness

Filters created with [`eth_newFilter`](#eth_newfilter), [`eth_newBlockFilter`](#eth_newblockfilter), and [`eth_newPendingTransactionFilter`](#eth_newpendingtransactionfilter) are scoped to the connection that created them. On load-balanced public RPC endpoints, subsequent requests may be routed to a different backend, causing [`eth_getFilterChanges`](#eth_getfilterchanges) to return empty results or [`eth_uninstallFilter`](#eth_uninstallfilter) to return `false`. For reliable filter-based polling, use a dedicated RPC endpoint or switch to [`eth_subscribe`](#eth_subscribe) over WebSocket.

Subscriptions created with [`eth_subscribe`](#eth_subscribe) are also connection-scoped. [`eth_unsubscribe`](#eth_unsubscribe) returns `false` if the subscription id is unknown or belongs to a different connection.

#### Missing Resource Semantics

Somnia does not use a single convention for "not found". The main cases are:

| Method(s)                                                                                                                                                                                                                                                                                                | Missing resource behaviour                                            |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [`eth_getTransactionByHash`](#eth_gettransactionbyhash), [`eth_getTransactionReceipt`](#eth_gettransactionreceipt), [`eth_getTransactionByBlockHashAndIndex`](#eth_gettransactionbyblockhashandindex), [`eth_getTransactionByBlockNumberAndIndex`](#eth_gettransactionbyblocknumberandindex)             | Returns `null`                                                        |
| [`eth_getBlockByHash`](#eth_getblockbyhash), [`eth_getBlockByNumber`](#eth_getblockbynumber), [`eth_getHeaderByHash`](#eth_getheaderbyhash), [`eth_getHeaderByNumber`](#eth_getheaderbynumber), [`somnia_getBlockByHash`](#somnia_getblockbyhash), [`somnia_getBlockByNumber`](#somnia_getblockbynumber) | Returns `null`                                                        |
| [`eth_getBlockTransactionCountByHash`](#eth_getblocktransactioncountbyhash), [`eth_getBlockTransactionCountByNumber`](#eth_getblocktransactioncountbynumber)                                                                                                                                             | Returns `"0x0"`                                                       |
| [`eth_getBlockReceipts`](#eth_getblockreceipts)                                                                                                                                                                                                                                                          | RPC error: `"unknown block"`                                          |
| [`somnia_getPrivilegedTransactionReceiptsForBlockByHash`](#somnia_getprivilegedtransactionreceiptsforblockbyhash), [`somnia_getPrivilegedTransactionReceiptsForBlockByNumber`](#somnia_getprivilegedtransactionreceiptsforblockbynumber)                                                                 | RPC error: `"Block does not exist"`                                   |
| [`eth_uninstallFilter`](#eth_uninstallfilter), [`eth_unsubscribe`](#eth_unsubscribe)                                                                                                                                                                                                                     | Returns `false` if the id is unknown or belongs to another connection |

#### Somnia vs Ethereum Differences

While Somnia is EVM-compatible and supports the standard Ethereum JSON-RPC, there are a few behavioural differences to be aware of:

* [**`eth_syncing`**](#eth_syncing) returns only a `bool` (`true` while syncing, `false` when ready). It does not return a sync-status object with `startingBlock`/`currentBlock`/`highestBlock` fields as Ethereum clients do.
* [**`eth_getHeaderByHash`**](#eth_getheaderbyhash) **/** [**`eth_getHeaderByNumber`**](#eth_getheaderbynumber) omit the `transactions` field entirely rather than returning an empty array.
* [**`eth_subscribe`**](#eth_subscribe) supports the standard `"newHeads"` and `"logs"` types, plus three Somnia-specific types: `"somnia_finishedTransactions"`, `"somnia_finishedBlocks"`, and `"somnia_watch"`. It does **not** support `"newPendingTransactions"`.
* [**`realtime_sendRawTransaction`**](#realtime_sendrawtransaction) is a Somnia extension that submits a transaction and blocks until the receipt is available, combining [`eth_sendRawTransaction`](#eth_sendrawtransaction) + polling [`eth_getTransactionReceipt`](#eth_gettransactionreceipt) into a single call.
* **Block structure**: Somnia blocks have a fixed `difficulty` of `0x0`, `miner` of `0x0...0`, and no uncle blocks. The `gasLimit` reflects per-block execution gas rather than a global gas target.
* **EIP-4844 blob transactions** (type `0x3`) are not supported.

For more detail on gas behaviour, see [Somnia Gas Differences to Ethereum](/developer/deployment-and-production/somnia-gas-differences-to-ethereum).

## Appendix

### Request Schemas

**TransactionRequest** (used by [`eth_call`](#eth_call), [`eth_estimateGas`](#eth_estimategas), [`eth_createAccessList`](#eth_createaccesslist), [`debug_traceCall`](#debug_tracecall), and `somnia_watch.eth_calls`):

| Field               | Type                                                              | Notes                                         |
| ------------------- | ----------------------------------------------------------------- | --------------------------------------------- |
| `from`              | `address` or `null`                                               | Sender address for simulation                 |
| `to`                | `address` or `null`                                               | Omit or set `null` for contract creation      |
| `gas`               | `quantity` or `null`                                              | Gas limit for the simulated transaction       |
| `value`             | `quantity` or `null`                                              | Wei transferred                               |
| `input`             | `bytes` or `null`                                                 | Calldata alias accepted by Geth-style clients |
| `data`              | `bytes` or `null`                                                 | Calldata alias                                |
| `authorizationList` | [`AuthorizationListEntry`](#authorizationlistentry)`[]` or `null` | EIP-7702 authorization list                   |

> `input` and `data` are interchangeable aliases. If both are provided, Somnia uses `input`.

**AuthorizationListEntry** (entries within `authorizationList`):

| Field     | Type       | Notes                                  |
| --------- | ---------- | -------------------------------------- |
| `chainId` | `quantity` | Chain id covered by the authorization  |
| `address` | `address`  | Contract address to delegate code from |
| `nonce`   | `quantity` | Authorization nonce                    |
| `yParity` | `quantity` | Signature recovery parity              |
| `r`       | `quantity` | Signature component                    |
| `s`       | `quantity` | Signature component                    |

**StateOverrideSet** (used by [`eth_call`](#eth_call), [`eth_estimateGas`](#eth_estimategas), and [`debug_traceCall`](#debug_tracecall)):

A JSON object keyed by account address, where each value is a [`StateOverride`](#stateoverride) object.

**StateOverride**:

| Field       | Type                 | Notes                                                       |
| ----------- | -------------------- | ----------------------------------------------------------- |
| `balance`   | `quantity` or `null` | Override account balance                                    |
| `nonce`     | `quantity` or `null` | Override account nonce                                      |
| `code`      | `bytes` or `null`    | Override deployed bytecode                                  |
| `state`     | `object` or `null`   | Replace the entire storage map with `slot -> value` entries |
| `stateDiff` | `object` or `null`   | Patch individual storage slots with `slot -> value` entries |

> `state` and `stateDiff` are mutually exclusive. Providing both returns an error.

Several methods take similar log-filter-shaped inputs, but they are not one shared request type in source.

### Response Schemas

**Transaction** (returned by [`eth_getTransactionByHash`](#eth_gettransactionbyhash) and similar):

| Field                  | Type                 | Notes                                                           |
| ---------------------- | -------------------- | --------------------------------------------------------------- |
| `blockHash`            | `hash`               |                                                                 |
| `blockNumber`          | `quantity`           |                                                                 |
| `from`                 | `address`            |                                                                 |
| `to`                   | `address` or `null`  | `null` for contract creation                                    |
| `gas`                  | `quantity`           | Gas limit                                                       |
| `gasPrice`             | `quantity`           | Effective gas price                                             |
| `hash`                 | `hash`               | Transaction hash                                                |
| `input`                | `bytes`              | Calldata (`"0x"` for simple transfers)                          |
| `nonce`                | `quantity`           |                                                                 |
| `transactionIndex`     | `quantity`           | Position within the block                                       |
| `value`                | `quantity`           | Wei transferred                                                 |
| `type`                 | `quantity`           | `0x0` legacy, `0x1` access list, `0x2` EIP-1559, `0x4` EIP-7702 |
| `chainId`              | `quantity`           |                                                                 |
| `v`, `r`, `s`          | `quantity`           | ECDSA signature components                                      |
| `maxPriorityFeePerGas` | `quantity` or `null` | Present for type `0x2` and `0x4` only                           |
| `maxFeePerGas`         | `quantity` or `null` | Present for type `0x2` and `0x4` only                           |
| `authorizationList`    | `array` or `null`    | Present for type `0x4` only                                     |

**TransactionReceipt** (returned by [`eth_getTransactionReceipt`](#eth_gettransactionreceipt) and similar):

| Field               | Type                | Notes                                  |
| ------------------- | ------------------- | -------------------------------------- |
| `transactionHash`   | `hash`              |                                        |
| `transactionIndex`  | `quantity`          |                                        |
| `blockHash`         | `hash`              |                                        |
| `blockNumber`       | `quantity`          |                                        |
| `from`              | `address`           |                                        |
| `to`                | `address` or `null` | `null` for contract creation           |
| `cumulativeGasUsed` | `quantity`          |                                        |
| `gasUsed`           | `quantity`          | Gas consumed by this transaction       |
| `contractAddress`   | `address` or `null` | Address of created contract, or `null` |
| `type`              | `quantity`          | Transaction type                       |
| `status`            | `quantity`          | `0x1` success, `0x0` revert            |
| `logsBloom`         | `bytes`             | 256-byte bloom filter                  |
| `logs`              | [`Log`](#log)`[]`   | Array of log entries (see below)       |
| `effectiveGasPrice` | `quantity`          | Actual gas price paid                  |

**Log** (entries within `logs` arrays):

| Field              | Type       | Notes                                                                     |
| ------------------ | ---------- | ------------------------------------------------------------------------- |
| `address`          | `address`  | Contract that emitted the event                                           |
| `topics`           | `hash[]`   | Up to 4 indexed event parameters; `topics[0]` is the event signature hash |
| `data`             | `bytes`    | ABI-encoded non-indexed parameters                                        |
| `blockNumber`      | `quantity` |                                                                           |
| `transactionHash`  | `hash`     |                                                                           |
| `transactionIndex` | `quantity` |                                                                           |
| `blockHash`        | `hash`     |                                                                           |
| `logIndex`         | `quantity` | Position within the block's logs                                          |

**Block** (returned by [`eth_getBlockByNumber`](#eth_getblockbynumber) and similar):

| Field              | Type                                          | Notes                                                                                                |
| ------------------ | --------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `hash`             | `hash`                                        |                                                                                                      |
| `parentHash`       | `hash`                                        |                                                                                                      |
| `sha3Uncles`       | `hash`                                        | Always the empty uncles hash (no uncle blocks)                                                       |
| `miner`            | `address`                                     | Always `0x0000...0000`                                                                               |
| `stateRoot`        | `hash`                                        |                                                                                                      |
| `transactionsRoot` | `hash`                                        |                                                                                                      |
| `receiptsRoot`     | `hash`                                        |                                                                                                      |
| `logsBloom`        | `bytes`                                       | 256-byte bloom filter                                                                                |
| `difficulty`       | `quantity`                                    | Always `0x0`                                                                                         |
| `number`           | `quantity`                                    | Block number                                                                                         |
| `gasLimit`         | `quantity`                                    | Per-block execution gas                                                                              |
| `gasUsed`          | `quantity`                                    |                                                                                                      |
| `baseFeePerGas`    | `quantity`                                    | EIP-1559 base fee                                                                                    |
| `timestamp`        | `quantity`                                    | Unix timestamp                                                                                       |
| `size`             | `quantity`                                    | RLP-encoded block size in bytes                                                                      |
| `extraData`        | `bytes`                                       |                                                                                                      |
| `mixHash`          | `hash`                                        | Always zero                                                                                          |
| `nonce`            | `bytes`                                       | Always zero (8 bytes)                                                                                |
| `transactions`     | `hash[]` or [`Transaction`](#transaction)`[]` | Tx hashes if `fullTxObjects=false`, full objects if `true`. Omitted entirely for `eth_getHeaderBy*`. |

**SomniaLedgerBlock** (returned by [`somnia_getBlockByNumber`](#somnia_getblockbynumber) / [`somnia_getBlockByHash`](#somnia_getblockbyhash)):

| Field                      | Type                                            | Notes                      |
| -------------------------- | ----------------------------------------------- | -------------------------- |
| `consensus_block`          | [`SomniaConsensusBlock`](#somniaconsensusblock) | Consensus-layer block data |
| `execution_block`          | [`SomniaExecutionBlock`](#somniaexecutionblock) | Execution-layer block data |
| `parent_ledger_block_hash` | `hash`                                          | Parent ledger-block hash   |
| `ledger_block_hash`        | `hash`                                          | Ledger-block hash          |

**SomniaConsensusBlock**:

| Field                         | Type                                                  | Notes                          |
| ----------------------------- | ----------------------------------------------------- | ------------------------------ |
| `block_number`                | `quantity`                                            | Ledger block number            |
| `timestamp`                   | `quantity`                                            | Unix timestamp in milliseconds |
| `data_chain_blocks`           | `hash[]`                                              | Referenced data-chain blocks   |
| `delayed_ledger_block_number` | `quantity`                                            | Delayed ledger block number    |
| `delayed_ledger_block_hash`   | `hash`                                                | Delayed ledger block hash      |
| `parent_consensus_block_hash` | `hash`                                                | Parent consensus-block hash    |
| `proposer_address`            | `address`                                             | Block proposer                 |
| `block_resources`             | [`SomniaFrontendResources`](#somniafrontendresources) | Frontend resource accounting   |
| `consensus_block_hash`        | `hash`                                                | Consensus-block hash           |

**SomniaExecutionBlock**:

| Field                         | Type       | Notes                                         |
| ----------------------------- | ---------- | --------------------------------------------- |
| `transaction_ids_hash`        | `hash`     | Hash of transaction ids                       |
| `receipts_hash`               | `hash`     | Hash of receipts                              |
| `execution_gas_limit`         | `quantity` | Execution gas limit                           |
| `execution_gas_used`          | `quantity` | Execution gas used                            |
| `execution_state_snapshot`    | `hash`     | State snapshot hash                           |
| `state_snapshot_block_number` | `quantity` | Block number of the state snapshot            |
| `operation_sequence_hash`     | `quantity` | Operation sequence hash encoded as a quantity |

**SomniaFrontendResources**:

| Field                | Type       | Notes                                      |
| -------------------- | ---------- | ------------------------------------------ |
| `validation_gas`     | `quantity` | Validation gas used by the consensus block |
| `compressed_bytes`   | `quantity` | Compressed frontend payload size           |
| `uncompressed_bytes` | `quantity` | Uncompressed frontend payload size         |

**ReactivitySubscription** (returned by [`somnia_reactivityGetSubscriptionInfo`](#somnia_reactivitygetsubscriptioninfo) / [`somnia_reactivityGetSubscriptions`](#somnia_reactivitygetsubscriptions)):

| Field                       | Type       | Notes                                            |
| --------------------------- | ---------- | ------------------------------------------------ |
| `id`                        | `quantity` | Subscription ID                                  |
| `topics`                    | `data[4]`  | Log topics to match (up to 4)                    |
| `origin`                    | `address`  | Origin filter                                    |
| `caller`                    | `address`  | Caller filter (reserved, currently unused)       |
| `emitter`                   | `address`  | Contract address that emits the matched log      |
| `owner`                     | `address`  | Subscription owner address                       |
| `handler_contract_address`  | `address`  | Contract called when the subscription triggers   |
| `handler_function_selector` | `data`     | 4-byte function selector on the handler contract |
| `gas_limit`                 | `quantity` | Gas limit for the reactive callback              |
| `priority_fee_per_gas`      | `quantity` | Priority fee per gas for the callback            |
| `max_fee_per_gas`           | `quantity` | Max fee per gas for the callback                 |

### Subscription Schemas

All [`eth_subscribe`](#eth_subscribe) notifications use this envelope:

```json
{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x1","result":...}}
```

**`newHeads`**

* **Params:** none
* **Event result:** [`NewHead`](#newhead)

**`logs`**

**Params:**

| Field     | Type                     | Notes                                                                                              |
| --------- | ------------------------ | -------------------------------------------------------------------------------------------------- |
| `address` | `address` or `address[]` | Match logs from one or more contracts                                                              |
| `topics`  | `array` or `null`        | Same topic-position semantics as [`eth_getLogs`](#eth_getlogs) / [`eth_newFilter`](#eth_newfilter) |

* **Event result:** [`Log`](#log)

**`somnia_finishedTransactions`**

**Params:**

| Field                           | Type                  | Notes                                                |
| ------------------------------- | --------------------- | ---------------------------------------------------- |
| `transaction_senders`           | `address[]` or `null` | Only emit events for transactions from these senders |
| `subscribe_to_all_transactions` | `bool` or `null`      | Emit events for all finished transactions            |

**Event result:**

| Field                | Type           | Notes                                |
| -------------------- | -------------- | ------------------------------------ |
| `transaction_id`     | `hash`         | Transaction hash                     |
| `transaction_status` | `integer enum` | Internal `TransactionStatus` value   |
| `sender_address`     | `address`      | Transaction sender                   |
| `transaction_nonce`  | `integer`      | Sender nonce used by the transaction |

**`somnia_finishedBlocks`**

* **Params:** none

**Event result:**

| Field               | Type       | Notes                               |
| ------------------- | ---------- | ----------------------------------- |
| `blockNumber`       | `quantity` | Finished block number               |
| `totalTransactions` | `quantity` | Number of transactions in the block |

**`somnia_watch`**

**Params:**

| Field               | Type                     | Notes                                                                                                                                                         |
| ------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`           | `address` or `address[]` | Log emitter filter                                                                                                                                            |
| `topics`            | `array` or `null`        | Same topic-position semantics as [`eth_getLogs`](#eth_getlogs) / [`eth_newFilter`](#eth_newfilter)                                                            |
| `eth_calls`         | `array`                  | Each item is either a [`TransactionRequest`](#transactionrequest), or \[[`TransactionRequest`](#transactionrequest), [`StateOverrideSet`](#stateoverrideset)] |
| `context`           | `string` or `string[]`   | Extra values appended to each simulated call's calldata. Supported selectors: `address`, `data`, `topic1`, `topic2`, `topic3`, `topic4`                       |
| `push_changes_only` | `bool` or `null`         | Only emit when `simulationResults` change from the previous event for that subscription                                                                       |

**Event result:**

| Field               | Type      | Notes                                           |
| ------------------- | --------- | ----------------------------------------------- |
| `address`           | `address` | Log emitter                                     |
| `topics`            | `hash[]`  | Emitted topics                                  |
| `data`              | `bytes`   | Raw log data                                    |
| `simulationResults` | `bytes[]` | Return data from each configured simulated call |

**NewHead** (event payload for `eth_subscribe("newHeads")`):

| Field              | Type       | Notes                        |
| ------------------ | ---------- | ---------------------------- |
| `hash`             | `hash`     |                              |
| `parentHash`       | `hash`     |                              |
| `sha3Uncles`       | `hash`     | Always the empty uncles hash |
| `miner`            | `address`  | Always `0x0000...0000`       |
| `stateRoot`        | `hash`     |                              |
| `transactionsRoot` | `hash`     |                              |
| `receiptsRoot`     | `hash`     |                              |
| `logsBloom`        | `bytes`    | 256-byte bloom filter        |
| `difficulty`       | `quantity` | Always `0x0`                 |
| `number`           | `quantity` | Block number                 |
| `gasLimit`         | `quantity` |                              |
| `gasUsed`          | `quantity` |                              |
| `baseFeePerGas`    | `quantity` |                              |
| `timestamp`        | `quantity` | Unix timestamp               |
| `extraData`        | `bytes`    |                              |


# Somnia Reactivity

Reactivity lets applications respond to on-chain events without running a polling loop. It's one of Somnia's standout features.

Somnia provides reactivity in two complementary forms:

* [**On-chain reactivity**](/developer/reactivity/reactivity-onchain) stores persistent event subscriptions in chain state and then executes contract handlers when matching events are committed. The reaction is part of chain execution.
* [**Off-chain reactivity**](/developer/reactivity/reactivity-offchain) is a WebSocket feature served by the node API. A client subscribes once, receives pushed events, and can ask the node to run read-only simulations for each event so that derived state arrives in the same message — minimising latency. The reaction stays in the client.

## Which one to use

Use on-chain reactivity when the reaction should be part of chain execution, i.e. a guaranteed, public effect. For example: automatic token rebalancing, scheduled settlement, or a callback that must fire even if the original owner is offline.

Use off-chain reactivity when the reaction should stay in your app. For example: a UI that updates when an event lands, an indexer that writes to a database, or a bot that watches a pool. Off-chain subscriptions are more flexible and don't require spending SOMI.

The two kinds of reactivity are independent. You can use both at once.


# On-chain Reactivity

On-chain reactivity lets a smart contract instantly react to events in the same block, without anyone sending an event handling transaction. A user creates a subscription through a call to a precompile, and the subscription persists in chain state. When an event log or system event matches the subscription's filter, validators include a synthetic transaction in the block that calls the subscription's handler contract. The creator of the subscription pays the gas.

This is a powerful feature. As far as we know, no other EVM can do this, and when combined with Somnia's 100 ms blocks, on-chain reactivity enables a class of applications that other chains can only approximate with off-chain infrastructure.

For client-side reactions to events, see [off-chain reactivity](/developer/reactivity/reactivity-offchain).

## Example use cases

* Trigger a callback every time a specific ERC-20 Transfer event is emitted, regardless of who sent the transaction.
* Run scheduled upkeep at every epoch boundary.
* Move funds out of escrow at a predetermined time.
* Build an automated liquidation bot as a contract rather than an off-chain service.
* Forward DEX events to a settlement contract without the trader paying the forwarding cost.

## Quick start

1. Install the Solidity reactivity package (see [Solidity package](#solidity-package) below).

   ```bash
   npm install @somnia-chain/reactivity-contracts
   ```
2. Write a handler contract. Inherit from `SomniaEventHandler` and override the protected `_onEvent` hook:

   ```solidity
   import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";

   contract MyHandler is SomniaEventHandler {
       function _onEvent(
           address emitter,
           bytes32[] calldata eventTopics,
           bytes calldata data
       ) internal override {
           // react to the event here
       }
   }
   ```
3. Fill in a `SomniaExtensions.SubscriptionFilter` and a `SomniaExtensions.SubscriptionOptions`. See [Solidity package](#solidity-package) for the struct shapes, and [Filter semantics](#filter-semantics) for what the filter fields mean.
4. Deploy the contract, and ensure the contract account holds at least 32 [SOMI](/developer/network-info/somi-coin) (see [Minimum balance](#minimum-balance)).
5. Create the subscription by calling `SomniaExtensions.subscribe(address(this), filter, options)`. The call returns the subscription ID, and the calling contract becomes the subscription owner. For one-shot subscriptions at a specific block, epoch, or timestamp, use `scheduleSubscriptionAtBlock` / `scheduleSubscriptionAtEpoch` / `scheduleSubscriptionAtTimestamp` instead.
6. The subscription runs until you remove it or the owner account runs out of SOMI (see [Automatic removal](#automatic-removal)).
7. Query subscriptions off-chain via the [`somnia_reactivityGetSubscriptionInfo`](#rpc-methods) or [`somnia_reactivityGetSubscriptions`](#rpc-methods) JSON-RPC methods, or on-chain via `SomniaExtensions.getSubscriptionInfo(id)`.
8. Remove the subscription by calling `SomniaExtensions.unsubscribe(id)` from the owning contract.

These steps assume the contract itself is the subscription owner, which is what the `SomniaExtensions` library is designed for. To own the subscription directly from an EOA, see the [TypeScript package](#typescript-package), or for the raw ABI-level surface, see [Precompile](#precompile) and [`SubscriptionData`](#subscriptiondata) in the Reference section.

## How it works

Subscriptions are created by calling the **Somnia reactivity precompile** at address `0x0100`.

When a transaction is executed, every event log it emits is checked against all active subscriptions. Matching subscriptions are placed on a per-block priority queue, ordered by each subscription's `priorityFeePerGas`.

After the block's user transactions have executed, the node drains the queue and executes each pending handler as a synthetic transaction.

These reactive transactions appear in the block's reactivity output alongside the ordinary transactions. They are visible on the block explorer, have normal receipts, emit normal logs, and pay normal gas.

Each reactive transaction has:

* `from` equal to the subscription owner
* `to` equal to the subscription's handler contract
* `msg.sender` inside the call equal to `0x0100`
* Calldata equal to `handlerFunctionSelector` followed by the ABI-encoded tuple `(address emitter, bytes32[] eventTopics, bytes data)` from the matching log
* A block-unique nonce derived from the block number and position in the reactivity queue (so that reactive transactions don't conflict with the owner's regular nonces)

If the handler reverts, runs out of gas, or the owner can't pay, the reactive transaction fails in the ordinary way. A failure doesn't itself remove the subscription, but see [Automatic removal](#automatic-removal) below.

Logs emitted by reactive transactions are checked against subscriptions the same way as user transactions, and any matches are immediately added to the block's reactivity queue. Note that this means that *a subscription can provoke a recursive explosion*, unstoppably draining the owner's balance.

## Reference

### Precompile

* Address: `0x0100`
* Solidity constant: `SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS`

The precompile exposes three functions. The function selectors are keccak256 of the signatures below.

| Function              | Signature                                                                                       |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| `subscribe`           | `subscribe((bytes32[4],address,address,address,address,bytes4,uint64,uint64,uint64,bool,bool))` |
| `unsubscribe`         | `unsubscribe(uint256)`                                                                          |
| `getSubscriptionInfo` | `getSubscriptionInfo(uint256)`                                                                  |

Note that `subscribe` takes a single tuple parameter (the `SubscriptionData` struct), not eleven positional parameters — the outer parentheses are significant.

### `SubscriptionData`

The tuple passed to `subscribe`, in order:

| Field                     | Type         | Purpose                                                                                                                                                       |
| ------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `eventTopics`             | `bytes32[4]` | Topic filters. A zero value means "match any". (See [Filter semantics](#filter-semantics).)                                                                   |
| `origin`                  | `address`    | Matches logs from transactions sent by this address. Zero means "match any".                                                                                  |
| `caller`                  | `address`    | Reserved. Pass `0x0`.                                                                                                                                         |
| `emitter`                 | `address`    | Matches logs emitted from this contract address. Zero means "match any". Set to `0x0100` to subscribe to system events (see [System events](#system-events)). |
| `handlerContractAddress`  | `address`    | The contract called when a match occurs. Must be non-zero.                                                                                                    |
| `handlerFunctionSelector` | `bytes4`     | The 4-byte function selector invoked on the handler contract.                                                                                                 |
| `priorityFeePerGas`       | `uint64`     | Tip to validators, in wei. Determines order in the reactivity queue (see [Limits](#limits)).                                                                  |
| `maxFeePerGas`            | `uint64`     | Maximum total fee per gas, in wei. If set to zero, the protocol chooses a maximum.                                                                            |
| `gasLimit`                | `uint64`     | Maximum gas the handler may consume per invocation. Must be non-zero, maximum of 200,000,000.                                                                 |
| `isGuaranteed`            | `bool`       | Reserved. Pass `false`.                                                                                                                                       |
| `isCoalesced`             | `bool`       | Reserved. Pass `false`.                                                                                                                                       |

### Validation at subscription creation

`subscribe` reverts if:

* `handlerContractAddress` is zero.
* None of `eventTopics[0..3]`, `origin`, or `emitter` are set. At least one filter is required — wildcard subscriptions that match every log on the chain are not allowed.
* `gasLimit` is zero or above `max_reactivity_handler_gas_limit` (200 million gas).
* `priorityFeePerGas` is above the gas price cap.
* `priorityFeePerGas + baseFee` exceeds `maxFeePerGas`.
* The owner does not hold at least 32 SOMI.

If `maxFeePerGas` is passed as zero, the protocol replaces it with the maximum gas price before storing the subscription. Use this default with caution: if the gas price spikes, your subscription may become unprofitable to execute and fail to fire.

### Filter semantics

A subscription matches an event log when every non-zero filter field equals the corresponding field on the log. Zero acts as a wildcard. The filter fields are: `origin`, `emitter` and `eventTopics`. Topic filters are positional: `eventTopics[0]` matches against the log's first topic (the event signature), `eventTopics[1]` against the second, and so on.

At least one field must be a non-wildcard. An all-zero subscription is rejected at creation.

There is no "OR" support on a single field — a filter value either matches a specific hash or matches everything. If you need disjunctive matching, create multiple subscriptions.

### System events

Instead of an event log, you can subscribe to one of three system events, by setting the following fields in the subscription:

| Event      | `emitter` | `eventTopics[0]`                        | `eventTopics[1]`          | When it fires                                                                                                                   |
| ---------- | --------- | --------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| Block tick | `0x100`   | `keccak256("BlockTick(uint64)")`        | block number              | Every block (i.e. ten times a second) if `eventTopics[1]` is zero, or otherwise that specific block                             |
| Epoch tick | `0x100`   | `keccak256("EpochTick(uint64,uint64)")` | epoch number              | At the end of every epoch (i.e. every five minutes) if `eventTopics[1]` is zero, or otherwise at the end of that specific epoch |
| Schedule   | `0x100`   | `keccak256("Schedule(uint256)")`        | timestamp in milliseconds | Once, in the first block whose timestamp is ≥ `eventTopics[1]`                                                                  |

`Schedule`, block-specific `BlockTick`, and epoch-specific `EpochTick` are **one-shot**: the subscription is automatically removed after it fires. Every-block and every-epoch subscriptions are recurring.

Scheduled timestamps must be strictly greater than the current block timestamp at the moment the subscription is created.

### Handler execution context

Inside a handler invocation:

* `msg.sender == 0x0100`
* `tx.origin == subscription owner address`
* `msg.value == 0` — reactive calls never carry value
* Calldata is `handlerFunctionSelector ++ abi.encode(address emitter, bytes32[] eventTopics, bytes data)` where the arguments are from the log that matched

The handler runs with the subscription's `gasLimit`. If it exceeds that limit the transaction reverts and the owner pays for the attempt.

### Minimum balance

The subscription owner must hold at least **32 SOMI** at the moment `subscribe` is called. This is a sybil-resistance barrier against spammy subscription creation.

The 32 SOMI is **not** an escrow and **not** consumed. It sits in the owner's regular balance. Gas for each handler invocation is paid from the owner's balance at the normal per-tx rate.

Note that the 32 SOMI threshold is only enforced at creation. A subscription can continue to fire after its owner's balance has dropped below 32 SOMI, as long as the owner can still pay the gas for each individual handler invocation. See [Automatic removal](#automatic-removal) for what happens when the owner can't pay.

### Gas costs

Creating a subscription (by calling `subscribe` on the reactivity precompile) costs `210,000` gas, charged to the transaction sender as usual.

Handler invocations are charged to the subscription owner. The price per gas is the block's current gas price plus the subscription's `priorityFeePerGas`. The handler can consume at most the subscription's `gasLimit`.

### Limits

There are per-block limits on total execution gas, reactivity-specific execution gas and the number of reactivity transactions. When any of these is hit, the remaining matches stay in the reactivity queue and are attempted again in the next block.

Matches are sorted by `priorityFeePerGas`, so those that pay lower fees may be indefinitely deferred.

There are also limits on the size of the reactivity queue itself. When these are hit, the lowest-priority matches are evicted.

### Automatic removal

A subscription is removed automatically when:

* It's a one-shot scheduled subscription and it has just fired.
* It was evicted from a full reactivity queue and was also one-shot.
* The owner's balance doesn't cover the subscription's `gasLimit` when it fires. i.e. It's less than `(execution price per gas + priorityFeePerGas) * gasLimit`.

A subscription is **not** removed when:

* The handler simply reverts.
* The handler exceeds `gasLimit` during execution.
* The owner's balance drops below 32 SOMI but is still enough to pay for individual handler invocations.
* The handler execution is deferred due to queue limits or low priority fee.

### Emitted events

`subscribe` and `unsubscribe` emit these logs from `0x0100`:

* `SubscriptionCreated(uint256 id, address owner, SubscriptionData data)` — data is the full tuple, ABI-encoded inline.
* `SubscriptionRemoved(uint256 id, address owner)`

These are the logs you should filter against off-chain if you want to track subscription lifecycle.

### RPC methods

The node exposes two reactivity-specific RPC methods:

* [**`somnia_reactivityGetSubscriptionInfo`**](/developer/json-rpc-api#somnia_reactivitygetsubscriptioninfo) — Takes a subscription ID (as a quantity string, e.g. `"23"` or `"0x15f019"`) and returns the details of the subscription. Can also take an array of such IDs. Response fields mirror `SubscriptionData` plus `id` and `owner`, with names in `snake_case` (`handler_contract_address`, `handler_function_selector`, `priority_fee_per_gas`, `max_fee_per_gas`, `gas_limit`).
* [**`somnia_reactivityGetSubscriptions`**](/developer/json-rpc-api#somnia_reactivitygetsubscriptions) — Takes an owner address, returns an array of subscription objects currently owned by that address. Each entry has the same shape as the `somnia_reactivityGetSubscriptionInfo` response.

Both are `eth_call`-style JSON-RPC methods available on any Somnia node.

### Solidity package

The [`@somnia-chain/reactivity-contracts`](https://www.npmjs.com/package/@somnia-chain/reactivity-contracts) npm package is the idiomatic Solidity wrapper around the precompile. Install it with `npm install @somnia-chain/reactivity-contracts` (or your toolchain's equivalent). It exposes four pieces:

* `ISomniaReactivityPrecompile` — the typed interface for the precompile at `0x0100` — the `SubscriptionData` struct and the `BlockTick`, `EpochTick`, `Schedule` system-event signatures (so topic hashes are `<Event>.selector`), plus the raw `subscribe`/`unsubscribe`/ `getSubscriptionInfo` functions.
* `ISomniaEventHandler` — the callback interface that `SomniaEventHandler` implements. Used as the canonical value for `handlerFunctionSelector` (`ISomniaEventHandler.onEvent.selector`).
* `SomniaEventHandler` — an abstract base contract that implements the "only the precompile may call me" check and the ERC-165 plumbing, leaving you to override a protected `_onEvent`.
* `SomniaExtensions` — the ergonomic helper library most user code will call into. Constants:

  | Constant                               | Value         |
  | -------------------------------------- | ------------- |
  | `SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS` | `0x0100`      |
  | `SUBSCRIPTION_OWNER_MINIMUM_BALANCE`   | `32 ether`    |
  | `MINIMUM_BASE_FEE_PER_GAS`             | `6 gwei`      |
  | `MAXIMUM_HANDLER_GAS_LIMIT`            | `200_000_000` |

  Helper structs:

  ```solidity
  struct SubscriptionFilter {
      bytes32[4] eventTopics;
      address origin;
      address emitter;
  }

  struct SubscriptionOptions {
      uint64 priorityFeePerGas;
      uint64 maxFeePerGas;
      uint64 gasLimit;
  }
  ```

  Functions (all `internal`, i.e. called from a Solidity contract, not an EOA):

  | Function                                                             | Purpose                                                                                                                                                                                                                                                                                                                                                                                              |
  | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `subscribe(handler, filter, options)`                                | Create a subscription with the given filter. Checks `handler != 0`, at least one filter is set, `gasLimit` is within range, `maxFeePerGas` is consistent with `priorityFeePerGas + MINIMUM_BASE_FEE_PER_GAS`, and the caller's balance ≥ `SUBSCRIPTION_OWNER_MINIMUM_BALANCE` — reverts with a typed error if any fails. Hardcodes `handlerFunctionSelector = ISomniaEventHandler.onEvent.selector`. |
  | `scheduleSubscriptionAtTimestamp(handler, timestampMillis, options)` | One-shot Schedule subscription firing when the block timestamp (ms) first reaches `timestampMillis`. Reverts if the timestamp is in the past.                                                                                                                                                                                                                                                        |
  | `scheduleSubscriptionAtBlock(handler, blockNumber, options)`         | One-shot BlockTick subscription firing on the specific block. Reverts if the block number is in the past.                                                                                                                                                                                                                                                                                            |
  | `scheduleSubscriptionAtEpoch(handler, epochNumber, options)`         | One-shot EpochTick subscription firing at the end of the specific epoch.                                                                                                                                                                                                                                                                                                                             |
  | `unsubscribe(subscriptionId)`                                        | Cancel a subscription owned by the caller. (Uses a low-level `.call` because a typed call would revert before reaching the precompile.)                                                                                                                                                                                                                                                              |
  | `getSubscriptionInfo(subscriptionId)`                                | Read a subscription's stored parameters and owner.                                                                                                                                                                                                                                                                                                                                                   |

  The `schedule*` helpers only cover **one-shot** subscriptions at a specific block/epoch/timestamp. For **recurring** every-block or every-epoch subscriptions, build a `SubscriptionFilter` with `eventTopics[1] = 0` and call `subscribe` directly — see the [Example](#example).

### TypeScript package

The [`@somnia-chain/reactivity`](https://www.npmjs.com/package/@somnia-chain/reactivity) package wraps the precompile with Viem for use from TypeScript scripts and Node.js services. It's intended for off-chain orchestration: creating a subscription owned by an externally-owned account, inspecting subscription state, or cancelling a subscription from a script.

| Method                                                      | Purpose                                                                                                                                                                  |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `createSoliditySubscription(data)`                          | Broadcast a `subscribe` transaction with the given `SoliditySubscriptionData`.                                                                                           |
| `cancelSoliditySubscription(id)`                            | Broadcast an `unsubscribe` transaction.                                                                                                                                  |
| `getSubscriptionInfo(id)`                                   | Read a subscription via `eth_call`.                                                                                                                                      |
| `createOnchainBlockTickSubscription({ blockNumber?, ... })` | Convenience wrapper for `BlockTick` subscriptions. Omit `blockNumber` for a recurring every-block subscription; pass it for a one-shot subscription on a specific block. |
| `scheduleOnchainCronJob({ timestampMs, ... })`              | Convenience wrapper for one-shot `Schedule` subscriptions.                                                                                                               |

If the subscription is owned by a Solidity contract — the pattern shown in [Quick start](#quick-start) and [Minimal example](#minimal-example) — use `SomniaExtensions` from the contract directly. The TypeScript SDK is only useful when the subscription owner is an externally-owned account, or when you want to inspect or cancel a subscription from off-chain code. (The same package also drives [off-chain reactivity](/developer/reactivity/reactivity-offchain) over WebSocket.)

## Minimal example

This contract subscribes itself to every block and counts them. It's built on the [`@somnia-chain/reactivity-contracts`](https://www.npmjs.com/package/@somnia-chain/reactivity-contracts) package (see [Solidity package](#solidity-package) above).

```solidity
pragma solidity 0.8.30;

import {
    SomniaEventHandler
} from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
import {
    ISomniaReactivityPrecompile
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/ISomniaReactivityPrecompile.sol";
import {
    SomniaExtensions
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/SomniaExtensions.sol";

/// @notice Counts the blocks it has been invoked on, starting as soon as it
///         is deployed.
contract CounterHandler is SomniaEventHandler {
    uint256 public count;
    uint256 public subscriptionId;

    /// @dev The contract itself owns the subscription and pays gas for each
    ///      invocation, so it must be funded with ≥ 32 SOMI at deploy time.
    constructor(uint64 gasLimit) payable {
        // The scheduleSubscriptionAt* helpers are all one-shot. For a
        // recurring every-block subscription, we build the BlockTick filter
        // ourselves with eventTopics[1] = 0 and call subscribe() directly.
        SomniaExtensions.SubscriptionFilter memory filter =
            SomniaExtensions.SubscriptionFilter({
                eventTopics: [
                    ISomniaReactivityPrecompile.BlockTick.selector,
                    bytes32(0), bytes32(0), bytes32(0)
                ],
                origin: address(0),
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS
            });

        SomniaExtensions.SubscriptionOptions memory options =
            SomniaExtensions.SubscriptionOptions({
                priorityFeePerGas: 1,
                maxFeePerGas: 0,
                gasLimit: gasLimit
            });

        subscriptionId = SomniaExtensions.subscribe(
            address(this), filter, options
        );
    }

    /// @dev Called by `SomniaEventHandler.onEvent` after it has verified the
    ///      caller is the precompile. Arguments are the matching log's
    ///      emitter, topics, and non-indexed data.
    function _onEvent(
        address /* emitter */,
        bytes32[] calldata /* eventTopics */,
        bytes calldata /* data */
    ) internal override {
        count += 1;
    }

    function stop() external {
        SomniaExtensions.unsubscribe(subscriptionId);
    }
}
```

Note that **the contract is the subscription owner**, not the deployer — the constructor calls `subscribe` from the contract's own address. If you don't call `stop()`, the subscription will keep running until the contract's balance can't cover `gasLimit`.

### Deploying and driving the example with Foundry

Deploy the handler with [Foundry](/developer/development-frameworks/deploy-with-foundry), funding the contract with 33 SOMI (just above the 32 SOMI minimum balance) and passing a 2,000,000 gas limit to the constructor:

```bash
export RPC_URL=<your Somnia RPC endpoint>
export PK=<your funded private key>

forge create \
    --rpc-url "$RPC_URL" \
    --private-key "$PK" \
    --broadcast \
    --value 33ether \
    src/CounterHandler.sol:CounterHandler \
    --constructor-args 2000000
```

`forge create` prints the deployed address. Read the counter — it should be advancing once per block:

```bash
export COUNTER=<address printed by forge create>

cast call --rpc-url "$RPC_URL" "$COUNTER" 'count()(uint256)'
cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)'
```

Inspect the subscription itself through the precompile's RPC:

```bash
SUB_ID=$(cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)' \
    | awk '{print $1}')
cast rpc --rpc-url "$RPC_URL" \
    somnia_reactivityGetSubscriptionInfo "$(cast to-hex "$SUB_ID")"
```

Stop the handler by calling `stop()`, which unsubscribes via the library:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" "$COUNTER" 'stop()'
```

After that, `count()` stops advancing and `somnia_reactivityGetSubscriptionInfo` on the old ID returns an empty result.

## Development advice

### Designing

* **Handlers are separate transactions, not callbacks.** A triggering transaction commits first; matching handlers are later executed as synthetic transactions, usually in the same block.
* **System events are just synthetic logs.** `BlockTick`, `EpochTick` and `Schedule` come from fabricated `0x100` logs at block end.
* **Avoid recursive explosions.** Make sure that your subscription's handler can't emit an event which re-triggers your subscription.

### Implementing

* **Use a dedicated account as the subscription owner.** An ill-judged subscription can quietly drain an account. A *recursive* subscription, that reacts to its own handler, can do the same in moments. Consider limiting the blast radius by funding a dedicated account.
* **Budget for gas reserves in subscription gas limits.** Somnia's storage operations require a `1,000,000` gas reserve in case execution requires a disk read or a new storage allocation. Neglecting this reserve is a common cause of out-of-gas errors. See [Somnia gas differences to Ethereum](/developer/deployment-and-production/somnia-gas-differences-to-ethereum) for the table of costs.
* **Recreate subscriptions after redeploying.** Subscriptions point at concrete contract addresses, so redeploying a handler requires creating a new subscription.

### Why isn't your handler getting invoked?

Common problems:

1. **Subscription isn't active.** A subscription is automatically removed if the owner's balance can't cover `gasLimit` when it fires. Check `somnia_reactivityGetSubscriptions` or `somnia_reactivityGetSubscriptionInfo`.
2. Implementation of `SomniaEventHandler` interface is invalid. See [Solidity package](#solidity-package) for the expected shape.
3. **Logs aren't matching.** One gotcha: only successful transaction logs trigger reactivity. If a transaction reverts, its logs won't be matched against subscriptions.
4. **Subscription is pointing at wrong handler.** Subscriptions point at concrete contract addresses, so redeploying a handler requires creating a new subscription.
5. **`maxFeePerGas` is too low.** Note that `maxFeePerGas` and `priorityFeePerGas` are denominated in wei, but Somnia's minimum base fee is 6 [nanoSomi](/developer/network-info/somi-coin), i.e. 6 gWei, i.e. 6,000,000,000 wei. A common mistake is to specify a fee in wei thinking that the unit is gWei.
6. **`priorityFeePerGas` is too low for the current queue pressure.** This is rare, but when reactivity execution hits its [limits](#limits), low-priority-fee matches are deferred or even evicted. A schedule that fires late or not at all can mean queue pressure rather than a bug.
7. **`gasLimit` is too low.** This is very common: Somnia operates on a different gas model to Ethereum, and some Somnia gas costs, measured in units of gas, are much higher than Ethereum's. Unlike the previous problems, this can be identified in the chain's history — see below.

### Debugging reactive transactions

* **Reactive transactions can be recognised by their nonces.** They're legacy-typed (`type == 0x0`) and their nonces pack the block number in the high bytes and the queue position (0-indexed) in the low three bytes. E.g. the first reactive in block `0xabcdef` has nonce `0xabcdef000000`, the second `0xabcdef000001`, and so on. Wallets and explorers don't yet render this specially; the nonce pattern is the quickest way to spot reactive txs in `eth_getBlockByNumber(..., true)` output.
* **Reactive transactions have hashes and receipts, but fake signatures.** They're encoded into history as legacy-style transactions with placeholder signature fields. They aren't actually signed by their owner.


# Off-chain Reactivity

Off-chain reactivity works through a WebSocket subscription served by the node API. A client subscribes to matching logs and receives pushed events as they land in blocks. Each event notification can optionally include the results of read-only simulations run against the same block, so that an application can react to both the event and its derived state at once. There's no additional latency, and no risk of inconsistency.

The subscription is served by the RPC node the client is connected to. It is not stored on chain, does not cost [SOMI](/developer/network-info/somi-coin), and disappears when the client disconnects or unsubscribes.

For reactions that are part of chain execution rather than client-side, see [on-chain reactivity](/developer/reactivity/reactivity-onchain).

## Example use cases

* Drive a UI that updates the moment a token is transferred, without polling balances.
* Pipe live swap and liquidity events into an indexer or analytics database.
* Attach a `balanceOf` call to every matching Transfer log, so downstream consumers see both the event and the resulting balance atomically.
* Build a bot that watches a DEX pool and only receives a message when the pool state has actually changed.

## Quick start

1. Open a WebSocket connection to a Somnia node's API endpoint.
2. Call [`eth_subscribe`](/developer/json-rpc-api#eth_subscribe) using the method `somnia_watch` and a parameters object. The node returns a subscription ID.
3. Read pushed `eth_subscription` notifications on the same connection. The `result` field of each notification is the event payload.
4. To stop, call [`eth_unsubscribe`](/developer/json-rpc-api#eth_unsubscribe) with the subscription ID, or simply close the connection.

Minimal JSON-RPC sequence:

```json
// -> request
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": [
    "somnia_watch",
    {
      "eth_calls": []
    }
  ]
}

// <- response
{ "jsonrpc": "2.0", "id": 1, "result": "0x1234..." }

// <- notifications
{
  "jsonrpc": "2.0",
  "method": "eth_subscription",
  "params": {
    "subscription": "0x1234...",
    "result": {
      "address": "0x...",
      "topics": ["0x...", ...],
      "data": "0x...",
      "simulationResults": []
    }
  }
}
```

## Using from TypeScript

TypeScript and JavaScript apps can use the [`@somnia-chain/reactivity`](https://www.npmjs.com/package/@somnia-chain/reactivity) package. It wraps the WebSocket protocol with Viem and manages subscription/unsubscription, so you don't have to drive `eth_subscribe` directly. (The same package can also create and manage [on-chain reactivity](/developer/reactivity/reactivity-onchain#typescript-package) subscriptions owned by an EOA.)

```bash
npm install @somnia-chain/reactivity viem
```

```typescript
import { createPublicClient, defineChain, webSocket } from 'viem';
import { SDK } from '@somnia-chain/reactivity';

const chain = defineChain({ /* see viem docs */ });

const sdk = new SDK({
  public: createPublicClient({ chain, transport: webSocket() }),
});

const subscription = await sdk.watch({
  eventContractSources: ['0x046EDe9564A72571df6F5e44d0405360c0f4dCab'],
  topicOverrides: ['0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'],
  ethCalls: [],
  onData: (data) => {
    const event = data.result;
    console.log(event);
  },
});

if (subscription instanceof Error) {
  throw subscription;
}

// later
await subscription.unsubscribe();
```

The SDK requires the public client to use a WebSocket transport. You don't need a wallet client — off-chain subscriptions don't sign anything.

`sdk.subscribe` accepts a `WebsocketSubscriptionInitParams` object that maps to the [Request parameters](#request-parameters) below:

| SDK field              | RPC field                           |
| ---------------------- | ----------------------------------- |
| `eventContractSources` | `address`                           |
| `topicOverrides`       | `topics`                            |
| `ethCalls`             | `eth_calls`                         |
| `context`              | `context`                           |
| `onlyPushChanges`      | `push_changes_only`                 |
| `onData` / `onError`   | (callback hooks, no RPC equivalent) |

For step-by-step walkthroughs, see the [Wildcard Off-Chain Reactivity Tutorial](/developer/reactivity/tutorials/wildcard-off-chain-reactivity-tutorial) and [Off-Chain Reactivity: Filtered Subscriptions tutorial](/developer/reactivity/tutorials/off-chain-reactivity-filtered-subscriptions-tutorial).

## Reference

### Subscription method

`somnia_watch`. Subscribed via the standard `eth_subscribe` request over a WebSocket transport. Unsubscribed via `eth_unsubscribe`.

### Request parameters

The parameters object follows [`eth_getLogs`](/developer/json-rpc-api#eth_getlogs)-style filter conventions with additions for read-only simulation.

| Field               | Type                         | Purpose                                                                                                                                                                                |
| ------------------- | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`           | `address` or `address[]`     | Optional. Matches only logs emitted by one of these addresses. Omit or pass `null` for any address.                                                                                    |
| `topics`            | `(Hash \| Hash[] \| null)[]` | Optional. Positional topic filter. Entry `i` constrains topic `i` of the log. Each entry can be a single hash, an array of hashes (OR), or `null` (any). Omit for any topics.          |
| `eth_calls`         | array                        | Required. A list of read-only calls the node runs for each matching log. Pass `[]` if you only want log notifications with no attached simulation. See [eth\_calls](#eth_calls) below. |
| `context`           | `string` or `string[]`       | Optional. Selectors that tell the node which fields of the matching log to append to each simulated call's calldata. See [context](#context) below.                                    |
| `push_changes_only` | `bool`                       | Optional, default false. If true, and `eth_calls` is non-empty, only events whose simulation results differ from the previous event are pushed.                                        |

#### eth\_calls

Each element is a tuple of `(callRequest, stateOverrides)`, mirroring [`eth_call`](/developer/json-rpc-api#eth_call):

* `callRequest` — an `eth_call`-style transaction request object (`to`, `data`, optional `from`, `gas`, etc.).
* `stateOverrides` — an optional `eth_call`-style state override set, applied for the duration of the simulation.

For every matching log the node executes each call as a read-only simulation against the end-of-transaction state. The raw returned bytes are included in `simulationResults`, in order.

The same single-item-or-list shortcut used elsewhere in Ethereum JSON-RPC applies: you can pass just the call request to skip state overrides.

#### context

When `context` is non-empty, for each simulated call the node appends 32-byte chunks drawn from the matching log to the call's calldata, in the order given. This lets a single simulation template be parameterised by the log that triggered it.

Valid selectors:

| Selector  | What is appended                                       |
| --------- | ------------------------------------------------------ |
| `address` | The log's emitter address, left-padded to 32 bytes.    |
| `data`    | The log's data bytes (not padded — appended verbatim). |
| `topic1`  | The log's first topic (`topics[0]`).                   |
| `topic2`  | The log's second topic (`topics[1]`).                  |
| `topic3`  | The log's third topic (`topics[2]`).                   |
| `topic4`  | The log's fourth topic (`topics[3]`).                  |

Note that the 1-based names refer to position within the log, so `topic1` is the first topic (usually the event signature), not the second. Selectors that ask for a topic the log does not have are silently skipped.

If `context` is empty or absent, the node runs each `eth_call` exactly as written.

#### push\_changes\_only

If true and `eth_calls` is non-empty, the node compares the new `simulationResults` against the previous *pushed* event on this subscription. If they are byte-identical, the new event is dropped. The comparison is strictly adjacent — not against any reference value — so a zero result will still be pushed whenever the prior push was non-zero. Use this to avoid re-notifying on a genuinely unchanged view (e.g. a wash transfer that returns a balance to its previous value), not to filter out uninteresting ones.

`push_changes_only` has no effect when `eth_calls` is empty, since there is nothing to compare.

### Event payload

Each pushed notification has a `result` field shaped as:

| Field               | Type      | Meaning                                                                                                                                       |
| ------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `address`           | `address` | The log's emitter.                                                                                                                            |
| `topics`            | `Hash[]`  | The log's topics, up to four entries.                                                                                                         |
| `data`              | `bytes`   | The log's data payload.                                                                                                                       |
| `simulationResults` | `bytes[]` | One entry per `eth_calls` entry, in order. Empty if no `eth_calls` were configured. Each entry is the raw returndata from the simulated call. |

Topics and data are exactly as they appear in the committed block, so decoding follows the same rules as the event's Solidity definition.

### Filter matching semantics

The `address` and `topics` fields follow the same rules as `eth_getLogs` and `eth_subscribe("logs", ...)`:

* A log matches when its emitter matches the `address` filter (or `address` is omitted).
* For each topic position, a filter entry of `null` or omitted matches anything, a single hash matches exactly, and an array of hashes matches any member (OR).
* The subscription only considers logs from transactions that were included in a block. It does not see pending pool activity.

### Lifetime

The subscription lives on the API connection that created it. If the connection closes (client disconnect, node restart, network blip) the subscription is gone and must be recreated. The node does not buffer missed events across disconnects.

Subscriptions are not authenticated or owner-scoped in any way. Every client creates and sees only its own subscriptions.

### Historical data

Off-chain reactivity only pushes events from the moment the subscription is created onwards. For historical event retrieval, use [`eth_getLogs`](/developer/json-rpc-api#eth_getlogs) or another indexing solution.

## Minimal example

Here's how to subscribe to all Wrapped SOMI transfer events on mainnet, checking the token balance of each recipient:

```bash
wscat --connect wss://api.infra.mainnet.somnia.network/ws --wait 300 --execute '
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": [
    "somnia_watch",
    {
      "address": "0x046EDe9564A72571df6F5e44d0405360c0f4dCab",
      "topics": [
        "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
      ],
      "eth_calls": [
        {
          "to": "0x046EDe9564A72571df6F5e44d0405360c0f4dCab",
          "data": "0x70a08231"
        }
      ],
      "context": ["topic3"],
      "push_changes_only": false
    }
  ]
}'
```

Walking through each field:

* `address` — restricts matches to logs emitted by this specific ERC-20 token contract (see [Smart contracts](/developer/smart-contracts) for the canonical mainnet address).
* `topics` — the single entry is `keccak256("Transfer(address,address,uint256)")`, so that only ERC-20 Transfer logs match. The log's other two topics (`from` and `to`) are left unconstrained.
* `eth_calls` — a single read-only call, back to the token contract. Its `data` is the 4-byte selector `keccak256("balanceOf(address)")`; the 32-byte recipient argument is not written inline, but supplied by `context` below.
* `context` — the `topic3` selector instructs the node to append the log's third topic (the Transfer `to`, already left-padded to 32 bytes) to the simulated call's calldata. The result is a well-formed `balanceOf(recipient)` call, even though the subscription template is fixed.
* `push_changes_only` — enabling this here would discard events when two successive recipients happened to have the same token balance — not very helpful.

On running the command, you'll first receive the subscription ack:

```json
{"jsonrpc":"2.0","id":1,"result":"0x..."}
```

And then one `eth_subscription` notification per WSOMI Transfer, as they happen:

```json
{"jsonrpc":"2.0","method":"eth_subscription","params":{"subscription":"0x...","result":{"address":"0x046ede9564a72571df6f5e44d0405360c0f4dcab","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x...","0x..."],"data":"0x...","simulationResults":["0x..."]}}}
```

Each `simulationResults[0]` is the recipient's WSOMI balance after the transfer, denoted in wei.

Close the connection (Ctrl-C) to unsubscribe.


# Tutorials

## On-chain

* [ERC-20 transfer subscription in Solidity](/developer/reactivity/tutorials/solidity-on-chain-reactivity-tutorial)
* [System event subscriptions via TypeScript SDK](/developer/reactivity/tutorials/cron-subscriptions-via-sdk)

## Off-chain

* [Wildcard subscription via TypeScript SDK](/developer/reactivity/tutorials/wildcard-off-chain-reactivity-tutorial)
* [ERC-20 transfer subscription via TypeScript SDK](/developer/reactivity/tutorials/off-chain-reactivity-filtered-subscriptions-tutorial)


# Wildcard Off-Chain Reactivity Tutorial

Subscribe to all new logs over WebSocket and receive optional read-only call results with each event

This tutorial shows how to create an off-chain Reactivity subscription from TypeScript using the `@somnia-chain/reactivity` SDK. A wildcard subscription omits both address and topic filters, so the node pushes every new log it sees on the WebSocket connection.

Wildcard subscriptions are useful for quick testing and exploratory scripts. Production applications should usually add filters, as shown in the [filtered subscriptions tutorial](/developer/reactivity/tutorials/off-chain-reactivity-filtered-subscriptions-tutorial).

Off-chain subscriptions:

* live only for the WebSocket connection that created them
* are not stored on-chain
* do not require a wallet client or gas
* can include `ethCalls` whose raw return data is delivered with each event

For the full protocol reference, see [Off-chain Reactivity](/developer/reactivity/reactivity-offchain).

## Prerequisites

You'll need Node.js 20+.

Install the SDK and Viem:

```bash
npm install @somnia-chain/reactivity viem
npm install --save-dev tsx typescript @types/node
```

## Step 1: Define the Chain

This example uses Somnia Testnet. You can switch to mainnet by changing the chain ID, native currency and RPC URLs to the mainnet values from [Network Info](/developer/network-info).

```typescript
import { defineChain } from 'viem';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
      webSocket: ['wss://api.infra.testnet.somnia.network/ws'],
    },
  },
});
```

## Step 2: Create the SDK

Off-chain reactivity requires a public client with a WebSocket transport. You do not need a wallet client because no transaction is signed.

```typescript
import { SDK } from '@somnia-chain/reactivity';
import { createPublicClient, webSocket } from 'viem';

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: webSocket(somniaTestnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });
```

## Step 3: Subscribe to All Logs

Pass `ethCalls: []` when you only want log notifications. Omitting `eventContractSources` and `topicOverrides` makes the subscription wildcard.

```typescript
const subscription = await sdk.subscribe({
  ethCalls: [],
  onData: (data) => {
    const event = data.result;

    console.log('Emitter:', event.address);
    console.log('Topics:', event.topics);
    console.log('Data:', event.data);
    console.log('Simulation results:', event.simulationResults);
  },
  onError: (error) => {
    console.error('Subscription error:', error);
  },
});

if (subscription instanceof Error) {
  throw subscription;
}
```

Each notification's event payload contains:

| Field               | Meaning                                                |
| ------------------- | ------------------------------------------------------ |
| `address`           | Contract address that emitted the log.                 |
| `topics`            | Event topics, with the event signature at `topics[0]`. |
| `data`              | ABI-encoded non-indexed event data.                    |
| `simulationResults` | Raw return data from configured `ethCalls`, in order.  |

## Step 4: Decode a Known Event

A wildcard subscription receives many different event types. Decode only logs whose signature matches the ABI you are using. This example recognises ERC-20 `Transfer` logs.

```typescript
import {
  decodeEventLog,
  erc20Abi,
  toEventSelector,
} from 'viem';

const transferTopic = toEventSelector('Transfer(address,address,uint256)');

function handleEvent(event: {
  address: `0x${string}`;
  topics: `0x${string}`[];
  data: `0x${string}`;
  simulationResults: `0x${string}`[];
}) {
  if (event.topics[0] !== transferTopic) {
    return;
  }

  const decoded = decodeEventLog({
    abi: erc20Abi,
    topics: event.topics,
    data: event.data,
  });

  console.log('ERC-20 Transfer:', decoded.args);
}
```

Then call `handleEvent(data.result)` inside `onData`.

## Full Script

Save this as `main.ts`:

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  decodeEventLog,
  defineChain,
  erc20Abi,
  toEventSelector,
  webSocket,
} from 'viem';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
      webSocket: ['wss://api.infra.testnet.somnia.network/ws'],
    },
  },
});

const transferTopic = toEventSelector('Transfer(address,address,uint256)');

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: webSocket(somniaTestnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });

async function main() {
  const subscription = await sdk.subscribe({
    ethCalls: [],
    onData: (data) => {
      const event = data.result;

      console.log('Raw event:', event);

      if (event.topics[0] !== transferTopic) {
        return;
      }

      const decoded = decodeEventLog({
        abi: erc20Abi,
        topics: event.topics,
        data: event.data,
      });

      console.log('Decoded ERC-20 Transfer:', decoded.args);
    },
    onError: (error) => {
      console.error('Subscription error:', error);
    },
  });

  if (subscription instanceof Error) {
    throw subscription;
  }

  console.log('Subscribed:', subscription.subscriptionId);

  process.on('SIGINT', async () => {
    await subscription.unsubscribe();
    process.exit(0);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

Run it:

```bash
npx tsx main.ts
```

## Next Steps

Add `eventContractSources` and `topicOverrides` once you know which contracts and events you want. Add `ethCalls` when you want the node to attach read-only simulation results to each pushed event.


# Off-Chain Reactivity: Filtered Subscriptions tutorial

This tutorial shows how to narrow an off-chain Reactivity subscription to a specific contract and event. It subscribes to ERC-20 `Transfer` logs and asks the node to run `balanceOf(recipient)` for each matching log, so the pushed notification contains both the event and the recipient's post-transfer balance.

For the lower-level protocol reference, see [Off-chain Reactivity](/developer/reactivity/reactivity-offchain).

## What You Will Build

The script will:

1. Connect to a Somnia WebSocket RPC endpoint.
2. Subscribe to `Transfer(address,address,uint256)` logs from one ERC-20.
3. Use `context: "topic3"` to append the Transfer recipient to a `balanceOf` `ethCall`.
4. Decode both the event log and the returned balance.
5. Unsubscribe cleanly when the process exits.

## Prerequisites

You'll need Node.js 20+.

Install the SDK and Viem:

```bash
npm install @somnia-chain/reactivity viem
npm install --save-dev tsx typescript @types/node
```

## Step 1: Set Up the SDK

This example uses Somnia mainnet. To use testnet, replace the chain ID, native currency and RPC URLs with the testnet values from [Network Info](/developer/network-info).

```typescript
import { SDK } from '@somnia-chain/reactivity';
import { createPublicClient, defineChain, webSocket } from 'viem';

const somniaMainnet = defineChain({
  id: 5031,
  name: 'Somnia Mainnet',
  nativeCurrency: {
    decimals: 18,
    name: 'SOMI',
    symbol: 'SOMI',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.mainnet.somnia.network'],
      webSocket: ['wss://api.infra.mainnet.somnia.network/ws'],
    },
  },
});

const publicClient = createPublicClient({
  chain: somniaMainnet,
  transport: webSocket(somniaMainnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });
```

## Step 2: Build the Filter and Read-Only Call

The SDK fields map directly to the `somnia_watch` RPC parameters:

| SDK field              | RPC field           | Meaning                                                         |
| ---------------------- | ------------------- | --------------------------------------------------------------- |
| `eventContractSources` | `address`           | Emitters to watch.                                              |
| `topicOverrides`       | `topics`            | Positional topic filter.                                        |
| `ethCalls`             | `eth_calls`         | Read-only calls to run for each matching log.                   |
| `context`              | `context`           | Log fields to append to each call's calldata.                   |
| `onlyPushChanges`      | `push_changes_only` | Drop events whose call results match the previous pushed event. |

For ERC-20 `Transfer`, `topics[2]` is the recipient address. The off-chain Reactivity context selector is one-based, so `topic3` appends `topics[2]` to the call data.

```typescript
import { toEventSelector, toFunctionSelector } from 'viem';

const tokenAddress = '0x046EDe9564A72571df6F5e44d0405360c0f4dCab';
const transferTopic = toEventSelector('Transfer(address,address,uint256)');
const balanceOfSelector = toFunctionSelector('balanceOf(address)');

const ethCall = {
  to: tokenAddress,
  data: balanceOfSelector,
};
```

`balanceOfSelector` contains only the 4-byte function selector. The node appends the recipient address from the log because the subscription uses `context: "topic3"`.

## Step 3: Subscribe

```typescript
const subscription = await sdk.watch({
  eventContractSources: [tokenAddress],
  topicOverrides: [transferTopic],
  ethCalls: [ethCall],
  context: 'topic3',
  onlyPushChanges: false,
  onData: (data) => {
    const event = data.result;
    console.log('Filtered notification:', event);
  },
  onError: (error) => {
    console.error('Subscription error:', error);
  },
});

if (subscription instanceof Error) {
  throw subscription;
}
```

`onlyPushChanges` compares the raw `simulationResults` with the previous pushed event on this subscription. It is useful when you care only about changed view results, but it is not a general event filter. For this recipient-balance example, leave it false unless you are comfortable dropping consecutive transfers whose recipients happen to have identical balances.

## Step 4: Decode the Event and Balance

```typescript
import { decodeEventLog, decodeFunctionResult, erc20Abi } from 'viem';

function decodeTransferWithBalance(event: {
  topics: `0x${string}`[];
  data: `0x${string}`;
  simulationResults: `0x${string}`[];
}) {
  const transfer = decodeEventLog({
    abi: erc20Abi,
    topics: event.topics,
    data: event.data,
  });

  const recipientBalance = decodeFunctionResult({
    abi: erc20Abi,
    functionName: 'balanceOf',
    data: event.simulationResults[0],
  });

  console.log('Transfer:', transfer.args);
  console.log('Recipient balance:', recipientBalance);
}
```

Call `decodeTransferWithBalance(data.result)` from `onData`.

## Full Script

Save this as `filtered.ts`:

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  decodeEventLog,
  decodeFunctionResult,
  defineChain,
  erc20Abi,
  toEventSelector,
  toFunctionSelector,
  webSocket,
} from 'viem';

const somniaMainnet = defineChain({
  id: 5031,
  name: 'Somnia Mainnet',
  nativeCurrency: {
    decimals: 18,
    name: 'SOMI',
    symbol: 'SOMI',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.mainnet.somnia.network'],
      webSocket: ['wss://api.infra.mainnet.somnia.network/ws'],
    },
  },
});

// Wrapped SOMI - see Troubleshooting below.
const tokenAddress = '0x046EDe9564A72571df6F5e44d0405360c0f4dCab';

const transferTopic = toEventSelector('Transfer(address,address,uint256)');
const balanceOfSelector = toFunctionSelector('balanceOf(address)');

const publicClient = createPublicClient({
  chain: somniaMainnet,
  transport: webSocket(somniaMainnet.rpcUrls.default.webSocket[0]),
});

const sdk = new SDK({ public: publicClient });

async function main() {
  const subscription = await sdk.watch({
    eventContractSources: [tokenAddress],
    topicOverrides: [transferTopic],
    ethCalls: [
      {
        to: tokenAddress,
        data: balanceOfSelector,
      },
    ],
    context: 'topic3',
    onlyPushChanges: false,
    onData: (data) => {
      const event = data.result;

      const transfer = decodeEventLog({
        abi: erc20Abi,
        topics: event.topics,
        data: event.data,
      });

      const recipientBalance = decodeFunctionResult({
        abi: erc20Abi,
        functionName: 'balanceOf',
        data: event.simulationResults[0],
      });

      console.log('Transfer:', transfer.args);
      console.log('Recipient balance:', recipientBalance);
    },
    onError: (error) => {
      console.error('Subscription error:', error);
    },
  });

  if (subscription instanceof Error) {
    throw subscription;
  }

  console.log('Subscribed:', subscription.subscriptionId);

  process.on('SIGINT', async () => {
    await subscription.unsubscribe();
    process.exit(0);
  });
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

Run it:

```bash
npx tsx filtered.ts
```

## Troubleshooting

* Wrapped SOMI is not always actively transferred, so you may not see many events. You can pick another from [Smart contracts](/developer/smart-contracts) or from the recent transactions on the [block explorer](https://explorer.somnia.network).
* If the subscription starts but decoding fails, confirm the ABI matches the event signature in `topicOverrides`.


# Solidity on-chain Reactivity Tutorial

Create a contract-owned on-chain subscription that reacts to ERC-20 Transfer events

This tutorial builds a Solidity handler contract that subscribes to ERC-20 `Transfer` events and counts them on-chain. The subscription is owned by the handler contract itself, so the contract pays gas for each reactive transaction.

Use on-chain reactivity when the reaction should be part of chain execution. For client-side subscriptions over WebSocket, use [off-chain reactivity](/developer/reactivity/reactivity-offchain).

## How This Example Works

The contract:

1. Inherits from `SomniaEventHandler`.
2. Builds a non-wildcard `SubscriptionFilter` for one token's `Transfer` logs.
3. Calls `SomniaExtensions.subscribe(address(this), filter, options)` from the constructor.
4. Receives matching callbacks through `_onEvent`.
5. Can unsubscribe later through `stop()`.

On-chain wildcard subscriptions are not allowed. At least one of `eventTopics`, `origin`, or `emitter` must be set.

## Prerequisites

You'll need:

* Node.js 20+
* A Foundry project
* A Somnia RPC endpoint (see [Network info](/developer/network-info))
* A deployer account with enough SOMI or STT to fund the handler contract with at least 32 native tokens, plus gas for deployment and callback execution
* An ERC-20 token address to watch (see [Smart contracts](/developer/smart-contracts))

Install the Solidity package:

```bash
npm install @somnia-chain/reactivity-contracts
```

## Step 1: Write the Handler

Create `src/TransferCounter.sol`:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity 0.8.30;

import {
    SomniaEventHandler
} from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
import {
    SomniaExtensions
} from "@somnia-chain/reactivity-contracts/contracts/interfaces/SomniaExtensions.sol";

contract TransferCounter is SomniaEventHandler {
    bytes32 private constant TRANSFER_TOPIC =
        keccak256("Transfer(address,address,uint256)");

    event TransferObserved(address indexed from, address indexed to, uint256 value);

    address public immutable owner;
    address public immutable token;
    uint256 public subscriptionId;
    uint256 public transferCount;
    mapping(address recipient => uint256 totalReceived) public receivedBy;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    constructor(address token_, uint64 gasLimit) payable {
        require(token_ != address(0), "Token is zero address");

        owner = msg.sender;
        token = token_;

        SomniaExtensions.SubscriptionFilter memory filter =
            SomniaExtensions.SubscriptionFilter({
                eventTopics: [
                    TRANSFER_TOPIC,
                    bytes32(0),
                    bytes32(0),
                    bytes32(0)
                ],
                origin: address(0),
                emitter: token_
            });

        SomniaExtensions.SubscriptionOptions memory options =
            SomniaExtensions.SubscriptionOptions({
                priorityFeePerGas: 1,
                maxFeePerGas: 0,
                gasLimit: gasLimit
            });

        subscriptionId = SomniaExtensions.subscribe(
            address(this),
            filter,
            options
        );
    }

    function _onEvent(
        address emitter,
        bytes32[] calldata eventTopics,
        bytes calldata data
    ) internal override {
        require(emitter == token, "Unexpected emitter");
        require(eventTopics.length >= 3, "Missing Transfer topics");

        address from = address(uint160(uint256(eventTopics[1])));
        address to = address(uint160(uint256(eventTopics[2])));
        uint256 value = abi.decode(data, (uint256));

        transferCount += 1;
        receivedBy[to] += value;

        emit TransferObserved(from, to, value);
    }

    function stop() external onlyOwner {
        require(subscriptionId != 0, "No active subscription");
        SomniaExtensions.unsubscribe(subscriptionId);
        subscriptionId = 0;
    }

    receive() external payable {}
}
```

`SomniaEventHandler` verifies that only the reactivity precompile can call the external `onEvent` entry point. Your contract only implements `_onEvent`.

## Step 2: Deploy and Fund the Handler

The subscription owner must hold at least 32 SOMI when the subscription is created. Because this constructor creates the subscription, send the funding with the deploy transaction.

```bash
export RPC_URL=<your Somnia RPC endpoint>
export PK=<your funded private key>
export TOKEN=<erc20 token address to watch>

forge create \
  --rpc-url "$RPC_URL" \
  --private-key "$PK" \
  --broadcast \
  --value 33ether \
  src/TransferCounter.sol:TransferCounter \
  --constructor-args "$TOKEN" 2000000
```

`2000000` is the handler gas limit. Increase it if your `_onEvent` logic does more work. Somnia storage operations can require more gas than the same pattern on Ethereum, so leave margin. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain#development-advice) for more gas-limit guidance.

## Step 3: Inspect the Subscription

Set the deployed address:

```bash
export COUNTER=<address printed by forge create>
```

Read the subscription ID:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)'
```

Fetch subscription details from the node:

```bash
SUB_ID=$(cast call --rpc-url "$RPC_URL" "$COUNTER" 'subscriptionId()(uint256)' | awk '{print $1}')

cast rpc --rpc-url "$RPC_URL" \
  somnia_reactivityGetSubscriptionInfo "$(cast to-hex "$SUB_ID")"
```

You should see the `Transfer` event topic and the token address in the stored filter.

## Step 4: Trigger a Matching Transfer

Send or otherwise trigger a transfer on the watched token:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" \
  "$TOKEN" 'transfer(address,uint256)' <recipient> <amount>
```

After the transfer lands, the chain checks the log against active subscriptions. If it matches, validators include a synthetic transaction that calls the handler.

Read the counter:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" 'transferCount()(uint256)'
```

Read the tracked amount for a recipient:

```bash
cast call --rpc-url "$RPC_URL" "$COUNTER" \
  'receivedBy(address)(uint256)' <recipient>
```

## Step 5: Stop the Subscription

When you no longer want the contract to react, unsubscribe:

```bash
cast send --rpc-url "$RPC_URL" --private-key "$PK" \
  "$COUNTER" 'stop()'
```

After `stop()` succeeds, the old subscription ID should return an empty result from `somnia_reactivityGetSubscriptionInfo`.

## Notes

* The handler runs in a separate reactive transaction after the triggering transaction has executed.
* `msg.sender` inside `onEvent` is the reactivity precompile at `0x0100`.
* `tx.origin` inside the handler is the subscription owner.
* The owner pays for every handler invocation.
* `caller`, `isGuaranteed`, and `isCoalesced` are reserved fields on the raw precompile struct. `SomniaExtensions` sets them to their required defaults.
* Avoid feedback loops where the handler emits an event that also matches its own subscription. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain#development-advice) before subscribing to events your handler might emit.

For the full API reference and failure modes, see [On-chain Reactivity](/developer/reactivity/reactivity-onchain).


# Cron subscriptions via SDK

Create block-tick and scheduled subscriptions using SDK

The `@somnia-chain/reactivity` TypeScript SDK can create on-chain subscriptions owned by an externally-owned account (EOA). This tutorial covers the two system event helpers:

* `scheduleSubscriptionAtBlock`
* `scheduleSubscriptionAtTimestamp`

Use these helpers when a wallet or backend should own and pay for an on-chain callback, while a Solidity handler contract receives the reactive transaction.

For the system-event reference, see [On-chain Reactivity](/developer/reactivity/reactivity-onchain#system-events).

## What the Helpers Create

System events are synthetic logs emitted by the reactivity precompile at `0x0100`.

| Helper                            | System event        | Behavior                                                                 |
| --------------------------------- | ------------------- | ------------------------------------------------------------------------ |
| `scheduleSubscriptionAtBlock`     | `BlockTick(uint64)` | Every block if `blockNumber` is omitted, or once at the specified block. |
| `scheduleSubscriptionAtTimestamp` | `Schedule(uint256)` | Once, when block time first reaches the requested millisecond timestamp. |

The handler contract must implement `onEvent(address,bytes32[],bytes)`. The usual way to do that is to inherit from `SomniaEventHandler`, as shown in the [Solidity on-chain tutorial](/developer/reactivity/tutorials/solidity-on-chain-reactivity-tutorial).

## Prerequisites

You'll need Node.js 20+.

Install the SDK and Viem:

```bash
npm install @somnia-chain/reactivity viem
npm install --save-dev tsx typescript @types/node
```

To create a subscription, you'll need:

* a deployed handler contract
* an EOA funded with at least 32 SOMI

## Step 1: Prepare the SDK

This example uses Somnia Testnet. For mainnet, use the mainnet chain ID, currency and RPC URL from [Network Info](/developer/network-info).

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
    },
  },
});

const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined;

if (!privateKey) {
  throw new Error('Set PRIVATE_KEY=0x... before running this script');
}

const account = privateKeyToAccount(privateKey);

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const walletClient = createWalletClient({
  account,
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const sdk = new SDK({
  public: publicClient,
  wallet: walletClient,
});
```

## Step 2: Subscribe to Block Ticks

Omit `blockNumber` for a recurring every-block subscription. Use this carefully: it can invoke your handler every block until the subscription is removed. See the on-chain Reactivity [development advice](/developer/reactivity/reactivity-onchain#development-advice) before using a recurring system-event subscription in production.

```typescript
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtBlock({
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}

console.log('Block tick subscription tx:', txHash);
```

Pass `blockNumber` for a one-shot subscription to a specific future block:

```typescript
const currentBlock = await publicClient.getBlockNumber();
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtBlock({
  blockNumber: currentBlock + 20n,
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}
```

Internally, the SDK sets:

* `emitter` to the reactivity precompile address
* `eventTopics[0]` to the `BlockTick(uint64)` selector
* `eventTopics[1]` to the block number, or zero for every block
* `handlerFunctionSelector` to `ISomniaEventHandler.onEvent.selector` when you omit it

## Step 3: Schedule a One-Off Callback

`scheduleSubscriptionAtTimestamp` creates a one-shot `Schedule(uint256)` subscription. The timestamp is an absolute Unix timestamp in milliseconds.

```typescript
const timestampMs = Date.now() + 5 * 60 * 1000;
const handlerContractAddress = process.env.HANDLER_ADDRESS as `0x${string}`;

const txHash = await sdk.scheduleSubscriptionAtTimestamp({
  timestampMs,
  handlerContractAddress,
  priorityFeePerGas: 1n,
  maxFeePerGas: 0n,
  gasLimit: 2_000_000n,
  isGuaranteed: false,
  isCoalesced: false,
});

if (txHash instanceof Error) {
  throw txHash;
}

console.log('Scheduled callback tx:', txHash);
```

The SDK returns an `Error` when `timestampMs < Date.now() + 12_000`, so schedule at least 12 seconds ahead. It does not throw for that validation path, so always check `instanceof Error`.

## Full Script

Save this as `cron.ts`:

```typescript
import { SDK } from '@somnia-chain/reactivity';
import {
  createPublicClient,
  createWalletClient,
  defineChain,
  http,
} from 'viem';
import { privateKeyToAccount } from 'viem/accounts';

const handlerContractAddress = process.argv[2] as `0x${string}`;
const privateKey = process.env.PRIVATE_KEY as `0x${string}` | undefined;

if (!handlerContractAddress) {
  throw new Error('Usage: npx tsx cron.ts <handlerContractAddress>');
}

if (!privateKey) {
  throw new Error('Set PRIVATE_KEY=0x... before running this script');
}

const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  nativeCurrency: {
    decimals: 18,
    name: 'STT',
    symbol: 'STT',
  },
  rpcUrls: {
    default: {
      http: ['https://api.infra.testnet.somnia.network'],
    },
  },
});

const account = privateKeyToAccount(privateKey);

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const walletClient = createWalletClient({
  account,
  chain: somniaTestnet,
  transport: http(somniaTestnet.rpcUrls.default.http[0]),
});

const sdk = new SDK({
  public: publicClient,
  wallet: walletClient,
});

async function main() {
  const currentBlock = await publicClient.getBlockNumber();

  const blockTickTx = await sdk.scheduleSubscriptionAtBlock({
    blockNumber: currentBlock + 20n,
    handlerContractAddress,
    priorityFeePerGas: 1n,
    maxFeePerGas: 0n,
    gasLimit: 2_000_000n,
    isGuaranteed: false,
    isCoalesced: false,
  });

  if (blockTickTx instanceof Error) {
    throw blockTickTx;
  }

  console.log('One-shot block tick subscription tx:', blockTickTx);

  // Wait for the first subscription to land before broadcasting the next one,
  // so the two writes don't race on the wallet's nonce.
  await publicClient.waitForTransactionReceipt({ hash: blockTickTx });

  const scheduleTx = await sdk.scheduleSubscriptionAtTimestamp({
    timestampMs: Date.now() + 5 * 60 * 1000,
    handlerContractAddress,
    priorityFeePerGas: 1n,
    maxFeePerGas: 0n,
    gasLimit: 2_000_000n,
    isGuaranteed: false,
    isCoalesced: false,
  });

  if (scheduleTx instanceof Error) {
    throw scheduleTx;
  }

  console.log('Scheduled callback tx:', scheduleTx);
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
```

Run it:

```bash
PRIVATE_KEY=0x... npx tsx cron.ts <handlerContractAddress>
```

## Cancel from TypeScript

The SDK can also cancel subscriptions owned by the same wallet client. Pass the subscription ID, not the creation transaction hash:

```typescript
const cancelTx = await sdk.cancelSoliditySubscription(subscriptionId);

if (cancelTx instanceof Error) {
  throw cancelTx;
}

console.log('Cancelled subscription tx:', cancelTx);
```

## Equivalent Solidity Helpers

If a Solidity contract owns the subscription, prefer `SomniaExtensions` from inside that contract instead of the TypeScript SDK. The snippets below assume `options` is a `SomniaExtensions.SubscriptionOptions` value.

One-shot block subscription:

```solidity
// Any future block works. This example uses a small offset for easy testing.
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtBlock(
    address(this),
    uint64(block.number + 20),
    options
);
```

One-shot timestamp subscription:

```solidity
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtTimestamp(
    address(this),
    (block.timestamp + 5 minutes) * 1000,
    options
);
```

Recurring every-block subscription:

```solidity
SomniaExtensions.SubscriptionFilter memory filter =
    SomniaExtensions.SubscriptionFilter({
        eventTopics: [
            ISomniaReactivityPrecompile.BlockTick.selector,
            bytes32(0),
            bytes32(0),
            bytes32(0)
        ],
        origin: address(0),
        emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS
    });

uint256 subscriptionId = SomniaExtensions.subscribe(
    address(this),
    filter,
    options
);
```

The `scheduleSubscriptionAt*` Solidity helpers are one-shot. For recurring every-block or every-epoch subscriptions, build the filter and call `SomniaExtensions.subscribe` directly.


# Somnia Data Streams

Read, write, and react to structured data or events broadcast on-chain. With a sharp focus on reusability and composability, applications can interoperate and coalesce around commonly agreed-upon data structures that are built around the native reactivity offered by the protocol suite.


# What is Somnia Data Streams?

Somnia Data Streams is a structured data layer for EVM chains. Somnia Data streams enable developers to build applications that both emit EVM event logs and write data to the Somnia chain without Solidity. This means developers do not need to know Solidity to build applications using Somnia Data Streams.

Somnia Data streams allow parsing schema data into contract storage, where developers define a schema (a typed, ordered layout of fields), then publish and subscribe to data that conforms to that schema.

Think of reading data from Streams as an emitted event, but with an SDK: publishers write strongly-typed records; subscribers read them by schema and publisher, and decode to rich objects.

***

## Why Streams?

Traditional approaches each have trade-offs:

* Contract events are great for signaling, but untyped at the app level (you still write your own ABI and decoders across projects). Events are also hard to stitch into reusable data models.<br>
* Custom contract storage is powerful but heavyweight, and you maintain the whole schema logic, CRUD, indexing patterns, and migrations.<br>
* Off-chain DB and proofs are flexible but brittle; either centralized or require extra machinery.<br>
* Oracles are useful for external data, but not a generic modeling layer for app-originated data.<br>

Streams solves this by standardizing:

1. Schemas (the “data ABI”)<br>
2. Publish/Subscribe primitives (SDK, not boilerplate contracts)<br>
3. Deterministic IDs (schemaId, dataId) and provenance (publisher address)<br>

This results in interoperable, verifiable, composable data with minimal app code.

***

## When to Use Streams

Use Streams when you need:

* Typed, shareable data across apps (chat messages, GPS, player stats, telemetry)
* Multiple publishers writing the same kind of record
* A standard decode flow with minimal custom indexing
* You need instant push to clients (Streams also works well with polling; you can add WS if desired)

Avoid Streams if:

* You need complex transactional logic/state machines tightly bound to contract invariants (build a contract)
* You must store large blobs (store off-chain, publish references/URIs in Streams)

***

## Definition of Terms

* **Schema**: a canonical string describing fields in order, e.g.\
  uint64 timestamp, bytes32 roomId, string content, string senderName, address sender\
  The exact string determines a schemaId (hash).<br>
* **Publisher**: The signer that writes data. EOA or Smart Contract that writes data under a schema. Readers trust provenance by address.<br>
* **Subscriber**: reader that fetches all records for a (schemaId, publisher) pair.<br>
* **Data ID (dataId)**: developer-chosen 32-byte key per record (helps with lookups, dedup, pagination). Pick dataIds with predictable structure to enable point lookups or pagination seeds. E.g:
  * Game: toHex('matchId-index', { size: 32 })
  * Chat: toHex('room-timestamp', { size: 32 })
  * GPS: toHex('device-epoch', { size: 32 })<br>
* **Encoder**: converts typed values ⇄ bytes according to the schema.<br>
* **schemaId**: computed from the schema string. Treat it like a contract address for data shape.

#### Data flow

```markup
+-------------+        publishData(payload)         +--------------------+
|  Publisher  |  -------------------------------->  | Somnia Streams L1  |
|  (wallet)   |                                     |   (on-chain data)  |
+-------------+                                     +--------------------+
       ^                                                     |
       |                                     getAllPublisherDataForSchema
       |                                                     v
+-------------+                                        +-----------+
| Subscriber  |  <------------------------------------ |  Reader   |
| (frontend)  |                                        | (SDK)     |
+-------------+                                        +-----------+
```

You can have multiple publishers writing under the same schema; subscribers can aggregate them if desired.

***

## The Schema: Your “Data ABI”

A schema is a compact, ordered list of typed fields. The exact string determines the computed `schemaId` Even whitespace and order matter.

Design guidance

* Put stable fields first (e.g., timestamp, entityId, type).
* Prefer fixed-width ints (e.g., uint64 for timestamps).
* Use bytes32 for keys/IDs (room, device, etc.).
* Use string for human-readable info (names, messages), but keep it short for gas efficiency.<br>

***

### Data Writing Patterns

* Single Publisher\
  One server wallet publishes; User Interaces can read the schema using `getByKey` , `getAtIndex` , `getAllPublisherDataForSchema`.<br>
* Multi-Publisher\
  Many devices publish under a shared schema. Your API aggregates across a list of publisher addresses.<br>
* Derived Views\
  Build REST endpoints that query Streams and derive higher-level views (e.g., “latest per room”).

***

## Quickstart in 5 Minutes

You’ll register a schema, publish one message, and read it back — just to feel the flow.

### Install

```bash
npm i @somnia-chain/streams viem
```

### Set up Env

```markup
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
```

### Define Chain

```typescript
// lib/chain.ts
import { defineChain } from 'viem'
export const somniaTestnet = defineChain({
  id: 50312, name: 'Somnia Testnet', network: 'somnia-testnet',
  nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
  rpcUrls: { default: { http: ['https://dream-rpc.somnia.network'] }, public: { http: ['https://dream-rpc.somnia.network'] } },
} as const)
```

### Define Client

```typescript
// lib/clients.ts
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'

const RPC = process.env.RPC_URL as string
const PK  = process.env.PRIVATE_KEY as `0x${string}`

export const publicClient = createPublicClient({ chain: somniaTestnet, transport: http(RPC) })
export const walletClient = createWalletClient({ account: privateKeyToAccount(PK), chain: somniaTestnet, transport: http(RPC) })
```

### Schema

```typescript
// lib/schema.ts
export const chatSchema =
  'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
```

#### Register Schema (optional but recommended)

<details>

<summary>scripts/register.ts</summary>

```typescript
import 'dotenv/config'
import { SDK, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { waitForTransactionReceipt } from 'viem/actions'


async function main() {
  const sdk = new SDK({ public: publicClient, wallet: walletClient })
  const id = await sdk.streams.computeSchemaId(chatSchema)
  const exists = await sdk.streams.isDataSchemaRegistered(id)
  if (!exists) {
    const tx = await sdk.streams.registerDataSchemas([
      { schemaName: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }
    ])
    if (tx instanceof Error) throw tx
    await waitForTransactionReceipt(publicClient, { hash: tx })
  }
  console.log('schemaId:', id)
}
main()
```

</details>

### Publish (Write)

```typescript
// scripts/publish-one.ts
import 'dotenv/config'
import { SDK, SchemaEncoder } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { toHex, type Hex } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

async function main() {
  const sdk = new SDK({ public: publicClient, wallet: walletClient })
  const schemaId = await sdk.streams.computeSchemaId(chatSchema)
  const enc = new SchemaEncoder(chatSchema)

  const payload: Hex = enc.encodeData([
    { name: 'timestamp',  value: Date.now().toString(),    type: 'uint64' },
    { name: 'roomId',     value: toHex('general', { size: 32 }), type: 'bytes32' },
    { name: 'content',    value: 'Hello Somnia!',          type: 'string' },
    { name: 'senderName', value: 'Alice',                  type: 'string' },
    { name: 'sender',     value: walletClient.account!.address, type: 'address' },
  ])

  const dataId = toHex(`general-${Date.now()}`, { size: 32 })
  const tx = await sdk.streams.set([
    { id: dataId, schemaId, data: payload }
  ])

  if (tx instanceof Error) throw tx
  await waitForTransactionReceipt(publicClient, { hash: tx })
  return { txHash: tx }
}
main()
```

### Read Data

```typescript
// scripts/read-all.ts
import 'dotenv/config'
import { SDK } from '@somnia-chain/streams'
import { publicClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { toHex } from 'viem'

type Field = { name: string; type: string; value: any }
const val = (f: Field) => f?.value?.value ?? f?.value

async function main() {
  const sdk = new SDK({ public: publicClient })
  const schemaId = await sdk.streams.computeSchemaId(chatSchema)
  const publisher = process.env.PUBLISHER as `0x${string}` || '0xYOUR_PUBLISHER_ADDR'

  const rows = (await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)) as Field[][]
  const want = toHex('general', { size: 32 }).toLowerCase()

  for (const r of rows || []) {
    const ts = Number(val(r[0]))
    const ms = String(ts).length <= 10 ? ts * 1000 : ts
    if (String(val(r[1])).toLowerCase() !== want) continue
    console.log({
      time: new Date(ms).toLocaleString(),
      content: String(val(r[2])),
      senderName: String(val(r[3])),
      sender: String(val(r[4])),
    })
  }
}
main()

```

That’s your first end-to-end loop.

***

## FAQs

Q: Do I need to register a schema?\
A: Registration is optional but recommended. You can publish to an unregistered schema (readers just need the exact string to decode). Registration helps discoverability and tooling.

Q: Can I change a schema later?\
A: Changing order or types yields a new schemaId. Plan for versioning (run v1 + v2 together).

Q: How do I page data?\
A: Use structured dataIds, or build a thin index off-chain that records block numbers / tx hashes per record.

Q: How does Streams differ from subgraphs?\
A: Streams defines how you write/read structured records with an SDK. Subgraphs (or other indexers) sit on top to query across many publishers, paginate, and filter efficiently.

Q: How do I handle large payloads?\
A: Store the payload elsewhere (IPFS, Arweave, S3) and put the URI + hash in Streams. Optionally encrypt off-chain.


# Quickstart

Example pseudo code for publishing data associated with a schema (public or private)

## Pre-requisites

A typescript environment with [`viem`](https://viem.sh/) and [`@somnia-chain/streams`](https://www.npmjs.com/package/@somnia-chain/streams) installed

## Steps

### 1. Define your schema as a string and plug it into the schema encoder

```typescript
import { SDK, zeroBytes32, SchemaEncoder } from "@somnia-chain/streams"

const gpsSchema = `uint64 timestamp, int32 latitude, int32 longitude, int32 altitude, uint32 accuracy, bytes32 entityId, uint256 nonce`
const schemaEncoder = new SchemaEncoder(gpsSchema)
```

`schemaEncoder` can now be used to encode data for broadcast and also decode data when reading it from Somnia Data Stream SDK.

### 2. Compute your unique schema identifier from the schema

<pre class="language-typescript"><code class="lang-typescript"><strong>const sdk = new SDK({
</strong>    public: getPublicClient(),
    wallet: getWalletClient(),
})
const schemaId = await sdk.streams.computeSchemaId(gpsSchema)
console.log(`Schema ID ${schemaId}`)
</code></pre>

All data broadcast with the Somnia Data Stream SDK write mechanism must be linked to a schema ID so that we know how to decode the data on read.

### 3. Encode the data you want to store that is compatible with the schema

```typescript
const encodedData: Hex = schemaEncoder.encodeData([
    { name: "timestamp", value: Date.now().toString(), type: "uint64" },
    { name: "latitude", value: "51509865", type: "int32" },
    { name: "longitude", value: "-0118092", type: "int32" },
    { name: "altitude", value: "0", type: "int32" },
    { name: "accuracy", value: "0", type: "uint32" },
    { name: "entityId", value: zeroBytes32, type: "bytes32" }, // object providing GPS data
    { name: "nonce", value: "0", type: "uint256" },
])
```

The value returned is a raw hex encoded bytes value that can be broadcast on-chain via the Somnia Data Stream SDK.

### 4. Publish data (with our without a public schema)

```typescript
const publishTxHash = await sdk.streams.set([{
    id: toHex("london", { size: 32 }),
    schemaId: computedGpsSchemaId,
    data: encodedData,
}])
```

`set` has the following parameter `dataStreams` which is a list of data points being written to chain\
\
`dataStreams` has the `DataStream[]` type:

```typescript
type Hex = `0x{string}`
type DataStream = {
    id: Hex // Unique data key for the publisher
    schemaId: Hex // Computed from the raw schema string
    data: Hex // From step 3, raw bytes data formated as a hex string
}
```

### 5. Direct data read without reactivity

```typescript
const data = await sdk.streams.getByKey(
  computedGpsSchemaId,
  publisherWalletAddress,
  dataKey
)
```

This last step shows how you request data from Somnia data streams filtering on:

1. Schema ID
2. Address of the account that wrote the data to chain
   1. This could be an EOA or another smart contract

The response from `getByKey` will be the data published but decoded for the specified schema.

Note: where the schema ID is associated with a public data schema that has been registered on-chain, the SDK will automatically decode the raw data published on-chain and return that decoded data removing the need for the decoder. If the schema is not public, the schema decoder will be required outside of the SDK and you will instead get raw bytes from the chain. Example:

```typescript
if (data) {
  schemaEncoder.decode(data)
}
```

Further filters can be applied client side to the data in order to filter for specifics within the data. GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.


# Concepts


# Understanding Schemas, Schema IDs, Data IDs, and Publisher

Somnia Data Streams uses a schema-driven architecture to store and manage blockchain data. Every piece of information stored on the network, whether it’s a chat message, leaderboard score, or todo item, follows a structured schema, is identified by a Schema ID, written with a Data ID, and associated with a Publisher.

In this guide, you’ll learn the difference between Schemas and Schema IDs, how Data IDs uniquely identify records, and how Publishers own and manage their data streams.

* Schemas define the structure of your data.
* Data IDs uniquely identify individual records.
* Publisher determines who owns or controls the data stream.

By the end, you’ll understand how to organize, reference, and manage your application’s data on Somnia.

## What Are Schemas?

A Schema defines the structure and types of the data you want to store onchain. It’s like a blueprint for how your application’s data is encoded, stored, and decoded. A Schema ID, on the other hand, is a unique deterministic hash computed from that schema definition.

When you register or compute a schema, the SDK automatically generates a unique hash (Schema ID) that permanently represents that schema definition.

A schema describes the structure of your data, much like a table in a relational database defines its columns.

#### Example: Defining a Schema

```typescript
const userSchema = `
  uint64 timestamp,
  string username,
  string bio,
  address owner
`
```

This schema tells the Somnia Data Streams system how your data is structured and typed.

## Schema ID: The Unique Identifier

A Schema ID is derived from your schema using a hashing algorithm. It uniquely represents this structure onchain, ensuring consistency and integrity. You can compute its Schema ID before even deploying it onchain.

```typescript
import { SDK } from '@somnia-chain/streams'
import { getSdk } from './clients'

const sdk = getSdk()
const schemaId = await sdk.streams.computeSchemaId(userSchema)

console.log("Computed Schema ID:", schemaId)
```

Example Output:

```bash
Computed Schema ID: 0x5e4bce54a39b42b5b8a235b5d9e27e7031e39b65d7a42a6e0ac5e8b2c79e17b0

```

{% hint style="danger" %}
This hash (schemaId) uniquely identifies the schema onchain. If you change even one character in the schema definition, the Schema ID will change.
{% endhint %}

The Schema ID is the hash that ensures the same structure is used everywhere, preventing mismatched or corrupted data.

## Registering a Schema

To make the schema usable onchain, it has to be registered by calling the `registerDataSchemas()` method. This ensures other nodes and apps can decode your data correctly:

```typescript
import { zeroBytes32 } from '@somnia-chain/streams'

const ignoreExistingSchemas = true
await sdk.streams.registerDataSchemas([
  { schemaName: "MySchema", schema: userSchema, parentSchemaId: zeroBytes32 }
], ignoreExistingSchemas)
```

`id` is a string. human human-readable identifier`ignoreExistingSchemas` is for telling the SDK not to worry about already registered schemas.\
Once registered, any publisher can use this Schema ID to store or retrieve data encoded according to this structure. The schema defines structure. The Schema ID becomes its permanent onchain reference.

| Concept   | Database Equivalent | Description                                |
| --------- | ------------------- | ------------------------------------------ |
| Schema    | Table Definition    | Defines data fields and types              |
| Schema ID | Table Hash          | Uniquely identifies that schema definition |

For instance:

`Schema → CREATE TABLE Users (id INT, name TEXT)`

`Schema ID → 0x9f3a...a7c (hash of the above definition)`

## What Are Data IDs?

Every record written to Somnia (e.g., a single message, transaction, or post) must have a Data ID, a unique key representing that entry. It uniquely identifies a specific record (or row). The Data ID ensures that:

* Each entry can be updated or replaced deterministically.
* Developers can reference or fetch a specific record by key.
* Duplicate writes can be prevented.

#### Example: Creating a Data ID

A Data ID can be created by hashing a string, typically by combining context and timestamp.

```typescript
import { toHex } from 'viem'

const dataId = toHex(`username-${Date.now()}`, { size: 32 })
console.log("Data ID:", dataId)
```

Example Output:

```bash
Data ID: 0x757365726e616d652d31373239303239323435
```

You can now use this ID to publish structured data to the blockchain. A Data ID ensures every record written is unique and can be referenced or updated deterministically.

#### Example: Writing Data with a Schema and Data ID

```typescript
import { SchemaEncoder } from '@somnia-chain/streams'

const encoder = new SchemaEncoder(userSchema)
const encodedData = encoder.encodeData([
  { name: 'timestamp', value: Date.now().toString(), type: 'uint64' },
  { name: 'username', value: 'Victory', type: 'string' },
  { name: 'bio', value: 'Blockchain Developer', type: 'string' },
  { name: 'owner', value: '0xYourWalletAddress', type: 'address' },
])

await sdk.streams.set([
  { id: dataId, schemaId, data: encodedData }
])
```

Think of a Data ID like a primary key in a SQL table.

| Data ID (Primary Key) | username | bio                  |
| --------------------- | -------- | -------------------- |
| 0x1234abcd...         | Emmanuel | Blockchain Developer |

If you write another record with the same Data ID, it updates the existing entry rather than duplicating it, thereby maintaining data integrity. `schemaId` defines how to encode/decode the data, and `dataId` identifies which record this is. The data itself is encoded and written to the blockchain

## What Are Publishers?

A Publisher is any wallet address that sends data to Somnia Streams. Each publisher maintains its own isolated namespace for all schema-based data it writes. This means:

* Data from two different publishers never conflict.
* Apps can filter or query data from a specific publisher.
* Publishers serve as the data owners for all records they create.

#### Example: Getting a Publisher Address

If you’re using a connected wallet, your publisher is automatically derived using the `createWalletClient` from viem:

```typescript
const {
    ...
    createWalletClient,
} = require("viem");

const { privateKeyToAccount } = require("viem/accounts");

// Create wallet client
const walletClient = createWalletClient({
    account: privateKeyToAccount(process.env.PRIVATE_KEY),
    chain: dreamChain,
    transport: http(dreamChain.rpcUrls.default.http[0]),
});

// Initialize SDK
const sdk = new SDK({
    ...
    wallet: walletClient,
});

const encodedData = schemaEncoder.encodeData([
       ...
    { name: "sender", value: wallet.account.address, type: "address" },
]);
```

Where `publisher = wallet.account.address`

When reading data, you can specify which publisher’s records to fetch:

```typescript
const messages = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
```

Example Output:

```bash
[
  { timestamp: 1729302920, username: "Victory", bio: "Blockchain Developer" }
]
```

This retrieves all data published under that schema by that particular address.

Think of Publishers like individual database owners. Each one maintains their own “tables” (schemas) and “records” (data entries) under their unique namespace.

| Publisher (Wallet) | Schema ID | Data ID | Description      |
| ------------------ | --------- | ------- | ---------------- |
| 0x123...abc        | Schema A  | Data 1  | Paul’s todos     |
| 0x789...def        | Schema A  | Data 2  | Emmanuel’s todos |

## Putting It All Together

When you publish data on Somnia, three identifiers always work together:

| Concept   | Role                   | Example         |
| --------- | ---------------------- | --------------- |
| Schema ID | Identifies schema hash | 0x5e4bce54...   |
| Data ID   | Identifies record      | 0x75736572...   |
| Publisher | Identifies sender      | 0x3dC360e038... |

These three make your data verifiable, queryable, and uniquely name-spaced across the blockchain. These form the foundation of the Somnia Data Streams architecture:

* The Schema tells the system what kind of data this is.
* The Schema ID ensures it’s stored consistently across the network.
* The Data ID identifies which record this is.
* The Publisher records who wrote it.

## Example Use Case: Chat Messages

Here’s how they interact in a real-world scenario, a decentralized chat room.

### Step 1: Define Schema

```typescript
const chatSchema = `
  uint64 timestamp,
  bytes32 roomId,
  string content,
  string senderName,
  address sender
`
```

### Step 2: Compute Schema ID

```typescript
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
```

### Step 3: Generate Data ID for each message

```typescript
const dataId = toHex(`${roomName}-${Date.now()}`, { size: 32 })
```

### Step 4: Publish Message

```typescript
const encoded = encoder.encodeData([
  { name: 'timestamp', value: Date.now().toString(), type: 'uint64' },
  { name: 'roomId', value: toHex(roomName, { size: 32 }), type: 'bytes32' },
  { name: 'content', value: 'Hello world!', type: 'string' },
  { name: 'senderName', value: 'Victory', type: 'string' },
  { name: 'sender', value: publisherAddress, type: 'address' }
])

await sdk.streams.set([{ id: dataId, schemaId, data: encoded }])
```

Now each message:

* Conforms to a schema
* Is identified by a Schema ID
* Is stored under a unique Data ID
* Is published by a specific Publisher

### Common Pitfalls

| Mistake                       | Description                        | Fix                                                                                                             |
| ----------------------------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Reusing Data IDs incorrectly  | Causes overwrites of older records | Use unique IDs like title-timestamp                                                                             |
| Forgetting to register schema | Data won’t decode properly         | Always call registerDataSchemas() once                                                                          |
| Mixing publisher data         | Leads to incomplete reads          | Query by the correct publisher address and consider aggregating many publishers under a single contract address |

### Conclusion

Now that you understand Schemas, Data IDs, and Publishers, you’re ready to build your own data model for decentralized apps and query live data across multiple publishers


# Extending and composing data schemas

The best blockchain primitives are composable and schemas are no exception. Promoting re-use is a priority

New schemas can extend other schemas by setting a parent schema ID. Remember, you can take any raw schema string and compute a schema ID from it. When registering a new schema that builds upon and extends another, you would specify the raw schema string for the new schema as well as specifying the optional parent schema ID. The parent schema ID will be critical later for deserialising data written to chain.\
\
For schemas that do not extend other schemas (when nothing is available), then one does not need to specify a parent schema ID or can optionally specify the zero value for the bytes32 solidity type.\
\
For maximum composability, all schemas should be public.

### Extension in practice (Example 1)

<pre class="language-typescript"><code class="lang-typescript"><strong>import { SDK } from "@somnia-chain/streams"
</strong><strong>const sdk = new SDK({
</strong>    public: getPublicClient(),
    wallet: getWalletClient(),
})

// The parent schema here will be the GPS schema from the quick start guide
const gpsSchema = `uint64 timestamp, int32 latitude, int32 longitude, int32 altitude, uint32 accuracy, bytes32 entityId, uint256 nonce`
const parentSchemaId = await sdk.streams.computeSchemaId(gpsSchema)

// Lets extend the gps schema and add F1 data since every car will have a gps position
const formulaOneSchema = `uint256 driverNumber`

// We can also extend the gps schema for FR data i.e. aircraft identifier
const flightRadarSchema = `bytes32 ICAO24`

await sdk.streams.registerDataSchemas([
    { schemaName: "gps", schema: gpsSchema },
    { schemaName: "f1", schema: formulaOneSchema, parentSchemaId }, // F1 extends GPS
    { schemaName: "FR", schema: flightRadarSchema, parentSchemaId },// FR extends GPS
])
</code></pre>

The typescript code shows how two new schemas re-use the GPS schema in order to append an additional field

### Extension in practice (Example 2)

Versioned schemas

```typescript
import { SDK } from "@somnia-chain/streams"
const sdk = new SDK({
    public: getPublicClient(),
    wallet: getWalletClient(),
})

const versionSchema = `uint16 version`
const parentSchemaId = await sdk.streams.computeSchemaId(versionSchema)

// Now lets register a person schema with expectation there will be many versions of the person schema
const personSchema = `uint8 age` 
await sdk.streams.registerDataSchemas([
    { schemaName: "version", schema: versionSchema },
    { schemaName: "person", schema: personSchema, parentSchemaId }
])
```

Client's that are reading data associated with the derived schemas, use the SDK to get the fully decoded data since data is retrieved by schema ID (See `getByKey` from the quick start guide). Essentially the SDK does a number of the following pseudo steps:

1. Fetch schema and recursively fetch parent schema until the end of the chain is reached
2. Join all schemas together seperated by comma
3. Spin up the decoder and pass through the raw data stored on-chain
4. Return the decoded data to the caller


# Somnia Data vs Event Streams

Serving different purposes, data and event streams can be used independently or together

### tl;dr

* Data Streams: Raw bytes calldata written to chain with contextual information on how to parse the data using a public or private `data schema`
* Event Streams: [EVM logs](https://docs.chainstack.com/docs/ethereum-logs-tutorial-series-logs-and-filters) emitted by the Somnia Streams protocol. Protocol users register and `event schema` that can be referenced they want to emit an event that others can `subscribe` to with Somnia streams reactivity

Both data and event streams can be done without knowing Solidity and without deploying any smart contracts

### TypeScript SDK interface

```typescript
/**
 * @param somniaStreamsEventId The identifier of a registered event schema within Somnia streams protocol or null if using a custom event source
 * @param ethCalls Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array
 * @param context Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, topic4, data and address
 * @param onData Callback for a successful reactivity notification
 * @param onError Callback for a failed attempt 
 * @param eventContractSource Alternative contract event source (any on somnia) that will be emitting the logs specified by topicOverrides
 * @param topicOverrides Optional when using Somnia streams as an event source but mandatory when using a different event source
 * @param onlyPushChanges Whether the data should be pushed to the subscriber only if eth_call results are different from the previous
 */
export type SubscriptionInitParams = {
    somniaStreamsEventId?: string
    ethCalls: EthCall[]
    context?: string
    onData: (data: any) => void
    onError?: (error: Error) => void
    eventContractSource?: Address
    topicOverrides?: Hex[]
    onlyPushChanges: boolean
}

export interface StreamsInterface {
    // Write
    set(d: DataStream[]): Promise<Hex | null>;
    emitEvents(e: EventStream[]): Promise<Hex | Error | null>;
    setAndEmitEvents(d: DataStream[], e: EventStream[]): Promise<Hex | Error | null>;

    // Manage
    registerDataSchemas(registrations: DataSchemaRegistration[]): Promise<Hex | Error | null>;
    registerEventSchemas(ids: string[], schemas: EventSchema[]): Promise<Hex | Error | null>;
    manageEventEmittersForRegisteredStreamsEvent(
        streamsEventId: string,
        emitter: Address,
        isEmitter: boolean
    ): Promise<Hex | Error | null>;

    // Read
    getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>;
    getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>;
    getBetweenRange(
        schemaId: SchemaID,
        publisher: Address,
        startIndex: bigint,
        endIndex: bigint
    ): Promise<Hex[] | SchemaDecodedItem[][] | Error | null>;
    getAllPublisherDataForSchema(
        schemaReference: SchemaReference,
        publisher: Address
    ): Promise<Hex[] | SchemaDecodedItem[][] | null>;
    getLastPublishedDataForSchema(
        schemaId: SchemaID,
        publisher: Address
    ): Promise<Hex[] | SchemaDecodedItem[][] | null>;
    totalPublisherDataForSchema(schemaId: SchemaID, publisher: Address): Promise<bigint | null>;
    isDataSchemaRegistered(schemaId: SchemaID): Promise<boolean | null>;
    computeSchemaId(schema: string): Promise<Hex | null>;
    parentSchemaId(schemaId: SchemaID): Promise<Hex | null>;
    schemaIdToId(schemaId: SchemaID): Promise<string | null>;
    idToSchemaId(id: string): Promise<Hex | null>;
    getAllSchemas(): Promise<string[] | null>;
    getEventSchemasById(ids: string[]): Promise<EventSchema[] | null>;

    // Helper
    deserialiseRawData(
        rawData: Hex[],
        parentSchemaId: Hex,
        schemaLookup: {
            schema: string;
            schemaId: Hex;
        } | null
    ): Promise<Hex[] | SchemaDecodedItem[][] | null>;

    // Subscribe
    subscribe(initParams: SubscriptionInitParams): Promise<{ subscriptionId: string, unsubscribe: () => void } | undefined>;

    // Protocol
    getSomniaDataStreamsProtocolInfo(): Promise<GetSomniaDataStreamsProtocolInfoResponse | Error | null>;
}
```


# Intersection with Somnia Reactivity

How to build applications that react to data being streamed to the Somnia chain by creating subscriptions

## Reactivity background

For detailed information about reactivity please visit the Reactivity docs:

<table data-view="cards"><thead><tr><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td>Reactivity</td><td><a href="/pages/JDHkSy1BgMgqLw7PtCx9#welcome-to-the-somnia-reactivity-docs">/pages/JDHkSy1BgMgqLw7PtCx9#welcome-to-the-somnia-reactivity-docs</a></td></tr></tbody></table>

## Writing data, events and reacting

When an ERC20 transfer takes place, balance state is updated and an event is emitted. The ERC20 transfer scenario is very common in smart contracts i.e. publishing state and emitting an event (also known as a log). Somnia Data Streams offers you the tooling to do this without the requirements of having to write your own custom Solidity contract. It also allows you to take advantage of existing schemas for publishing data yielding composibility benefits for applications.\
\
Example:

```typescript
import { SDK } from "@somnia-chain/streams"
import { zeroAddress, erc721Abi } from "viem"

// Use WebSocket transport in the public client for subscription tasks
// For the SDK instance that executes transactions, stick with htttp
const sdk = new SDK({
    public: getPublicClient(),
    wallet: getWalletClient(),
})

// Encode view function calls to be executed when an event takes place
const ethCalls = [{
    to: "0x23B66B772AE29708a884cca2f9dec0e0c278bA2c",
    data: encodeFunctionData({
        abi: erc721Abi,
        functionName: "balanceOf",
        args: ["0x3dC360e0389683cA0341a11Fc3bC26252b5AF9bA"]
    })
}]

// Start a subsciption
const subscription = await sdk.streams.subscribe({

    ethCalls,
    onData: (data) => {
        const decodedLog = decodeEventLog({
            abi: fireworkABI,
            topics: data.result.topics,
            data: data.result.data,
        });

        const decodedFunctionResult = decodeFunctionResult({
            abi: erc721Abi,
            functionName: 'balanceOf',
            data: data.result.simulationResults[0],
        });

        console.log("Decoded event", decodedLog);
        console.log("Decoded function call result", decodedFunctionResult);
    }
})

// Write data and emit events that will trigger the above callback!
const dataStreams = [{
    id,
    schemaId: driverSchemaId,
    data: encodedData
}]

const eventStreams = [{
    id: somniaStreamsEventId,
    argumentTopics,
    data
}]

const setAndEmitEventsTxHash = await sdk.streams.setAndEmitEvents(
    dataStreams,
    eventStreams
)
```

Writing data and emitting events will trigger a call back to subscribers that care about a specified event emitted from the Somnia Data Streams protocol (or any contract for that matter) without having the need to poll the chain. It follows the observer pattern meaning push rather than pull which is always a more efficient paradigm.


# Data Provenance and Verification in Streams

When consuming data from any source, especially in a decentralized environment, the most critical question is: **"Can I trust this data?"**

This question is not just about the data's content, but its *origin*. How do you know that data claiming to be from a trusted oracle, a specific device, or another user *actually* came from them and not from an imposter?

This is the challenge of **Data Provenance**.

In Somnia Data Streams, provenance is not an optional feature or a "best practice". It is a fundamental, cryptographic guarantee built into the core smart contract. This article explains how Streams ensures authenticity via publisher signatures and how you can verify data origin.

## The Cryptographic Guarantee: `msg.sender` as Provenance

The trust layer of Somnia Streams is elegantly simple. It does not rely on complex off-chain signature checking or data fields like `senderName`. Instead, it leverages the most basic and secure primitive of the EVM: `msg.sender`.

All data published to Streams is stored in the core `Streams` smart contract. The data storage mapping has a specific structure:

#### **Conceptual Contract Storage**

```solidity
// mapping: schemaId => publisherAddress => dataId => data
mapping(bytes32 => mapping(address => mapping(bytes32 => bytes))) public dsstore;
```

When a publisher calls `sdk.streams.set(...)` or `sdk.streams.setAndEmitEvents(...)`, their wallet signs a transaction. The `Streams` smart contract receives this transaction and identifies the signer's address via the `msg.sender` variable.

The contract then stores the data *at the `msg.sender`'s address* within the schema's mapping.

**This is the cryptographic guarantee.**

It is **impossible** for `0xPublisher_A` to send a transaction that writes data into the slot for `0xPublisher_B`. They cannot fake their `msg.sender`. The data is automatically and immutably tied to the address of the account that paid the gas to publish it.

* An attacker **cannot** write data as if it came from a trusted oracle.
* A user **cannot** send a chat message pretending to be another user.
* Data integrity is linked directly to wallet security.

## Verification Is Implicit in the Read Operation

Because the `publisher` address is a fundamental key in the storage mapping, you don't need to perform complex "verification" steps. **Verification is implicit in the read operation.**

When you use the SDK to read data, you must specify which publisher you are interested in:

* `sdk.streams.getByKey(schemaId, publisher, key)`
* `sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)`

When you call `getAllPublisherDataForSchema(schemaId, '0xTRUSTED_ORACLE_ADDRESS')`, you are not *filtering* data. You are asking the smart contract to retrieve data from the specific storage slot that *only* `0xTRUSTED_ORACLE_ADDRESS` could have written to.

If an imposter (`0xIMPOSTER_ADDRESS`) publishes data using the same `schemaId`, their data is stored in a completely different location (`dsstore[schemaId]['0xIMPOSTER_ADDRESS']`). It will never be returned when you query for the trusted address.

## Deliverable: Building a Verification Script

Let's build a utility to prove this concept.

**Scenario:** We have a shared `oraclePrice` schema. Two different, trusted oracles (`0xOracle_A` and `0xOracle_B`) publish prices to it. We will build a script that verifies the origin of data and proves that an `imposter` cannot pollute their feeds.

### **Project Setup**

We will use the same project setup as the "[Multi-Publisher Aggregator](https://emre-gitbook.gitbook.io/emre-gitbook-docs/data-streams/working-with-multiple-publishers-in-a-shared-stream#tutorial-building-a-multi-publisher-aggregator-app)" tutorial. You will need a `.env` file with at least one private key to act as a publisher, and we will simulate the other addresses.

[**`src/lib/clients.ts`**](https://emre-gitbook.gitbook.io/emre-gitbook-docs/data-streams/working-with-multiple-publishers-in-a-shared-stream#chain-and-client-configuration) (No changes needed from the previous tutorial. We just need `publicClient`.)

**`src/lib/schema.ts`**

```typescript
export const oraclePriceSchema = 'uint256 price, uint64 timestamp'
```

### **The Verification Script**

This script will not publish data. We will assume our two trusted oracles (`PUBLISHER_1_PK` and `PUBLISHER_2_PK` from the previous tutorial) have already published data using the `oraclePriceSchema`.

Our script will:

1. Define a list of `TRUSTED_ORACLES`.
2. Define an `IMPOSTER_ORACLE` (a random address that has *not* published).
3. Create a `verifyPublisher` function that fetches data *only* for a specific publisher address.
4. Run verification for all addresses and show that data is only returned for the correct publishers.

**`src/scripts/verifyOrigin.ts`**

```typescript
import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { publicClient } from '../lib/clients' // Assuming you have clients.ts from previous tutorial
import { oraclePriceSchema } from '../lib/schema'
import { Address, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'

// --- Setup: Define our trusted and untrusted addresses ---

function getEnv(key: string): string {
  const value = process.env[key]
  if (!value) throw new Error(`Missing environment variable: ${key}`)
  return value
}

// These are the addresses we trust for this schema.
// We get them from our .env file for this example.
const TRUSTED_ORACLES: Address[] = [
  privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`).address,
  privateKeyToAccount(getEnv('PUBLISHER_2_PK') as `0x${string}`).address,
]

// This is a random, untrusted address.
const IMPOSTER_ORACLE: Address = '0x1234567890123456789012345678901234567890'

// --- Helper Functions ---

// Helper to decode the oracle data
function decodePriceRecord(row: SchemaDecodedItem[]): { price: bigint, timestamp: number } {
  const val = (field: any) => field?.value?.value ?? field?.value ?? ''
  return {
    price: BigInt(val(row[0])),
    timestamp: Number(val(r[1])),
  }
}

/**
 * Verification Utility
 * Fetches data for a *single* publisher to verify its origin.
 */
async function verifyPublisher(sdk: SDK, schemaId: `0x${string}`, publisherAddress: Address) {
  console.log(`\n--- Verifying Publisher: ${publisherAddress} ---`)
  
  try {
    const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
    
    if (!data || data.length === 0) {
      console.log('[VERIFIED] No data found for this publisher.')
      return
    }

    const records = (data as SchemaDecodedItem[][]).map(decodePriceRecord)
    console.log(`[VERIFIED] Found ${records.length} record(s) cryptographically signed by this publisher:`)
    
    records.forEach(record => {
      console.log(`  - Price: ${record.price}, Time: ${new Date(record.timestamp).toISOString()}`)
    })

  } catch (error: any) {
    console.error(`Error during verification: ${error.message}`)
  }
}

// --- Main Execution ---

async function main() {
  const sdk = new SDK({ public: publicClient })
  
  const schemaId = await sdk.streams.computeSchemaId(oraclePriceSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')

  console.log('Starting Data Provenance Verification...')
  console.log(`Schema: oraclePriceSchema (${schemaId})`)

  // 1. Verify our trusted oracles
  for (const oracleAddress of TRUSTED_ORACLES) {
    await verifyPublisher(sdk, schemaId, oracleAddress)
  }

  // 2. Verify the imposter
  // This will securely return NO data, even if the imposter
  // published data to the same schemaId under their *own* address.
  await verifyPublisher(sdk, schemaId, IMPOSTER_ORACLE)
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

### **Expected Output**

To run this, first publish some data (using the script from the previous tutorial, but adapted for `oraclePriceSchema`) from both `PUBLISHER_1_PK` and `PUBLISHER_2_PK`. Then, run the verification script.

```bash
# Add to package.json
"verify": "ts-node src/scripts/verifyOrigin.ts"

# Run it
npm run verify
```

You will see an output similar to this:

```bash
Starting Data Provenance Verification...
Schema: oraclePriceSchema (0x...)

--- Verifying Publisher: 0xPublisher1Address... ---
[VERIFIED] Found 2 record(s) cryptographically signed by this publisher:
  - Price: 3200, Time: 2025-10-31T12:30:00.000Z
  - Price: 3201, Time: 2025-10-31T12:31:00.000Z

--- Verifying Publisher: 0xPublisher2Address... ---
[VERIFIED] Found 1 record(s) cryptographically signed by this publisher:
  - Price: 3199, Time: 2025-10-31T12:30:30.000Z

--- Verifying Publisher: 0x1234567890123456789012345678901234567890 ---
[VERIFIED] No data found for this publisher.
```

## Conclusion: Key Takeaways

* **Provenance is Built-In:** Data provenance in Somnia Streams is not an optional feature; it is a core cryptographic guarantee of the `Streams` smart contract, enforced by `msg.sender`.
* **Verification is Implicit:** You verify data origin every time you perform a read operation with `getAllPublisherDataForSchema` or `getByKey`. The `publisher` address acts as the ultimate verification key.
* **Trust Layer:** This architecture creates a robust trust layer. Your application logic can be certain that any data returned for a specific publisher was, without question, signed and submitted by that publisher's wallet.


# SDK Methods Guide

Detailed SDK interface handbook for interacting with the Somnia Data Streams protocol via the typescript SDK

Somnia Data Streams is the on-chain data streaming protocol that powers real-time, composable applications on the Somnia Network. It is available as an SDK Package [@somnia-chain/streams](https://www.npmjs.com/package/@somnia-chain/streams).

This SDK exposes all the core functionality developers need to write, read, subscribe to, and manage Data Streams and events directly from their dApps.

Before using the Data Streams SDK, ensure you have a working Node.js or Next.js environment (Node 18+ recommended). You’ll need access to a Somnia RPC endpoint (Testnet or Mainnet) and a wallet private key for publishing data.

### Installation

```bash
npm i @somnia-chain/streams viem dotenv
```

The SDK depends on [viem](https://viem.sh/) for blockchain interactions.

### Project Setup

Create a `.env.local` or `.env` file in your project root:

```bash
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=your_private_key_here
```

⚠️ Never expose private keys in client-side code. Keep writes (publishing data) in server routes or backend environments.

### Basic Initialization

You’ll typically use two clients:

* A public client for reading and subscribing
* A wallet client for writing to the Somnia chain

```typescript
import { SDK } from '@somnia-chain/streams'
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from 'viem/chains'

const rpcUrl = process.env.RPC_URL!
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)

const sdk = new SDK({
  public: createPublicClient({ chain: somniaTestnet, transport: http(rpcUrl) }),
  wallet: createWalletClient({ chain: somniaTestnet, account, transport: http(rpcUrl) })
})
```

### Somnia Data Streams

Data Streams in Somnia represent structured, verifiable data channels. Every piece of data conforms to a schema that defines its structure (e.g. `timestamp`, `content`, `sender`), and publishers can emit this data either as on-chain transactions or off-chain event notifications.

The SDK follows a simple pattern:

```typescript
const sdk = new SDK({
  public: getPublicClient(),  // for reading and subscriptions
  wallet: getWalletClient()   // for writing
})
```

You’ll interact primarily through the `sdk.streams` interface.

### Core Methods Overview

### Write

#### `set(d: DataStream[]): Promise<Hex | null>`

**Description**

Publishes one or more data streams to the Somnia blockchain. Each stream must specify a `schema ID`, a `unique ID`, and the `encoded payload`.

**Use Case**

When you want to store data on-chain in a standardized format (e.g., chat messages, sensor telemetry, or leaderboard updates).

**Example**

```typescript
const tx = await sdk.streams.set([
  { id: dataId, schemaId, data }
])
console.log('Data published with tx hash:', tx)
```

Always register your schema before calling `set()` ; otherwise, the transaction will revert.

#### `emitEvents(e: EventStream[]): Promise<Hex | Error | null>`

**Description**

Emits a registered Streams event without persisting new data. This is used for off-chain reactivity, triggering listeners subscribed via WebSocket.

**Example**

```typescript
await sdk.streams.emitEvents([
  {
    id: 'ChatMessage',
    argumentTopics: [topic],
    data: '0x' // optional encoded payload
  }
])
```

Common Use includes notifying subscribers when something happens, e.g., “new message sent” or “order filled”.

#### `setAndEmitEvents(d: DataStream[], e: EventStream[]): Promise<Hex | Error | null>`

**Description**

Performs an atomic on-chain operation that both writes data and emits a corresponding event. This ensures your data and notifications are always in sync.

**Example**

```typescript
await sdk.streams.setAndEmitEvents(
  [{ id: dataId, schemaId, data }],
  [{ id: 'ChatMessage', argumentTopics: [topic], data: '0x' }]
)
```

It is ideal for chat apps, game updates, or IoT streams — where data must be recorded and instantly broadcast.

### Manage

#### `registerDataSchemas(registrations: DataSchemaRegistration[], ignoreRegisteredSchemas?: boolean): Promise<Hex | Error | null>`

**Description**

Registers a new data schema on-chain. Schemas define the structure of your Streams data, like a table schema in a database. The optional `ignoreRegisteredSchemas` parameter allows skipping registration if the schema is already registered.

**Example**

```typescript
await sdk.streams.registerDataSchemas([
  {
    schemaName: "chat",
    schema: 'uint64 timestamp, string message, address sender',
    parentSchemaId: zeroBytes32 // root schema
  }
], true) // Optionally ignore if already registered
```

Register before writing any new data type. If you modify the schema structure later, register it again as a new schema version.

#### `registerEventSchemas(ids: string[], schemas: EventSchema[]): Promise<Hex | Error | null>`

**Description**

Registers event definitions that can later be emitted or subscribed to.

**Example**

```typescript
await sdk.streams.registerEventSchemas(
  ['ChatMessage'],
  [{
    params: [{ name: 'roomId', paramType: 'bytes32', isIndexed: true }],
    eventTopic: 'ChatMessage(bytes32 indexed roomId)'
  }]
)
```

Use before calling `emitEvents()` or `subscribe()` for a specific event.

#### `manageEventEmittersForRegisteredStreamsEvent(streamsEventId: string, emitter: Address, isEmitter: boolean): Promise<Hex | Error | null>`

**Description**

Grants or revokes permission for an address to emit a specific event.

**Example**

```typescript
await sdk.streams.manageEventEmittersForRegisteredStreamsEvent(
  'ChatMessage',
  '0x1234abcd...',
  true // allow this address to emit
)
```

Used for access control in multi-publisher systems.

### Read

#### `getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>`

**Description**

Retrieves data stored under a schema by its unique ID.

**Example**

```typescript
const msg = await sdk.streams.getByKey(schemaId, publisher, dataId)
console.log('Data:', msg)
```

An example includes fetching a specific record, e.g., “fetch message by message ID”.

#### `getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>`

**Description**

Fetches the record at a given index (0-based).

**Example**

```typescript
const record = await sdk.streams.getAtIndex(schemaId, publisher, 0n)
```

It is useful for sequential datasets like logs or telemetry streams.

#### `getBetweenRange(schemaId: SchemaID, publisher: Address, startIndex: bigint, endIndex: bigint): Promise<Hex[] | SchemaDecodedItem[][] | Error | null>`

**Description**

Fetches records within a specified index range (0-based, inclusive start, exclusive end).

**Use Case**

Retrieving a batch of historical data, such as paginated logs or time-series entries.

**Example**

```typescript
const records = await sdk.streams.getBetweenRange(schemaId, publisher, 0n, 10n)
console.log('Records in range:', records)
```

#### `getAllPublisherDataForSchema(schemaReference: SchemaReference, publisher: Address): Promise<Hex[] | SchemaDecodedItem[][] | null>`

**Description**

Retrieves all data published by a specific address under a given schema.

**Use Case**

Fetching complete datasets for analysis or synchronization.

**Example**

```typescript
const allData = await sdk.streams.getAllPublisherDataForSchema(schemaReference, publisher)
console.log('All publisher data:', allData)
```

#### `getLastPublishedDataForSchema(schemaId: SchemaID, publisher: Address): Promise<Hex[] | SchemaDecodedItem[][] | null>`

**Description**

Retrieves the most recently published data under a schema by a publisher.

**Use Case**

Getting the latest update, such as the most recent sensor reading or message.

**Example**

```typescript
const latest = await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher)
console.log('Latest data:', latest)
```

#### `totalPublisherDataForSchema(schemaId: SchemaID, publisher: Address): Promise<bigint | null>`

**Description**

Returns how many records a publisher has stored under a schema.

**Example**

```typescript
const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
console.log(`Total entries: ${total}`)
```

#### `isDataSchemaRegistered(schemaId: SchemaID): Promise<boolean | null>`

**Description**

Checks if a schema exists on-chain.

**Example**

```typescript
const exists = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!exists) console.log('Schema not found')
```

#### `parentSchemaId(schemaId: SchemaID): Promise<Hex | null>`

**Description**

Finds the parent schema of a given schema, if one exists.

**Example**

```typescript
const parent = await sdk.streams.parentSchemaId(schemaId)
console.log('Parent Schema ID:', parent)
```

#### `schemaIdToId(schemaId: SchemaID): Promise<string | null>`

**Description**

Converts a schema ID (Hex) to its corresponding string identifier.

**Use Case**

Mapping hashed IDs back to human-readable names for display or logging.

**Example**

```typescript
const id = await sdk.streams.schemaIdToId(schemaId)
console.log('Schema ID string:', id)
```

#### `idToSchemaId(id: string): Promise<Hex | null>`

**Description**

Converts a string identifier to its corresponding schema ID (Hex).

**Use Case**

Looking up hashed IDs from known names for queries.

**Example**

```typescript
const schemaId = await sdk.streams.idToSchemaId('chat')
console.log('Schema ID:', schemaId)
```

#### `getAllSchemas(): Promise<string[] | null>`

**Description**

Retrieves a list of all registered schema identifiers.

**Use Case**

Discovering available schemas in the protocol.

**Example**

```typescript
const schemas = await sdk.streams.getAllSchemas()
console.log('All schemas:', schemas)
```

#### `getEventSchemasById(ids: string[]): Promise<EventSchema[] | null>`

**Description**

Fetches event schema details for given identifiers.

**Use Case**

Inspecting registered event structures before subscribing or emitting.

**Example**

```typescript
const eventSchemas = await sdk.streams.getEventSchemasById(['ChatMessage'])
console.log('Event schemas:', eventSchemas)
```

#### `computeSchemaId(schema: string): Promise<Hex | null>`

**Description**

Computes the deterministic `schemaId` without registering it.

**Example**

```typescript
const schemaId = await sdk.streams.computeSchemaId('uint64 timestamp, string content')
```

#### `getSchemaFromSchemaId(schemaId: SchemaID): Promise<{ baseSchema: string, finalSchema: string, schemaId: Hex } | Error | null>`

**Description**

Request a schema given the schema id used for data publishing and let the SDK take care of schema extensions.

**Use Case**

Retrieving schema details, including the base schema and the final extended schema, for a given schema ID.

**Example**

```typescript
const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
console.log('Schema info:', schemaInfo)
```

### Helpers

#### `deserialiseRawData(rawData: Hex[], parentSchemaId: Hex, schemaLookup: { schema: string; schemaId: Hex; } | null): Promise<Hex[] | SchemaDecodedItem[][] | null>`

**Description**

Deserializes raw data using the provided schema information.

**Use Case**

Decoding fetched raw bytes into structured objects for application use.

**Example**

```typescript
const decoded = await sdk.streams.deserialiseRawData(rawData, parentSchemaId, schemaLookup)
console.log('Decoded data:', decoded)
```

### Subscribe

#### `subscribe(initParams: SubscriptionInitParams): Promise<{ subscriptionId: string, unsubscribe: () => void } | undefined>`

**Description**

Creates a real-time WebSocket subscription to a Streams event. Whenever the specified event fires, the SDK calls your `onData` callback — optionally including enriched data from on-chain calls.

**Parameters**

* `ethCalls`: Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array.
* `context`: Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, topic4, data and address.
* `onData`: Callback for a successful reactivity notification.
* `onError`: Callback for a failed attempt.
* `eventContractSource`: Optional but is the contract event source (any on Somnia) that will be emitting the logs specified by topicOverrides.
* `topicOverrides`: Optional but this argument is a filter applied to the subscription. Up to 4 bytes32 event topics can be supplied. By not defining, this is the equivalent of a wildcard subscription to all event topics
* `onlyPushChanges`: Whether the data should be pushed to the subscriber only if eth\_call results are different from the previous.

**Example**

```typescript
// Wildcard subscription to all events emitted by all contracts
await sdk.streams.subscribe({
    ethCalls: [], // No view calls
    onData: (data) => {}
})
```

**With `ethCalls`**

```typescript
import { toEventSelector } from "viem"
const transferSelector = toEventSelector({
    name: 'Transfer',
    type: 'event',
    inputs: [
      { type: 'address', indexed: true, name: 'from' },
      { type: 'address', indexed: true, name: 'to' },
      { type: 'uint256', indexed: false, name: 'value' }
    ]
  })

await sdk.streams.subscribe({
  topicOverrides: [
    transferSelector, // Topic 0 (Transfer event)
  ],
  ethCalls: [{
    to: '0xERC20Address',
    data: encodeFunctionData({
      abi: erc20Abi,
      functionName: 'balanceOf',
      args: ['0xUserAddress']
    })
  }],
  onData: (data) => console.log('Trade + balance data:', data)
})
```

Useful for off-chain reactivity: real-time dashboards, chat updates, live feeds, or notifications.

**Notes**

* Requires `createPublicClient({ transport: webSocket() })`
* Use `setAndEmitEvents()` on the publisher side to trigger matching subscriptions.

### Protocol

#### `getSomniaDataStreamsProtocolInfo(): Promise<GetSomniaDataStreamsProtocolInfoResponse | Error | null>`

**Description**

Retrieves information about the Somnia Data Streams protocol.

**Use Case**

Fetching protocol-level details, such as version or configuration.

**Example**

```typescript
const info = await sdk.streams.getSomniaDataStreamsProtocolInfo()
console.log('Protocol info:', info)
```

### Key Types Reference

| Type                     | Description                                                                                              |
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `DataStream`             | `{ id: Hex, schemaId: Hex, data: Hex }` – Used with `set()` or `setAndEmitEvents()` .                    |
| `EventStream`            | `{ id: string, argumentTopics: Hex[], data: Hex }` – Used with `emitEvents()` and `setAndEmitEvents()` . |
| `DataSchemaRegistration` | `{ schemaName: string, schema: string, parentSchemaId: Hex }` – For `registerDataSchemas()` .            |
| `EventSchema`            | `{ params: EventParameter[], eventTopic: string }` – For `registerEventSchemas()` .                      |
| `EthCall`                | `{ to: Address, data: Hex }` – Defines on-chain calls for event enrichment.                              |

### Developer Tips

* Always compute your schema ID locally before deploying: `await sdk.streams.computeSchemaId(schema)`.
* For chat-like or telemetry apps, pair `setAndEmitEvents()` (write) with `subscribe()` (read).
* Use `zeroBytes32` for base schemas that don’t extend others.
* All write methods return transaction hashes, use `waitForTransactionReceipt()` to confirm.
* Data Streams focus on persistent, schema-based storage, while Event Streams enable reactive notifications; use them together for comprehensive applications.


# Tutorials


# “Hello World” App

Build a Hello World program to understand Somnia Data Streams.

If you’ve ever wanted to see your data travel onchain in real time, this is the simplest way to begin. In this guide, we’ll build and run a Hello World Publisher and Subscriber using the Somnia Data Streams SDK. It demonstrates how to define a schema, publish onchain data, and read it in real time.

Somnia Data Streams enables developers to store, retrieve, and react to real-time blockchain data without needing to build indexers or manually poll the chain.

Each app works around three key ideas:

1. Schemas – define the data format.
2. Data IDs – uniquely identify each record.
3. Publishers – wallet addresses that own and post data.

Your app can write (“publish”) data using one account, and another app (or user) can “subscribe” to read or monitor that data stream. This “Hello World” project demonstrates exactly how that works.

## Prerequisites

Before you begin:

* Node.js 20+
* A Somnia Testnet wallet with STT test tokens
* `.env` file containing your wallet credentials

## Project Setup

Create a Project Directory and install dependencies [@somnia-chain/streams](https://www.npmjs.com/package/@somnia-chain/streams) and [viem](https://viem.sh/):

```bash
npm i @somnia-chain/streams viem dotenv
```

Now create a .env file to hold your test wallet’s private key:

```bash
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
PUBLIC_KEY=0xYOUR_PUBLIC_ADDRESS
```

## Project Overview

The project contains four files:

| File           | Description                                         |
| -------------- | --------------------------------------------------- |
| publisher.js   | Sends “Hello World” messages to Somnia Data Streams |
| subscriber.js  | Reads and displays those messages                   |
| dream-chain.js | Configures the Somnia Dream testnet connection      |
| package.json   | Handles dependencies and npm scripts                |

## Network Configuration

The file `dream-chain.js` defines the blockchain network connection.

```javascript
const { defineChain } = require("viem");
const dreamChain = defineChain({
  id: 50312,
  name: "Somnia Dream",
  network: "somnia-dream",
  nativeCurrency: { name: "STT", symbol: "STT", decimals: 18 },
  rpcUrls: {
    default: { http: ["https://dream-rpc.somnia.network"] },
  },
});

module.exports = { dreamChain };

```

This allows both publisher and subscriber scripts to easily reference the same testnet environment.

## Hello World Publisher

The publisher connects to the blockchain, registers a schema if necessary, and sends a “Hello World” message every few seconds.

```javascript
const { SDK, SchemaEncoder, zeroBytes32 } = require("@somnia-chain/streams")
const { createPublicClient, http, createWalletClient, toHex } = require("viem")
const { privateKeyToAccount } = require("viem/accounts")
const { waitForTransactionReceipt } = require("viem/actions")
const { dreamChain } = require("./dream-chain")
require("dotenv").config()

async function main() {
  const publicClient = createPublicClient({ chain: dreamChain, transport: http() })
  const walletClient = createWalletClient({
    account: privateKeyToAccount(process.env.PRIVATE_KEY),
    chain: dreamChain,
    transport: http(),
  })

  const sdk = new SDK({ public: publicClient, wallet: walletClient })

  // 1️⃣ Define schema
  const helloSchema = `string message, uint256 timestamp, address sender`
  const schemaId = await sdk.streams.computeSchemaId(helloSchema)
  console.log("Schema ID:", schemaId)

  // 2️⃣ Safer schema registration
  const ignoreAlreadyRegistered = true

  try {
    const txHash = await sdk.streams.registerDataSchemas(
      [
        {
          schemaName: 'hello_world',
          schema: helloSchema,
          parentSchemaId: zeroBytes32
        },
      ],
      ignoreAlreadyRegistered
    )

    if (txHash) {
      await waitForTransactionReceipt(publicClient, { hash: txHash })
      console.log(`✅ Schema registered or confirmed, Tx: ${txHash}`)
    } else {
      console.log('ℹ️ Schema already registered — no action required.')
    }
  } catch (err) {
    // fallback: if the SDK doesn’t support the flag yet
    if (String(err).includes('SchemaAlreadyRegistered')) {
      console.log('⚠️ Schema already registered. Continuing...')
    } else {
      throw err
    }
  }

  // 3️⃣ Publish messages
  const encoder = new SchemaEncoder(helloSchema)
  let count = 0

  setInterval(async () => {
    count++
    const data = encoder.encodeData([
      { name: 'message', value: `Hello World #${count}`, type: 'string' },
      { name: 'timestamp', value: BigInt(Math.floor(Date.now() / 1000)), type: 'uint256' },
      { name: 'sender', value: walletClient.account.address, type: 'address' },
    ])

    const dataStreams = [{ id: toHex(`hello-${count}`, { size: 32 }), schemaId, data }]
    const tx = await sdk.streams.set(dataStreams)
    console.log(`✅ Published: Hello World #${count} (Tx: ${tx})`)
  }, 3000)
}

main()

```

This function connects to Somnia Dream Testnet using your wallet and computes the schema ID for the message structure. It then registers the schema if not already registered.\
The \`encodeData\` method encodes each message as a structured data packet, and it then publishes data to the chain using sdk.streams.set().

Each transaction is a verifiable, timestamped on-chain record.

***

## Hello World Subscriber

The subscriber listens for any messages published under the same schema and publisher address.\
It uses a simple polling mechanism, executed every 3 seconds, to fetch and decode updates.

```javascript
const { SDK, SchemaEncoder } = require("@somnia-chain/streams");
const { createPublicClient, http } = require("viem");
const { dreamChain } = require("./dream-chain");
require('dotenv').config();

async function main() {
  const publisherWallet = process.env.PUBLISHER_WALLET;
  const publicClient = createPublicClient({ chain: dreamChain, transport: http() });
  const sdk = new SDK({ public: publicClient });

  const helloSchema = `string message, uint256 timestamp, address sender`;
  const schemaId = await sdk.streams.computeSchemaId(helloSchema);

  const schemaEncoder = new SchemaEncoder(helloSchema);
  const result = new Set();

  setInterval(async () => {
    const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherWallet);
    for (const dataItem of allData) {
      const fields = dataItem.data ?? dataItem;
      let message = "", timestamp = "", sender = "";
      for (const field of fields) {
        const val = field.value?.value ?? field.value;
        if (field.name === "message") message = val;
        if (field.name === "timestamp") timestamp = val.toString();
        if (field.name === "sender") sender = val;
      }

      const id = `${timestamp}-${message}`;
      if (!result.has(id)) {
        result.add(id);
        console.log(`🆕 ${message} from ${sender} at ${new Date(Number(timestamp) * 1000).toLocaleTimeString()}`);
      }
    }
  }, 3000);
}

main();
```

This function computes the same Schema ID used by the publisher. It polls the blockchain for all messages from that publisher and decodes data according to the schema fields. Then, it displays any new messages with timestamps and sender addresses.

***

## Run the App

Run both scripts in separate terminals:

```bash
npm run publisher
```

and then

```bash
npm run subscriber
```

You’ll see Publisher Output:

```bash
Schema ID: 0x27c30fa6547c34518f2de6a268b29ac3b54e51c98f8d0ef6018bbec9153e9742
⚠️ Schema already registered. Continuing...
✅ Published: Hello World #1 (Tx: 0xf21ad71a6c7aa54c171ad38b79ef417e8488fd750ce00c1357918b7c7fa5c951)
✅ Published: Hello World #2 (Tx: 0xe999b0381ba9d937d85eb558fefe214fa4e572767c4e698c6e31588ff0e68f0a)
```

Subscriber Output

```bash
🆕 Hello World #2 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:04 PM
🆕 Hello World #3 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:07 PM
```

Congratulations 🎉 You’ve just published and read blockchain data using Somnia Data Streams!

This is the foundation for real-time decentralized apps, chat apps, dashboards, IoT feeds, leaderboards, and more.

***

## Conclusion

You’ve just learned how to:

* Define and compute a schema and schema ID
* Register it on the Somnia Testnet
* Publish and subscribe to on-chain structured data
* Decode and render blockchain messages in real time

This simple 'Hello World' app is your first step toward building real-time, decentralized applications on Somnia.


# Build Your First Schema

Before you can publish or read structured data on the Somnia Network using Somnia Data Streams, you must first define a Schema.\
A schema acts as the blueprint or data contract between your publisher and all subscribers who wish to interpret your data correctly.

In the Somnia Data Streams system, every schema is expressed as a canonical string. A strict, ordered list of fields with Solidity compatible types.

\
For example, a chat application schema:

```solidity
uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
```

This simple definition:

* Establishes how data should be encoded and decoded on-chain.
* Produces a unique schemaId derived from its exact string representation.
* Enables multiple publishers and readers to exchange data consistently, without needing to redeploy contracts or agree on custom ABIs.

Each schema you define becomes a typed, reusable data model, similar to a table definition in a database or an ABI for events, but far simpler. Once created, schemas can be:

* Reused across many applications.
* Extended to create hierarchical data definitions (e.g., “GPS coordinates” → “Vehicle telemetry”).
* Versioned by creating new schemas when structure changes occur.<br>

This tutorial will walk you through building, registering, and validating your first schema step by step.

## Prerequisites

Before continuing, ensure you have the following:

1. Node.js 20+
2. TypeScript configured in your project
3. `.env.local` file for environment variables

   Add your credentials to .env.local:

   ```markup
   RPC_URL=https://dream-rpc.somnia.network
   PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
   ```
4. A Funded Testnet Account. You’ll need an address with test tokens on the Somnia Testnet to register schemas or publish data.<br>

{% hint style="danger" %}
NOTE: The Private Key is only required if connecting a Private Key via a Viem wallet account.\
\
Important: Never expose your private key to a client-side environment. Keep it in server scripts or backend environments only.
{% endhint %}

***

## What You’ll Build

In this tutorial, you will:

* Create a canonical schema string (your “data ABI”)
* Compute the schema ID
* Register your schema on-chain (idempotently)
* Validate your schema with a simple encode/decode test<br>

We’ll use a chat message schema as a running example:

```solidity
uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
```

This schema represents a single chat message, which can be used later to build a full on-chain chat application.

***

## Project Setup

### Install dependencies

```bash
npm i @somnia-chain/streams viem
npm i -D @types/node
```

### Define Chain configuration

```typescript
// src/lib/chain.ts
import { defineChain } from 'viem'

export const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  network: 'somnia-testnet',
  nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://dream-rpc.somnia.network'] },
    public:  { http: ['https://dream-rpc.somnia.network'] },
  },
})
```

### Set up your clients

```typescript
// src/lib/clients.ts
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'

function need(key: 'RPC_URL' | 'PRIVATE_KEY') {
  const v = process.env[key]
  if (!v) throw new Error(`Missing ${key} in .env.local`)
  return v
}

export const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(need('RPC_URL')),
})

export const walletClient = createWalletClient({
  account: privateKeyToAccount(need('PRIVATE_KEY') as `0x${string}`),
  chain: somniaTestnet,
  transport: http(need('RPC_URL')),
})
```

***

## Define the Schema String

```typescript
// src/lib/chatSchema.ts
export const chatSchema =
  'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
```

Field order matters, and ensure to always use Solidity-compatible types. It is important to keep the `string` fields short to minimize gas. Note that changing type or order creates a new schema ID.<br>

***

## Compute the schemaId

```typescript
// scripts/compute-schema-id.ts
import 'dotenv/config'
import { SDK } from '@somnia-chain/streams'
import { publicClient } from '../src/lib/clients'
import { chatSchema } from '../src/lib/chatSchema'

async function main() {
  const sdk = new SDK({ public: publicClient })
  const id = await sdk.streams.computeSchemaId(chatSchema)
  console.log('Schema ID:', id)
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

The SDK computes a unique hash of the schema string. This `schemaId` is your permanent identifier. Anyone using the same schema string will derive the same ID *\[confirm with Vincent for correctness]*.<br>

***

## Register the Schema

Registration makes your schema discoverable and reusable by others. *\[confirm with Vincent for correctness]*.

```typescript
// scripts/register-schema.ts
import 'dotenv/config'
import { SDK, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../src/lib/clients'
import { chatSchema } from '../src/lib/chatSchema'
import { waitForTransactionReceipt } from 'viem/actions'

async function main() {
  const sdk = new SDK({ public: publicClient, wallet: walletClient })
  const id = await sdk.streams.computeSchemaId(chatSchema)

  const isRegistered = await sdk.streams.isSchemaRegistered(id)
  if (isRegistered) {
    console.log('Schema already registered.')
    return
  }

  const txHash = await sdk.streams.registerDataSchemas({ schemaName: "chat", schema: chatSchema })
  console.log('Register tx:', txHash)

  const receipt = await waitForTransactionReceipt(publicClient, { hash: txHash })
  console.log('Registered in block:', receipt.blockNumber)
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

`isSchemaRegistered()` checks chain state. `registerSchema()` publishes the schema definition to Streams. Thus, the transaction is idempotent, meaning that it is safe to re-run.

***

## Encode and Decode a Sample Payload

Test your schema locally before publishing any data.

```typescript
// scripts/encode-decode.ts
import 'dotenv/config'
import { SchemaEncoder } from '@somnia-chain/streams'
import { toHex, type Hex } from 'viem'
import { chatSchema } from '../src/lib/chatSchema'

const encoder = new SchemaEncoder(chatSchema)

const encodedData: Hex = encoder.encodeData([
  { name: 'timestamp',  value: Date.now().toString(),     type: 'uint64' },
  { name: 'roomId',     value: toHex('general', { size: 32 }), type: 'bytes32' },
  { name: 'content',    value: 'Hello Somnia!',           type: 'string' },
  { name: 'senderName', value: 'Victory',                 type: 'string' },
  { name: 'sender',     value: '0x0000000000000000000000000000000000000001', type: 'address' },
])

console.log('Encoded:', encodedData)
console.log('Decoded:', encoder.decodeData(encodedData))
```

`encodeData()` serializes the payload according to the schema definition. `decodeData()` restores readable field values from the encoded hex. This step ensures your schema fields align correctly.

***

## Conclusion

You’ve just built and registered your first schema on Somnia Data Streams.

Your schema now acts as a public data contract between any publisher and subscriber that wants to communicate using this structure.<br>


# Streams Case Study: Formula 1

Streaming data from OpenF1 on-chain and building reactive applications

### Schemas

Driver schema

```
uint32 number, string name, string abbreviation, string teamName, string teamColor
```

Cartesian 3D coordinates schema

```
int256 x, int256 y, int256 z
```

The driver schema can extend the cartesian coordinates since the 3D coordinates will be used widely for other applications. Again this promotes re-usability of schemas.

### Schema registration and re-use

```javascript
const { SDK, zeroBytes32, SchemaEncoder } = require("@somnia-chain/streams");
const {
    createPublicClient,
    http,
    createWalletClient,
    toHex,
    defineChain,
} = require("viem");
const { privateKeyToAccount } = require("viem/accounts");

const dreamChain = defineChain({
  id: 50312,
  name: "Somnia Testnet",
  network: "testnet",
  nativeCurrency: {
    decimals: 18,
    name: "STT",
    symbol: "STT",
  },
  rpcUrls: {
    default: {
      http: [
        "https://dream-rpc.somnia.network",
      ],
    },
    public: {
      http: [
        "https://dream-rpc.somnia.network",
      ],
    },
  },
})

async function main() {
    // Connect to the blockchain to read data with the public client
    const publicClient = createPublicClient({
      chain: dreamChain,
      transport: http(),
    })

    const walletClient = createWalletClient({
      account: privateKeyToAccount(process.env.PRIVATE_KEY),
      chain: dreamChain,
      transport: http(),
    })

    // Connect to the SDK
    const sdk = new SDK({
      public: publicClient,
      wallet: walletClient,
    })

    // Setup the schemas
    const coordinatesSchema = `int256 x, int256 y, int256 z`
    const driverSchema = `uint32 number, string name, string abbreviation, string teamName, string teamColor`

    // Derive Etherbase schema metadata
    const coordinatesSchemaId = await sdk.streams.computeSchemaId(
      coordinatesSchema
    )
    if (!coordinatesSchemaId) {
      throw new Error("Unable to compute the schema ID for the coordinates schema")
    }

    const driverSchemaId = await sdk.streams.computeSchemaId(
      driverSchema
    )
    if (!driverSchemaId) {
      throw new Error("Unable to compute the schema ID for the driver schema")
    }

    const extendedSchema = `${driverSchema}, ${coordinatesSchema}`
    console.log("Schemas in use", {
      coordinatesSchemaId,
      driverSchemaId,
      coordinatesSchema,
      driverSchema,
      extendedSchema 
    })

    const isCoordinatesSchemaRegistered = await sdk.streams.isDataSchemaRegistered(coordinatesSchemaId)
    if (!isCoordinatesSchemaRegistered) {
      // We want to publish the driver schema but we need to publish the coordinates schema first before it can be extended
      const registerCoordinatesSchemaTxHash =
        await sdk.streams.registerDataSchemas([
          { schemaName: "coords", schema: coordinatesSchema }
        ])

      if (!registerCoordinatesSchemaTxHash) {
        throw new Error("Failed to register coordinates schema")
      }
      console.log("Registered coordinates schema on-chain", {
        registerCoordinatesSchemaTxHash
      })

      await publicClient.waitForTransactionReceipt({ 
        hash: registerCoordinatesSchemaTxHash
      })
    }

    const isDriverSchemaRegistered = await sdk.streams.isDataSchemaRegistered(driverSchemaId)
    if (!isDriverSchemaRegistered) {
      // Now, publish the driver schema but extend the coordinates schema!
      const registerDriverSchemaTxHash = sdk.streams.registerDataSchemas([
        { schemaName: "driver", schema: driverSchema, parentSchemaId: coordinatesSchemaId }
      ])
      if (!registerDriverSchemaTxHash) {
        throw new Error("Failed to register schema on-chain")
      }
      console.log("Registered driver schema on-chain", {
        registerDriverSchemaTxHash,
      })

      await publicClient.waitForTransactionReceipt({ 
        hash: registerDriverSchemaTxHash
      })
    }

    // Publish some data!! 
    const schemaEncoder = new SchemaEncoder(extendedSchema)
    const encodedData = schemaEncoder.encodeData([
        { name: "number", value: "44", type: "uint32" },
        { name: "name", value: "Lewis Hamilton", type: "string" },
        { name: "abbreviation", value: "HAM", type: "string" },
        { name: "teamName", value: "Ferrari", type: "string" },
        { name: "teamColor", value: "#F91536", type: "string" },
        { name: "x", value: "-1513", type: "int256" },
        { name: "y", value: "0", type: "int256" },
        { name: "z", value: "955", type: "int256" },
    ])
    console.log("encodedData", encodedData)

    const dataStreams = [{
      // Data id: DRIVER number - index will be a helpful lookup later and references ./data/f1-coordinates.js Cube 4 coordinates (driver 44) - F1 telemetry data
      id: toHex(`44-0`, { size: 32 }),
      schemaId: driverSchemaId,
      data: encodedData
    }]

    const publishTxHash = await sdk.streams.set(dataStreams)
    console.log("\nPublish Tx Hash", publishTxHash)
}
```


# READ Stream Data from a UI (Next.js Example)

In this guide, you’ll learn how to read data published to Somnia Data Streams directly from a Next.js frontend, the same way you’d use readContract with Viem.

We’ll build a simple HelloWorld schema and use it to demonstrate all the READ methods in the Somnia Data Streams SDK, from fetching the latest message to retrieving complete datasets or schema metadata.

***

## Prerequisites

Before we begin, make sure you have:

```bash
npm i @somnia-chain/streams viem
```

Also ensure:

* Node.js 20+
* A Somnia Testnet wallet with STT test tokens
* `.env` file containing your wallet credentials:
* A working Next.js app (npx create-next-app somnia-streams-read)
* Access to a publisher address and schema ID (or one you’ve created earlier)

***

## Set up the SDK and Client

We’ll initialize the SDK using Viem’s createPublicClient to communicate with Somnia’s blockchain.

```typescript
// lib/store.ts
import { SDK } from '@somnia-chain/streams'
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from 'viem/chains'

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(),
})

export const sdk = new SDK(publicClient)
```

This sets up the data-reading connection between your frontend and the Somnia testnet.

Think of it as the Streams version of [readContract()](https://viem.sh/docs/contract/readContract#readcontract); it lets you pull structured data (not just variables) directly from the blockchain.

***

## Define Schema and Publisher

A schema describes the structure of data stored in Streams, just like how a smart contract defines the structure of state variables.

```typescript
// lib/schema.ts
export const helloWorldSchema = 'uint64 timestamp, string message'
export const schemaId = '0xabc123...'   // Example Schema ID
export const publisher = '0xF9D3...E5aC' // Example Publisher Address
```

If you don’t have the schema ID handy, you can generate it from its definition:

```typescript
const computedId = await sdk.streams.computeSchemaId(helloWorldSchema)
console.log('Computed Schema ID:', computedId)
```

This ensures that you’re referencing the same schema ID under which the data was published.

***

## Fetch Latest “Hello World” Message

This is the most common use case: getting the most recent data point. For example, displaying the latest sensor reading or chat message.

```typescript
// lib/read.ts
import { sdk } from './store'
import { schemaId, publisher } from './schema'

export async function getLatestMessage() {
  const latest = await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher)
  console.log('Latest data:', latest)
  return latest
}
```

This method retrieves the newest record from that schema-publisher combination.

It’s useful when:

* You’re showing a live dashboard
* You need real-time data polling
* You want to auto-refresh a view (e.g., “Last Updated at…”)

***

## Fetch by Key (e.g., message ID)

Each record can have a unique key, such as a message ID, sensor UUID, or user reference. When you know that key, you can fetch the exact record.

```typescript
export async function getMessageById(messageKey: `0x${string}`) {
  const msg = await sdk.streams.getByKey(schemaId, publisher, messageKey)
  console.log('Message by key:', msg)
  return msg
}
```

When to use:

* Fetching a message by its ID (e.g., “message #45a1”)
* Retrieving a transaction or sensor entry when you know its hash
* Building a detail view (e.g., /message/\[id] route in Next.js)

Think of it like calling readContract for one item by ID.

***

## Fetch by Index (Sequential Logs)

In sequential datasets such as logs, chat history, and telemetry, each record is indexed numerically.\
You can fetch a specific record by its position:

```typescript
export async function getMessageAtIndex(index: bigint) {
  const record = await sdk.streams.getAtIndex(schemaId, publisher, index)
  console.log(`Record at index ${index}:`, record)
  return record
}
```

When to use:

* When looping through entries in order (0, 1, 2, ...)
* To replay logs sequentially
* To test pagination logic

Example: getAtIndex(schemaId, publisher, 0n) retrieves the very first message.

***

## Fetch a Range of Records (Paginated View)

You can fetch multiple entries at once using index ranges.\
This is perfect for pagination or time-series queries.

```typescript
export async function getMessagesInRange(start: bigint, end: bigint) {
  const records = await sdk.streams.getBetweenRange(schemaId, publisher, start, end)
  console.log('Records in range:', records)
  return records
}
```

Example Use Cases:

* Displaying the last 10 chat messages: getBetweenRange(schemaId, publisher, 0n, 10n)
* Loading older telemetry data
* Implementing infinite scroll

{% hint style="info" %}
Tip: Treat start and end like array indices (inclusive start, exclusive end).\
`start` is inclusive and `end` is exclusive.
{% endhint %}

***

## Fetch All Publisher Data for a Schema

If you want to retrieve all content a publisher has ever posted to a given schema, use this.

```typescript
export async function getAllPublisherData() {
  const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
  console.log('All publisher data:', allData)
  return allData
}
```

When to use:

* Generating analytics or trend charts
* Migrating or syncing full datasets
* Debugging data integrity or history<br>

You can think of this as:

“Give me the entire dataset under this schema from this publisher.”

It’s the Streams equivalent of querying all events from a contract.

{% hint style="info" %}
This should be used for small data sets. For larger, paginated reading, `getBetweenRange` is recommended not to overwhelm the node returning the data.
{% endhint %}

***

## Count Total Entries

Sometimes, you just want to know how many entries exist.

```typescript
export async function getTotalEntries() {
  const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
  console.log(`Total entries: ${total}`)
  return Number(total)
}
```

When to use:

* To know the total record count for pagination
* To display dataset stats (“42 entries recorded”)
* To monitor the growth of a stream

This helps determine boundaries for getBetweenRange() or detect when new data arrives.

***

## Inspect Schema Metadata

Schemas define structure, and sometimes you’ll want to validate or inspect them before reading data. First, check that a schema exists when publishing a new schema:

```typescript
  const ignoreAlreadyRegistered = true
  try {
    const txHash = await sdk.streams.registerDataSchemas(
      [
        {
          schemaName: 'hello_world',
          schema: helloSchema,
          parentSchemaId: zeroBytes32
        },
      ],
      ignoreAlreadyRegistered
    )

    if (txHash) {
      await waitForTransactionReceipt(publicClient, { hash: txHash })
      console.log(`Schema registered or confirmed, Tx: ${txHash}`)
    } else {
      console.log('Schema already registered — no action required.')
    }
  } catch (err) {
    // fallback: if the SDK doesn’t support the flag yet
    if (String(err).includes('SchemaAlreadyRegistered')) {
      console.log('Schema already registered. Continuing...')
    } else {
      throw err
    }
  }
```

This is critical to ensure your app doesn’t attempt to query a non-existent or unregistered schema — useful for user-facing dashboards.

***

## Retrieve Full Schema Information

```typescript
const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
console.log('Schema Info:', schemaInfo)
```

This method retrieves both the base schema and its extended structure, if any.\
It automatically resolves inherited schemas, so you get the full picture of what fields exist.

Example output:

```json
{
  baseSchema: 'uint64 timestamp, string message',
  finalSchema: 'uint64 timestamp, string message',
  schemaId: '0xabc123...'
}
```

This is important when you’re visualizing or decoding raw stream data, you can use the schema structure to parse fields correctly (timestamp, string, address, etc.).

***

## Example Next.js App

Now let’s render our fetched data in the UI.

### Project Setup

```bash
npx create-next-app somnia-streams-reader --typescript
cd somnia-streams-reader
npm install @somnia-chain/streams viem
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

***

### Folder Structure

```
somnia-streams-reader/
├── app/
│   ├── api/
│   │   └── latest/route.ts
│   ├── page.tsx
│   ├── layout.tsx
│   └── globals.css
├── components/
│   ├── StreamViewer.tsx
│   └── SchemaInfo.tsx
├── lib/
│   ├── store.ts
│   ├── schema.ts
│   └── read.ts
├── tailwind.config.js
└── package.json
```

***

### lib/store.ts

Sets up the Somnia SDK and connects to the testnet.

```ts
import { SDK } from "@somnia-chain/streams"
import { createPublicClient, http } from "viem"
import { somniaTestnet } from "viem/chains"

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(),
})

export const sdk = new SDK(publicClient)
```

***

### lib/schema.ts

Defines your schema and publisher.

```ts
export const helloWorldSchema = "uint64 timestamp, string message"
export const schemaId = "0xabc123..." // replace with actual schemaId
export const publisher = "0xF9D3...E5aC" // replace with actual publisher
```

If you don’t know your schema ID yet, you can compute it later using:

```ts
const computed = await sdk.streams.computeSchemaId(helloWorldSchema)
console.log("Schema ID:", computed)
```

***

### lib/read.ts

Implements read helpers for your API and UI.

```ts
import { sdk } from "./store"
import { schemaId, publisher } from "./schema"

export async function getLatestMessage() {
  return await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher)
}

export async function getMessagesInRange(start: bigint, end: bigint) {
  return await sdk.streams.getBetweenRange(schemaId, publisher, start, end)
}

export async function getSchemaInfo() {
  return await sdk.streams.getSchemaFromSchemaId(schemaId)
}
```

***

### app/api/latest/route.ts

A serverless route to fetch the latest message (you can add more routes for range or schema info).

```ts
import { NextResponse } from "next/server"
import { getLatestMessage } from "@/lib/read"

export async function GET() {
  const data = await getLatestMessage()
  return NextResponse.json({ data })
}
```

***

### components/StreamViewer.tsx

A live component with interactive buttons for fetching data.

<details>

<summary>StreamViewer.tsx</summary>

```tsx
"use client"
import { useState } from "react"

export default function StreamViewer() {
  const [data, setData] = useState<any>(null)
  const [loading, setLoading] = useState(false)

  const fetchLatest = async () => {
    setLoading(true)
    try {
      const res = await fetch("/api/latest")
      const { data } = await res.json()
      setData(data)
    } catch (err) {
      console.error(err)
    } finally {
      setLoading(false)
    }
  }

  return (
    <div className="bg-white shadow-md p-6 rounded-2xl border">
      <h2 className="text-xl font-semibold mb-4">HelloWorld Stream Reader</h2>

      <button
        onClick={fetchLatest}
        className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md"
        disabled={loading}
      >
        {loading ? "Loading..." : "Fetch Latest Message"}
      </button>

      {data && (
        <pre className="bg-gray-900 text-green-300 p-4 mt-4 rounded overflow-x-auto text-sm">
          {JSON.stringify(data, null, 2)}
        </pre>
      )}
    </div>
  )
}
```

</details>

***

### components/SchemaInfo.tsx

Displays the schema metadata.

```tsx
"use client"

import { useState } from "react"

export default function SchemaInfo() {
  const [info, setInfo] = useState<any>(null)

  const fetchInfo = async () => {
    const res = await fetch("/api/latest") // just for demo; replace with /api/schema if separate route
    const { data } = await res.json()
    setInfo(data)
  }

  return (
    <div className="bg-gray-50 p-6 rounded-xl shadow">
      <h2 className="font-semibold text-lg mb-3">Schema Information</h2>
      <button
        onClick={fetchInfo}
        className="bg-gray-800 text-white px-3 py-2 rounded-md"
      >
        Load Schema Info
      </button>
      {info && (
        <pre className="bg-black text-green-300 mt-3 p-3 rounded">
          {JSON.stringify(info, null, 2)}
        </pre>
      )}
    </div>
  )
}
```

***

### app/page.tsx

Main dashboard combining both components.

```tsx
import StreamViewer from "@/components/StreamViewer"
import SchemaInfo from "@/components/SchemaInfo"

export default function Home() {
  return (
    <main className="p-10 min-h-screen bg-gray-100">
      <h1 className="text-3xl font-bold mb-8">🛰️ Somnia Data Streams Reader</h1>

      <div className="grid gap-6 md:grid-cols-2">
        <StreamViewer />
        <SchemaInfo />
      </div>
    </main>
  )
}
```

***

### app/layout.tsx

Wraps the layout globally.

```tsx
import "./globals.css"

export const metadata = {
  title: "Somnia Streams Reader",
  description: "Read on-chain data from Somnia Data Streams",
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className="antialiased">{children}</body>
    </html>
  )
}
```

***

### Run the App

```bash
npm run dev
```

Visit <http://localhost:3000> to open your dashboard.\
You’ll see a **“Fetch Latest Message”** button that retrieves data via `/api/latest` and a **Schema Info** section (ready to expand)

***

## Summary Table

| Method                        | Purpose                          | Example                                            |
| ----------------------------- | -------------------------------- | -------------------------------------------------- |
| getByKey                      | Fetch a record by unique ID      | getByKey(schemaId, publisher, dataId)              |
| getAtIndex                    | Fetch record at position         | getAtIndex(schemaId, publisher, 0n)                |
| getBetweenRange               | Retrieve records in range        | getBetweenRange(schemaId, publisher, 0n, 10n)      |
| getAllPublisherDataForSchema  | Fetch all data by publisher      | getAllPublisherDataForSchema(schemaRef, publisher) |
| getLastPublishedDataForSchema | Latest record only               | getLastPublishedDataForSchema(schemaId, publisher) |
| totalPublisherDataForSchema   | Count of entries                 | totalPublisherDataForSchema(schemaId, publisher)   |
| isDataSchemaRegistered        | Check if schema exists           | isDataSchemaRegistered(schemaId)                   |
| schemaIdToId / idToSchemaId   | Convert between Hex and readable | Useful for UI & schema mapping                     |
| getSchemaFromSchemaId         | Inspect full schema definition   | Retrieves base + extended schema                   |

<br>


# Integrate Chainlink Oracles

Somnia Data Streams provides a powerful, on-chain, and composable storage layer. [Chainlink Oracles](https://docs.chain.link/data-feeds/price-feeds/addresses?page=1\&testnetPage=1\&networkType=testnet\&search=\&testnetSearch=) provide secure, reliable, and decentralized external data feeds.

When you combine them, you unlock a powerful new capability: **creating historical, queryable, on-chain data streams from real-world data.**

Chainlink Price Feeds are designed to provide the *latest* price of an asset. They are not designed to provide a queryable history. You cannot easily ask a Price Feed, "What was the price of ETH 48 hours ago?"

By integrating Chainlink with Somnia Streams, you can build a "snapshot bot" that reads from Chainlink at regular intervals and appends the price to a Somnia Data Stream. This creates a permanent, verifiable, and historical on-chain feed that any other DApp or user can read and trust.

## **Objectives & Deliverable**

* **Objective:** Fetch off-chain data (a price feed) with Chainlink and store it historically via Somnia Streams.
* **Key Takeaway:** Combining external "truth" sources with Somnia's composable storage to create new, valuable on-chain data products.
* **Deliverable:** A hybrid "Snapshot Bot" that reads from Chainlink on the Sepolia testnet and publishes to a historical price feed on the Somnia Testnet.

## What You'll Build

1. **A New Schema:** A `priceFeedSchema` to store price data.
2. **A Chainlink Reader:** A script using `viem` to read the `latestRoundData` from Chainlink's ETH/USD feed on the Sepolia testnet.
3. **A Snapshot Bot:** A script that reads from Chainlink (Sepolia) and writes to Somnia Data Streams (Somnia Testnet).
4. **A History Reader:** A script to read our new historical price feed from Somnia Data Streams.

This tutorial demonstrates a true hybrid-chain application.

## Prerequisites

* Node.js 20+.
* `@somnia-chain/streams`, `viem`, and `dotenv` installed.
* A wallet with Somnia Testnet tokens (for publishing) and Sepolia testnet ETH (for gas, though we are only reading, so a public RPC is fine).

## **Environment Setup**

Create a `.env` file. You will need RPC URLs for **both** chains and a private key for the Somnia Testnet (to pay for publishing).

```bash
# .env
RPC_URL_SOMNIA=[https://dream-rpc.somnia.network]
RPC_URL_SEPOLIA=[https://sepolia.drpc.org]
PRIVATE_KEY_SOMNIA=0xYOUR_SOMNIA_PRIVATE_KEY
```

## Project Setup

Set up your project with `viem` and the Streams SDK.

```bash
npm i @somnia-chain/streams viem dotenv
npm i -D @types/node typescript ts-node
```

## **Chain Configuration**

We need to define both chains we are interacting with.

**`src/lib/chain.ts`**

```typescript
import { defineChain } from 'viem'
import { sepolia as sepoliaBase } from 'viem/chains'

// 1. Somnia Testnet
export const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  network: 'somnia-testnet',
  nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
  rpcUrls: {
    default: { http: [process.env.RPC_URL_SOMNIA || ''] },
    public:  { http: [process.env.RPC_URL_SOMNIA || ''] },
  },
} as const)

// 2. Sepolia Testnet (for Chainlink)
export const sepolia = sepoliaBase
```

## **Client Configuration**

We will create two separate clients:

* A **Somnia SDK client** (with a wallet) to *write* data.
* A **Sepolia Public Client** (read-only) to *read* from Chainlink.

**`src/lib/clients.ts`**

```typescript
import 'dotenv/config'
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { SDK } from '@somnia-chain/streams'
import { somniaTestnet, sepolia } from './chain'

function getEnv(key: string): string {
  const value = process.env[key]
  if (!value) throw new Error(`Missing environment variable: ${key}`)
  return value
}

// === Client 1: Somnia SDK (Read/Write) ===
const somniaWalletClient = createWalletClient({
  account: privateKeyToAccount(getEnv('PRIVATE_KEY_SOMNIA') as `0x${string}`),
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL_SOMNIA')),
})

const somniaPublicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL_SOMNIA')),
})

export const somniaSdk = new SDK({
  public: somniaPublicClient,
  wallet: somniaWalletClient,
})

// === Client 2: Sepolia Public Client (Read-Only) ===
export const sepoliaPublicClient = createPublicClient({
  chain: sepolia,
  transport: http(getEnv('RPC_URL_SEPOLIA')),
})
```

## Define the Price Feed Schema

Our schema will store the core data from Chainlink's feed.

**`src/lib/schema.ts`**

```typescript
// This schema will store historical price snapshots
export const priceFeedSchema = 
  'uint64 timestamp, int256 price, uint80 roundId, string pair'
```

* `timestamp`: The `updatedAt` time from Chainlink.
* `price`: The `answer` (e.g., ETH price).
* `roundId`: The Chainlink round ID, to prevent duplicates.
* `pair`: A string to identify the feed (e.g., "ETH/USD").

## Create the Chainlink Reader

Let's create a dedicated file to handle fetching data from Chainlink. We will use the `ETH/USD` feed on Sepolia.

**`src/lib/chainlinkReader.ts`**

```typescript
import { parseAbi, Address } from 'viem'
import { sepoliaPublicClient } from './clients'

// Chainlink ETH/USD Feed on Sepolia Testnet
const CHAINLINK_FEED_ADDRESS: Address = '0x694AA1769357215DE4FAC081bf1f309aDC325306'

// Minimal ABI for AggregatorV3Interface
const CHAINLINK_ABI = parseAbi([
  'function latestRoundData() external view returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)',
  'function decimals() external view returns (uint8)',
])

export interface PriceData {
  roundId: bigint
  price: bigint
  timestamp: bigint
  decimals: number
}

/**
 * Fetches the latest price data from the Chainlink ETH/USD feed on Sepolia.
 */
export async function fetchLatestPrice(): Promise<PriceData> {
  console.log('Fetching latest price from Chainlink on Sepolia...')
  
  try {
    const [roundData, decimals] = await Promise.all([
      sepoliaPublicClient.readContract({
        address: CHAINLINK_FEED_ADDRESS,
        abi: CHAINLINK_ABI,
        functionName: 'latestRoundData',
      }),
      sepoliaPublicClient.readContract({
        address: CHAINLINK_FEED_ADDRESS,
        abi: CHAINLINK_ABI,
        functionName: 'decimals',
      })
    ])

    const [roundId, answer, , updatedAt] = roundData
    
    console.log(`Chainlink data received: Round ${roundId}, Price ${answer}`)
    
    return {
      roundId,
      price: answer,
      timestamp: updatedAt,
      decimals,
    }
  } catch (error: any) {
    console.error(`Failed to read from Chainlink: ${error.message}`)
    throw error
  }
}
```

## Build the Snapshot Bot (The Hybrid App)

This is the core of our project. This script will:

1. Fetch the latest price from Chainlink (using our module).
2. Encode this data using our `priceFeedSchema`.
3. Publish the data to Somnia Data Streams.

**`src/scripts/snapshotBot.ts`**

<pre class="language-typescript"><code class="lang-typescript">import 'dotenv/config'
import { somniaSdk } from '../lib/clients'
import { priceFeedSchema } from '../lib/schema'
import { fetchLatestPrice } from '../lib/chainlinkReader'
import { SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { toHex, Hex } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

const PAIR_NAME = "ETH/USD"

async function main() {
  console.log('--- Starting Snapshot Bot ---')
  
  // 1. Initialize SDK and Encoder
  const sdk = somniaSdk
  const encoder = new SchemaEncoder(priceFeedSchema)
  const publisherAddress = sdk.wallet.account?.address
  if (!publisherAddress) throw new Error('Wallet client not initialized.')

  // 2. Compute Schema ID and Register (idempotent)
  const schemaId = await sdk.streams.computeSchemaId(priceFeedSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')
  
  const ignoreAlreadyRegisteredSchemas = true
<strong>  const regTx = await sdk.streams.registerDataSchemas([
</strong>        { id: 'price-feed-v1', schema: priceFeedSchema }
    ], ignoreAlreadyRegisteredSchemas)
    if (!regTx) throw new Error('Failed to register schema')
    await waitForTransactionReceipt(sdk.public, { hash: regTx })

  // 3. Fetch data from Chainlink
  const priceData = await fetchLatestPrice()

  // 4. Encode data for Somnia Streams
  const encodedData: Hex = encoder.encodeData([
    { name: 'timestamp', value: priceData.timestamp.toString(), type: 'uint64' },
    { name: 'price', value: priceData.price.toString(), type: 'int256' },
    { name: 'roundId', value: priceData.roundId.toString(), type: 'uint80' },
    { name: 'pair', value: PAIR_NAME, type: 'string' },
  ])

  // 5. Create a unique Data ID (using the roundId to prevent duplicates)
  const dataId = toHex(`price-${PAIR_NAME}-${priceData.roundId}`, { size: 32 })

  // 6. Publish to Somnia Data Streams
  console.log(`Publishing price data to Somnia Streams...`)
  const txHash = await sdk.streams.set([
    { id: dataId, schemaId, data: encodedData }
  ])

  if (!txHash) throw new Error('Failed to publish to Streams')
  
  await waitForTransactionReceipt(sdk.public, { hash: txHash })
  
  console.log('\n--- Snapshot Complete! ---')
  console.log(`  Publisher: ${publisherAddress}`)
  console.log(`  Schema ID: ${schemaId}`)
  console.log(`  Data ID: ${dataId}`)
  console.log(`  Tx Hash: ${txHash}`)
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
</code></pre>

**To run your bot:**

Add a script to `package.json`: `"snapshot": "ts-node src/scripts/snapshotBot.ts"`

Run it: `npm run snapshot`

You can run this script multiple times. It will only add new data if Chainlink's `roundId` has changed.

## Read Your Historical Price Feed

Now for the payoff. Let's create a script that reads our new on-chain history from Somnia Streams.

**`src/scripts/readHistory.ts`**

```typescript
import 'dotenv/config'
import { somniaSdk } from '../lib/clients'
import { priceFeedSchema } from '../lib/schema'
import { SchemaDecodedItem } from '@somnia-chain/streams'

// Helper to decode the SDK's output
interface PriceRecord {
  timestamp: number
  price: bigint
  roundId: bigint
  pair: string
}

function decodePriceRecord(row: SchemaDecodedItem[]): PriceRecord {
  const val = (field: any) => field?.value?.value ?? field?.value ?? ''
  return {
    timestamp: Number(val(row[0])),
    price: BigInt(val(row[1])),
    roundId: BigInt(val(row[2])),
    pair: String(val(row[3])),
  }
}

async function main() {
  console.log('--- Reading Historical Price Feed from Somnia Streams ---')
  const sdk = somniaSdk
  
  // Use the *publisher address* from your .env file
  const publisherAddress = sdk.wallet.account?.address
  if (!publisherAddress) throw new Error('Wallet client not initialized.')

  const schemaId = await sdk.streams.computeSchemaId(priceFeedSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')

  console.log(`Reading all data for publisher: ${publisherAddress}`)
  console.log(`Schema: ${schemaId}\n`)

  // Fetch all data for this schema and publisher
  const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)

  if (!data || data.length === 0) {
    console.log('No price history found. Run the snapshot bot first.')
    return
  }

  const records = (data as SchemaDecodedItem[][]).map(decodePriceRecord)
  
  // Sort by timestamp
  records.sort((a, b) => a.timestamp - b.timestamp)

  console.log(`Found ${records.length} historical price points:\n`)
  
  records.forEach(record => {
    // We assume the decimals are 8 for this ETH/USD feed
    const priceFloat = Number(record.price) / 10**8
    console.log(
      `[${new Date(record.timestamp * 1000).toISOString()}] ${record.pair} - $${priceFloat.toFixed(2)} (Round: ${record.roundId})`
    )
  })
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

**To read the history:**

Add to `package.json`: `"history": "ts-node src/scripts/readHistory.ts"`

Run it: `npm run history`

**Expected Output:**

```bash
--- Reading Historical Price Feed from Somnia Streams ---
...
Found 3 historical price points:

[2025-11-06T14:30:00.000Z] ETH/USD - $3344.50 (Round: 110...)
[2025-11-06T14:35:00.000Z] ETH/USD - $3362.12 (Round: 111...)
[2025-11-06T14:40:00.000Z] ETH/USD - $3343.90 (Round: 112...)
```

## Conclusion: Key Takeaways

You have successfully built a hybrid, cross-chain application.

* You combined an **external "truth source"** (Chainlink) with Somnia's **composable storage layer** (Somnia Data Streams).
* You created a new, valuable, on-chain data product: a **historical, queryable price feed** that any dApp on Somnia can now read from and trust.
* You demonstrated the power of the `publisher` address as a verifiable source. Any dApp can now consume your feed, knowing it was published by *your* trusted bot.

This pattern can be extended to any external data source: weather, sports results, IoT data, and more. You can run the `snapshotBot.ts` script as a cron job or serverless function to create a truly autonomous, on-chain oracle.


# Working with Multiple Publishers in a Shared Stream

The core architecture of Somnia Data Streams decouples data schemas from publishers. This allows multiple different accounts (or devices) to publish data using the **same schema**. The data conforms to the same data structure, regardless of who published it.

This model is perfect for multi-source data scenarios, such as:

* **Multi-User Chat:** Multiple users sending messages under the same `chatMessage` schema.
* **IoT (Internet of Things):** Hundreds of sensors submitting data under the same `telemetry` schema.
* **Gaming:** All players in a game publishing their positions and scores under the same `playerUpdate` schema.

In this tutorial, we will demonstrate how to build an "aggregator" application that collects and merges data from two different "devices" (two separate wallet accounts) publishing to the same "telemetry" schema.

## Prerequisites

* Node.js 20+
* `@somnia-chain/streams` and `viem` libraries installed
* An `RPC_URL` for access to the Somnia Testnet
* **Two (2)** funded Somnia Testnet wallets for publishing data.

## What You’ll Build

In this tutorial, we will build two main components:

1. A **Publisher Script** that simulates two different wallets sending data to the same telemetry schema.
2. An **Aggregator Script** that fetches *all* data from a specified list of publishers, merges them into a single list, and sorts them by timestamp.

## Project Setup

Create a new directory for your application and install the necessary packages.

```bash
mkdir somnia-aggregator
cd somnia-aggregator
npm init -y
npm i @somnia-chain/streams viem dotenv
npm i -D @types/node typescript ts-node
```

Create a `tsconfig.json` file in your project root:

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}
```

## Configure Environment Variables

Create a `.env` file in your project root. For this tutorial, we will need **two** different private keys.

```bash
# .env
RPC_URL=https://dream-rpc.somnia.network/ 

# Simulates two different devices/publishers
PUBLISHER_1_PK=0xPUBLISHER_ONE_PRIVATE_KEY
PUBLISHER_2_PK=0xPUBLISHER_TWO_PRIVATE_KEY
```

{% hint style="info" %}
**IMPORTANT:** Never expose private keys in client-side (browser) code or public repositories. The scripts in this tutorial are intended to be run server-side.
{% endhint %}

## Chain and Client Configuration

Create a folder named `src/lib` and set up your `chain.ts` and `clients.ts` files.

**`src/lib/chain.ts`**

```typescript
import { defineChain } from 'viem'

export const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  network: 'somnia-testnet',
  nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
  rpcUrls: {
    default: { http: ['[https://dream-rpc.somnia.network]'] },
    public:  { http: ['[https://dream-rpc.somnia.network]'] },
  },
} as const)
```

**`src/lib/clients.ts`**

```typescript
import 'dotenv/config'
import { createPublicClient, createWalletClient, http, PublicClient } from 'viem'
import { privateKeyToAccount, PrivateKeyAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'

function getEnv(key: string): string {
  const value = process.env[key]
  if (!value) {
    throw new Error(`Missing environment variable: ${key}`)
  }
  return value
}

// A single Public Client for read operations
export const publicClient: PublicClient = createPublicClient({
  chain: somniaTestnet, 
  transport: http(getEnv('RPC_URL')),
})

// Two different Wallet Clients for simulation
export const walletClient1 = createWalletClient({
  account: privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`),
  chain: somniaTestnet, 
  transport: http(getEnv('RPC_URL')),
})

export const walletClient2 = createWalletClient({
  account: privateKeyToAccount(getEnv('PUBLISHER_2_PK') as `0x${string}`),
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL')),
})
```

## Define the Shared Schema

Let's define the common schema that all publishers will use.

**`src/lib/schema.ts`**

```typescript
// This schema will be used by multiple devices
export const telemetrySchema = 
  'uint64 timestamp, string deviceId, int32 x, int32 y, uint32 speed'
```

## Create the Publisher Script

Now, let's create a script that simulates how two different publishers will send data to this schema. This script will take which publisher to use as a command-line argument.

**`src/scripts/publishData.ts`**

```typescript
import 'dotenv/config'
import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient1, walletClient2 } from '../lib/clients'
import { telemetrySchema } from '../lib/schema'
import { toHex, Hex, WalletClient } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'

// Select which publisher to use
async function getPublisher(): Promise<{ client: WalletClient, deviceId: string }> {
  const arg = process.argv[2] // 'p1' or 'p2'
  if (arg === 'p2') {
    console.log('Using Publisher 2 (Device B)')
    return { client: walletClient2, deviceId: 'device-b-002' }
  }
  console.log('Using Publisher 1 (Device A)')
  return { client: walletClient1, deviceId: 'device-a-001' }
}

// Helper function to encode the data
function encodeTelemetry(encoder: SchemaEncoder, deviceId: string): Hex {
  const now = Date.now().toString()
  return encoder.encodeData([
    { name: "timestamp", value: now, type: "uint64" },
    { name: "deviceId", value: deviceId, type: "string" },
    { name: "x", value: Math.floor(Math.random() * 1000).toString(), type: "int32" },
    { name: "y", value: Math.floor(Math.random() * 1000).toString(), type: "int32" },
    { name: "speed", value: Math.floor(Math.random() * 120).toString(), type: "uint32" },
  ])
}

async function main() {
  const { client, deviceId } = await getPublisher()
  const publisherAddress = client.account.address
  console.log(`Publisher Address: ${publisherAddress}`)

  const sdk = new SDK({ public: publicClient, wallet: client })
  const encoder = new SchemaEncoder(telemetrySchema)

  // 1. Compute the Schema ID
  const schemaId = await sdk.streams.computeSchemaId(telemetrySchema)
  if (!schemaId) throw new Error('Could not compute schemaId')
  console.log(`Schema ID: ${schemaId}`)

  // 2. Register Schema (Updated for new API)
  console.log('Registering schema (if not already registered)...')
  
  const ignoreAlreadyRegisteredSchemas = true
  const regTx = await sdk.streams.registerDataSchemas([
    { 
      schemaName: 'telemetry', // Updated: 'id' is now 'schemaName'
      schema: telemetrySchema, 
      parentSchemaId: zeroBytes32 
    }
  ], ignoreAlreadyRegisteredSchemas)

  if (regTx) {
    console.log('Schema registration transaction sent:', regTx)
    await waitForTransactionReceipt(publicClient, { hash: regTx })
    console.log('Schema registered successfully!')
  } else {
    console.log('Schema was already registered. No transaction sent.')
  }

  // 3. Encode the data
  const encodedData = encodeTelemetry(encoder, deviceId)
  
  // 4. Publish the data
  // We make the dataId unique with a timestamp and device ID
  const dataId = toHex(`${deviceId}-${Date.now()}`, { size: 32 })
  
  const txHash = await sdk.streams.set([
    { id: dataId, schemaId, data: encodedData }
  ])

  if (!txHash) throw new Error('Failed to publish data')
  console.log(`Publishing data... Tx: ${txHash}`)

  await waitForTransactionReceipt(publicClient, { hash: txHash })
  console.log('Data published successfully!')
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

**To run this script:**

Add the following scripts to your `package.json` file:

```json
"scripts": {
  "publish:p1": "ts-node src/scripts/publishData.ts p1",
  "publish:p2": "ts-node src/scripts/publishData.ts p2"
}
```

Now, open two different terminals and send data from each:

```bash
# Terminal 1
npm run publish:p1
# Terminal 2
npm run publish:p2
```

Repeat this a few times to build up a dataset from both publishers.

## Create the Aggregator Script

Now that we have published our data, we can write the "aggregator" script that collects, merges, and sorts all data from these two (or more) publishers.

**`src/scripts/aggregateData.ts`**

```typescript
import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { publicClient, walletClient1, walletClient2 } from '../lib/clients'
import { telemetrySchema } from '../lib/schema'
import { Address } from 'viem'

// LIST OF PUBLISHERS TO TRACK
// You could also fetch this list dynamically (e.g., from a contract or database).
const TRACKED_PUBLISHERS: Address[] = [
  walletClient1.account.address,
  walletClient2.account.address,
]

// Helper function to convert SDK data into a cleaner object
// (Similar to the 'val' function in the Minimal On-Chain Chat App Tutorial)
function decodeTelemetryRecord(row: SchemaDecodedItem[]): TelemetryRecord {
  const val = (field: any) => field?.value?.value ?? field?.value ?? ''
  return {
    timestamp: Number(val(row[0])),
    deviceId: String(val(row[1])),
    x: Number(val(row[2])),
    y: Number(val(row[3])),
    speed: Number(val(row[4])),
  }
}

// Type definition for our data
interface TelemetryRecord {
  timestamp: number
  deviceId: string
  x: number
  y: number
  speed: number
  publisher?: Address // We will add this field later
}

async function main() {
  // The aggregator doesn't need to write data, so it only uses the publicClient
  const sdk = new SDK({ public: publicClient })
  
  const schemaId = await sdk.streams.computeSchemaId(telemetrySchema)
  if (!schemaId) throw new Error('Could not compute schemaId')

  console.log(`Aggregator started. Tracking ${TRACKED_PUBLISHERS.length} publishers...`)
  console.log(`Schema ID: ${schemaId}\n`)

  const allRecords: TelemetryRecord[] = []

  // 1. Loop through each publisher
  for (const publisherAddress of TRACKED_PUBLISHERS) {
    console.log(`--- Fetching data for ${publisherAddress} ---`)
    
    // 2. Fetch all data for the publisher based on the schema
    // Note: The SDK automatically decodes the data if the schema is registered
    const data = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
    
    if (!data || data.length === 0) {
      console.log('No data found for this publisher.\n')
      continue
    }

    // 3. Transform the data and add the 'publisher' field
    const records: TelemetryRecord[] = (data as SchemaDecodedItem[][]).map(row => ({
      ...decodeTelemetryRecord(row),
      publisher: publisherAddress // To know where the data came from
    }))

    console.log(`Found ${records.length} records.`)

    // 4. Add all records to the main list
    allRecords.push(...records)
  }

  // 5. Sort all data by timestamp
  console.log('\n--- Aggregation Complete ---')
  console.log(`Total records fetched: ${allRecords.length}`)

  allRecords.sort((a, b) => a.timestamp - b.timestamp)

  // 6. Display the result
  console.log('\n--- Combined and Sorted Telemetry Log ---')
  allRecords.forEach(record => {
    console.log(
      `[${new Date(record.timestamp).toISOString()}] [${record.publisher}] - Device: ${record.deviceId}, Speed: ${record.speed}`
    )
  })
}

main().catch((e) => {
  console.error(e)
  process.exit(1)
})
```

**To run this script:**

Add the script to your `package.json` file:

```json
"scripts": {
  "publish:p1": "ts-node src/scripts/publishData.ts p1",
  "publish:p2": "ts-node src/scripts/publishData.ts p2",
  "aggregate": "ts-node src/scripts/aggregateData.ts"
}
```

And run it:

```bash
npm run aggregate
```

## Conclusion

In this tutorial, you learned how to manage a multi-publisher architecture with Somnia Data Streams.

* **Publisher Side:** The logic remained unchanged. Each publisher independently published its data using its wallet and the `sdk.streams.set()` method.
* **Aggregator Side:** This is where the main logic came in.
  1. We maintained a list of publishers we were interested in.
  2. We fetched the data for each publisher separately using the `getAllPublisherDataForSchema` method.
  3. We combined the incoming data into a single array (`allRecords.push(...)`).
  4. Finally, we sorted all the data on the client-side to display them in a meaningful order (e.g., by timestamp).

This pattern can be scaled to support any number of publishers and provides a robust foundation for building decentralized, multi-source applications.


# The DApp Publisher Proxy Pattern

In the "[Working with Multiple Publishers](https://www.google.com/search?q=httpsa://emre-gitbook.gitbook.io/emre-gitbook-docs/data-streams/working-with-multiple-publishers-in-a-shared-stream)" tutorial, you learned the standard pattern for building an aggregator:

1. Maintain a list of all known publisher addresses.
2. Loop through this list.
3. Call `sdk.streams.getAllPublisherDataForSchema()` for each address.
4. Merge and sort the results on the client side.

This pattern is simple and effective for a known, manageable number of publishers (e.g., 50 IoT sensors from a single company).

**But what happens at a massive scale?**

## The Problem: The 10,000-Publisher Scenario

Imagine you are building a popular on-chain game. You have a `leaderboardSchema` and 10,000 players actively publishing their scores.

If you use the standard aggregator pattern, your "global leaderboard" DApp would need to:

1. Somehow find all 10,000 player addresses.
2. Perform **10,000 separate read calls** (`getAllPublisherDataForSchema`) to the Somnia RPC node.

This is not scalable, fast, or efficient. It creates an enormous (and slow) data-fetching burden on your application.

## The Solution: The DApp Publisher Proxy

This is an advanced architecture that inverts the model to solve the read-scalability problem.

Instead of having 10,000 publishers write to Streams *directly*, they all write to **your DApp's smart contract**, which then publishes to Streams on their behalf.

**The Flow:**

1. **User (Publisher):** Calls a function on your DApp's contract (e.g., `myGame.submitScore(100)`). The `msg.sender` is the user's address.
2. **DApp Contract (The Proxy):** Internally, your `submitScore` function:
   * Adds the user's address (`msg.sender`) *into the data payload* to preserve provenance.
   * Calls `somniaStreams.esstores(...)` using its *own* contract address.
3. **Somnia Data Streams:** Records the data. To the Streams contract, the **only publisher** is your DApp Contract's address.

The Result:

Your global leaderboard aggregator now only needs to make one single read call to fetch all 10,000 players' data:

`sdk.streams.getAllPublisherDataForSchema(schemaId, YOUR_DAPP_CONTRACT_ADDRESS)`

This is massively scalable and efficient for read-heavy applications.

## Tutorial: Building a `GameLeaderboard` Proxy

Let's build a conceptual example of this pattern.

## What You'll Build

1. **A new Schema** that *includes* the original publisher's address.
2. **A `GameLeaderboard.sol`** smart contract that acts as the proxy.
3. **A Client Script** that writes to the *proxy contract* instead of Streams.
4. **A new Aggregator** that reads from the *proxy contract's* address.

## The Schema (Solving for Provenance)

Since the `msg.sender` to the Streams contract will always be our *proxy contract*, we lose the built-in provenance. We must re-create it by adding the original player's address to the schema itself.

**`src/lib/schema.ts`**

```typescript
// Schema: 'uint64 timestamp, address player, uint256 score'
export const leaderboardSchema = 
  'uint64 timestamp, address player, uint256 score'

```

## The Proxy Smart Contract (Solidity)

This is a new smart contract you would write and deploy for your DApp. It acts as the gatekeeper.

{% hint style="info" %}
**SDK set() vs. Contract esstores()**\
This example uses the low-level contract function esstores().\
When you use sdk.streams.set() in your client-side code, the SDK is calling the esstores() function on the Somnia Streams contract "under the hood."\
This proxy contract is simply calling that same function directly.
{% endhint %}

**`src/contracts/GameLeaderboard.sol`**

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

// A simplified interface for the Somnia Streams contract
interface IStreams {
    struct DataStream {
        bytes32 id;
        bytes32 schemaId;
        bytes data;
    }
    // This is the correct low-level function name
    function esstores(DataStream[] calldata streams) external;
}

/**
 * @title GameLeaderboard
 * This contract is a DApp Publisher Proxy.
 * Users call submitScore() here.
 * This contract then calls somniaStreams.esstores() as a single publisher.
 */
contract GameLeaderboard {
    IStreams public immutable somniaStreams;
    bytes32 public immutable leaderboardSchemaId;

    event ScoreSubmitted(address indexed player, uint256 score);

    /**
     * @param _streamsAddress The deployed address of the Somnia Streams contract 
     * (e.g., 0x6AB397FF662e42312c003175DCD76EfF69D048Fc on Somnia Testnet).
     * @param _schemaId The pre-computed schemaId for 'uint64 timestamp, address player, uint256 score'.
     */
    constructor(address _streamsAddress, bytes32 _schemaId) {
        somniaStreams = IStreams(_streamsAddress);
        leaderboardSchemaId = _schemaId;
    }

    /**
     * @notice Players call this function to submit their score.
     * @param score The player's score.
     */
    function submitScore(uint256 score) external {
        // 1. Get the original publisher's address
        address player = msg.sender;
        uint64 timestamp = uint64(block.timestamp);

        // 2. Encode the data payload to match the schema
        // Schema: 'uint64 timestamp, address player, uint256 score'
        bytes memory data = abi.encode(timestamp, player, score);

        // 3. Create a unique dataId (e.g., hash of player and time)
        bytes32 dataId = keccak256(abi.encodePacked(player, timestamp));

        // 4. Prepare the DataStream struct
        IStreams.DataStream[] memory d = new IStreams.DataStream[](1);
        d[0] = IStreams.DataStream({
            id: dataId,
            schemaId: leaderboardSchemaId,
            data: data
        });

        // 5. Call Somnia Streams. The `msg.sender` for this call
        // is THIS contract (GameLeaderboard).
        somniaStreams.esstores(d);

        // 6. Emit a DApp-specific event for good measure
        emit ScoreSubmitted(player, score);
    }
}

```

## The Client Script (Publishing to the Proxy)

The client-side logic changes. The user no longer needs the Streams SDK to publish, but rather a way to call your DApp's `submitScore` function.

**`src/scripts/publishScore.ts`**

```typescript
import 'dotenv/config'
import { createWalletClient, http, createPublicClient, parseAbi } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from '../lib/chain' // From previous tutorials
import { waitForTransactionReceipt } from 'viem/actions'

// --- DApp Contract Setup ---
// This is the address you get after deploying GameLeaderboard.sol
const DAPP_CONTRACT_ADDRESS = '0x...' // Your deployed GameLeaderboard contract address

// A minimal ABI for our GameLeaderboard contract
const DAPP_ABI = parseAbi([
  'function submitScore(uint256 score) external',
])
// --- --- ---

function getEnv(key: string): string {
  const value = process.env[key]
  if (!value) throw new Error(`Missing environment variable: ${key}`)
  return value
}

// We can use any publisher wallet
const walletClient = createWalletClient({
  account: privateKeyToAccount(getEnv('PUBLISHER_1_PK') as `0x${string}`),
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL')),
})

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL')),
})

async function main() {
  const newScore = Math.floor(Math.random() * 10000)
  console.log(`Player ${walletClient.account.address} submitting score: ${newScore}...`)

  try {
    const { request } = await publicClient.simulateContract({
      account: walletClient.account,
      address: DAPP_CONTRACT_ADDRESS,
      abi: DAPP_ABI,
      functionName: 'submitScore',
      args: [BigInt(newScore)],
    })

    const txHash = await walletClient.writeContract(request)
    console.log(`Transaction sent, hash: ${txHash}`)

    await waitForTransactionReceipt(publicClient, { hash: txHash })
    console.log('Score submitted successfully!')

  } catch (e: any) {
    console.error(`Failed to submit score: ${e.message}`)
  }
}

main().catch(console.error)

```

## The Aggregator Script (Simple, Scalable Reads)

This is the pay-off. The aggregator script is now *dramatically* simpler and more scalable. It only needs to know the single DApp contract address.

**`src/scripts/readLeaderboard.ts`**

```typescript
import 'dotenv/config'
import { SDK, SchemaDecodedItem } from '@somnia-chain/streams'
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from '../lib/chain'
import { leaderboardSchema } from '../libL/schema' // Our new schema

// --- DApp Contract Setup ---
const DAPP_CONTRACT_ADDRESS = '0x...' // Your deployed GameLeaderboard contract address
// --- --- ---

function getEnv(key: string): string {
  const value = process.env[key]
  if (!value) throw new Error(`Missing environment variable: ${key}`)
  return value
}

const publicClient = createPublicClient({
  chain: somniaTestnet,
  transport: http(getEnv('RPC_URL')),
})

// Helper to decode the leaderboard data
interface ScoreRecord {
  timestamp: number
  player: `0x${string}`
  score: bigint
}

function decodeScoreRecord(row: SchemaDecodedItem[]): ScoreRecord {
  const val = (field: any) => field?.value?.value ?? field?.value ?? ''
  return {
    timestamp: Number(val(row[0])),
    player: val(row[1]) as `0x${string}`,
    score: BigInt(val(row[2])),
  }
}

async function main() {
  // The aggregator only needs a public client
  const sdk = new SDK({ public: publicClient })
  
  const schemaId = await sdk.streams.computeSchemaId(leaderboardSchema)
  if (!schemaId) throw new Error('Could not compute schemaId')

  console.log('--- Global Leaderboard Aggregator ---')
  console.log(`Reading all data from proxy: ${DAPP_CONTRACT_ADDRESS}\n`)

  // 1. Make ONE call to get all data for the DApp
  const data = await sdk.streams.getAllPublisherDataForSchema(
    schemaId,
    DAPP_CONTRACT_ADDRESS
  )

  if (!data || data.length === 0) {
    console.log('No scores found.')
    return
  }

  // 2. Decode and sort the records
  const allScores = (data as SchemaDecodedItem[][]).map(decodeScoreRecord)
  allScores.sort((a, b) => (b.score > a.score ? 1 : -1)) // Sort descending by score

  // 3. Display the leaderboard
  console.log(`Total scores found: ${allScores.length}\n`)
  allScores.forEach((record, index) => {
    console.log(
      `#${index + 1}: Player ${record.player} - Score: ${record.score} (at ${new Date(record.timestamp).toISOString()})`
    )
  })
}

main().catch(console.error)

```

## Trade-Offs & Considerations

This pattern is powerful, but it's important to understand the trade-offs.

| **Feature**            | **Standard Pattern (Multi-Publisher)**                | **Proxy Pattern (Single Publisher)**                                               |
| ---------------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------- |
| **Read Scalability**   | **Low.** Requires N read calls (N = # of publishers). | **High.** Requires 1 read call, regardless of publisher count.                     |
| **Publisher Gas Cost** | **Low.** 1 transaction (`streams.set`).               | **High.** 1 transaction + 1 internal transaction. User pays more gas.              |
| **Provenance**         | **Automatic & Implicit.** `msg.sender` is the user.   | **Manual.** Must be built into the schema (`address player`).                      |
| **Complexity**         | **Simple.** Requires only the SDK.                    | **Complex.** Requires writing, deploying, and maintaining a custom smart contract. |

#### Conclusion

The **DApp Publisher Proxy** is an advanced but essential pattern for any Somnia Data Streams application that needs to scale to thousands or millions of publishers (e.t., games, social media, large IoT networks).

It simplifies the data aggregation logic from **`N+1`** read calls down to **`1`**, at the cost of higher gas fees for publishers and increased development complexity.

For most DApps, we recommend starting with the simpler "Multi-Publisher Aggregator" pattern. When your application's read performance becomes a bottleneck due to a high number of publishers, you can evolve to this proxy pattern to achieve massive read scalability.


# Build a Minimal On-Chain Chat App

In this Tutorial, you’ll build a tiny chat app where messages are published on-chain using the Somnia Data Streams SDK and then read back using a Subscriber pattern (fixed schema ID and publisher). The User Interface updates with simple polling and does not rely on WebSocket.

We will build a Next.js project using app router and Typescript, and create a simple chat schema for messages. Using Somnia Data Streams, we will create a publisher API that writes to the Somnia chain and create a Subscriber API that reads from the Somnia chain by `schemaId` and publisher. The User Interface will poll new messages every few seconds.

## Prerequisites

* Node.js 20+
* A funded Somnia Testnet wallet. Kindly get some from the [Faucet](https://testnet.somnia.network/)
* Basic familiarity with TypeScript and Next.js

## Project Setup

Create the app by creating a directory where the app will live

```bash
npx create-next-app@latest somnia-chat --ts --app --no-tailwind
cd somnia-chat
```

Install the [Somnia Streams](https://www.npmjs.com/package/@somnia-chain/streams) and ViemJS dependencies

```bash
npm i @somnia-chain/streams viem
```

Somnia Data Streams is a Typescript SDK with methods that power off-chain reactivity. The application requires the ViemJS `provider` and `wallet` methods to enable queries over `https` embedded in the Somnia Data Streams SDK.

`viem` is a web3 library for the JS/TS ecosystem that simplifies reading and writing data from the Somnia chain. Importantly, it detaches wallets (and sensitive info) from the SDS SDK\
Set up the TypeScript environment by running the command:

```bash
npm i -D @types/node
```

Next.js provides a simple, full-stack environment.

## Configure environment variables

Create `.env.local` file.

```markup
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
CHAT_PUBLISHER=0xYOUR_WALLET_ADDRESS
```

The `RPC_URL` establishes the connection to Somnia Testnet, and the `PRIVATE_KEY` is used to sign and publish transactions. Note that it is kept server-side only. `CHAT_PUBLISHER` define what the Subscriber reads.

{% hint style="danger" %}
Never expose PRIVATE\_KEY to the browser. Keep all publishing code in API routes or server code only.\
\
NOTE: You can connect a Privy Wallet (or equivalent) to the SDK, avoiding the need entirely for private keys
{% endhint %}

## Chain Configuration

Create a `lib` folder and define the Somnia Testnet chain. This tells `viem` which chain we’re on, so clients know the RPC and formatting rules. `src/lib/chain.ts`

```typescript
import { defineChain } from 'viem'
export const somniaTestnet = defineChain({
  id: 50312,
  name: 'Somnia Testnet',
  network: 'somnia-testnet',
  nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://dream-rpc.somnia.network'], 
               webSocket: ['wss://dream-rpc.somnia.network/ws'] },
    public:  { http: ['https://dream-rpc.somnia.network'],
               webSocket: ['wss://dream-rpc.somnia.network/ws'] },
  },
} as const)
```

## SDK Clients

Create public and wallet clients. Public client read-only RPC calls, and the Wallet client publishes transactions signed with your server wallet. We intentionally don’t set up WebSocket clients since we’re using polling for the User Interface. Create a file `clients` `src/lib/clients.ts`

```typescript
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount, type PrivateKeyAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'

export function getPublicHttpClient() {
  return createPublicClient({
    chain: somniaTestnet,
    transport: http(RPC_URL),
  })
}

export function getWalletClient() {
  return createWalletClient({
    account: privateKeyToAccount(need('PRIVATE_KEY') as `0x${string}`),
    chain: somniaTestnet,
    transport: http(RPC_URL),
  })
}

export const publisherAddress = () => getAccount().address
```

## Schema

Chat schema is the structure of each message. This ordered list of typed fields defines how messages are encoded/decoded on-chain. Create a file `chatSchema` `src/lib/chatSchema.ts`

```typescript
export const chatSchema = 'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
```

## Chat Service

We’ll build `chatService.ts` in small pieces so it’s easy to follow.

### Imports and helpers

```typescript
import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { getPublicHttpClient, getWalletClient, publisherAddress } from './clients'
import { waitForTransactionReceipt } from 'viem/actions'
import { toHex, type Hex } from 'viem'
import { chatSchema } from './chatSchema'

const encoder = new SchemaEncoder(chatSchema)

const sdk = new SDK({
  public: getPublicHttpClient(),
  wallet: getWalletClient(),
})
```

`SchemaEncoder` handles encoding/decoding for the exact schema string.

`getSdk(true)` attaches the wallet client for publishing; read-only otherwise.

`assertHex` ensures transaction hashes are hex strings.

### Ensure the schema is registered

```typescript
// Register schema
  const schemaId = await sdk.streams.computeSchemaId(chatSchema)
  const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
  if (!isRegistered) {
    const ignoreAlreadyRegistered = true
    const txHash = await sdk.streams.registerDataSchemas(
      [{ schemaName: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }],
      ignoreAlreadyRegistered
    )
    if (!txHash) throw new Error('Failed to register schema')
    await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
  }
```

If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.

### Publish a message

```typescript
const now = Date.now().toString()
  const roomId = toHex(room, { size: 32 })
  const data: Hex = encoder.encodeData([
    { name: 'timestamp', value: now, type: 'uint64' },
    { name: 'roomId', value: roomId, type: 'bytes32' },
    { name: 'content', value: content, type: 'string' },
    { name: 'senderName', value: senderName, type: 'string' },
    { name: 'sender', value: getWalletClient().account.address, type: 'address' },
  ])

  const dataId = toHex(`${room}-${now}`, { size: 32 })
  const tx = await sdk.streams.set([{ id: dataId, schemaId, data }])
  if (!tx) throw new Error('Failed to publish chat message')
  await waitForTransactionReceipt(getPublicHttpClient(), { hash: tx })
  return { txHash: tx }
```

* We encode fields in the exact order specified in the schema.
* `setAndEmitEvents` writes the encoded payload.

The `sendMessage` function publishes a structured chat message to **Somnia Data Streams** while simultaneously emitting an event that can be captured in real time by subscribers. It creates a schema encoder for the chat message structure, encodes the message data, and prepares event topics for the `ChatMessage` event. Then, with a single `setAndEmitEvents()` transaction, it both stores the message and emits an event on-chain. Once the transaction is confirmed, the function returns the transaction hash, confirming the message was successfully written to the network. Complete code below:

<details>

<summary>chatService.ts</summary>

```typescript
// src/lib/chatService.ts
import { SDK, SchemaEncoder, zeroBytes32 } from '@somnia-chain/streams'
import { getPublicHttpClient, getWalletClient } from './clients'
import { waitForTransactionReceipt } from 'viem/actions'
import { toHex, type Hex } from 'viem'
import { chatSchema } from './chatSchema'

const encoder = new SchemaEncoder(chatSchema)

export async function sendMessage(room: string, content: string, senderName: string) {
  const sdk = new SDK({
    public: getPublicHttpClient(),
    wallet: getWalletClient(),
  })

  // Compute or register schema
  const schemaId = await sdk.streams.computeSchemaId(chatSchema)
  const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
  if (!isRegistered) {
    const ignoreAlreadyRegistered = true
    const txHash = await sdk.streams.registerDataSchemas(
      [{ id: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }],
      ignoreAlreadyRegistered
    )
    if (!txHash) throw new Error('Failed to register schema')
    await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
  }

  const now = Date.now().toString()
  const roomId = toHex(room, { size: 32 })
  const data: Hex = encoder.encodeData([
    { name: 'timestamp', value: now, type: 'uint64' },
    { name: 'roomId', value: roomId, type: 'bytes32' },
    { name: 'content', value: content, type: 'string' },
    { name: 'senderName', value: senderName, type: 'string' },
    { name: 'sender', value: getWalletClient().account.address, type: 'address' },
  ])

  const dataId = toHex(`${room}-${now}`, { size: 32 })
  const tx = await sdk.streams.set([{ id: dataId, schemaId, data }])
  if (!tx) throw new Error('Failed to publish chat message')
  await waitForTransactionReceipt(getPublicHttpClient(), { hash: tx })
  return { txHash: tx }
}

```

</details>

### Read Messages

Create a `chatMessages.ts` file to write the script for reading messages.

* `getAllPublisherDataForSchema` reads all publisher data for your (schemaId, publisher).

The `fetchChatMessages` function connects to the **Somnia Data Streams SDK** using a public client and derives the schema ID from the local chat schema. It then retrieves all data entries published by the specified wallet for that schema, decodes them into readable message objects, and filters them by room if a name is provided. Each message is timestamped, ordered chronologically, and limited to a given count before being returned. The result is a clean, decoded list of on-chain messages. Complete code below:

<details>

<summary>chatMessages.ts</summary>

```typescript
'use client'
import { useEffect, useState, useCallback, useRef } from 'react'
import { SDK } from '@somnia-chain/streams'
import { getPublicHttpClient } from './clients'
import { chatSchema } from './chatSchema'
import { toHex, type Hex } from 'viem'

// Helper to unwrap field values
const val = (f: any) => f?.value?.value ?? f?.value

// Message type
export type ChatMsg = {
  timestamp: number
  roomId: `0x${string}`
  content: string
  senderName: string
  sender: `0x${string}`
}

/**
 * Fetch chat messages from Somnia Streams (read-only, auto-refresh, cumulative)
 */
export function useChatMessages(
  roomName?: string,
  limit = 100,
  refreshMs = 5000
) {
  const [messages, setMessages] = useState<ChatMsg[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const timerRef = useRef<NodeJS.Timeout | null>(null)

  const loadMessages = useCallback(async () => {
    try {
      const sdk = new SDK({ public: getPublicHttpClient() })

      // Compute schema ID from the chat schema
      const schemaId = await sdk.streams.computeSchemaId(chatSchema)
      const publisher =
        process.env.NEXT_PUBLIC_PUBLISHER_ADDRESS ??
        '0x0000000000000000000000000000000000000000'

      // Fetch all publisher data for schema
      const resp = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)

      // Ensure array structure (each row corresponds to an array of fields)
      const rows: any[][] = Array.isArray(resp) ? (resp as any[][]) : []
      if (!rows.length) {
        setMessages([])
        setLoading(false)
        return
      }

      // Convert room name to bytes32 for filtering (if applicable)
      const want = roomName ? toHex(roomName, { size: 32 }).toLowerCase() : null

      const parsed: ChatMsg[] = []
      for (const row of rows) {
        if (!Array.isArray(row) || row.length < 5) continue

        const ts = Number(val(row[0]))
        const ms = String(ts).length <= 10 ? ts * 1000 : ts // handle seconds vs ms
        const rid = String(val(row[1])) as `0x${string}`

        // Skip messages from other rooms if filtered
        if (want && rid.toLowerCase() !== want) continue

        parsed.push({
          timestamp: ms,
          roomId: rid,
          content: String(val(row[2]) ?? ''),
          senderName: String(val(row[3]) ?? ''),
          sender: (String(val(row[4])) as `0x${string}`) ??
            '0x0000000000000000000000000000000000000000',
        })
      }

      // Sort by timestamp (ascending)
      parsed.sort((a, b) => a.timestamp - b.timestamp)

      // Deduplicate and limit
      setMessages((prev) => {
        const combined = [...prev, ...parsed]
        const unique = combined.filter(
          (msg, index, self) =>
            index ===
            self.findIndex(
              (m) =>
                m.timestamp === msg.timestamp &&
                m.sender === msg.sender &&
                m.content === msg.content
            )
        )
        return unique.slice(-limit)
      })

      setError(null)
    } catch (err: any) {
      console.error('❌ Failed to load chat messages:', err)
      setError(err.message || 'Failed to load messages')
    } finally {
      setLoading(false)
    }
  }, [roomName, limit])

  // Initial load + polling
  useEffect(() => {
    setLoading(true)
    loadMessages()
    timerRef.current = setInterval(loadMessages, refreshMs)
    return () => timerRef.current && clearInterval(timerRef.current)
  }, [loadMessages, refreshMs])

  return { messages, loading, error, reload: loadMessages }
}

```

</details>

## API Routes

To parse the data to the NextJS UI, we will create API route files that will enable us to call `sendMessage` and `fetchChatMessages` functions

### Write Messages Endpoint

`src/app/api/send/route.ts`

```typescript
import { NextResponse } from 'next/server'
import { sendMessage } from '@/lib/chatService'

export async function POST(req: Request) {
  try {
    const { room, content, senderName } = await req.json()
    if (!room || !content) throw new Error('Missing fields')
    const { txHash } = await sendMessage(room, content, senderName)
    return NextResponse.json({ success: true, txHash })
  } catch (e: any) {
    console.error(e)
    return NextResponse.json({ error: e.message || 'Failed to send' }, { status: 500 })
  }
}
```

Publishes messages with the server wallet. We validate the input and return the tx hash.

## Frontend

Update `src/app/page.tsx` Connect the Somnia Data Streams logic to a simple frontend. Fetch messages stored on-chain, display them in real time, and send new ones.

#### Component Setup

```tsx
use client'
import { useState } from 'react'
import { useChatMessages } from '@/lib/chatMessages'

export default function Page() {
  const [room, setRoom] = useState('general')
  const [content, setContent] = useState('')
  const [senderName, setSenderName] = useState('Victory')
  const [error, setError] = useState<string | null>(null)

  const {
    messages,
    loading,
    error: fetchError,
    reload,
  } = useChatMessages(room, 200)

  // --- Send new message via API route ---
  async function send() {
    try {
      if (!content.trim()) {
        setError('Message content cannot be empty')
        return
      }

      const res = await fetch('/api/send', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ room, content, senderName }),
      })

      const data = await res.json()
      if (!res.ok) throw new Error(data?.error || 'Failed to send message')

      setContent('')
      setError(null)
      reload() // refresh after sending
    } catch (e: any) {
      console.error('❌ Send message failed:', e)
      setError(e?.message || 'Failed to send message')
    }
  }
```

* `room`, `content`, and `senderName` store user input.
* `useChatMessages(room, 200)` reads the latest 200 messages from Somnia Data Streams using a **read-only SDK instance**.\
  The hook automatically polls for new messages every few seconds.
* The `send()` function publishes a new message by calling the `/api/send` endpoint, which writes on-chain using `sdk.streams.setAndEmitEvents()`.\
  After a message is successfully sent, the input clears and `reload()` is called to refresh the messages list.

The hook handles loading and error states internally, while the component keeps a separate `error` state for send failures.

#### UI Rendering

```tsx
  return (
    <main style={{ padding: 24, fontFamily: 'system-ui, sans-serif' }}>
      <h1>💬 Somnia Streams Chat (read-only)</h1>

      <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
        <input
          value={room}
          onChange={(e) => setRoom(e.target.value)}
          placeholder="room"
        />
        <input
          value={senderName}
          onChange={(e) => setSenderName(e.target.value)}
          placeholder="name"
        />
        <button onClick={reload} disabled={loading}>
          Refresh
        </button>
      </div>

      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <input
          style={{ flex: 1 }}
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Type a message"
        />
        <button onClick={send}>Send</button>
      </div>

      {(error || fetchError) && (
        <div style={{ color: 'crimson', marginBottom: 12 }}>
          Error: {error || fetchError}
        </div>
      )}

      {loading ? (
        <p>Loading messages...</p>
      ) : !messages.length ? (
        <p>No messages yet.</p>
      ) : (
        <ul style={{ paddingLeft: 16 }}>
          {messages.map((m, i) => (
            <li key={i}>
              <small>{new Date(m.timestamp).toLocaleTimeString()} </small>
              <b>{m.senderName || m.sender}</b>: {m.content}
            </li>
          ))}
        </ul>
      )}
    </main>
  )
}
```

* The top input fields let users change the chat room or display name, and manually refresh messages if needed.
* The second input and **Send** button allow posting new messages.
* Error messages appear in red if either sending or fetching fails.
* Below, the app dynamically renders one of three states:
  * “Loading messages…” while fetching.
  * “No messages yet.” if the room is empty.
  * A chat list showing messages with timestamps, names, and content.

Each message represents **on-chain data** fetched via Somnia Data Streams, fully verified, timestamped, and appended as part of the schema structure. We clear the input and trigger a delayed refresh so the message appears soon after mining.

#### How It Works

This component bridges the Somnia SDK’s on-chain capabilities with React’s reactive rendering model.\
Whenever the user sends a message, the SDK publishes it to Somnia Data Streams via the backend `/api/send` route.\
Meanwhile, `useChatMessages` polls the blockchain for updates, decoding structured data stored by the same schema.\
As a result, each message displayed in the chat window is **a verifiable blockchain record**, yet the experience feels as fluid and fast as a typical Web2 chat.

```markup
+-----------+       +--------------------+      +---------------------+
|  User UI  | --->  |  Next.js API /send | ---> | Somnia Data Streams |
+-----------+       +--------------------+      +---------------------+
      ^                       |                           |
      |                       v                           |
      |             +------------------+                  |
      |             |   Blockchain     |                  |
      |             |  (Transaction)   |                  |
      |             +------------------+                  |
      |                       |                           |
      |<----------------------|                           |
      |     Poll via SDK or Subscribe (useChatMessages)   |
      +---------------------------------------------------+

```

Complete code below:

<details>

<summary>page.tsx</summary>

```typescript
'use client'
import { useState } from 'react'
import { useChatMessages } from '@/lib/chatMessages'

export default function Page() {
  const [room, setRoom] = useState('general')
  const [content, setContent] = useState('')
  const [senderName, setSenderName] = useState('Victory')
  const [error, setError] = useState<string | null>(null)

  const {
    messages,
    loading,
    error: fetchError,
    reload,
  } = useChatMessages(room, 200)

  // --- Send new message via API route ---
  async function send() {
    try {
      if (!content.trim()) {
        setError('Message content cannot be empty')
        return
      }

      const res = await fetch('/api/send', {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ room, content, senderName }),
      })

      const data = await res.json()
      if (!res.ok) throw new Error(data?.error || 'Failed to send message')

      setContent('')
      setError(null)
      reload() // refresh after sending
    } catch (e: any) {
      console.error('❌ Send message failed:', e)
      setError(e?.message || 'Failed to send message')
    }
  }

  // --- Render UI ---
  return (
    <main
      style={{
        padding: 24,
        fontFamily: 'system-ui, sans-serif',
        maxWidth: 640,
        margin: '0 auto',
      }}
    >
      <h1>💬 Somnia Data Streams Chat</h1>
      <p style={{ color: '#666' }}>
        Messages are stored <b>onchain</b> and read using Somnia Data Streams.
      </p>

      {/* Room + Name inputs */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
        <input
          value={room}
          onChange={(e) => setRoom(e.target.value)}
          placeholder="room name"
          style={{ flex: 1, padding: 6 }}
        />
        <input
          value={senderName}
          onChange={(e) => setSenderName(e.target.value)}
          placeholder="your name"
          style={{ flex: 1, padding: 6 }}
        />
        <button
          onClick={reload}
          disabled={loading}
          style={{
            background: '#0070f3',
            color: 'white',
            border: 'none',
            padding: '6px 12px',
            cursor: 'pointer',
            borderRadius: 4,
          }}
        >
          Refresh
        </button>
      </div>

      {/* Message input */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        <input
          style={{ flex: 1, padding: 6 }}
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="Type your message..."
        />
        <button
          onClick={send}
          style={{
            background: '#28a745',
            color: 'white',
            border: 'none',
            padding: '6px 12px',
            cursor: 'pointer',
            borderRadius: 4,
          }}
        >
          Send
        </button>
      </div>

      {/* Error messages */}
      {(error || fetchError) && (
        <div style={{ color: 'crimson', marginBottom: 12 }}>
          Error: {error || fetchError}
        </div>
      )}

      {/* Message list */}
      {loading ? (
        <p>Loading messages...</p>
      ) : !messages.length ? (
        <p>No messages yet.</p>
      ) : (
        <ul style={{ paddingLeft: 16, listStyle: 'none' }}>
          {messages.map((m, i) => (
            <li key={i} style={{ marginBottom: 8 }}>
              <small style={{ color: '#666' }}>
                {new Date(m.timestamp).toLocaleTimeString()}
              </small>{' '}
              <b>{m.senderName || m.sender}</b>: {m.content}
            </li>
          ))}
        </ul>
      )}
    </main>
  )
}

```

</details>

## Run the App

Run the program using the command:

```bash
npm run dev
```

Your app will be LIVE at <http://localhost:3000> in your browser

{% hint style="success" %}
Tip

Open two browser windows to simulate two users watching the same room. Both will see new messages as the poller fetches fresh data.<br>
{% endhint %}

## Codebase

<https://github.com/emmaodia/somnia-streams-chat-demo>


# Build a Realtime On-Chain Game

Build a Tap-to-Play Onchain Game

This tutorial shows how to build a Tap-to-Play Onchain Game using Somnia Data Streams, where every player’s tap is written directly to the blockchain and the leaderboard updates in realtime.

Each tap is stored onchain as a structured data record following a schema.\
The game uses MetaMask for wallet identity and Somnia Streams SDK to:

* Store tap events onchain using `sdk.streams.set()`
* Retrieve and rank all players from onchain data

By the end of this guide, you’ll have:\
\- A working Next.js app\
\- Onchain data storage using Somnia Data Streams\
\- A live leaderboard that reads blockchain state\
\- MetaMask integration for identity and transaction signing

***

## Prerequisites

* Node.js 20+
* A funded Somnia Testnet wallet. Kindly get some from the [Faucet](https://testnet.somnia.network/)
* Basic familiarity with TypeScript and Next.js

***

## Project Setup

Initialize a new Next.js app and install dependencies. Create the app by creating a directory where the app will live

```bash
npx create-next-app@latest somnia-chat --ts --app --no-tailwind
cd somnia-chat
```

Install the [Somnia Streams](https://www.npmjs.com/package/@somnia-chain/streams) and ViemJS dependencies

```bash
npm i @somnia-chain/streams viem
```

Create a .env.local file for storing secrets and environmental variables

```bash
NEXT_PUBLIC_PUBLISHER_ADDRESS=0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
NEXT_PUBLIC_RPC_URL=https://dream-rpc.somnia.network
```

{% hint style="danger" %}
Never expose PRIVATE\_KEY to the browser. Keep all publishing code in API routes or server code only. NOTE: You can connect a Privy Wallet (or equivalent) to the SDK, avoiding the need entirely for private keys.
{% endhint %}

***

## Define Tap Schema <a href="#chain-configuration" id="chain-configuration"></a>

Create a file `lib/schema.ts`:

```typescript
// lib/schema.ts
export const tapSchema = 'uint64 timestamp, address player'
```

This schema defines the structure of each tap event:

* `timestamp`: when the tap occurred
* `player`: who tapped (wallet address)
* A `nonce` will be added when the schema is deployed to ensures each record is unique

The Schema ID will be automatically computed by the SDK from this schema.

***

## Setup Clients

Create `lib/serverClient.ts` for server-side reads:

```typescript
// lib/serverClient.ts
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from 'viem/chains'

export function getServerPublicClient() {
  return createPublicClient({
    chain: somniaTestnet,
    transport: http(process.env.RPC_URL || 'https://dream-rpc.somnia.network'),
  })
}
```

Create `lib/clients.ts` for client-side access:

```typescript
// lib/clients.ts
'use client'
import { createPublicClient, http } from 'viem'
import { somniaTestnet } from 'viem/chains'

export function getPublicHttpClient() {
  return createPublicClient({
    chain: somniaTestnet,
    transport: http(process.env.NEXT_PUBLIC_RPC_URL || 'https://dream-rpc.somnia.network'),
  })
}
```

***

## Writing Tap Data Onchain

Each tap is recorded onchain with the `sdk.streams.set()` method. In this section, we’ll walk through how each part of the `sendTap()` logic works from wallet connection to writing structured schema data onchain.

***

### **Set up state variables**

We’ll start by tracking a number of states, such as:

* the connected wallet address
* the wallet client (MetaMask connection)
* and a few helper states for loading, cooldowns, and errors.

```tsx
const [address, setAddress] = useState('')
const [walletClient, setWalletClient] = useState<any>(null)
const [cooldownMs, setCooldownMs] = useState(0)
const [pending, setPending] = useState(false)
const [error, setError] = useState('')
```

These ensure that you can access the connected wallet address (`address`) and track transaction state (`pending`). It also prevents spam taps with a 1-second cooldown (`cooldownMs`)

***

### **Connect MetaMask**

We use the browser’s `window.ethereum` API to connect to MetaMask.\
Once connected, we create a **wallet client** that Somnia’s SDK can use for signing transactions.

```tsx
async function connectWallet() {
  if (typeof window !== "undefined" && window.ethereum !== undefined)
    try {
      await window.ethereum.request({ method: "eth_requestAccounts" });
      const walletClient = createWalletClient({
          chain: somniaDream,
          transport: custom(window.ethereum),
      });
      const [account] = await walletClient.getAddresses();
      setWalletClient(walletClient)
      setAddress(account)
    } catch (e: any) {
      setError(e?.message || String(e))
    }  setWalletClient(wallet)
}
```

`createWalletClient` from Viem wraps MetaMask into a signer object that the Somnia SDK can use.\
This is how the UI and the blockchain are bridged securely.

***

### **Initialize the SDK**

The **Somnia Data Streams SDK** provides methods to compute schema IDs, encode structured data, and publish to the blockchain. We initialize it with both the **public client** (for chain access) and the **wallet client** (for signing transactions).

```tsx
const sdk = new SDK({
  public: getPublicHttpClient(),
  wallet: walletClient,
})
```

This gives you full read/write access to the Somnia Streams contract on Somnia Testnet.

***

### **Compute the Schema ID**

Schemas define how the onchain data is structured. In this case, the tap schema looks like this:

```ts
tapSchema = 'uint64 timestamp, address player'
```

Before writing data, we must compute its **unique Schema ID**:

```tsx
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
```

This produces a deterministic ID derived from the schema text, ensuring that any app using the same schema can read or decode your data.

***

### **Register the Schema**

```typescript
// Register schema
  const schemaId = await sdk.streams.computeSchemaId(chatSchema)
  const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
  if (!isRegistered) {
    const ignoreAlreadyRegistered = true
    const txHash = await sdk.streams.registerDataSchemas(
      [{ schemaName: 'tap', schema: tapSchema, parentSchemaId: zeroBytes32 }],
      ignoreAlreadyRegistered
    )
    if (!txHash) throw new Error('Failed to register schema')
    await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
  }
```

If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.

### **Encode the Data**

Somnia Streams stores structured data using its `SchemaEncoder` class. We create an encoder and provide each field according to the schema definition.

```tsx
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())

const data = encoder.encodeData([
  { name: 'timestamp', value: now, type: 'uint64' },
  { name: 'player', value: address, type: 'address' },
])
```

This converts your JavaScript values into the precise binary format that can be stored onchain and later decoded.

***

### **Generate a Unique Data ID**

Each record needs a **unique identifier** within the schema. We use the `keccak256` hash of the player’s address and timestamp to ensure that it is packed into 32 bits of data.

```tsx
const id = keccak256(toHex(`${address}-${Number(nonce)}`))
```

This ensures no two taps collide, even if the same player taps rapidly.

***

### **Store the Tap Onchain**

Finally, we push the structured data to the blockchain using:

```tsx
await sdk.streams.set([{ id, schemaId, data }])
```

The `set()` method writes one or more records (called *Data Streams*) to the chain.\
Each record is cryptographically signed by the player’s wallet, and gets stored on Somnia’s decentralized data infrastructure. It can also be retrieved instantly using the same schema

***

### **Manage Cooldowns and Feedback**

After the tap is sent, we apply a 1-second cooldown to avoid flooding transactions and reset the pending state.

```tsx
setCooldownMs(1000)
setPending(false)
```

This gives players a smooth UX while maintaining blockchain transaction integrity.

***

#### Putting It All Together

Here’s the complete `sendTap()` method with all steps combined:

```tsx
async function sendTap() {
  if (!walletClient || !address) return
  setPending(true)

  const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
  const schemaId = await sdk.streams.computeSchemaId(tapSchema)
  const encoder = new SchemaEncoder(tapSchema)
  const now = BigInt(Date.now())

  const data = encoder.encodeData([
    { name: 'timestamp', value: now, type: 'uint64' },
    { name: 'player', value: address, type: 'address' },
  ])

  const id = keccak256(toHex(`${address}-${Number(now)}`))
  await sdk.streams.set([{ id, schemaId, data }])
  setCooldownMs(1000)
  setPending(false)
}
```

### Complete \`page.tsx\` Code

<details>

<summary>page.tsx</summary>

```typescript
'use client'
import { useState, useEffect, useRef } from 'react'
import { SDK, SchemaEncoder } from '@somnia-chain/streams'
import { getPublicHttpClient } from '@/lib/clients'
import { tapSchema } from '@/lib/schema'
import { keccak256, toHex, createWalletClient, custom } from 'viem'
import { somniaTestnet } from 'viem/chains'

export default function Page() {
  const [address, setAddress] = useState('')
  const [walletClient, setWalletClient] = useState<any>(null)
  const [leaderboard, setLeaderboard] = useState<{ address: string; count: number }[]>([])
  const [cooldownMs, setCooldownMs] = useState(0)
  const [pending, setPending] = useState(false)
  const [error, setError] = useState('')
  const lastNonce = useRef<number>(0)

  async function connectWallet() {
    const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
    const wallet = createWalletClient({
      chain: somniaTestnet,
      transport: custom(window.ethereum),
    })
    setAddress(accounts[0])
    setWalletClient(wallet)
  }
  async function sendTap() {
    if (!walletClient || !address) return
    setPending(true)
    const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
    const schemaId = await sdk.streams.computeSchemaId(tapSchema)
    const encoder = new SchemaEncoder(tapSchema)
    const now = BigInt(Date.now())
    const data = encoder.encodeData([
      { name: 'timestamp', value: now, type: 'uint64' },
      { name: 'player', value: address, type: 'address' },
      { name: 'nonce', value: BigInt(lastNonce.current++), type: 'uint256' },
    ])
    const id = keccak256(toHex(`${address}-${Number(now)}`))
    await sdk.streams.set([{ id, schemaId, data }])
    setCooldownMs(1000)
    setPending(false)
  }

  return (
    <main style={{ padding: 24 }}>
      <h1>🚀 Somnia Tap Game</h1>
      {!address ? (
        <button onClick={connectWallet}>🦊 Connect MetaMask</button>
      ) : (
        <p>Connected: {address.slice(0, 6)}...{address.slice(-4)}</p>
      )}
      <button onClick={sendTap} disabled={pending || cooldownMs > 0 || !address}>
        {pending ? 'Sending...' : '🖱️ Tap'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      <Leaderboard leaderboard={leaderboard} />
    </main>
  )
}

function Leaderboard({ leaderboard }: { leaderboard: { address: string; count: number }[] }) {
  if (!leaderboard.length) return <p>No taps yet</p>
  return (
    <ol>
      {leaderboard.map((p, i) => (
        <li key={p.address}>
          #{i + 1} {p.address} — {p.count} taps
        </li>
      ))}
    </ol>
  )
}
```

</details>

***

## Reading Leaderboard Data Onchain

The leaderboard is calculated server-side by reading all tap data stored onchain. Create a `lib/store.ts` file and add the following code:

```typescript
lib/store.ts
import { SDK } from '@somnia-chain/streams'
import { getServerPublicClient } from './serverClient'
import { tapSchema } from './schema'

const publisher =
  process.env.NEXT_PUBLIC_PUBLISHER_ADDRESS ||
  '0x0000000000000000000000000000000000000000'
const val = (f: any) => f?.value?.value ?? f?.value

export async function getLeaderboard() {
  const sdk = new SDK({ public: getServerPublicClient() })
  const schemaId = await sdk.streams.computeSchemaId(tapSchema)
  const rows = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
  if (!Array.isArray(rows)) return []

  const counts = new Map<string, number>()
  for (const row of rows) {
    const player = String(val(row[1]) ?? '').toLowerCase()
    if (!player.startsWith('0x')) continue
    counts.set(player, (counts.get(player) || 0) + 1)
  }

  return Array.from(counts.entries())
    .map(([address, count]) => ({ address, count }))
    .sort((a, b) => b.count - a.count)
}
```

The leaderboard logic begins inside the `getLeaderboard()` function, where we use the **SDK** to read structured tap data directly from the blockchain. First, the function initializes the SDK with a **server-compatible public client**, which allows read-only access to the chain without a connected wallet. The next step computes the `schemaId` by passing our `tapSchema` to `sdk.streams.computeSchemaId()`. This produces a deterministic identifier that ensures we’re always referencing the correct data structure.

Once the `schemaId` is known, the core operation happens through `sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)`. This method queries the blockchain for all records written by the specified publisher under that schema. Each returned record is an array of fields that align with the schema’s definition, in this case `[timestamp, player]`. The helper function `val()` is then used to unwrap nested field values (`f?.value?.value`) from the SDK’s response format, giving us clean, readable values.

`getAllPublisherDataForSchema` acts like a decentralized “SELECT \* FROM” query, fetching all onchain data tied to a schema and publisher, while the rest of the function transforms that raw blockchain data into a structured leaderboard the app can display.<br>

***

Creat a api route to retrieve Leaderboard score. Create the file `app/api/leaderboard/route.ts`

```typescript
import { NextResponse } from 'next/server'
import { getLeaderboard } from '@/lib/store'

export async function GET() {
  const leaderboard = await getLeaderboard()
  return NextResponse.json({ leaderboard })
}
```

This endpoint imports the `getLeaderboard()` function from `lib/store.ts`, which handles the heavy lifting of querying Somnia Data Streams, and then exposes that onchain data as a clean, JSON-formatted response for your application. The client simply fetches the leaderboard via `/api/leaderboard`.<br>

The page.tsx fetches /api/leaderboard every few seconds to stay updated.

***

Every tap executes a real blockchain transaction:

| Field     | Description                  |
| --------- | ---------------------------- |
| timestamp | Time of the tap              |
| player    | Wallet address of the player |

When the `set()` call succeeds, Somnia Data Streams stores the record and indexes it under your publisher’s address. Any application (including yours) can then read this data and build dashboards, analytics, or game leaderboards.

***

## Run the App

```bash
npm run dev
```

Open[ http://localhost:3000](http://localhost:3000) and connect your MetaMask wallet. Click 🖱️ Tap to send onchain transactions, and watch your leaderboard update live.

***

## Conclusion

You’ve built a fully onchain game where player interactions are stored via Somnia Data Streams and leaderboard rankings are derived from immutable blockchain data. MetaMask provides secure, user-friendly authentication. This same pattern powers realtime Web3 experiences, from social apps to competitive games, using Somnia’s high-performance onchain data infrastructure.


# Smart Contracts

List of critical Smart Contract addresses for Somnia network.

<table><thead><tr><th width="290.111328125">Contract</th><th>Address</th></tr></thead><tbody><tr><td>MultiCallV3</td><td><a href="https://explorer.somnia.network/address/0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11">0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11</a></td></tr><tr><td>WSOMI</td><td><a href="https://explorer.somnia.network/token/0x046EDe9564A72571df6F5e44d0405360c0f4dCab">0x046EDe9564A72571df6F5e44d0405360c0f4dCab</a></td></tr><tr><td>USDC</td><td><a href="https://explorer.somnia.network/address/0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00">0x28bec7e30e6faee657a03e19bf1128aad7632a00</a></td></tr><tr><td>WETH</td><td><a href="https://explorer.somnia.network/token/0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8">0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8</a></td></tr><tr><td>WBTC</td><td><a href="https://explorer.somnia.network/token/0xC5098b3cA516784323872F17235fa074E167D3D2">0xC5098b3cA516784323872F17235fa074E167D3D2</a></td></tr><tr><td>USDT</td><td><a href="https://explorer.somnia.network/token/0x67B302E35Aef5EEE8c32D934F5856869EF428330">0x67B302E35Aef5EEE8c32D934F5856869EF428330</a></td></tr><tr><td>USDso</td><td><a href="https://explorer.somnia.network/token/0x00000022dA000002656c64D9eA6011ea952D008A">0x00000022dA000002656c64D9eA6011ea952D008A</a></td></tr></tbody></table>

## Omnichain SOMI Deployments

<table><thead><tr><th width="123.634765625">Mainnet</th><th width="459.333984375">Address</th><th>Type</th></tr></thead><tbody><tr><td>BNB Chain</td><td><a href="https://bscscan.com/token/0xa9616e5e23ec1582c2828b025becf3ef610e266f">0xa9616e5e23ec1582c2828b025becf3ef610e266f</a></td><td>OFT</td></tr><tr><td>Base</td><td><a href="https://basescan.org/token/0x47636b3188774a3E7273D85A537b9bA4Ee7b2535">0x47636b3188774a3E7273D85A537b9bA4Ee7b253</a></td><td>OFT</td></tr><tr><td>Ethereum</td><td><a href="https://etherscan.io/token/0x1B0F6590d21dc02B92ad3A7D00F8884dC4f1aed9">0x1B0F6590d21dc02B92ad3A7D00F8884dC4f1aed9</a></td><td>OFT</td></tr><tr><td>Somnia</td><td><a href="https://explorer.somnia.network/address/0xC3D4E9Ac47D7f37bB07C2f8355Bb4940DEA3bbC3">0xC3D4E9Ac47D7f37bB07C2f8355Bb4940DEA3bbC3</a></td><td>NativeOFTAdapter</td></tr></tbody></table>

## LayerZero Contracts

* **`chainKey` :** `somnia`
* **`stage` :** `mainnet`
* **`EID` :** `30380`

<table><thead><tr><th width="286.4140625">Contract</th><th>Address</th></tr></thead><tbody><tr><td>endpointV2 (main entrypoint)</td><td><a href="https://explorer.somnia.network/address/0x6F475642a6e85809B1c36Fa62763669b1b48DD5B">0x6F475642a6e85809B1c36Fa62763669b1b48DD5B</a></td></tr><tr><td>sendUln302</td><td><a href="https://explorer.somnia.network/address/0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7">0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7</a></td></tr><tr><td>receiveUln302</td><td><a href="https://explorer.somnia.network/address/0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7">0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043</a></td></tr><tr><td>executor</td><td><a href="https://explorer.somnia.network/address/0x4208D6E27538189bB48E603D6123A94b8Abe0A0b">0x4208D6E27538189bB48E603D6123A94b8Abe0A0b</a></td></tr><tr><td>deadDVN</td><td><a href="https://explorer.somnia.network/address/0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842">0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842</a></td></tr></tbody></table>

## Oracles

### DIA - Mainnet [Price Feeds](/developer/building-dapps/oracles/dia-price-feeds)

<table><thead><tr><th width="276.751953125">Contract</th><th>Address</th></tr></thead><tbody><tr><td>Oracle</td><td><a href="https://explorer.somnia.network/address/0xbA0E0750A56e995506CA458b2BdD752754CF39C4">0xbA0E0750A56e995506CA458b2BdD752754CF39C4</a></td></tr><tr><td>Gas Wallet</td><td><a href="https://mainnet.somnia.w3us.site/address/0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a">0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a</a></td></tr><tr><td>USDT</td><td><a href="https://mainnet.somnia.w3us.site/address/0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267">0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267</a></td></tr><tr><td>USDC</td><td><a href="https://explorer.somnia.network/address/0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93">0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93</a></td></tr><tr><td>BTC</td><td><a href="https://explorer.somnia.network/address/0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE">0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE</a></td></tr><tr><td>ARB</td><td><a href="https://explorer.somnia.network/address/0x6a96a0232402c2BC027a12C73f763b604c9F77a6">0x6a96a0232402c2BC027a12C73f763b604c9F77a6</a></td></tr><tr><td>SOL</td><td><a href="https://mainnet.somnia.w3us.site/address/0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234">0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234</a></td></tr><tr><td>WETH</td><td><a href="https://explorer.somnia.network/address/0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52">0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52</a></td></tr><tr><td>SOMI</td><td><a href="https://explorer.somnia.network/address/0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27">0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27</a></td></tr></tbody></table>

### Protofire - Mainnet [VRF Smart Contracts](/developer/building-dapps/oracles/using-verifiable-randomness-vrf)

<table><thead><tr><th width="261.458984375">Contract</th><th>Address</th></tr></thead><tbody><tr><td>VRFV2PlusWrapper</td><td><a href="https://explorer.somnia.network/address/0x606b2B36516AB7479D1445Ec14B6B39B44901bf8">0x606b2B36516AB7479D1445Ec14B6B39B44901bf8</a></td></tr><tr><td>LINK Token</td><td><a href="https://explorer.somnia.network/address/0x0a4Db7035284566F6f676991ED418140dC01A2aa">0x0a4Db7035284566F6f676991ED418140dC01A2aa</a></td></tr><tr><td>LINK/NATIVE oracle</td><td><a href="https://explorer.somnia.network/address/0xEBD41881413dD76F42DF2902ee865099af9099B4">0xEBD41881413dD76F42DF2902ee865099af9099B4</a></td></tr></tbody></table>


# Development Frameworks

Developing a robust decentralized application (dApp) requires a complex stack of technologies. Software frameworks streamline this process by bundling essential features or offering flexible plugin architectures to customize your toolkit. These frameworks provide immediate, out-of-the-box utility, including:

* Local Environments: Tools to instantly launch a local blockchain instance.
* Development Suite: Utilities for compiling and testing smart contracts.
* Integrated Frontend: Add-ons that allow for client-side development within the same repository.
* Deployment Management: Configurations for connecting to networks (local or public) and deploying contracts.


# Local Testing and Forking

{% hint style="info" %}
This page shows how to spin up a local EVM node with **Hardhat** (and optionally **Anvil**) and fork **Somnia Testnet (Shannon)** or **Somnia Mainnet** for realistic testing.
{% endhint %}

***

## Prerequisites

* Node.js 20+
* Hardhat (or Foundry/Anvil if you prefer)
* A `.env` file with Somnia RPCs:

{% code title=".env (example)" %}

```bash
# .env (example)
SOMNIA_RPC_MAINNET=https://api.infra.mainnet.somnia.network/
SOMNIA_RPC_TESTNET=https://dream-rpc.somnia.network/

# Optional: pin block numbers for reproducible forks
FORK_BLOCK_MAINNET=
FORK_BLOCK_TESTNET=
```

{% endcode %}

> Do not commit real keys/tokens. Use environment variables.

***

## Quick Start (Hardhat)

{% stepper %}
{% step %}
{% code title="Install packages" %}

```bash
npm i -D hardhat @nomicfoundation/hardhat-ethers ethers dotenv
npx hardhat # if project not initialized yet
cp .env.example .env || true
```

{% endcode %}
{% endstep %}

{% step %}
{% code title="hardhat.config.ts" %}

```ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import * as dotenv from "dotenv";
dotenv.config();

const cfgFromEnv = {
  mainnetUrl: process.env.SOMNIA_RPC_MAINNET || "https://api.infra.mainnet.somnia.network/",
  testnetUrl: process.env.SOMNIA_RPC_TESTNET || "https://dream-rpc.somnia.network/",
  forkMainnetBlock: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
  forkTestnetBlock: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
};

const config: HardhatUserConfig = {
  solidity: "0.8.19",
  networks: {
    hardhat: {
      chainId: 31337,
    },
    localhost: {
      url: "http://127.0.0.1:8545",
      chainId: 31337,
    },
    somnia_testnet: {
      url: cfgFromEnv.testnetUrl,
      chainId: 50312,
    },
    somnia_mainnet: {
      url: cfgFromEnv.mainnetUrl,
      chainId: 5031,
    },
  },
};

export default config;
```

{% endcode %}
{% endstep %}
{% endstepper %}

***

## Testing Your DApp Locally

Once your Hardhat environment is set up, you can write and run tests for your smart contracts. This ensures your code works as expected before deploying it.

**Create a Simple Smart Contract**

First, create a basic smart contract to test. The `Counter.sol` contract below includes fundamental functions for incrementing a counter and retrieving its current value. Save this file in your project's `contracts` directory.

{% code title="contracts/Counter.sol" %}

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

contract Counter {
    uint256 private count;

    event CountedTo(uint256 number);

    function getCount() public view returns (uint256) {
        return count;
    }

    function increment() public {
        count += 1;
        emit CountedTo(count);
    }
}
```

{% endcode %}

### **Write a Test File**

Next, write a test file to verify the functionality of your smart contract. The `Counter.test.ts` file below deploys the `Counter` contract on a local test network, calls the `increment` function, and checks whether the outcome is as expected. Save this file in your project's `test` directory.

{% code title="test/Counter.test.ts" %}

```ts
import { expect } from "chai";
import { ethers } from "hardhat";

describe("Counter Contract", function () {
  it("Should increment the count by 1", async function () {
    const Counter = await ethers.getContractFactory("Counter");
    const counter = await Counter.deploy();
    await counter.waitForDeployment();

    expect(await counter.getCount()).to.equal(0);

    const tx = await counter.increment();
    await tx.wait();

    expect(await counter.getCount()).to.equal(1);
  });

  it("Should emit a CountedTo event", async function () {
    const Counter = await ethers.getContractFactory("Counter");
    const counter = await Counter.deploy();
    await counter.waitForDeployment();

    await expect(counter.increment()).to.emit(counter, "CountedTo").withArgs(1);
  });
});
```

{% endcode %}

### **Run Your Tests**

To run your tests, navigate to your project's root directory in the terminal and use the following command. This command will execute your test scripts using Hardhat's built-in test network and display the results.

```bash
# Run tests on the default in-process Hardhat network
npx hardhat test

# Or, start a local node in a separate terminal
npx hardhat node

# Then run tests against it
npx hardhat test --network localhost
```

***

## Forking Somnia (Testnet vs Mainnet)

*Forking is the process of copying the state of a live network, like Somnia Mainnet or Testnet, at a specific block and creating a simulation of it on your local machine. This powerful feature allows you to test how your contract will interact with other deployed contracts on the live network (such as a DEX, oracle, or NFT marketplace) using real-world data, but without any of the risk or cost.*

You can fork Shannon Testnet for faster iteration or Mainnet for production-like state. For deterministic CI, always pin a blockNumber.

{% tabs %}
{% tab title="Fork Testnet (Shannon)" %}

```ts
hardhat: {
  forking: {
    url: process.env.SOMNIA_RPC_TESTNET!,
    blockNumber: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
  },
}
```

```bash
# in-process fork
npx hardhat test

# persistent node
npx hardhat node --fork $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET}
```

{% endtab %}

{% tab title="Fork Mainnet" %}

```ts
hardhat: {
  forking: {
    url: process.env.SOMNIA_RPC_MAINNET!,
    blockNumber: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
  },
}
```

```bash
# in-process fork
npx hardhat test

# persistent node
npx hardhat node --fork $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET}
```

{% endtab %}
{% endtabs %}

> Testnet (STT) is ideal for validating flows and cheaper RPC limits; Mainnet (SOMI) reflects real contract/state and gas rules.

***

## Handy RPC Tricks (Hardhat)

These RPC methods provided by Hardhat Network allow you to manipulate the blockchain state for advanced testing scenarios.

### **Impersonate an account**

This allows you to execute transactions from any wallet address on the forked chain, which is perfect for testing functions with admin privileges or interacting with contracts using an account that holds a large amount of tokens ("whale").

{% code title="impersonate.ts" %}

```ts
import { ethers, network } from "hardhat";

async function main() {
  const target = "0xYourSomniaAddress"; // account to impersonate
  await network.provider.request({ method: "hardhat_impersonateAccount", params: [target] });
  const signer = await ethers.getSigner(target);

  await network.provider.send("hardhat_setBalance", [
    target,
    "0x152d02c7e14af6800000" // 1000 ether in wei
  ]);

  console.log("Impersonating:", await signer.getAddress());
}

main().catch((e) => { console.error(e); process.exit(1); });
```

{% endcode %}

### **Time travel**

This feature lets you change the timestamp of future blocks. It's incredibly useful for testing time-dependent smart contract logic, such as vesting schedules, lock-up periods, or any functionality that relies on `block.timestamp`.

```ts
await network.provider.send("evm_setNextBlockTimestamp", [Math.floor(Date.now()/1000) + 3600]);
await network.provider.send("evm_mine");
```

### **Snapshot and Revert**

This allows you to save the current state of the blockchain and later restore it instantly. It's an efficient way to isolate your tests, ensuring that each test case starts from the same clean state without needing to restart the local node.

```ts
const id = await network.provider.send("evm_snapshot");
// ... run actions ...
await network.provider.send("evm_revert", [id]);
```

### **Reset fork**

This command resets the local Hardhat node to a fresh state forked from the blockchain. You can use it to switch to a different block number or even a different RPC endpoint without restarting your entire testing process.

```ts
await network.provider.request({
  method: "hardhat_reset",
  params: [{ forking: { url: process.env.SOMNIA_RPC_TESTNET!, blockNumber: Number(process.env.FORK_BLOCK_TESTNET||0) || undefined } }]
});
```

***

## Alternative: Anvil (Foundry)

{% tabs %}
{% tab title="Fork Testnet" %}

```bash
anvil --fork-url $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET} --port 8546
```

{% endtab %}

{% tab title="Fork Mainnet" %}

```bash
anvil --fork-url $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET} --port 8546
```

{% endtab %}

{% tab title="Notes" %}
Point your app/tests to `http://127.0.0.1:8546`.

Anvil JSON-RPC is Hardhat-compatible for most calls (`evm_*`). Replace `hardhat_*` with Anvil equivalents where needed.
{% endtab %}
{% endtabs %}

***

## Tips & Best Practices

* Always pin block numbers in forks for reproducibility.
* Prefer `localhost` network when you need state persistence across multiple test files.
* Keep Anvil/Hardhat on separate ports if you run both simultaneously.
* Use labels/comments in tests to describe assumptions tied to a specific fork block.
* Avoid draining RPC rate limits: cache fixtures, use snapshots, and fork testnet for most flows.

***

## Common Issues

<details>

<summary>I<strong>nsufficient funds for gas</strong></summary>

Top up native balance with \`hardhat\_setBalance\`.

</details>

<details>

<summary>N<strong>once too high / replacement underpriced</strong></summary>

Reset account state (new snapshot) or use a fresh signer.

</details>

<details>

<summary><strong>Contract already deployed at address</strong></summary>

On persistent localhost, restart node or change deployer nonce.

</details>


# Deploy with RemixIDE

The Somnia mission is to enable the building of mass-consumer real-time applications. As a Developer, you must understand the quick steps to deploy your first Smart Contract on the Somnia Network. This guide will teach you how to connect to and deploy your first Smart Contract to the Somia Network using the Remix IDE.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

{% embed url="<https://www.youtube.com/watch?v=pUQI7a_jYqo>" %}

## Pre-requisites:

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](/get-started/connect-your-wallet-to-mainnet).

[Remix](https://remix.ethereum.org/) is an IDE for Smart Contract development, which includes compilation, deployment, testing, and debugging. It makes it easy for developers to create, debug, and deploy Smart Contracts to the Somnia Network. In this example, we will deploy a Greeter Smart contract, where we can update the state of the Contract to say “Hello” + Name.

## Connect to Somnia Testnet

Ensure you are logged into your MetaMask, connected to the Somnia Testnet, and have some STT Tokens. [Get STT Tokens](broken://pages/ZcwxHL0OlYPu0mGkI25U) from the faucet.

## Create the Smart Contract

Go to the Remix IDE and create a new file. Paste the Smart Contract below:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

contract Greeter {
    string public name;
    address public owner;

    event NameChanged(string oldName, string newName);

    modifier onlyOwner() {
        require(msg.sender == owner, "Only the owner can perform this action");
        _;
    }
    
    constructor(string memory _initialName) {
        name = _initialName;
        owner = msg.sender;
    }


    function changeName(string memory _newName) external onlyOwner {
        string memory oldName = name;
        name = _newName;
        emit NameChanged(oldName, _newName);
    }


    function greet() external view returns (string memory) {
        return string(abi.encodePacked("Hello, ", name, "!"));
    }
}
```

## Compile the Smart Contract

On the left tab, click the “Solidity Compiler” menu item and then the “ Compile Greeter.sol” button. This will compile the Solidity file and convert the Solidity code into machine-readable bytecode.

<figure><img src="/files/x0ZWVlNH5T1W3YrNJFNE" alt=""><figcaption></figcaption></figure>

## Deploy the Smart Contract

The Smart Contract has been created and compiled into ByteCode, and the ABI has also been created. The next step is to deploy the Smart Contract to the Somnia DevNet so that you can perform READ and WRITE operations.

On the left tab, click the “Deploy and run transactions” menu item. To deploy the Smart Contract, we will require a wallet connection. In the Environment dropdown, select the option: “Injected Provider - MetaMask”. Then select the MetaMask account where you have STT Tokens.

In the “DEPLOY” field, enter a value for the “\_INITIALNAME” variable, and click deploy.

When prompted, approve the Contract deployment on your MetaMask.

<figure><img src="/files/2L4voMHnRCoJa50ejVfK" alt=""><figcaption></figcaption></figure>

Look at the terminal for the response and the deployed Smart Contract address. You can interact with the Smart Contract via the Remix IDE. Send a transaction to change the name.

<figure><img src="/files/371IZiQZIOcvPHOAvTvQ" alt=""><figcaption></figcaption></figure>

Congratulations. 🎉 You have deployed your first Smart Contract to the Somnia Network. 🎉

<br>


# Deploy with Thirdweb

[Thirdweb](https://thirdweb.com/) is a complete web3 development framework that offers everything you need to connect your apps or games to the Somnia network. Its service allows developers to build, manage, and analyze their Web3 applications.

This tutorial will guide you through deploying a Smart contract to the Somnia Devnet using Thirdweb’s command-line tool (\`thirdweb deploy\`). Thirdweb simplifies deployment and interaction with smart contracts on Somnia.

{% embed url="<https://www.youtube.com/watch?v=JcrsFdRKcgY>" %}

## Prerequisites

* This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
* To complete this guide, you will need MetaMask installed and the Somnia DevNet added to the list of Networks. If you have yet to install MetaMask, please follow this guide to Connect Your Wallet.
* Thirdweb CLI: Install globally with:

```bash
npm thirdweb install
```

## Set Up the Project

First, create a new folder for your project and initialize it.

```bash
mkdir somnia-thirdweb-example
cd somnia-thirdweb-example
```

## Write the Smart Contract

You can write your Smart Contract using the [Remix IDE](/developer/development-frameworks/deploy-with-remixide) to ensure it works. Create a file **`OpenGreeter.sol`** and add the following code:

<details>

<summary>OpenGreeter.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract OpenGreeter {
    string public name;
    address public owner;

    event NameChanged(string oldName, string newName);

    constructor(string memory _initialName) {
        name = _initialName;
        owner = msg.sender;
           }

    function changeName(string memory _newName) public {
        string memory oldName = name;
        name = _newName;
        emit NameChanged(oldName, _newName);
    }

    function greet() external view returns (string memory) {
        return string(abi.encodePacked("Hello, ", name, "!"));
    }
}
```

</details>

This is a simple Greeter Smart Contract that any address can call the **`changeName`** function. The Smart Contract has two functions:\
\
\&#xNAN;**`changeName`** - This function allows anyone to change the name variable. It stores the old name, updates the name variable, and emits the **`NameChanged`** event with the old and new names.

**`greet`** - This function returns a greeting message that includes the current name. It uses abi.encodePacked to concatenate strings efficiently.

## Deploy the Smart Contract using Thirdweb.

First, go to Thirdweb and create a profile. After you have completed the onboarding process, create a Project.

<figure><img src="/files/SSIrb1BijuhrT963CKek" alt=""><figcaption></figcaption></figure>

Go to the project **settings.**

<figure><img src="/files/3ue1mXWDRIbGZKmRaC6h" alt=""><figcaption></figcaption></figure>

**C**opy your secret key, and keep it safe. The secret key will be used to deploy the Smart Contract.

<figure><img src="/files/m3XLHHHAkOfOOO0zwOGm" alt=""><figcaption></figcaption></figure>

Go to the terminal and paste the following command to deploy the Smart Contract:

```bash
npx thirdweb deploy -k your_secret_key
```

Select the **`solc`** option to be **`true`** in the prompts on Terminal.

<figure><img src="/files/JotRg5GsTH54W0YZtZbV" alt=""><figcaption></figcaption></figure>

Click the link to open the User Interface in your Browser to deploy the Smart Contract.

<figure><img src="/files/VYJoRY47ZA3jeK95Z5tS" alt=""><figcaption></figcaption></figure>

Enter an initialName. Select the Network as Somnia Devnet. Check the option to import it to the list of Contracts in your Thirdweb Dashboard. Click on **`Deploy Now`** and approve the Metamask prompts.

<div><figure><img src="/files/vqM6uzAbqNvhGqXTVXAN" alt=""><figcaption></figcaption></figure> <figure><img src="/files/jNKYorElQsAgeKXKewC9" alt=""><figcaption></figcaption></figure></div>

Your Smart Contract is deployed, and you can view it on your Thirdweb Dashboard.

<figure><img src="/files/WZCzF48XgDeBYVLVBfSF" alt=""><figcaption></figcaption></figure>

Visit the Explorer section to simulate interactions with your deployed Smart Contract and carry out actual transactions.

<figure><img src="/files/CHLsnjxtulvQ94u8ruQ5" alt=""><figcaption></figcaption></figure>

Congratulations. 🎉 You have deployed your Smart Contract to the Somnia Network using Thirdweb. 🎉

<br>


# Deploy with Foundry

Somnia empowers developers to build applications for mass adoption. Foundry is a tool for building Smart Contracts for mass adoption, making it easy for developers to create and deploy Smart Contracts to the Somnia Network.

[Foundry](https://book.getfoundry.sh/) is a blazing fast, portable and modular toolkit for EVM application development written in Rust.

This guide will teach you how to deploy a “Voting” Smart Contract to the Somia Network using Foundry.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

## Pre-requisites:

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](/get-started/connect-your-wallet-to-mainnet).
3. Foundry is installed and set up on your local machine. See [Guide](https://getfoundry.sh/)

## Initialise Foundry Project

To start a new project with Foundry, run the command:

```
forge init BallotVoting
```

This creates a new directory `hello_foundry` from the default template. Open `BallotVoting` directory, and the open the `src` directory where you will find a default `Counter.sol` solidity file. Delete the `Counter.sol` file.

## Create the Smart Contract

Create a new file inside the `src` directory and name it `BallotVoting.sol` and paste the following code:

```
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract BallotVoting {
    struct Ballot {
        string name; 
        string[] options; 
        mapping(uint256 => uint256) votes; 
        mapping(address => bool) hasVoted; 
        bool active; 
        uint256 totalVotes; 
    }

    uint256 public ballotCount; 
    mapping(uint256 => Ballot) public ballots; 

    event BallotCreated(uint256 indexed ballotId, string name, string[] options);
    event VoteCast(uint256 indexed ballotId, address indexed voter, uint256 optionIndex);
    event BallotClosed(uint256 indexed ballotId);

    function createBallot(string memory name, string[] memory options) public {
        require(options.length > 1, "Ballot must have at least two options");

        ballotCount++;
        Ballot storage ballot = ballots[ballotCount];
        ballot.name = name;
        ballot.options = options;
        ballot.active = true;

        emit BallotCreated(ballotCount, name, options);
    }

    function vote(uint256 ballotId, uint256 optionIndex) public {
        Ballot storage ballot = ballots[ballotId];
        require(ballot.active, "This ballot is closed");
        require(!ballot.hasVoted[msg.sender], "You have already voted");
        require(optionIndex < ballot.options.length, "Invalid option index");
        ballot.votes[optionIndex]++;
        ballot.hasVoted[msg.sender] = true;
         ballot.totalVotes++;
        emit VoteCast(ballotId, msg.sender, optionIndex);
    }

    function closeBallot(uint256 ballotId) public {
        Ballot storage ballot = ballots[ballotId];
        require(ballot.active, "Ballot is already closed");
        ballot.active = false;

        emit BallotClosed(ballotId);
    }

    function getBallotDetails(uint256 ballotId)
        public
        view
        returns (
            string memory name,
            string[] memory options,
            bool active,
             uint256 totalVotes
        )
    {
        Ballot storage ballot = ballots[ballotId];
        return (ballot.name, ballot.options, ballot.active, ballot.totalVotes);
    }

    function getBallotResults(uint256 ballotId) public view returns (uint256[] memory results) {
        Ballot storage ballot = ballots[ballotId];
        uint256[] memory voteCounts = new uint256[](ballot.options.length);
        for (uint256 i = 0; i < ballot.options.length; i++) {
            voteCounts[i] = ballot.votes[i];
        }

        return voteCounts;
    }
}
```

## Compile the Smart Contract

Compiling the Smart Contract will convert the Solidity code into machine-readable bytecode.

To compile the Smart Contract, run the command:

```
forge build
```

It will return the response:

```
[⠊] Compiling...
[⠢] Compiling 27 files with Solc 0.8.28
[⠆] Solc 0.8.28 finished in 2.22s
Compiler run successful!
```

You can learn more about parsing arguments using flags by reading the [Foundry book](https://book.getfoundry.sh/reference/forge/forge-build).

## Deploy Contract.

Deploying Smart Contracts to the Somnia Network is very straightforward. All you need the RPC URL and the Private Key from[ ](#user-content-fn-1)[^1]an Ethereum address which contains some STT tokens to pay for Gas during deployment. You can get some STT Tokens from the Somnia [Faucet](https://devnet.somnia.network/). Follow this [guide](https://support.metamask.io/managing-my-wallet/secret-recovery-phrase-and-private-keys/how-to-export-an-accounts-private-key/) to get your Private Key on MetaMask.\
\
To deploy the Smart Contract, run this command in the terminal:

```
forge create --rpc-url 
https://dream-rpc.somnia.network
 --private-key PRIVATE_KEY src/BallotVoting.sol:BallotVoting
```

You will see a status response:

```
[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
Deployed to: 0x46639fB6Ce28FceC29993Fc0201Cd5B6fb1b7b16
Transaction hash: 0xb3f8fe0443acae4efdb6d642bbadbb66797ae1dcde2c864d5c00a56302fb9a34
```

Copy the Transaction hash and paste it into the Somnia Network [Explorer](https://shannon-explorer.somnia.network/). You will find the deployed Smart Contract address.\
\
Congratulations. 🎉 You have deployed your “BallotVoting” Smart Contract to the Somnia Network using Foundry. 🎉<br>

[^1]: I would change this for "an"


# Deploy with Hardhat

Various developer tools can be used to build on Somnia to enable the Somnia mission of empowering developers to build Mass applications. One such development tool is Hardhat.\
\
[Hardhat](https://hardhat.org) is a development environment for the EVM i.e. Somnia. It consists of different components for editing, compiling, debugging, and deploying your smart contracts and dApps, all working together to create a complete development environment.\
\
This guide will teach you how to deploy a “Buy Me Coffee” Smart Contract to the Somia Network using Hardhat Development tools.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

{% embed url="<https://www.youtube.com/watch?v=4F9zrCsNjmE>" %}

## Pre-requisites

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](/get-started/connect-your-wallet-to-mainnet).
3. Hardhat is installed and set up on your local machine. See [Guide](https://hardhat.org/hardhat-runner/docs/getting-started#installation).

## Initialise Hardhat Project

Start a new Hardhat project by running the following command in your Terminal:

```bash
npx hardhat init
```

This will give you a series of prompts. Select the option to “Create a TypeScript Project (with Viem)”

<figure><img src="/files/OIMPZw7X6WS0NMqk9ThK" alt=""><figcaption></figcaption></figure>

This will install the required dependencies for your project.\
\
Once the installation is complete, open the project directory and check the directories where you will find the \`contracts\` directory. This is where the Smart Contract will be added.

## Create the Smart Contract

Open the Smart Contracts folder and delete the default `Lock.sol` file. Create a new file, `BuyMeCoffee.sol` and paste the following code:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract BuyMeCoffee {
    event CoffeeBought(
        address indexed supporter,
        uint256 amount,
        string message,
        uint256 timestamp
    );

    address public owner;

    struct Contribution {
        address supporter;
        uint256 amount;
        string message;
        uint256 timestamp;
    }
    
    Contribution[] public contributions;

    constructor() {
        owner = msg.sender;
    }

    function buyCoffee(string memory message) external payable {
        require(msg.value > 0, "Amount must be greater than zero.");
        contributions.push(
            Contribution(msg.sender, msg.value, message, block.timestamp)
        );

        emit CoffeeBought(msg.sender, msg.value, message, block.timestamp);
    }

    function withdraw() external {
        require(msg.sender == owner, "Only the owner can withdraw funds.");
        payable(owner).transfer(address(this).balance);
    }

    function getContributions() external view returns (Contribution[] memory) {
        return contributions;
    }

    function setOwner(address newOwner) external {
        require(msg.sender == owner, "Only the owner can set a new owner.");
        owner = newOwner;
    }
}
```

## Compile the Smart Contract

To compile your contracts, you need to customize the Solidity compiler options, open the `hardhat.config.js` file and ensure the Solidity version is `0.8.28` and then run the command:

```bash
npx hardhat compile
```

It will return the response:

```
Compiling...
Compiled 1 contract successfully
```

This will compile the Solidity file and convert the Solidity code into machine-readable bytecode. By default, the compiled *artifacts* will be saved in the newly created `artifacts` directory.\
\
The next step is to deploy the contracts to the Somnia Network. In Hardhat, deployments are defined through **Ignition Modules**. These modules are abstractions that describe a deployment, specifically, JavaScript functions that process the file you want to deploy.\
\
Open the `ignition` directory inside the project root's directory, then enter the directory named `modules`. Delete the `Lock.ts` file. Create a `deploy.ts` file and paste the following code:

```typescript
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const BuyMeCoffee = buildModule("BuyMeCoffee", (m) => {
  const contract = m.contract("BuyMeCoffee");
  return { contract };
});

module.exports = BuyMeCoffee;
```

## Deploy Contract

Open the `hardhat.config.js` file and update the network information by adding Somnia Network to the list of networks. Copy your Wallet Address Private Key from MetaMask, and add it to the **accounts** section. Ensure there are enough STT Token in the Wallet Address to pay for Gas. You can get some from the Somnia [Faucet](https://devnet.somnia.network/).

```
module.exports = {
  // ...
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};
```

> The "**0xPRIVATE\_KEY**" is used to sign the Transaction from your EOA without permission. When deploying the smart contract, you must ensure the EOA that owns the Private Key is funded with enough STT Tokens to pay for gas. Follow this [guide](https://support.metamask.io/managing-my-wallet/secret-recovery-phrase-and-private-keys/how-to-export-an-accounts-private-key/) to get your Private Key on MetaMask.

Open a new terminal and deploy the smart contract to the Somnia Network. Run the command:

```bash
npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia
```

You will see a confirmation message asking if you want to deploy to the Somnia Network. Answer by hitting “**y**” on your keyboard. This will confirm the deployment of the Smart Contract to the Somnia Network.

<figure><img src="/files/FXdFH5mggQMDSsfPq34S" alt=""><figcaption></figcaption></figure>

Congratulations. 🎉 You have deployed your “BuyMeCoffee” Smart Contract to the Somnia Network using Hardhat. 🎉

## **Verify Your Smart Contract**

After deploying your contract, you can verify it using the [Hardhat Verify plugin](https://hardhat.org/hardhat-runner/plugins/nomicfoundation-hardhat-verify). This allows your source code to be visible and validated on the [Somnia Explorer](https://shannon-explorer.somnia.network).

#### Update `hardhat.config.ts`Add the following to your config file:

```js
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";

const config: HardhatUserConfig = {
  solidity: "0.8.28",
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["YOUR_PRIVATE_KEY"],
    },
  },
  sourcify: {
    enabled: false,
  },
  etherscan: {
    apiKey: {
      somnia: "empty",
    },
    customChains: [
      {
        network: "somnia",
        chainId: 50312,
        urls: {
          apiURL: "https://shannon-explorer.somnia.network/api",
          browserURL: "https://shannon-explorer.somnia.network",
        },
      },
    ],
  },
};
```

> Store your private key in a `.env` file and import it securely to avoid hardcoding.

After deploying your contract, run the Verify command. Copy the deployed address and run:

```bash
npx hardhat verify --network somnia DEPLOYED_CONTRACT_ADDRESS "ConstructorArgument1" ...
```

Example for a contract with one string constructor arg:

```bash
npx hardhat verify --network somnia 0xYourContractAddress "YourDeployerWalletAddress"
```

Visit the [Somnia Explorer](https://shannon-explorer.somnia.network) and search for your contract address. If successful, the source code will appear under the **“Contract”** tab and show as **verified**.

<div data-full-width="true"><figure><img src="/files/M4ufGYemH2Mx4K8GdhMH" alt=""><figcaption></figcaption></figure> <figure><img src="/files/dks5HHgPrGonDmr13mZM" alt=""><figcaption></figcaption></figure></div>

The verified Smart Contracts contain the Source Code, which anyone can review for bugs and malicious code. Users can also connect with and interact with the Verified Smart Contract.


# Using the Viem Library

Somnia empowers developers to build applications for mass adoption. Smart Contracts deployed on Somnia will require front-end user interfaces to interact with them. These front-end user interfaces will require middleware libraries to establish a connection to the Somnia Network and enable interaction with Smart Contracts.\
\
In this Guide, you will learn how to use the Viem Library to establish a connection between your deployed Smart Contracts on Somnia Network and your Front-end User application. You will also learn how to perform READ and WRITE operations using Viem.\
\
[Viem](https://viem.sh) is a TypeScript interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

{% embed url="<https://www.youtube.com/watch?v=1mdtoC5Dk4E>" %}

## How does Viem enable UI interaction?

When a Smart Contract is programmed using any development tool such as RemixIDE, Hardhat or Foundry, the Smart Contract undergoes a “compilation” stage. Compiling a Smart Contract, among other things will convert the Solidity code into machine-readable bytecode. An ABI file is also produced when a Smart Contrac is compiled. ABI stand for Application Binary Interface. You can think of an ABI like the Interface that make it possible for a User Interface to connect with the Smart Contract functions in a way similar to how an API makes it possible to to connect a UI and Backend server in web2.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfaRoTuAtASsyoeB7jqiU2urm-g50ESwc_Ty4iH2h7DlLcwQak0uXG-eew1-J40_S4Cr0pJwGPxgWzjCjfo4_sHX8aV20snkM5w1dcJUeGpJx8igiTMGUqatgy1OpaYTnbINqXV6A?key=HxFSDQZc_LGgImSmo9E6Cd6w" alt=""><figcaption></figcaption></figure>

## Example Smart Contract

Here is an example \`Greeter.sol\` Smart Contract:

<br>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

contract Greeter {
    string public name;
    address public owner;

    event NameChanged(string oldName, string newName);

    modifier onlyOwner() {
        require(msg.sender == owner, "Only the owner can perform this action");
        _;
    }

    constructor(string memory _initialName) {
     name = _initialName;
        owner = msg.sender;
    }

    function changeName(string memory _newName) external onlyOwner {
        string memory oldName = name;
        name = _newName;
        emit NameChanged(oldName, _newName);
    }

    function greet() external view returns (string memory) {
        return string(abi.encodePacked("Hello, ", name, "!"));
    }
}
```

## Example ABI

When the Greeter Smart Contract is compiled, below is its ABI:

<details>

<summary>ABI</summary>

<https://gist.github.com/emmaodia/bdb9b84998b1e4f3f19d0ae27c541e63>

</details>

The ABI is an array of JSON objects containing the constructor, event, and four functions in the Smart Contract. Using a Library such as Viem, you can perform READ and WRITE operations, for each of the ABI objects. You can READ the events, and other “view” only methods. You can perform WRITE operations on the “changeName” function.\
\
A cursory look at each ABI method will help you understand the function and what can be accomplished by interacting with the method. For example:

<br>

```json
{
"inputs": [  --->specifies it is an input, i.e. a WRITE function
{
"internalType": "string", ---> the data type
"name": "_newName", ---> params name
"type": "string" ---> data type
}
],
"name": "changeName", ---> function name
"outputs": [], ---> it does not have a return property
"stateMutability": "nonpayable", ---> It changes the Blockchain State without Token exchange, it simply stores information.
"type": "function" ---> It is a function.
},
```

## How to use Viem.

To use Viem, it has to be installed in the project directory where you want to perform READ and WRITE operations.\
\
First, create a directory and initialize a new project using npm.

```bash
mkdir viem-example && cd viem-example
```

Initialize a project in the directory by running the command:

```bash
npm init -y
```

Install Viem by running the following command.

```bash
npm i viem
```

## Set Up Viem

To connect to the deployed Example Greeter Smart Contract using Viem, it is necessary to have access to the Smart Contract’s ABI and its Contract Address. Viem sets up a “`transport`” infrastructure to connect with a node in the EVM Network and the deployed Smart Contracts.\
\
We will use some Viem methods to connect to your Smart Contract deployed on the Somnia Network. Viem has a \`createPublicClient\` and a \`createWalletClient\` method. The PublicClient is used to perform READ operations, while the WalletClient is used to perform WRITE operations.\
\
Create a new file `index.js` Import the method classes from the Library:

```javascript
import { createPublicClient, createWalletClient, http } from "viem";
```

The `http` is the transport protocol for interacting with the Node of the Somnia Blockchain via RPC. It uses the default Somnia RPC URL: [`https://dream-rpc.somnia.network`](https://dream-rpc.somnia.network). In the future developers can use RPC providers to avoid rate limiting.

```javascript
import { somniaTestnet } from "viem/chains"
```

## Set up PublicClient

We will start with setting up the `publicClient` to read `view` only methods. Set up a **`publicClient`** where the default Transport is `http` and `chain` is `SOMNIA` network created using the `defineChain` method.

```javascript
const publicClient = createPublicClient({ 
  chain: somniaTestnet, 
  transport: http(), 
}) 
```

## Consume Actions

Now that you have a Client set up, you can interact with Somnia Blockchain and consume Actions!\
\
An example will be to call the `greet` method on the deployed Smart Contract.\
\
To do this, we have to create a file name `abi.js` and add the exported ABI in the file.

```javascript
export const ABI = [//...ABI here]
```

In the `index.js` we can import the ABI file and start calling methods on the deployed Smart Contract. Import the ABI:

```javascript
import { ABI } from "./abi.js";
```

Set Contract Address

```
const CONTRACT_ADDRESS = "0x2e7f682863a9dcb32dd298ccf8724603728d0edd";
```

> This is an example Greeter Smart Contract deployed on Somnia Testnet

Write a Function \`interactWithContract\`:

```javascript
const interactWithContract = async () => {
  try {
    console.log("Reading message from the contract...");


    // Read the "greet" function
    const greeting = await publicClient.readContract({
      address: CONTRACT_ADDRESS,
      abi: ABI,
      functionName: "greet",
    });
    console.log("Current greeting:", greeting);
 } catch (error) {
    console.error("Error interacting with the contract:", error);
  }
};


interactWithContract();
```

Open your terminal and run the following:

```bash
node index.js
```

You will see the response from the Smart Contract logged into the Console!

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeerGko1tZ3XeNPJPAiHOIw4DsOM8hL4HSurOu3-CgEP7dQeAUc_rbGJdXRx2jYX16DxCCUslCrEFQmmOmZJ5MDAB3-iWw8h2j4pWbbucO0YUO4Wp8i9xJ6JcI6zFUvztoPdUgbPw?key=HxFSDQZc_LGgImSmo9E6Cd6w" alt=""><figcaption></figcaption></figure>

Congratulations, you have successfully performed a READ operation on your Smart Contract deployed on Somnia.

## Set up Wallet Client

To perform a write operation, we will parse the \`createWalletClient\` method to a \`walletClient\` variable. It is important to understand that carrying out WRITE operations changes the state of the Blockchain, unlike READ operations, where you read the state of the Blockchain. So, to perform WRITE operations, a user will have to spend Gas, and to be able to spend Gas, a user will have to parse his Private Key from an EOA to give the Library masked permission to carry out transactions on behalf of the user.\
\
To read the Private Key from an EOA, we will use a Viem method:

```javascript
import { privateKeyToAccount } from "viem/accounts";
```

Then, create a variable `walletClient`

```javascript
const walletClient = createWalletClient({
  account: privateKeyToAccount($YOUR_PRIVATE_KEY),
  chain: somniaTestnet,
  transport: http(),
});
```

> The variable \`$YOUR\_PRIVATE\_KEY\` variable can be parsed using a dotenv file.

After sending a WRITE operation, we also have to be able to read the transaction to see the state changes. We will rely on a READ method to read a transaction, \`waitForTransactionReceipt\`.\
\
Update the \`interactWithContract\` function with the code below:

```javascript
   // Write to the "changeName" function
    const txHash = await walletClient.writeContract({
      address: CONTRACT_ADDRESS,
      abi: ABI,
      functionName: "changeName",
      args: ["Emmanuel!"],
    });
    console.log("Transaction sent. Hash:", txHash);
    console.log("Waiting for transaction confirmation...");

    // Wait for the transaction to be confirmed
    const receipt = await publicClient.waitForTransactionReceipt({ hash: txHash });
    console.log("Transaction confirmed. Receipt:", receipt);

    // Read the updated "greet" function
    const updatedGreeting = await publicClient.readContract({
      address: CONTRACT_ADDRESS,
      abi: ABI,
      functionName: "greet",
    });
    console.log("Updated greeting:", updatedGreeting);
```

Save the file and run the node command to see your responses logged into the console.

```bash
node index.js
```

Congratulations, you have successfully performed a WRITE operation on your Smart Contract deployed on Somnia. 🎉

<br>


# Verifying via Explorer

This document explains how to verify smart contracts on Somnia Mainnet using Blockscout. It replaces the old explorer-based verification flow.

## How It Works

Somnia Mainnet Explorer is Blockscout-based and uses Blockscout verification endpoints.

There are 4 primary verification methods:

1. Verification via Explorer UI
2. Verification via Blockscout Smart Contract Verification API
3. Verification via Hardhat plugin
4. Verification via Foundry (forge)

{% hint style="info" %}
Recommendation: For production contracts, use Standard JSON Input whenever possible. It includes full compiler metadata and gives the most reliable results.
{% endhint %}

## 1. Explorer UI

{% stepper %}
{% step %}

#### Go to the explorer

Go to <https://explorer.somnia.network/> .
{% endstep %}

{% step %}

#### Open the verification page

Open `Other -> Verify Contract`.\
\
![](/files/5eaCBa8TVicZ46owPMlI)
{% endstep %}

{% step %}

#### Enter contract details

Enter your contract address and select a verification method.
{% endstep %}

{% step %}

#### Match compilation parameters

Match all compilation parameters exactly with your deployment settings:

* Compiler version
* EVM version
* Optimizer and runs
* Constructor args
* License type
  {% endstep %}

{% step %}

#### Verify and publish

Click `Verify and Publish` to complete the process.
{% endstep %}
{% endstepper %}

### Main UI verification options

* Solidity (Flattened source code)
* Solidity (Standard JSON input)
* Solidity (Multi-part files)
* Solidity (Sourcify)
* Vyper (Single contract file)
* Vyper (Multi-part files)
* Vyper (Standard JSON input)

## 2. API Verification (Blockscout v2)

### Base URL

`https://mainnet.somnia.w3us.site/api/v2`

### Service health check

To check whether the verification service is active:

```bash
curl -L "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/verification/config"
```

If healthy, it returns `200`.

### 2.1 Flattened Solidity verification

```bash
curl -L \
  --request POST \
  --url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/flattened-code" \
  --header "Content-Type: application/json" \
  --data '{
    "compiler_version": "v0.8.24+commit.e11b9ed9",
    "license_type": "mit",
    "source_code": "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.24;\ncontract A { }",
    "is_optimization_enabled": true,
    "optimization_runs": 200,
    "contract_name": "A",
    "evm_version": "paris",
    "autodetect_constructor_args": true
  }'
```

### 2.2 Standard JSON Input verification

```bash
curl -L \
  --request POST \
  --url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/standard-input" \
  --header "Content-Type: multipart/form-data" \
  --form "compiler_version=v0.8.24+commit.e11b9ed9" \
  --form "contract_name=MyContract" \
  --form "files[0]=@./standard-input.json" \
  --form "autodetect_constructor_args=true" \
  --form "license_type=mit"
```

### 2.3 Multi-part Solidity verification

```bash
curl -L \
  --request POST \
  --url "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/<CONTRACT_ADDRESS>/verification/via/multi-part" \
  --header "Content-Type: multipart/form-data" \
  --form "compiler_version=v0.8.24+commit.e11b9ed9" \
  --form "license_type=mit" \
  --form "is_optimization_enabled=true" \
  --form "optimization_runs=200" \
  --form "evm_version=paris" \
  --form "files[0]=@./contracts/MyContract.sol" \
  --form "files[1]=@./contracts/Lib.sol"
```

### License values (example)

In Blockscout API, `license_type` is sent as a string:

* `none`
* `unlicense`
* `mit`
* `gnu_gpl_v2`
* `gnu_gpl_v3`
* `gnu_lgpl_v2_1`
* `gnu_lgpl_v3`
* `bsd_2_clause`
* `bsd_3_clause`
* `mpl_2_0`
* `osl_3_0`
* `apache_2_0`
* `gnu_agpl_v3`
* `bsl_1_1`

## 3. Hardhat Verification

For Somnia Blockscout, use the `@nomicfoundation/hardhat-verify` plugin.

### Installation

```bash
npm install --save-dev @nomicfoundation/hardhat-verify
```

### Example `hardhat.config.ts`

```ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-verify";

const config: HardhatUserConfig = {
  solidity: "0.8.24",
  networks: {
    "somnia-mainnet": {
      url: "<SOMNIA_RPC_URL>",
      accounts: ["<PRIVATE_KEY>"]
    }
  },
  etherscan: {
    apiKey: {
      // A real API key is not required for Blockscout verification
      "somnia-mainnet": "abc"
    },
    customChains: [
      {
        network: "somnia-mainnet",
        chainId: 5031,
        urls: {
          apiURL: "https://mainnet.somnia.w3us.site/api",
          browserURL: "https://mainnet.somnia.w3us.site/"
        }
      }
    ]
  },
  sourcify: {
    enabled: false
  }
};

export default config;
```

### Verification command

```bash
npx hardhat verify \
  --network somnia-mainnet \
  <DEPLOYED_CONTRACT_ADDRESS> \
  "<CONSTRUCTOR_ARG_1>" "<CONSTRUCTOR_ARG_2>"
```

## 4. Foundry Verification

Use Blockscout verifier in Foundry as follows:

```bash
forge verify-contract \
  --rpc-url <SOMNIA_RPC_URL> \
  <DEPLOYED_CONTRACT_ADDRESS> \
  src/MyContract.sol:MyContract \
  --verifier blockscout \
  --verifier-url https://mainnet.somnia.w3us.site/api/
```

To verify at deployment time:

```bash
forge create \
  --rpc-url <SOMNIA_RPC_URL> \
  --private-key $PRIVATE_KEY \
  src/MyContract.sol:MyContract \
  --verify \
  --verifier blockscout \
  --verifier-url https://mainnet.somnia.w3us.site/api/
```

## Troubleshooting

* Compiler version must match exactly (`0.8.24` vs `0.8.24+commit...` matters).
* Optimizer settings must exactly match deployment settings.
* Verification fails if constructor arguments are incorrect or missing.
* For multi-file projects, prefer Standard JSON Input or Multi-part over Flattened.
* If errors persist, retry first via UI and then via API, and compare payload differences.

## Useful Links

* Blockscout Verification API docs: <https://docs.blockscout.com/devs/verification/blockscout-smart-contract-verification-api>
* Blockscout UI docs: <https://docs.blockscout.com/devs/verification/blockscout-ui>
* Hardhat verify plugin (Blockscout): <https://docs.blockscout.com/devs/verification/hardhat-verification-plugin>
* Foundry verification (Blockscout): <https://docs.blockscout.com/devs/verification/foundry-verification>


# Debug Playbook

## 1. Revert Decoding

Debugging smart contracts on the **Somnia Network** requires an understanding of how and why transactions fail. Every reverted transaction carries encoded data that can reveal the root cause of failure, whether it’s due to logic errors, insufficient gas, failed access checks, or internal Solidity panics.

### 1.1 Anatomy of a Revert

When a transaction fails on Somnia, the EVM halts execution and returns **revert data**, an ABI-encoded payload that follows one of these three formats:

<table><thead><tr><th>Type</th><th>Selector</th><th width="217.08984375">Description</th><th>Example</th></tr></thead><tbody><tr><td><code>Error(string)</code></td><td><code>0x08c379a0</code></td><td>Standard revert reason with message</td><td><code>require(balance > 0, "Zero balance");</code></td></tr><tr><td><code>Panic(uint256)</code></td><td><code>0x4e487b71</code></td><td>Internal error (e.g., overflow, div/0, invalid enum)</td><td><code>assert(x > 0);</code></td></tr><tr><td>Custom Errors</td><td>Function selector of custom error</td><td><code>error Unauthorized(address caller);</code></td><td></td></tr></tbody></table>

On Somnia, these behave identically to Ethereum but may differ in **gas costs** and **stack trace length** depending on the validator node configuration.

### 1.2 Catching and Displaying Reverts in Hardhat

When running tests or scripts on Somnia, wrap calls in `try/catch` to capture the revert reason.

{% code title="example.ts" %}

```ts
try {
  await treasury.withdraw(1000);
} catch (error: any) {
  console.log('Revert reason:', error.reason || error.message);
  console.log('Full error data:', error.data || error.error?.data);
}
```

{% endcode %}

In **Chai matchers**:

{% code title="chai-example.ts" %}

```ts
await expect(treasury.connect(user).withdraw(1000))
  .to.be.revertedWith('Insufficient funds');
```

{% endcode %}

If your contract uses **custom errors**, Hardhat will not automatically print the name. Decode it manually:

{% code title="decode-custom-error.ts" %}

```ts
const iface = new ethers.utils.Interface(contractABI);
try {
  await treasury.connect(attacker).withdraw(9999);
} catch (error: any) {
  const data = error.data || error.error?.data;
  if (data) {
    const decoded = iface.parseError(data);
    console.log(`Custom Error: ${decoded.name}`);
    console.log('Arguments:', decoded.args);
  }
}
```

{% endcode %}

### 1.3 Decoding Panic Codes

Internal Solidity panics correspond to low-level EVM exceptions. Somnia propagates these codes like any other EVM chain.

| Panic Code | Description                       | Typical Cause              |
| ---------- | --------------------------------- | -------------------------- |
| `0x01`     | Assertion failed                  | Logic invariant broken     |
| `0x11`     | Arithmetic overflow/underflow     | Unchecked math operation   |
| `0x12`     | Division by zero                  | Incorrect math division    |
| `0x21`     | Invalid enum conversion           | Out-of-bounds value        |
| `0x31`     | Storage array index out of bounds | Bad loop or mapping access |
| `0x32`     | Memory array index out of bounds  | Corrupt array operation    |

To detect Panic errors dynamically:

{% code title="detect-panic.ts" %}

```ts
if (error.data?.startsWith('0x4e487b71')) {
  const code = parseInt(error.data.slice(10), 16);
  console.log('Panic Code:', `0x${code.toString(16)}`);
}
```

{% endcode %}

### 1.4 Advanced Revert Inspection with Hardhat Traces

Hardhat’s tracing layer can reveal the full execution path of a revert.

{% code title="trace" %}

```bash
npx hardhat test --trace
```

{% endcode %}

You’ll see nested calls, gas usage per function, and exactly where the failure occurred. This is invaluable for multi-contract interactions like on-chain governance or liquidity management.

Example output:

```
CALL treasury.withdraw
 └─ CALL token.transfer -> reverted with reason: 'Insufficient balance'
```

### 1.5 Custom Error Decoding for Verified Contracts

If a Somnia contract is verified on the explorer, you can fetch its ABI dynamically to decode errors programmatically:

{% code title="fetch-abi.ts" %}

```ts
import axios from 'axios';
const abiURL = `https://explorer.somnia.network/api?module=contract&action=getabi&address=${address}`;
const { data } = await axios.get(abiURL);
const iface = new ethers.utils.Interface(JSON.parse(data.result));
```

{% endcode %}

Then use `iface.parseError(error.data)` to decode reverts directly from on-chain logs or transactions.

***

## 2. Common Error Patterns on Somnia

Even experienced developers encounter recurring issues. Below are the **most common EVM-level errors** observed when deploying or testing on Somnia Testnet (Shannon) and Mainnet.

| Error Type                | Cause                              | Fix                                             |
| ------------------------- | ---------------------------------- | ----------------------------------------------- |
| `execution reverted`      | Fallback revert with no message    | Add explicit revert messages or decode ABI data |
| `out of gas`              | Gas exhausted mid-call             | Use `estimateGas()` or increase gas limit       |
| `invalid opcode`          | Calling a non-existent function    | Validate ABI and deployed bytecode              |
| `nonce too low`           | Pending transaction not mined yet  | Wait for confirmation or reset nonce            |
| `replacement underpriced` | Gas bump too small                 | Raise gas price by 10–20%                       |
| `static call violation`   | State-changing call via `eth_call` | Use `.sendTransaction()` instead                |

### 2.1 Example: Catching a Custom Error in Somnia Treasury Contract

{% code title="Treasury.sol" %}

```solidity
error Unauthorized(address caller);

function mint(address to, uint amount) external {
  if (msg.sender != owner) revert Unauthorized(msg.sender);
  _mint(to, amount);
}
```

{% endcode %}

Decoding in JS:

{% code title="catch-unauthorized.ts" %}

```ts
try {
  await treasury.connect(randomUser).mint(addr, 100);
} catch (e: any) {
  const iface = new ethers.utils.Interface(['error Unauthorized(address caller)']);
  const decoded = iface.parseError(e.data);
  console.log('Unauthorized address:', decoded.args[0]);
}
```

{% endcode %}

### 2.2 Handling Complex Contract Interactions

When interacting with multi-layered DeFi protocols or bridging modules on Somnia, reverts can originate **several calls deep**. Use Hardhat’s trace or Foundry’s `-vvvv` verbosity to see the full stack.

Foundry example:

{% code title="foundry-verbosity" %}

```bash
forge test -vvvv
```

{% endcode %}

This reveals each opcode execution, event emission, and revert reason.

### 2.3 Invalid ABI or Proxy Conflicts

Many Somnia projects use **upgradeable proxies**. Reverts from a proxy may originate in the implementation contract. If you get a generic `execution reverted`, verify you’re using the correct implementation ABI:

{% code title="get-impl.ts" %}

```ts
const implAddr = await provider.getStorageAt(proxyAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc');
const iface = new ethers.utils.Interface(implementationABI);
```

{% endcode %}

***

## 3. Transaction Simulation

Simulating transactions allows developers to predict revert causes, estimate gas usage, and test behaviors **without risking real SOMI or STT**.

### 3.1 Fork Somnia Networks Locally

Create a local fork of Somnia Mainnet or Shannon Testnet:

{% code title="hardhat-fork" %}

```bash
npx hardhat node --fork https://api.infra.mainnet.somnia.network
```

{% endcode %}

Or in configuration:

{% code title="hardhat-config.ts" %}

```ts
networks: {
  hardhat: {
    forking: {
      url: process.env.SOMNIA_RPC_TESTNET,
      blockNumber: 123456, //example
    }
  }
}
```

{% endcode %}

This mirrors on-chain state locally, so you can safely replay any transaction.

### 3.2 Using callStatic for Dry-Run Simulation

`callStatic` runs a transaction without broadcasting or altering state.

{% code title="callstatic.ts" %}

```ts
try {
  const result = await treasury.callStatic.withdraw(1000);
  console.log('Call successful:', result);
} catch (error: any) {
  console.log('Simulation failed with reason:', error.reason);
}
```

{% endcode %}

### 3.3 Using eth\_call Manually

For raw RPC simulation:

{% code title="eth-call.ts" %}

```ts
const tx = {
  to: contract.address,
  data: contract.interface.encodeFunctionData('stake', [amount])
};
const result = await provider.call(tx);
console.log('Returned data:', result);
```

{% endcode %}

If the call reverts, inspect `error.data` to decode it with the ABI.

See the [JSON-RPC API Reference](/developer/json-rpc-api) for `eth_call` parameter details and error codes.

### 3.4 Impersonating Accounts for Privileged Actions

Simulate admin or contract-controlled operations:

{% code title="impersonate.ts" %}

```ts
await network.provider.request({
  method: 'hardhat_impersonateAccount',
  params: ['0xAdminAddress']
});
const admin = await ethers.getSigner('0xAdminAddress');
await treasury.connect(admin).setFee(5);
```

{% endcode %}

Stop impersonation when finished:

{% code title="stop-impersonate.ts" %}

```ts
await network.provider.request({
  method: 'hardhat_stopImpersonatingAccount',
  params: ['0xAdminAddress']
});
```

{% endcode %}

### 3.5 Snapshot and Rollback Control

Snapshots let you test different outcomes quickly:

{% code title="snapshot.ts" %}

```ts
const snapshot = await network.provider.send('evm_snapshot', []);
await treasury.mint(100);
await network.provider.send('evm_revert', [snapshot]);
```

{% endcode %}

This resets the blockchain to its previous state instantly.

### 3.6 Simulating On-Chain Transactions

If you have a failed transaction hash from Somnia Mainnet:

{% code title="replay-tx.ts" %}

```ts
const tx = await provider.getTransaction('0x123...');
await provider.call({ to: tx.to!, data: tx.data });
```

{% endcode %}

This reproduces the failure locally and lets you inspect the revert reason directly in Hardhat.

### 3.7 Advanced Fork Testing with Foundry

{% code title="anvil-forge" %}

```bash
anvil --fork-url https://dream-rpc.somnia.network --fork-block-number 3456789
forge test -vvvv
```

{% endcode %}

You can use cheatcodes like:

{% code title="foundry-cheatcodes.sol" %}

```solidity
vm.startPrank(admin);
contract.withdraw(1000);
vm.stopPrank();
```

{% endcode %}

### 3.8 Gas Profiling and Cost Analysis

Somnia gas costs can differ from Ethereum due to consensus differences. Always estimate gas usage per function:

{% code title="estimate-gas.ts" %}

```ts
const gas = await contract.estimateGas.executeTrade(orderId);
console.log('Estimated gas:', gas.toString());
```

{% endcode %}

Compare against Shannon and Mainnet to identify anomalies.

### 3.9 Full Transaction Lifecycle Test

{% stepper %}
{% step %}
**Fork Somnia Testnet**

Create a fork of the testnet to reproduce on-chain state locally.
{% endstep %}

{% step %}
**Impersonate the deployer account**

Use impersonation to perform privileged actions and reproduce behavior.
{% endstep %}

{% step %}
**Run callStatic to simulate critical functions**

Dry-run core functions to inspect return values and revert reasons.
{% endstep %}

{% step %}
**Capture reverts and decode with ABI**

Decode revert data, custom errors, and panic codes to get actionable context.
{% endstep %}

{% step %}
**Use snapshot/revert to iterate quickly**

Take snapshots to test multiple scenarios and revert between them.
{% endstep %}

{% step %}
**Once clean, deploy and verify on testnet**

After local verification, deploy to the testnet and verify behavior.
{% endstep %}

{% step %}
**Run same steps on mainnet fork before release**

Final validation on a mainnet fork ensures production parity.
{% endstep %}
{% endstepper %}

***

{% hint style="info" %}
Summary

* Always **decode revert data** rather than relying on generic error strings.
* Decode **custom errors** to get structured failure context.
* Use **forked local environments** for safe and realistic debugging.
* Combine **callStatic**, **trace**, and **snapshot/revert** for fast iteration.
* Validate gas behavior across testnet and mainnet for accurate production cost.

Debugging on Somnia means understanding the EVM intimately. Every revert, panic, and trace is a clue—decode them, simulate safely, and ship with confidence.
{% endhint %}


# Deploying Smart Contracts


# Building DApps


# Tokens and NFTs

Tokens and NFTs form the backbone of value and ownership on Somnia. This section walks you through how to **create, deploy, and interact** with ERC-20, ERC-721, and ERC-1155 contracts on the Somnia Network.

You’ll learn how to:

* Deploy fungible tokens (ERC-20) and configure supply and minting logic
* Create and manage NFTs (ERC-721 / ERC-1155) for digital assets and collectibles
* Integrate token interactions into your dApps and subgraphs
* Understand token standards and best practices for gas optimization

> Whether you’re launching a governance token, building an NFT marketplace, or gamifying user engagement, Somnia’s speed and scalability make asset creation seamless.


# Create ERC20 Tokens

The Somnia mission is to enable the development of mass-consumer real-time applications. To achieve this as a developer, you will need to build applications that are Token enabled, as this is a requirement for many Blockchain applications. This guide will teach you how to connect to and deploy your ERC20 Smart Contract to the Somia Network using the [Remix IDE](https://remix.ethereum.org/).

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

{% embed url="<https://www.youtube.com/watch?v=EovEiYJNKTM>" %}

## Pre-requisite

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](/get-started/connect-your-wallet-to-mainnet).

Somnia Network is an EVM-compatible Layer 1 Blockchain. This means that critical implementation on Ethereum is available on Somnia, with higher throughput and faster finality. Smart Contract that follows the [ERC-20 standard](https://eips.ethereum.org/EIPS/eip-20) is an ERC-20 token; these Smart Contracts are often referred to as Token Contracts.

ERC-20 tokens provide functionality to

* Transfer tokens
* Allow others to transfer tokens on behalf of the token holder

It is important to note that ERC20 Smart Contracts are different from the Native Somnia Contracts, which are used to pay gas fees when transacting on Somnia. In the following steps, we will create an ERC20 Token by following the EIP Standard and also demonstrate the option to use a Library to create the ERC20 Token.

## IERC-20

According to the EIP Standard, certain Smart Contract methods must be implemented adhering to the standard so that other Smart Contracts can interact with the deployed Smart Contracts and call the method. To achieve this, we will use the Solidity Interface Type to create the Smart Contract standard.

\
In Solidity, an interface is a special contract that defines a set of function signatures without implementation. It acts as a "blueprint" for other contracts, ensuring they adhere to a specific structure. Interfaces are crucial for creating standards, such as the ERC-20 token standards, allowing different contracts to interact seamlessly within the EVM ecosystem.

Create an Interface for the ERC20 Token. Copy and paste the code below into a file named `IERC20.sol`

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount)
        external
        returns (bool);
    function allowance(address owner, address spender)
        external
        view
        returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool);
}
```

## ERC-20 Token Contract

With the Interface created and all the Token standard methods implemented, the next step is to import the Interface into the ERC20 Smart Contract implementation.

Create a file called `ERC20.sol` and paste the code below into it.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;

import "./IERC20.sol";

contract ERC20 is IERC20 {
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(
        address indexed owner, address indexed spender, uint256 value
    );

    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    string public name;
    string public symbol;
    uint8 public decimals;

    constructor(string memory _name, string memory _symbol, uint8 _decimals) {
        name = _name;
        symbol = _symbol;
        decimals = _decimals;
    }

    function transfer(address recipient, uint256 amount)
        external
        returns (bool)
    {
        balanceOf[msg.sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(msg.sender, recipient, amount);
        return true;
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function transferFrom(address sender, address recipient, uint256 amount)
        external
        returns (bool)
    {
        allowance[sender][msg.sender] -= amount;
        balanceOf[sender] -= amount;
        balanceOf[recipient] += amount;
        emit Transfer(sender, recipient, amount);
        return true;
    }

    function _mint(address to, uint256 amount) internal {
        balanceOf[to] += amount;
        totalSupply += amount;
        emit Transfer(address(0), to, amount);
    }

    function _burn(address from, uint256 amount) internal {
        balanceOf[from] -= amount;
        totalSupply -= amount;
        emit Transfer(from, address(0), amount);
    }

    function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }


    function burn(address from, uint256 amount) external {
        _burn(from, amount);
    }
}
```

We have implemented the various requirements for the ERC20 Token Standard:

**`constructor`**: Initializes the token's basic properties.

Accepts the token's `name`, `symbol`, and the number of `decimals` as parameters and sets these values as the token's immutable metadata.

Example: `ERC20("MyToken", "MTK", 18)` initializes a token named `MyToken` with the symbol

`MTK` and 18 decimal places (the standard for ERC-20).

### Functions

**`transfer`**: Moves a specified `amount` of tokens from the *sender* to a *recipient*.

It checks if the sender has enough balance and deducts the amount from the sender's balance, and adds it to the recipient's. It also **emits a Transfer event** to log the transaction and returns \`**true**\` if the transfer is successful.

**`approve`**: It allows a *spender* to spend up to a certain amount of tokens on behalf of the *owner*. It sets the `allowance` for the *spender* to the specified `amount`. **The spender is usually another Smart Contract**. It then **emits an Approval event** to record the spender's `allowance`. It returns **true** if the `approval` is successful. A common use case is to enable spending through third-party contracts like decentralized exchanges (DEXs).

**`transferFrom`**: It allows a *spender* to transfer tokens on behalf of another account.

It checks if the *sender* has an approved `allowance` for the spender and ensures it is sufficient for the *amount* and deducts the amount from the `allowance` and the sender's *balance* and adds the *amount* to the recipient's balance. It also **emits a Transfer event** to log the transaction. It returns \`**true**\` if the transfer is successful. A common use case is DEXs or smart contracts to handle token transactions on behalf of users.

**`_mint`**: Creates new tokens and adds them to a specified account's *balance*.

Calling the method increases the recipient's **`balanceOf`** by the specified amount. It also increases the **`totalSupply`** of the ERC20 Tokens by the same amount. A transfer event with the from address as address(0) (indicating tokens are created).

**`_burn`**: Destroys a specified number of *tokens* from an account's *balance*.

It reduces the account's **`balanceOf`** and decreases the **`totalSupply`** by the amount. It emits a Transfer event with the to address as address(0) (indicating tokens are burned).

It is typically implemented in token-burning mechanisms to reduce supply, increasing scarcity.

**`mint`**: Public wrapper for **`_mint`**. It calls **`_mint`** to create new tokens for a specified to address. Allows the contract owner or authorized accounts to mint new tokens.

**`burn`**: Public wrapper for **`_burn`**. Calls \_burn to destroy tokens from a specified from address.

### Events

**`Transfer`**:

Logs token transfers, including minting and burning events. Parameters: from (sender), to (recipient), value (amount).

**`Approval`**:

Logs approvals of allowances for spenders. Parameters: owner, spender, value (allowance amount).

## Compile Smart Contract

The ERC20 Token is now ready to be deployed to the Somnia Blockchain.

On the left tab, click the “Solidity Compiler” menu item and then the “ Compile ERC20.sol” button. This will compile the Solidity file and convert the Solidity code into machine-readable bytecode.

<figure><img src="/files/vLWvzJ8WMhpDhbUUsIo9" alt=""><figcaption></figcaption></figure>

## Deploy Smart Contract

The Smart Contract has been created and compiled into ByteCode, and the ABI has also been created. The next step is to deploy the Smart Contract to the Somnia DevNet so that you can perform READ and WRITE operations.

On the left tab, click the “Deploy and run transactions” menu item. To deploy the Smart Contract, we will require a wallet connection. In the Environment dropdown, select the option: “Injected Provider - MetaMask”. Then select the MetaMask account where you have STT Tokens.

In the “DEPLOY” field, enter the property values for the ERC20 Token:

* “\_NAME” - type string
* “\_SYMBOL” - type string
* “\_DECIMALS” - type uint8

Click Deploy.

When prompted, approve the Contract deployment on your MetaMask.<br>

<figure><img src="/files/CoqlrV0eOkGRFHE1PYT7" alt=""><figcaption></figcaption></figure>

Look at the terminal for the response and the deployed Smart Contract address. You can interact with the Smart Contract via the Remix IDE. Send a transaction to change the name.

<figure><img src="/files/8PGVQdJDZDqkzqdrh1CV" alt=""><figcaption></figcaption></figure>

Congratulations. 🎉 You have deployed an ERC20 Smart Contract to the Somnia Network. 🎉

***

### OpenZeppelin

As was mentioned at the beginning of this Tutorial, there is the option to use a Library to create the ERC20 Token. The OpenZeppelin Smart Contract Library can be used to create an ERC20 Token, and developers can rely on the Smart Contracts wizard to specify particular properties for the created Token. See an example below:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.22;


import "@openzeppelin/contracts@4.0.0/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@4.0.0/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts@4.0.0/access/Ownable.sol";


contract MyToken is ERC20, ERC20Burnable, Ownable {
    constructor(address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable()
    {}


    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}
```

The process for deploying this Smart Contract implementation built with OpenZeppelin is the same as Steps 3 and 4 above.

<br>


# Create ERC721 NFT Collections

ERC721 is the EVM compatible standard for Non Fungible Tokens, NFTs.\
\
NFTS are digital assets where each token is unique. Unlike ERC20 (fungible) tokens that are interchangeable, ERC721 tokens represent distinct items such as game assets, collectibles, tickets, certificates, or onchain identities.

Somnia, being EVM-compatible, supports the ERC721 standard natively. ERC721 has the following functionalities:

* Every token has a distinct `tokenId` and (optionally) distinct metadata, making it unique.
* Wallets can own, transfer, and approve NFTs using a common interface.
* NFTs are composable; therefore, Wallets, marketplaces, and dApps understand ERC-721 uniformly, enabling easy listing, trading, and display.
* ERC721 has a clean base that can be extended with metadata, enumeration, royalties (ERC-2981), permit (EIP-4494), etc.<br>

This guide will teach you how to connect to and deploy your ERC20 Smart Contract to the Somia Network using Hardhat.

## Pre-requisite

* This guide is not an introduction to Solidity Programming; you are expected to have a basic understanding of Solidity Programming.
* To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](/get-started/connect-your-wallet-to-mainnet).<br>

The Smart Contract is minimal, production-friendly ERC-721 without royalties (no ERC-2981). Per-token tokenURI set during mint (works great with IPFS). It also demonstrates how to deploy, mint, and verify on Somnia networks.

## ERC721 Smart Contract

<details>

<summary><code>NFTTest.sol</code></summary>

```solidity
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.4.0
pragma solidity ^0.8.27;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract NFTTest is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("NFTTest", "NFTT")
        Ownable(initialOwner)
    {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ipfs.io";
    }

    function safeMint(address to, string memory uri)
        public
        onlyOwner
        returns (uint256)
    {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri);
        return tokenId;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
```

</details>

## Code Breakdown

Below is a breakdown explanation of the code:

### Imports

```solidity
import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
```

`ERC721` is the Core NFT standard that has all the methods that control `ownership`, `transfers`, `approvals`, and `metadata` hook.

`ERC721URIStorage` adds per-token URI storage via `_setTokenURI` and overrides `tokenURI`. ERC721URIStorage extends ERC721 to store URIs per token (costs storage gas, but very flexible).

`Ownable` is a simple access control library that enables the Smart Contract owner to set one account as the “owner”.<br>

### Contract Declaration

```solidity
contract NFTTest is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;
    ...
}
```

`_nextTokenId` is an Internal counter for new token IDs. Starts at `0` by default (since not set), so your first mint is `tokenId = 0`.

### Constructor

```
constructor(address initialOwner)
    ERC721("NFTTest", "NFTT")
    Ownable(initialOwner)
{}
```

Calls `ERC721` constructor with collection name "NFTTest" and symbol "NFTT". Initializes Ownable with `initialOwner` the address allowed to mint tokens. In this instance, the body is empty because all setup is done via parent constructors.

This ERC-721 contract follows a straightforward ownership and metadata model designed for simplicity and marketplace compatibility. The contract owner is the sole minter, and each mint assigns the next sequential identifier—beginning at 0—to the specified recipient. This ensures that token IDs remain predictable (0, 1, 2, …) and facilitates aligning your metadata files with minted tokens.

### Base URI

```solidity
function _baseURI() internal pure override returns (string memory) {
    return "https://ipfs.io";
}
```

Returns a prefix used by `ERC721.tokenURI` when a token’s stored URI is relative (e.g., "/ipfs/\<CID>/1.json").

In this contract you set full URIs at mint (commonly ipfs\://...), which are returned as-is by `ERC721URIStorage`. So this Base URI only matters if you mint with relative paths. An example Base URI when using IPFS is then `https://ipfs.io/`

### Minting

```solidity
function safeMint(address to, string memory uri)
    public
    onlyOwner
    returns (uint256)
{
    uint256 tokenId = _nextTokenId++;
    _safeMint(to, tokenId);
    _setTokenURI(tokenId, uri);
    return tokenId;
}
```

In this contract, minting is restricted by `onlyOwner`, ensuring that only the contract owner can create new tokens. Token IDs are assigned sequentially using `_nextTokenId++`, starting from 0 and incrementing by one with each mint. The `_safeMint` function adds an extra layer of safety by checking if the recipient is a contract and, if so, requiring it to implement `IERC721Receiver` to prevent tokens from being locked. For metadata, `_setTokenURI` stores the exact URI string for each token, such as `ipfs://<CID>/1.json`, making it easy to reference unique files. The function also emits and returns the new `tokenId` upon minting. However, it’s worth noting that storing a full string per token consumes more gas; for larger drops with sequential files, a cheaper alternative is to use a base URI combined with the token ID rather than storing individual URIs.

### Required overrides

```solidity
function tokenURI(uint256 tokenId)
    public
    view
    override(ERC721, ERC721URIStorage)
    returns (string memory)
{
    return super.tokenURI(tokenId);
}
```

Because both ERC721 and ERC721URIStorage define tokenURI, Solidity requires you to pick which implementation to use. `super.tokenURI(tokenId)` resolves to ERC721URIStorage’s version, which:

* Returns the stored URI if present.
* Otherwise falls back to ERC721’s baseURI and tokenId behavior.

```solidity
function supportsInterface(bytes4 interfaceId)
    public
    view
    override(ERC721, ERC721URIStorage)
    returns (bool)
{
    return super.supportsInterface(interfaceId);
}
```

Also resolves multiple inheritance for supportsInterface (ERC165) and ensures the contract correctly reports ERC721 and metadata support.

Transfers and approvals behave exactly as the ERC721 standard specifies. Use `safeTransferFrom` by default, so the contract checks that recipients can handle NFTs, which prevents tokens from being sent to incompatible contracts. `transferFrom` is available when you are certain the recipient can accept NFTs without the receiver check, and approvals can be granted either per token with approve or globally with `setApprovalForAll`. Interface support is advertised through supportsInterface, allowing wallets, marketplaces, and indexers to recognize ERC721 core and metadata compatibility automatically.

A few best practices will keep deployments robust. Favor the `ipfs://` scheme for `metadata` and media to avoid locking yourself to a single HTTP gateway. If you expect very large drops, remember that `ERC721URIStorage` stores a full string per token, which is convenient but more expensive at scale; collections with sequential filenames can reduce costs by adopting a `baseURI` and `tokenId` pattern instead of per token URI storage.

## Initialize Hardhat

```bash
mkdir somnia-nft && cd somnia-nft
npm init -y
npm install --save-dev hardhat typescript ts-node @types/node
npx hardhat                              
npm install @openzeppelin/contracts      
npm install --save-dev @nomicfoundation/hardhat-verify @nomicfoundation/hardhat-ignition @nomicfoundation/hardhat-ignition-ethers
```

\
Create `.env`:

```
PRIVATE_KEY=0xYourPrivateKey
SOMNIA_RPC_HTTPS=https://dream-rpc.somnia.network
```

<details>

<summary><code>hardhat.config.ts</code></summary>

```typescript
import "dotenv/config";
import "@nomicfoundation/hardhat-verify";
import "@nomicfoundation/hardhat-ignition-ethers";
import { HardhatUserConfig } from "hardhat/config";

const config: HardhatUserConfig = {
  solidity: "0.8.28",
  networks: {
    somnia: {
      url: process.env.SOMNIA_RPC_HTTPS!,
      accounts: [process.env.PRIVATE_KEY!],
    },
  },
  sourcify: { enabled: false },
  etherscan: {
    apiKey: { somnia: process.env.SOMNIA_EXPLORER_API_KEY || "" },
    customChains: [
      {
        network: "somnia",
        chainId: 50312,
        urls: {
          apiURL: "https://shannon-explorer.somnia.network/api",
          browserURL: "https://shannon-explorer.somnia.network",
        },
      },
    ],
  },
};

export default config;

```

</details>

Add the `Smart Contract` to the Contract directory

## Deploy with Ignition

Create `ignition/modules/NFTTest.ts`:

```ts
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const NFTTestModule = buildModule("NFTTestModule", (m) => {
  const initialOwner = m.getParameter("initialOwner", "0xYourOwnerAddress");

  const nft = m.contract("NFTTest", [initialOwner]);

  return { nft };
});

export default NFTTestModule;
```

Deploy:

```bash
npx hardhat ignition deploy ignition/modules/NFTTest.ts --network somnia
```

Copy the **deployed address** from the output.

***

## Verify Smart Contract

```bash
npx hardhat verify --network somnia <DEPLOYED_ADDRESS> 0xYourOwnerAddress
```

> Ensure compiler version, optimizer, and constructor args match.

***

### Mint your collection

We’ll mint by calling `safeMint(to, uri)` **10 times**, matching your uploaded metadata files.

<details>

<summary><code>scripts/mint.ts</code></summary>

```ts
import { ethers } from "hardhat";

async function main() {
  const contractAddr = "<DEPLOYED_ADDRESS>";
  const nft = await ethers.getContractAt("NFTTest", contractAddr);

  const owner = (await ethers.getSigners())[0];

  // Example: 10 tokens at /ipfs/<CID>/{0..9}.json
  const CID = "<YOUR_METADATA_CID>";
  for (let i = 0; i < 10; i++) {
    const uri = `/ipfs/${CID}/${i}.json`;
    const tx = await nft.safeMint(owner.address, uri);
    console.log(`Mint tx ${i}:`, tx.hash);
    await tx.wait();
  }

  const lastId = await nft.callStatic.safeMint(owner.address, `/ipfs/${CID}/999.json`).catch(()=>null);
  console.log("Minted 10 tokens. Next simulated ID (no state change):", lastId ?? "N/A");
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
```

</details>

Run the project:

```bash
npx hardhat run scripts/mint.ts --network somnia
```

***

## Inspect Token metadata

Read a token’s URI (e.g., tokenId `0`) and open it in your browser.

`scripts/read-uri.ts`:

```ts
import { ethers } from "hardhat";

async function main() {
  const contractAddr = "<DEPLOYED_ADDRESS>";
  const nft = await ethers.getContractAt("NFTTest", contractAddr);
  const uri = await nft.tokenURI(0);
  console.log("tokenURI(0):", uri);
}

main().catch(console.error);
```

```bash
npx hardhat run scripts/read-uri.ts --network somnia
```

Open the printed URL (it should be `https://ipfs.io/ipfs/<CID>/0.json`) in your browser to confirm JSON and `image` render correctly.

Congratulations. 🎉 You have deployed your first ERC721 Smart Contract to the Somnia Network. 🎉


# Managing NFT Metadata with IPFS

In this guide, you will rely on an IPFS workflow for ERC721 collections on Somnia.

This guide will walk you through how\
\- You will prepare and upload artwork to IPFS via Pinata.\
\- Generate clean, wallet/marketplace-friendly metadata JSON\
\- Upload the metadata to IPFS via Pinata Cloud\
\- Deploy a Solidity contract that stores the metadata URIs onchain (for each token)\
\- Mint your NFTs.

All metadata resides on IPFS, while only the URIs (pointers) are stored onchain.

Please note that this guide provides a Smart Contract option for fully onchain metadata.

## Concepts You Should Know

* [IPFS](https://ipfs.tech/) (InterPlanetary File System) is a “Content Addressed Storage Network”. Files are addressed by their CID (content identifier). If any bit changes, the CID changes, and it is great for integrity and verifiability.
* [Pinata](https://pinata.cloud/): Pinata pins your files to IPFS and keeps them available. You’ll get CIDs (content identifiers) that never change for the same content.
* TokenURI: an ERC721 method that returns a URL (often an ipfs\:// URI) pointing to a JSON file describing the NFT token (name, description, image, attributes, etc.).
* Per token URI vs `baseURI`:
  * Per token URI (this guide): store the full JSON URI onchain for each token with ERC721URIStorage. Easiest and most flexible.
  * `baseURI` pattern: compute tokenURI = baseURI + tokenId (no per token storage). Cheaper for large drops, but requires sequential naming.

## Prerequisites

* MetaMask is configured for a Somnia RPC and has sufficient funds for gas
* Node.js 20+ (only if you’ll run the helper scripts below)
* Pinata account and a JWT (recommended): Get your JWT on Pinata
  * `Pinata Dashboard → API Keys → New Key → choose Admin/Scoped as needed → copy JWT`

## Prepare Images

If your art varies wildly in size or format, normalize once for a consistent user experience. Create an `assets` directory for your images. Run the node script below, targeting the `assets` directory to give uniformity to your images.

Install the dependencies:

```bash
npm i sharp fast-glob
```

Run the script:

```typescript
// scripts/resize.js
import fg from "fast-glob";
import sharp from "sharp";
import { mkdirSync } from "fs";
import path from "path";

const INPUT = "assets/raw-images"; // put your original images here
const OUTPUT = "assets/images";

mkdirSync(OUTPUT, { recursive: true });

const files = (await fg(`${INPUT}/*.{png,jpg,jpeg,webp}`)).sort();
for (let i = 0; i < files.length; i++) {
  const out = path.join(OUTPUT, `${i}.png`);
  await sharp(files[i]).resize(1024, 1024, { fit: "cover" }).toFile(out);
}
console.log("Normalized images written to", OUTPUT);
```

## Recommended Folder Structure

```markup
project/
  assets/
    images/
      0.png
      1.png
      2.png
      ...
    metadata/
      0.json
      1.json
      2.json
      ...
  scripts/
    resize.js              
    pinata.ts              (Pinata SDK init)
    upload-images.ts       (pin images folder → IMAGES_CID)
    make-metadata.ts       (generate JSON → uses IMAGES_CID)
    upload-metadata.ts     (pin metadata folder → METADATA_CID)
```

* `images/` contain consistent dimensions and formats (e.g., 1024×1024 PNG).
* `metadata/` contain one JSON per token ID, matching the filename (e.g., 7.json for tokenId 7).

## Upload Images to IPFS via Pinata

Install Pinata SDK:

```bash
npm i pinata dotenv
```

Initialise Pinata using your JWT credentials:

```typescript
// pinata.ts
import 'dotenv/config'
import { PinataSDK } from 'pinata'


export const pinata = new PinataSDK({
  pinataJwt: process.env.PINATA_JWT!,               // from Pinata “API Keys”
  pinataGateway: process.env.PINATA_GATEWAY!,       // e.g. myxyz.mypinata.cloud
})
```

Create a `.env` file in your project root:

```
PINATA_JWT=eyJhbGciOi...        # your Pinata JWT (keep secret)
PINATA_GATEWAY=myxyz.mypinata.cloud
IMAGES_CID=                     # leave blank until you upload images
```

\
Upload the images directory and get the root CID:

```typescript
// scripts/upload-images.ts
import 'dotenv/config'
import { pinata } from './pinata'
import fs from 'node:fs/promises'
import path from 'node:path'
import { File } from 'node:buffer'

const DIR = 'assets/images'

async function main() {
  const names = (await fs.readdir(DIR))
    .filter(n => n.match(/\.(png|jpg|jpeg|webp)$/i))
    .sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0]))
    
  const files: File[] = []
  
  for (const name of names) {
    const bytes = await fs.readFile(path.join(DIR, name))
    files.push(new File([bytes], name)) // uploaded as a single folder
  }
  
  const res = await pinata.upload.public.fileArray(files)
  // res.cid is the directory root CID
  console.log('Images CID:', res.cid)
}

main().catch(console.error)
```

Run the following command to upload the images:

```bash
node dist/scripts/upload-images.js
# => Images CID: bafybe...
```

Copy the CID into `.env` as `IMAGES_CID=....`<br>

## Generate Metadata JSON

Each \*.json should follow the de facto standards:

```json
{
  "name": "NFTTest #0",
  "description": "A clean ERC-721 on Somnia with per-token metadata.",
  "image": "ipfs://IMAGES_CID/0.png",
  "external_url": "https://your-site.example",
  "attributes": [
    { "trait_type": "Edition", "value": 0 }
  ]
}

```

Install `fast-glob` library:

```bash
npm i fs-extra fast-glob
```

Run the script below to generate a file per image:

```typescript
/ scripts/make-metadata.js
import fg from "fast-glob";
import { writeJSON, mkdirs } from "fs-extra";
import path from "path";


const IMAGES_CID = process.env.IMAGES_CID || "bafy..."; // paste from Step 2
const OUT_DIR = "assets/metadata";


sync function main() {
  await mkdirs(OUT_DIR)
  const imgs = (await fg('assets/images/*.{png,jpg,jpeg,webp}')).sort((a, b) =>
    Number(path.basename(a).split('.')[0]) - Number(path.basename(b).split('.')[0])
  )


  for (const p of imgs) {
    const id = Number(path.basename(p).split('.')[0])
    const json = {
      name: `NFTTest #${id}`,
      description: 'ERC-721 on Somnia with Pinata-hosted IPFS metadata.',
      image: `ipfs://${IMAGES_CID}/${id}.png`,
      attributes: [{ trait_type: 'Edition', value: id }]
    }
    await writeJSON(path.join(OUT_DIR, `${id}.json`), json, { spaces: 2 })
  }
  console.log('Metadata written to', OUT_DIR)
}
main().catch(console.error)

```

To create the metadata files, run the command:

```bash
node dist/scripts/make-metadata.js
```

## Upload Metadata to IPFS

Upload the metadata folder to generate the `METADATA_CID`

```typescript
// scripts/upload-metadata.ts
import { pinata } from './pinata'
import fs from 'node:fs/promises'
import path from 'node:path'
import { File } from 'node:buffer'

const DIR = 'assets/metadata'
async function main() {
  const names = (await fs.readdir(DIR))
    .filter(n => n.endsWith('.json'))
    .sort((a, b) => Number(a.split('.')[0]) - Number(b.split('.')[0]))
  const files: File[] = []
  for (const name of names) {
    const bytes = await fs.readFile(path.join(DIR, name))
    files.push(new File([bytes], name, { type: 'application/json' }))
  }
  const res = await pinata.upload.public.fileArray(files)
  console.log('Metadata CID:', res.cid)
}
main().catch(console.error)
```

Run the command:

```
node dist/scripts/upload-metadata.js
# => Metadata CID: bafybe...
```

Your tokenURI format is now: `ipfs://<METADATA_CID>/<tokenId>.json` Make sure the script uploads `assets/metadata` and records the printed Metadata CID; you’ll mint with: `ipfs://<METADATA_CID>/<tokenId>.json`

## NFT Smart Contract

We’ll use `ERC721URIStorage` Smart Contract and mint with full URIs. Paste this into Remix as `NFTTest.sol`

<details>

<summary>NFTTest.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
// Compatible with OpenZeppelin Contracts ^5.4.0
pragma solidity ^0.8.27;


import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {ERC721URIStorage} from "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";

contract NFTTest is ERC721, ERC721URIStorage, Ownable {
    uint256 private _nextTokenId;

    constructor(address initialOwner)
        ERC721("NFTTest", "NFTT")
        Ownable(initialOwner)
    {}

    function _baseURI() internal pure override returns (string memory) {
        return "https://ipfs.io"; // only affects relative paths
    }

    function safeMint(address to, string memory uri)
        public
        onlyOwner
        returns (uint256)
    {
        uint256 tokenId = _nextTokenId++;
        _safeMint(to, tokenId);
        _setTokenURI(tokenId, uri); // store full URI (e.g., ipfs://CID/0.json)
        return tokenId;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }


    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
```

</details>

## Contract Walkthrough

### Imports

* `ERC721` is the core NFT logic (ownership, transfers, approvals).
* `ERC721URIStorage` adds per-token storage for URIs via `_setTokenURI`.
* `Ownable` simple admin model; exposes onlyOwner for minting control.

`ERC721URIStorage` is the most straightforward way to store a token’s exact URI. For large drops, consider using a baseURI pattern to save storage gas; otherwise, this is perfect for flexible, explicit URIs.

State (\_nextTokenId) is the sequential ID counter starting at 0. First mint → tokenId = 0, then 1, 2, … etc

### Constructor

Initializes collection name/symbol and sets the owner to `initialOwner` (the only account allowed to mint).

### \_baseURI()

Returns <https://ipfs.io>. This is only used if you mint with relative paths (e.g., /ipfs/CID/7.json). If you mint with absolute ipfs\://… URIs (recommended), this value is ignored.

### safeMint(to, uri):

Owner-only mint. Creates a new token ID, mints safely (checking receiver contracts implement IERC721Receiver), and stores the exact uri string for that token via \_setTokenURI.

### Compile and Deploy using Remix. Follow this [guide](/developer/building-dapps/tokens-and-nfts/create-erc20-tokens)

Copy the deployed contract address.<br>

## How To Mint NFTs (Per Token URIs)

n Remix (Deployed Contracts):

* Call `safeMint(to, uri)` where:
  * `to` is the recipient address (can be yours).
  * `uri` is `ipfs://<METADATA_CID>/<id>.json` (e.g., `ipfs://bafy.../0.json`).

Then call `tokenURI(0)` to confirm the stored URI.<br>

If you prefer scripts for multiple mints, you can write a script to Batch Mint using Hardhat, for example:

```typescript
scripts/mint.ts (Hardhat)
import { ethers } from "hardhat";


const CONTRACT = "0xYourDeployedAddress";
const RECEIVER = "0xReceiver";
const METADATA_CID = "bafy...";


async function main() {
  const nft = await ethers.getContractAt("NFTTest", CONTRACT);
  for (let id = 0; id < 10; id++) {
    const uri = `ipfs://${METADATA_CID}/${id}.json`;
    const tx = await nft.safeMint(RECEIVER, uri);
    await tx.wait();
    console.log(`Minted #${id} → ${uri}`);
  }
}
main().catch(console.error);

```

## Validation

To validate everything end to end, first do quick gateway smoke tests by opening `https://ipfs.io/ipfs/<IMAGES_CID>/0.png` and `https://ipfs.io/ipfs/<METADATA_CID>/0.json` in a browser to confirm the image and JSON are reachable. Next, call `tokenURI(0)` on your contract and verify it returns `ipfs://<METADATA_CID>/0.json`. Finally, check in a wallet or marketplace UI that the NFT renders correctly using the `ipfs://` image URI embedded in the JSON.<br>

## MetadataOptional Fully Onchain Metadata

<details>

<summary>Smart Contract</summary>

If your art is small (SVG) or you only need text metadata, you can **encode metadata JSON on-chain**:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.27;

import {ERC721} from "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Strings} from "@openzeppelin/contracts/utils/Strings.sol";
import {Base64} from "@openzeppelin/contracts/utils/Base64.sol";

contract OnchainMeta is ERC721, Ownable {
    using Strings for uint256;
    uint256 private _id;

    constructor(address owner_) ERC721("Onchain", "ONC") Ownable(owner_) {}

    function mint(address to) external onlyOwner returns (uint256) {
        uint256 tokenId = _id++;
        _safeMint(to, tokenId);
        return tokenId;
    }

    function tokenURI(uint256 tokenId) public view override returns (string memory) {
        _requireOwned(tokenId);
        // Example: trivial, static image via SVG data URI
        string memory image = string(
            abi.encodePacked(
                "data:image/svg+xml;base64,",
                Base64.encode(
                    bytes(
                        string(
                            abi.encodePacked(
                                "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'>",
                                "<rect width='512' height='512' fill='black'/>",
                                "<text x='50%' y='50%' dominant-baseline='middle' text-anchor='middle' fill='white' font-size='48'>#",
                                tokenId.toString(),
                                "</text></svg>"
                            )
                        )
                    )
                )
            )
        );

        bytes memory json = abi.encodePacked(
            '{"name":"Onchain #', tokenId.toString(),
            '","description":"Fully on-chain metadata","image":"', image, '"}'
        );

        return string(abi.encodePacked(
            "data:application/json;base64,",
            Base64.encode(json)
        ));
    }
}
```

**Trade-offs**

* No external dependencies; URIs are immutable, always available.
* Higher gas (long strings), limited to compact media (SVG/text). For large images, prefer IPFS.

</details>


# Using Native Coin (SOMI/STT)

SOMI is the native coin of the Somnia Network, similar to ETH on Ethereum. Unlike ERC20 tokens, SOMI is built into the protocol itself and does not have a contract address.

{% hint style="warning" %}
Kindly note that the Native Coin for Somnia Testnet is STT.
{% endhint %}

{% embed url="<https://www.youtube.com/watch?v=pq7sARCSTSk>" %}

This multi-part guide shows how to use SOMI for:

* Payments
* Escrow
* Donations & Tipping
* Sponsored gas via Account Abstraction

## Use SOMI for Payments in Smart Contracts

A simple contract that accepts exact SOMI payments:

```solidity
function payToAccess() external payable {
  require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI");
}
```

Use `msg.value` to access the native coin sent in a transaction. No ERC20 functions are needed.

To withdraw collected SOMI:

```solidity
function withdraw() external onlyOwner {
  payable(owner).transfer(address(this).balance);
}
```

Deploy using Hardhat Ignition or Viem and test with a `sendTransaction` call.

<details>

<summary>Example.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SOMIPayment {
    address public owner;

    constructor() {
        owner = msg.sender;
    }

    // Modifier to restrict access to the contract owner
    modifier onlyOwner() {
        require(msg.sender == owner, "Only owner can call this");
        _;
    }

    // User must send exactly 0.01 SOMI to access this feature
    function payToAccess() external payable {
        require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI");

        // Logic for access: mint token, grant download, emit event, etc.
    }

    // Withdraw collected SOMI to owner
    function withdraw() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }
}
```

</details>

## Build an SOMI Escrow Contract

A secure escrow contract allows a buyer to deposit SOMI and later release or refund:

```solidity
constructor(address payable _seller) payable {
  buyer = msg.sender;
  seller = _seller;
  amount = msg.value;
}
```

Release funds to the seller:

```solidity
function release() external onlyBuyer {
  seller.transfer(amount);
}
```

Deploy with Hardhat Ignition and pass SOMI as `value` during deployment.

<details>

<summary>Example.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SOMIEscrow {
    address public buyer;
    address payable public seller;
    uint256 public amount;
    bool public isDeposited;

    constructor(address payable _seller) payable {
        buyer = msg.sender;
        seller = _seller;
        amount = msg.value;
        require(amount > 0, "Must deposit SOMI");
        isDeposited = true;
    }

    modifier onlyBuyer() {
        require(msg.sender == buyer, "Only buyer can call this");
        _;
    }

    function release() external onlyBuyer {
        require(isDeposited, "No funds to release");
        isDeposited = false;
        seller.transfer(amount);
    }

    function refund() external onlyBuyer {
        require(isDeposited, "No funds to refund");
        isDeposited = false;
        payable(buyer).transfer(amount);
    }
}

```

</details>

## SOMI Tip Jar

Allow any wallet to send tips directly:

```solidity
receive() external payable {
  emit Tipped(msg.sender, msg.value);
}
```

Withdraw all tips:

```solidity
function withdraw() external onlyOwner {
  payable(owner).transfer(address(this).balance);
}
```

<details>

<summary>Example.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract SOMITipJar {
    address public owner;

    event Tipped(address indexed from, uint256 amount);
    event Withdrawn(address indexed to, uint256 amount);

    constructor() {
        owner = msg.sender;
    }

    receive() external payable {
        emit Tipped(msg.sender, msg.value);
    }

    function withdraw() external {
        require(msg.sender == owner, "Only owner can withdraw");
        uint256 balance = address(this).balance;
        require(balance > 0, "No tips available");
        payable(owner).transfer(balance);
        emit Withdrawn(owner, balance);
    }
}
```

</details>

## Frontend tip

```typescript
await walletClient.sendTransaction({
  to: '0xTipJarAddress',
  value: parseEther('0.05'),
});
```

## Sponsor SOMI Transactions with Account Abstraction

Using a smart account + relayer (e.g. via Privy, Thirdweb), the dApp can cover gas fees:

```typescript
await sendTransaction({
  to: contractAddress,
  data: mintFunctionEncoded,
  value: 0n, // user sends no SOMI
});
```

The Smart Contract function can execute mint or other logic as usual. The paymaster or relayer pays SOMI.

## Conclusion

* **SOMI is native** and used via `msg.value`, `.transfer()`, and `payable`.
* **There is no contract address for SOMI (STT for Testnet)**.
* You can integrate SOMI into any Solidity or Viem app with no ERC20 logic.
* Account Abstraction enables gasless dApps using SOMI as sponsor currency.

<br>


# Wallet Integration and Auth

Seamless onboarding and intuitive wallet experiences are at the heart of successful dApps. Somnia supports a range of modern wallet integration tools to help you deliver smooth, secure, and gas-efficient user journeys — from first interaction to advanced transactions.

In this section, you’ll learn how to:

* [Integrate **Privy** for embedded, Web2-friendly wallet flows](/developer/building-dapps/wallet-integration-and-auth/authenticating-with-privy)
* [Use **RainbowKit** to offer a beautiful, customizable wallet connection UI](/developer/building-dapps/wallet-integration-and-auth/authenticating-with-rainbowkit)
* [Leverage **ConnectKit** for flexible and extensible wallet integrations](/developer/building-dapps/wallet-integration-and-auth/authenticating-with-connectkit)

> Whether you're onboarding crypto-first users or Web2 newcomers, these wallet SDKs give you the tools to build dApps that are simple, secure, and delightful to use.


# Authenticating with MetaMask

Somnia empowers developers to build applications for mass adoption. Developers who deploy their Smart Contracts on Somnia, will require a User Interface to Connect to the Smart Contract. To enable users connect via the User Interface, it is necessary to set up an authentication process where only authorized users can access the functionality on the deployed Smart Contracts, for example, to carry out WRITE operations.\
\
[MetaMask](https://docs.metamask.io) is a wallet library that developers can use to build login functionality for applications on the Somnia Network.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

In this guide, you will learn how to use the MetaMask Library to set up authentication for your User Interface App and connect to the Somnia Network. We will build a simple NextJS application to walk through the process.

## Start a NextJS Project

Run the command below to start a NextJS project:

```bash
npx create-next-app metamask-example
```

Select Typescript, TailWind CSS, and Page Router in the build options.

<br>

Change the directory into the project folder. Delete the code inside of the `<main>` tags and replace them with the following:

```html
 <p>Hello, World!</p>
```

## Install Viem

```bash
npm i viem
```

[Viem](https://viem.sh) is a TypeScript interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum.\
\
Viem sets up a “`transport`” infrastructure to connect with a node in the EVM Network and the deployed Smart Contracts.\
\
We will use some ViemJS methods to connect to your Smart Contract deployed on the Somnia Network. ViemJS has a \`createPublicClient\` and a \`createWalletClient\` method. The PublicClient is used to perform READ operations, while the WalletClient is used to perform WRITE operations.

## Import Methods

The next step is to set up the React State methods and the ViemJS methods that we will require:

```typescript
import { useState } from "react";
import {
  createPublicClient,
  http,
  createWalletClient,
} from "viem";
```

The `http` is the transport protocol for interacting with the Node of the Somnia Blockchain via RPC. It uses the default Somnia RPC URL: [`https://dream-rpc.somnia.network`](https://dream-rpc.somnia.network). In the future developers can use RPC providers to avoid rate limiting.

## Import Somnia

Import Somnia Testnet

```typescript
import { somniaTestnet } from "viem/chains";
```

## Declare React States

State allows us to manage changing data in the User Interface. For this example application, we are going to manage two states:

* When we can read the User's Address
* When a User is connected (Authorization)

Add the states inside the export statement:

```typescript
const [address, setAddress] = useState<string>("");
const [connected, setConnected] = useState(false);
```

Now that the States are declared, we can declare a function to handle the MetaMask authentication process on Somnia Network.

## Connect MetaMask Function

Add the function below inside the export statement:

```typescript
const connectToMetaMask = async () => {
    if (typeof window !== "undefined" && window.ethereum !== undefined) {
      try {
        await window.ethereum.request({ method: "eth_requestAccounts" });
        const walletClient = createWalletClient({
          chain: SOMNIA,
          transport: custom(window.ethereum),
        });
        const [userAddress] = await walletClient.getAddresses();
        setClient(walletClient);
        setAddress(userAddress);
        setConnected(true);
        console.log("Connected account:", userAddress);
      } catch (error) {
        console.error("User denied account access:", error);
      }
    } else {
      console.log(
        "MetaMask is not installed or not running in a browser environment!"
      );
    }
  };
```

## Update the UI

MetaMask connection is set up, and the final step is to test the connection via the User Interface. Update the `<p>Hello, World!</p>` in the return statement to the following:

```typescript
{!connected ? (
        <button
          onClick={connectToMetaMask}
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        >
          Connect Wallet
        </button>
      ) : (
        <div>
          <p>Connected as: {address}</p>
         </div>
      )}
```

Open your terminal and run the following command to start the app:

```bash
npm run dev
```

Go to `localhost:3000` in your Web Browser to interact with the app and connect to Somnia Network via MetaMask.\
\
You can read more about using Viem to interact with the deployed Smart Contract methods on Somnia Network [here](/developer/development-frameworks/using-the-viem-library).\
\
Congratulations, you have successfully connected from MetaMask to Somnia Network. 🎉

<br>


# Authenticating with ConnectKit

In this guide, we'll integrate [ConnectKit](https://docs.family.co/connectkit) with the Somnia Network in a Next.js application. This will enable users to connect their wallets seamlessly, facilitating interactions with the Somnia blockchain.

## Prerequisites

Before we begin, ensure you have the following:

1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
2. To complete this guide, you will need MetaMask installed and the Somnia DevNet added to the list of Networks. If you have yet to install MetaMask, please follow the [Connect Your Wallet guide](/get-started/connect-your-wallet-to-mainnet).
3. Familiarity with React and Next.js is assumed.

## Create the Next.js Project

Open your terminal and run the following commands to set up a new Next.js project:

```bash
npx create-next-app@latest somnia-connectkit
cd somnia-connectkit
```

Install the required Dependencies, which are **`wagmi`**, **`viem`**, **`@tanstack/react-query`**, and **`connectkit`**. Run the following command:

```bash
npm install wagmi viem @tanstack/react-query connectkit
```

## Set Up Providers in Next.js

We'll set up several providers to manage the application's state and facilitate interactions with the blockchain.

\
Create a **`components`** directory in the app folder. Inside the components directory, create a file named **`ClientProvider.tsx`** with the following content:

```typescript
'use client';

import { WagmiConfig, createConfig } from 'wagmi';
import { ConnectKitProvider, getDefaultConfig } from 'connectkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { somniaTestnet } from 'viem/chains';


const queryClient = new QueryClient();


const config = createConfig(
  getDefaultConfig({
    autoConnect: true,
    appName: 'Somnia DApp',
    chains: [somniaTestnet],
  })
);

export default function ClientProvider({ children }) {
  return (
    <WagmiConfig config={config}>
      <QueryClientProvider client={queryClient}>
        <ConnectKitProvider>{children}</ConnectKitProvider>
      </QueryClientProvider>
    </WagmiConfig>
  );
}
```

In the app directory, locate the **`layout.tsx`** file and update it as follows:

```typescript
import ClientProvider from './components/ClientProvider';
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>Somnia DApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>
        <ClientProvider>{children}</ClientProvider>
      </body>
    </html>
  );
}
```

## Build the Home Page

We'll create a simple home page that allows users to connect their wallets and displays their address upon connection.

In the app directory, locate the **`page.tsx`** file and update it as follows:

```typescript
'use client';

import { useAccount } from 'wagmi';
import { ConnectKitButton } from 'connectkit';

export default function Home() {
  const { address, isConnected } = useAccount();
return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        Hello, world!
        {/* Connect Button */}
        <div className="mt-4">
          <ConnectKitButton />
        </div>
        {/* Show Wallet Address */}
        {isConnected && (
          <p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
        )}
      </main>
    </div>
  );
}
```

To run the application, start the Development Server by running the following command:

```bash
npm run dev
```

Open your browser and navigate to **`http://localhost:3000`**. You should see the ConnectKit button, which allows users to connect their wallets to the Somnia network.

## Conclusion

You've successfully integrated ConnectKit with the Somnia Network in a Next.js application. This setup provides a foundation for building decentralized applications on Somnia, enabling seamless wallet connections and interactions with the Somnia Network.

For further exploration, consider adding features such as interacting with smart contracts, displaying user balances, or implementing transaction functionalities.

If you encounter any issues or need assistance, join the[ Somnia Developer Discord](https://discord.gg/somnia).

<br>


# Authenticating with Privy

[Privy](https://docs.privy.io/) is a secure, embeddable wallet infrastructure provider that allows developers to authenticate users, manage sessions, and provide seamless wallet experiences within dApps.\
\
Privy embedded wallets can be made interoperable across apps. Somnia has adopted the global wallets setup to foster a cross-app ecosystem where users can easily port their wallets from one app to another in the Somnia Ecosystem.

Using **global wallets**, users can seamlessly move assets between different apps and easily prove ownership of, sign messages, or send transactions with their existing wallets. Developers do not have to worry that users will generate a new wallet to sign into different applications. Kindly read more [here](https://docs.privy.io/wallets/global-wallets/overview).\
\
This guide will integrate Privy with the Somnia Testnet, enabling users to create and connect wallets effortlessly.

<figure><img src="/files/ddcuSmnmCB4NJP6Ey67C" alt=""><figcaption></figcaption></figure>

## Prerequisites

* This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
* To complete this guide, sign up for [Privy](https://dashboard.privy.io/) and get an AppID and get the Somnia Provider AppID.
* Familiarity with React and Next.js is assumed.

## Installation

### Create the Next.js Project <a href="#create-the-next.js-project" id="create-the-next.js-project"></a>

Open your terminal and run the following commands to set up a new Next.js project:

```bash
npx create-next-app@latest somnia-privy
cd somnia-privy
```

Install the necessary packages

```bash
npm install @privy-io/react-auth viem
```

## Set Up PrivyProvider

Go to <https://dashboard.privy.io/> to set up an account.

Click "**`New App`**" to create a new application that will connect to the Somnia Provider AppID.

<figure><img src="/files/0DmQxJfLTSXvRxg2hB2z" alt=""><figcaption></figcaption></figure>

Open the newly created app and in the left side navigation menu navigate to:

**`User Management >>>> Global Wallet >>>> Integrations`**<br>

Click the toggle to turn ON the Somnia Provider App.

<figure><img src="/files/CaeKkxbXQCSjVHpVB7XU" alt=""><figcaption></figcaption></figure>

Wrap your application **`layout.ts`** file with PrivyProvider and supply your PrivateKey from Privy and the Somnia Provider App ID to the **`loginMethods`**:

```typescript
'use client';

import { PrivyProvider } from '@privy-io/react-auth';
import { somniaTestnet } from 'viem/chains';

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang='en'>
      <body
        className={`${geistSans.variable} ${geistMono.variable} antialiased`}
      >
        <PrivyProvider
          appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID!}
          config={{
            loginMethods: {
              primary: ['email', 'google', 'privy:cm8d9yzp2013kkr612h8ymoq8'],
            },
            defaultChain: somniaTestnet,
            supportedChains: [somniaTestnet],
            embeddedWallets: {
              createOnLogin: 'users-without-wallets',
            },
          }}
        >
          {children}
        </PrivyProvider>
      </body>
    </html>
  );
}
```

Add your environment variable in .env.local:

```bash
NEXT_PUBLIC_PRIVY_APP_ID=your-privy-app-id
```

## Privy Hooks

These hooks make it easy to authenticate users, manage wallets, and interact with the Somnia Network using Privy Global Wallet

```typescript
import { useCrossAppAccounts, usePrivy } from '@privy-io/react-auth';
```

## Authenticate

Use the provided hooks to authenticate users and access their wallets.

<details>

<summary>page.tsx</summary>

<pre class="language-typescript"><code class="lang-typescript">export default function Home() {

  const { loginWithCrossAppAccount } = useCrossAppAccounts();
<strong>  const { ready, authenticated, user, logout } = usePrivy();
</strong>  const disableLogin = !ready || (ready &#x26;&#x26; authenticated);
  
  const [loginError, setLoginError] = useState&#x3C;string | null>(null);
  const [walletAddress, setWalletAddress] = useState&#x3C;string | null>(null);
  
  const providerAppId = 'cm8d9yzp2013kkr612h8ymoq8';
  
  const startCrossAppLogin = async () => {
    try {
      setLoginError(null);
      const result = await loginWithCrossAppAccount({
        appId: providerAppId,
      });
      setWalletAddress(result.wallet?.address)
      console.log(
        'Logged in via global wallet:',
        result,
      );
    } catch (err) {
      console.warn('Cross-app login failed:', err);
      setLoginError('Failed to log in with Global Wallet.');
    }
  };
  
......
  
    {!ready ? (
          &#x3C;p>Loading...&#x3C;/p>
        ) : authenticated ? (
            {walletAddress ? (
              &#x3C;p>Connected as: {walletAddress}&#x3C;/p>
            ) : (
              &#x3C;p className='text-gray-600'>No wallet address found.&#x3C;/p>
            )}
            &#x3C;button
              onClick={logout}
              className='bg-red-600 text-white px-4 py-2 rounded'
            >
              Logout
            &#x3C;/button>
          &#x3C;/div>
          
        ) : (
          &#x3C;>
            &#x3C;button
              onClick={startCrossAppLogin}
              className='bg-purple-600 text-white px-4 py-2 rounded'
            >
              Login with Global Wallet
            &#x3C;/button>
            {loginError &#x26;&#x26; &#x3C;p className='text-red-500 text-sm'>{loginError}&#x3C;/p>}
          &#x3C;/>          &#x3C;/div>
        )}
        
}
</code></pre>

</details>

## Send Transactions

Once authenticated, use the **`useSendTransaction`** hook from `useCrossAppAccount` method to interact with Somnia Testnet:

```typescript
 const { sendTransaction } = useCrossAppAccounts();
 
 ......
 
 const sendSTT = async () => {
    if (!walletAddress) return;

     const txn = {
      to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03',
      value: 1000000000000000,
      chainId: 50312,
    };
    
    try {
      const tx = await sendTransaction(txn, { address: walletAddress });
      console.log('TX Sent:', tx);
    } catch (err) {
      console.error('TXN Failed:', err);
    }
  };
  
......
  
 <button onClick={sendSTT}> Send 0.001 STT</button>
```

## Complete Code

<details>

<summary>Complete <strong><code>page.tsx</code></strong> code</summary>

```typescript
'use client';

import {
  usePrivy,
  useCrossAppAccounts,
} from '@privy-io/react-auth';
import { useEffect, useState } from 'react';
import { createPublicClient, http, formatEther } from 'viem';
import { somniaTestnet } from 'viem/chains';

export default function Home() {
  const { ready, authenticated, user, logout } = usePrivy();
  const { loginWithCrossAppAccount, sendTransaction } = useCrossAppAccounts();
  
  const [loginError, setLoginError] = useState<string | null>(null);
  const [hydrated, setHydrated] = useState(false);
  const [walletAddress, setWalletAddress] = useState<string | null>(null);
  const [balance, setBalance] = useState<string>('');

  const providerAppId = 'cm8d9yzp2013kkr612h8ymoq8';

  const client = createPublicClient({
    chain: somniaTestnet,
    transport: http(),
  });

  const startCrossAppLogin = async () => {
    try {
      setLoginError(null);
      const result = await loginWithCrossAppAccount({
        appId: providerAppId,
      });
      console.log(
        'Logged in via global wallet:',
        result,
      );
    } catch (err) {
      console.warn('Cross-app login failed:', err);
      setLoginError('Failed to log in with Global Wallet.');
    }
  };

  useEffect(() => {
    if (authenticated) {
      const globalWallet = user?.linkedAccounts?.find(
        (account) =>
          account.type === 'cross_app' &&
          account.providerApp?.id === providerAppId
      );

      console.log(globalWallet);
      const wallet = globalWallet?.smartWallets?.[0];
      console.log(wallet);
      if (wallet?.address) {
        setWalletAddress(wallet.address);
        setHydrated(true);
        fetchBalance(wallet.address);
      } else if (user?.wallet?.address) {
        setWalletAddress(user.wallet.address);
        setHydrated(true);
        fetchBalance(user.wallet.address);
      } else {
        setHydrated(true);
      }
    }
  }, [authenticated, user]);

  const fetchBalance = async (address: string) => {
    try {
      const result = await client.getBalance({
        address: address as `0x${string}`,
      });
      const formatted = parseFloat(formatEther(result)).toFixed(3);
      setBalance(formatted);
    } catch (err) {
      console.error('Failed to fetch balance:', err);
    }
  };

  const sendSTT = async () => {
    if (!walletAddress) return;
    console.log(walletAddress);

   const txn = {
      to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03',
      value: 1000000000000000,
      chainId: 50312,
    };
    
    try {
      const tx = await sendTransaction(txn, { address: walletAddress });
      console.log('TX Sent:', tx);
      if (walletAddress) fetchBalance(walletAddress);
    } catch (err) {
      console.error('TXN Failed:', err);
    }
  };

  return (
    <div className='grid min-h-screen items-center justify-items-center p-8 sm:p-20'>
      <main className='flex flex-col gap-6 row-start-2 items-center'>
        {!ready ? (
          <p>Loading...</p>
        ) : !authenticated ? (
          <>
            <button
              onClick={startCrossAppLogin}
              className='bg-purple-600 text-white px-4 py-2 rounded'
            >
              Login with Global Wallet
            </button>
            {loginError && <p className='text-red-500 text-sm'>{loginError}</p>}
          </>
        ) : hydrated ? (
          <div className='space-y-4 text-center'>
            {walletAddress ? (
              <p>Connected as: {walletAddress}</p>
            ) : (
              <p className='text-gray-600'>No wallet address found.</p>
            )}
            <p>Balance: {balance ? `${balance} STT` : 'Loading...'} </p>
            <button
              onClick={sendSTT}
              className='bg-blue-600 text-white px-4 py-2 rounded'
            >
              Send 0.001 STT
            </button>
            <button
              onClick={logout}
              className='bg-red-600 text-white px-4 py-2 rounded'
            >
              Logout
            </button>
          </div>
        ) : (
          <p>🔄 Logging in... Please wait</p>
        )}
      </main>
    </div>
  );
}
```

</details>

By using Privy Global Wallet on the Somnia Testnet, developers can offer a seamless onboarding and wallet experience. This setup is ideal for onboarding Web2 users into Web3 with embedded wallets, abstracting away traditional wallet complexities.

<br>


# Authenticating with RainbowKit

In this guide, we'll integrate [RainbowKit](https://www.rainbowkit.com/docs/introduction) with the Somnia Network in a Next.js application. This will enable users to connect their wallets seamlessly, facilitating interactions with the Somnia blockchain.

## Prerequisites

Before we begin, ensure you have the following:

1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
2. To complete this guide, you will need MetaMask installed and the Somnia DevNet added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet guide](/get-started/connect-your-wallet-to-mainnet).
3. Familiarity with React and Next.js is assumed.

## Create the Next.js Project

Open your terminal and run the following commands to set up a new Next.js project:

```bash
npx create-next-app@latest somnia-rainbowkit
cd somnia-rainbowkit
```

Install the required Dependencies, which are **`wagmi`**, **`viem`**, @tanstack/react-query, and rainbowkit. Run the following command:

```bash
npm install wagmi viem @tanstack/react-query rainbowkit
```

### Set Up Providers in Next.js

We'll set up several providers to manage the application's state and facilitate interactions with the blockchain.

\
Create a **`components`** directory in the app folder. Inside the components directory, create a file named **`ClientProvider.tsx`** with the following content:

```typescript
'use client';

import { WagmiProvider } from "wagmi";
import {
  RainbowKitProvider,
  getDefaultConfig,
} from "@rainbow-me/rainbowkit";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { somniaTestnet } from "viem/chains";

const queryClient = new QueryClient();

const config = getDefaultConfig({
  appName: "Somnia Example App",
  projectId: "Get_WalletConnect_ID",
  chains: [somniaTestnet],
  ssr: true, 
});

export default function ClientProvider({ children }) {
  return (
    <WagmiConfig config={config}>
      <QueryClientProvider client={queryClient}>
        <RainbowkitProvider>{children}</RainbowkitProvider>
      </QueryClientProvider>
    </WagmiConfig>
  );
}
```

> Every Rainbowkit dApp relies on **WalletConnect** and needs to obtain a **`projectId`** from WalletConnect Cloud. Get one [here](https://cloud.walletconnect.com/).

In the app directory, locate the **`layout.tsx`** file and update it as follows:

```typescript
import ClientProvider from './components/ClientProvider';


export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>Somnia DApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>
        <ClientProvider>{children}</ClientProvider>
      </body>
    </html>
  );
}
```

### Build the Home Page

We'll create a simple home page that allows users to connect their wallets and displays their address upon connection.

In the app directory, locate the **`page.tsx`** file and update it as follows:

```typescript
'use client';

import { useAccount } from 'wagmi';
import { ConnectButton } from "@rainbow-me/rainbowkit";


export default function Home() {
  const { address, isConnected } = useAccount();


  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <ConnectButton />
        {isConnected && (
          <p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
        )}
      </main>
    </div>
  );
}
```

### Run the Application

Start the Development Server by running the following command:

```bash
npm run dev
```

Open your browser and navigate to <http://localhost:3000>. You should see the RainbowKit button, allowing users to connect their wallets to the Somnia network.

## Conclusion

You've successfully integrated RainbowKit with the Somnia Network in a Next.js application. This setup provides a foundation for building decentralized applications on Somnia, enabling seamless wallet connections and interactions with the Somnia Network.

For further exploration, consider adding features such as interacting with smart contracts, displaying user balances, or implementing transaction functionalities.

If you encounter any issues or need assistance, join the[ Somnia Developer Discord](https://discord.gg/somnia).

<br>


# OnRamps

OnRamps make it easy for users to move from the traditional financial system into the Somnia ecosystem by purchasing tokens directly with their local currency.

This section covers how to integrate **Banxa**, Somnia’s trusted onramping service, to enable seamless and secure fiat-to-crypto transactions within your dApps.

You’ll learn how to:

* Embed Banxa’s checkout flow in your application
* Support multiple payment methods (credit/debit card, bank transfer, etc.)
* Customize the onramp experience to match your app’s branding
* Streamline user onboarding with a direct path from fiat to onchain activity

> Banxa provides developers and users with a regulated, global, and developer-friendly solution for accessing the Somnia network — making your application more inclusive and easier to adopt.


# Buy SOMI Using Banxa Checkout

You can easily buy SOMI tokens using Banxa, a trusted fiat to crypto gateway partner.\
This guide walks you through purchasing SOMI directly from the[ Banxa Checkout Portal](https://checkout.banxa.com/).

***

Banxa lets you buy cryptocurrency with local payment methods, including credit/debit cards, bank transfers, and mobile wallets.\
In this guide, you’ll learn how to:

* Access the Banxa checkout page
* Select SOMI as the token to purchase
* Complete payment securely
* Receive SOMI in your wallet

***

## Prerequisites

Before you start, ensure you have:

* Your wallet address handy, this is where SOMI will be sent.
* A valid payment method supported in your region (Visa, Mastercard, bank transfer, etc.).

***

## Step 1 — Visit the Banxa Checkout Page

Go to[ https://checkout.banxa.com/](https://checkout.banxa.com/).\
You’ll see a simple interface to select your fiat currency and cryptocurrency.

***

<figure><img src="/files/jZgaFoQ0X06hjrBcnAsn" alt="" width="375"><figcaption></figcaption></figure>

## Step 2 — Choose Your Purchase Options

* In the “You Pay” field, enter how much fiat (e.g. USD, EUR, NGN) you want to spend.
* In the “You Get” field, select SOMI as the token.
* Select the network as Somnia Network.

Banxa will automatically show the conversion rate, fees, and delivery estimate.

***

## Step 3 — Enter Your Wallet Address

Copy your wallet address from your Somnia-compatible wallet (e.g. MetaMask) and paste it into the “Wallet Address” field.

{% hint style="danger" %}
***Triple check the address before proceeding. Funds sent to the wrong address cannot be recovered.***
{% endhint %}

***

## Step 4 — Complete KYC Verification

Banxa complies with international regulations and may ask you to verify your identity.\
Typical verification steps include:

* Uploading a government-issued ID
* Taking a selfie
* Confirming your billing address

This process only needs to be done once per account.

<div align="center"><img src="/files/XHdQaS8FVzKBUoPNM2sI" alt=""></div>

***

## Step 5 — Select a Payment Method

Choose one of the available payment options for your country:

* Credit/Debit Card
* Bank Transfer
* Apple Pay / Google Pay (where supported)

Follow the on-screen instructions to complete the payment.

***

## Step 6 — Receive SOMI in Your Wallet

Once payment is confirmed:

* Banxa will process the transaction.
* SOMI tokens will be sent directly to your wallet on the Somnia Network.
* You’ll receive a confirmation email once the transfer is complete.

This may take a few minutes, depending on network congestion and payment method.

***

## Need Help?

If your transaction is delayed or you need assistance, you can contact:

* Banxa Support:[ support.banxa.com](https://support.banxa.com/)
* Somnia Community:[ Discord → #support](https://discord.gg/somnia)

***

## Disclaimers

* Banxa is a third party provider. Always ensure you are using the official[ checkout.banxa.com](https://checkout.banxa.com/) link.
* Transaction fees, limits, and verification steps vary by country and payment method.
* SOMI purchases are non reversible once processed.

<br>


# Cross-chain Swaps and Bridging

Most users don't start their journey on Somnia, their assets live on Ethereum, Arbitrum, Base, Solana, and dozens of other chains. To grow your dApp, you need a smooth way to move that liquidity **into, out of, and across** the Somnia ecosystem.

There are two ways to bring assets across, depending on who is doing the work:

* **For end users (no code):** Somnia's official bridge partners, [**Relay**](https://relay.link/bridge) and [**Stargate Finance**](https://stargate.finance/), let anyone bridge assets through their own interfaces. If you just need users to move funds and don't want to build any UI, point them to the [Bridging Info](/get-started/bridging-info) guide.
* **For developers (this section):** embed swaps and bridging **directly inside your dApp** with the [**LI.FI**](https://li.fi/) **SDK**, so users never have to leave your app.

***

## Why LI.FI for in-app bridging

LI.FI is the routing and execution layer for cross-chain liquidity. A single integration aggregates dozens of bridges and DEX aggregators across 50+ chains **(including Somnia's official partners Relay and Stargate)** normalises token standards, picks the optimal route, and monitors execution for you.

In other words, integrating LI.FI doesn't replace Relay or Stargate, it routes *through* them (and many others) and chooses the best option per transfer. A live USDC transfer from Arbitrum to Somnia, for example, is currently routed via Stargate under the hood.

LI.FI is **infrastructure.** It does the routing and execution, while *you* own the user experience. (The same way [Jumper Exchange](https://jumper.exchange/) is LI.FI's own front-end, your dApp becomes its own bridge/swap interface.)

You'll learn how to:

* [Add cross-chain and same-chain swaps directly inside your Somnia dApp with the **LI.FI SDK**](/developer/building-dapps/cross-chain-swaps-and-bridging/integrating-the-li.fi-sdk)
* Let users **bridge assets to and from Somnia** in your own UI
* Request **routes and quotes**, execute them, and **track transaction status** in-app
* Query supported **chains, tokens, bridges, and exchanges** programmatically

***

## Choosing an integration path

LI.FI offers several integration surfaces. This section focuses on the **SDK**, which gives you full programmatic control over the route lifecycle.

| Path                                                                                    | Best for                                                               | Effort |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------ |
| **SDK** (`@lifi/sdk`)                                                                   | Custom swap/bridge flows where you build the UI and control every step | Medium |
| Widget (`@lifi/widget`)                                                                 | Dropping a ready-made, themeable swap/bridge UI into a React app       | Low    |
| REST API (`li.quest/v1`)                                                                | Backend services, bots, and AI agents that construct requests directly | Medium |
| Bridge UIs ([Relay](https://relay.link/bridge) / [Stargate](https://stargate.finance/)) | End users moving funds manually — no integration needed                | None   |

> Whichever path you choose, LI.FI's official documentation is the source of truth for the latest implementation details: [https://docs.li.fi](https://docs.li.fi/).

{% hint style="info" %}
**Somnia at a glance** — when configuring LI.FI for Somnia, use Chain ID **`5031`**, native currency **SOMI** (18 decimals), RPC `https://api.infra.mainnet.somnia.network/`. Always confirm live route and token support with the LI.FI API before going to production (see the SDK guide).
{% endhint %}


# Integrating the LI.FI SDK

The [**LI.FI**](https://li.fi/) **SDK** lets you add cross-chain and same-chain swaps and bridging directly inside your Somnia dApp. Instead of redirecting users to an external bridge, you request a route, execute it, and track its status. All from your own interface.

Under the hood, LI.FI aggregates dozens of bridges and DEX aggregators across 50+ chains, normalises token standards, and picks the optimal route. Your dApp only talks to one SDK.

In this guide, you'll learn how to:

* Install and configure the LI.FI SDK for Somnia
* Request a quote to **bridge assets into Somnia**
* Execute the route and **track its status** in your app
* Query supported **chains, tokens, and tools** to keep your UI accurate

{% hint style="info" %}
The LI.FI SDK is a JavaScript/TypeScript toolkit that runs in both front-end and back-end environments. This guide shows the front-end (browser wallet) flow. For the full API surface and the latest details, always refer to the official docs: <https://docs.li.fi/sdk/overview>.
{% endhint %}

## Prerequisites

Before you start, ensure you have:

* A working JavaScript/TypeScript dApp (React or Next.js recommended). This guide assumes familiarity with both.
* Wallet connection already set up. If not, see [Wallet Integration and Auth](/developer/building-dapps/wallet-integration-and-auth).
* [Node.js](https://nodejs.org/) 18+ and a package manager (npm, pnpm, or yarn).
* The Somnia network details on hand:

| Property        | Value                                       |
| --------------- | ------------------------------------------- |
| Chain ID        | `5031`                                      |
| Native currency | SOMI (18 decimals)                          |
| RPC URL         | `https://api.infra.mainnet.somnia.network/` |
| Explorer        | `https://explorer.somnia.network`           |

## Step 1: Install the SDK

The SDK uses [Viem](https://viem.sh/) under the hood, so install it alongside.

```bash
npm install @lifi/sdk viem
```

{% hint style="info" %}
This guide was written and verified against `@lifi/sdk` v3.16.x and `viem` v2.x. API surfaces evolve — if a signature differs in your version, the [official SDK docs](https://docs.li.fi/sdk/overview) are the source of truth.
{% endhint %}

## Step 2: Configure the SDK

Call `createConfig` once when your app boots. The `integrator` string identifies your app to LI.FI (and is used for fee monetisation later).

Because Somnia may not ship in Viem's default chain list, define it explicitly with `defineChain` so the SDK's wallet provider can switch to it.

{% code title="lib/lifi.ts" %}

```typescript
import { createConfig, EVM, ChainId } from '@lifi/sdk';
import { createWalletClient, custom, defineChain } from 'viem';
import { mainnet, arbitrum, optimism, base } from 'viem/chains';
import { switchChain } from 'viem/actions';

// Somnia may be newer than the ChainId enum in your installed SDK version,
// so cast the numeric id where the SDK's TypeScript types expect a ChainId.
const SOMNIA = 5031 as ChainId;

// Define Somnia Mainnet for Viem
export const somnia = defineChain({
  id: 5031,
  name: 'Somnia',
  nativeCurrency: { name: 'Somnia', symbol: 'SOMI', decimals: 18 },
  rpcUrls: {
    default: { http: ['https://api.infra.mainnet.somnia.network/'] },
  },
  blockExplorers: {
    default: { name: 'Somnia Explorer', url: 'https://explorer.somnia.network' },
  },
});

// Chains your dApp will let users move assets between
const chains = [mainnet, arbitrum, optimism, base, somnia];

// A Viem wallet client backed by the user's injected wallet (e.g. MetaMask)
const walletClient = createWalletClient({
  chain: mainnet,
  transport: custom(window.ethereum!),
});

createConfig({
  integrator: 'YourSomniaDApp', // required; max 23 chars (letters, numbers, . - _)
  rpcUrls: {
    [SOMNIA]: ['https://api.infra.mainnet.somnia.network/'], // let the SDK use Somnia's RPC
  },
  providers: [
    EVM({
      getWalletClient: async () => walletClient,
      switchChain: async (chainId) => {
        await switchChain(walletClient, { id: chainId });
        return walletClient;
      },
    }),
  ],
});
```

{% endcode %}

{% hint style="success" %}
The SDK automatically fetches chain, bridge, and token metadata from LI.FI at runtime — you don't have to hard-code bridge addresses. The `defineChain` block is only needed so the wallet can **switch networks** during execution, and `rpcUrls` lets the SDK use your preferred Somnia endpoint. For production, pass an authenticated RPC URL rather than a public one.
{% endhint %}

{% hint style="warning" %}
**TypeScript note:** the SDK's types use a `ChainId` enum for parameters like `rpcUrls` keys and `getTokens({ chains })`. If your installed SDK version predates Somnia's addition to that enum, the raw number `5031` will fail to type-check — cast it with `5031 as ChainId` (shown above). The runtime value is unaffected; this is purely a type-level workaround. `getQuote`'s `fromChain` / `toChain` accept a plain `number` and need no cast.
{% endhint %}

## Step 3: Request a quote

A **quote** is a single, ready-to-execute route with the optimal pricing LI.FI could find. The example below bridges **USDC on Arbitrum → USDC.e on Somnia** (routed via Stargate at the time of writing).

Amounts are always passed as strings in the token's **smallest unit** (USDC has 6 decimals, so `10` USDC = `"10000000"`). A chain's native token is represented by the zero address.

{% code title="Bridge USDC (Arbitrum) → USDC.e (Somnia)" %}

```typescript
import { getQuote } from '@lifi/sdk';

const quote = await getQuote({
  fromChain: 42161, // Arbitrum
  toChain: 5031,    // Somnia
  fromToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', // USDC on Arbitrum
  toToken: '0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00',   // USDC.e (bridged USDC) on Somnia
  fromAmount: '10000000', // 10 USDC (6 decimals)
  fromAddress: userWalletAddress, // the connected wallet
});

console.log(quote.estimate.toAmount); // estimated USDC.e received (6 decimals)
```

{% endcode %}

### Tokens routable to Somnia

These are the tokens LI.FI can currently route to Somnia (Chain ID `5031`):

| Symbol | Address                                      | Decimals | Notes                   |
| ------ | -------------------------------------------- | -------- | ----------------------- |
| SOMI   | `0x0000000000000000000000000000000000000000` | 18       | Native gas token        |
| WSOMI  | `0x046EDe9564A72571df6F5e44d0405360c0f4dCab` | 18       | Wrapped SOMI            |
| USDC.e | `0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00` | 6        | Bridged USDC (Stargate) |
| USDT   | `0x67B302E35Aef5EEE8c32D934F5856869EF428330` | 6        | Bridged USDT (Stargate) |
| WETH   | `0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8` | 18       | Wrapped ETH             |

{% hint style="warning" %}
This list reflects LI.FI's coverage at the time of writing and grows over time. Always fetch the current set with `getTokens` at runtime rather than hard-coding it (see [Step 6](#step-6-keep-your-ui-in-sync)).
{% endhint %}

## Step 4: Execute the route

A quote must first be converted to a **route** with `convertQuoteToRoute`, then passed to `executeRoute`. The SDK handles token approvals, network switching, and signing through the wallet provider you configured in Step 2. Use `updateRouteHook` to react to progress in your UI.

```typescript
import { convertQuoteToRoute, executeRoute } from '@lifi/sdk';

// A quote cannot be executed directly — convert it to a route first
const route = convertQuoteToRoute(quote);

const executedRoute = await executeRoute(route, {
  updateRouteHook: (updatedRoute) => {
    // Called whenever the route changes during execution — drive your progress UI from here
    updatedRoute.steps.forEach((step, i) => {
      step.execution?.process.forEach((process) => {
        console.log(`Step ${i + 1}:`, process.type, process.status);
        if (process.txHash) console.log('  tx hash:', process.txHash);
      });
    });
  },
});
```

{% hint style="info" %}
`executeRoute` also accepts other hooks such as `acceptExchangeRateUpdateHook` (confirm a changed rate before continuing) and `switchChainHook`. See the official [Execute Routes](https://docs.li.fi/sdk/execute-routes) guide for the full list.
{% endhint %}

## Step 5: Handle the outcome

Status updates arrive through the `updateRouteHook` from Step 4, read each step's `execution.status` and `execution.process` to drive your UI. Cross-chain transfers don't always end in an exact 1:1 result, so handle the final states explicitly:

| Final state | Meaning                                                        | What to show the user                       |
| ----------- | -------------------------------------------------------------- | ------------------------------------------- |
| Completed   | Success, exact destination token received                      | "Funds arrived on Somnia"                   |
| Partial     | Success, but a different token was received (still full value) | "Completed, received an alternate token"    |
| Refunded    | The transfer failed and funds were returned                    | "Refunded on the source chain"              |
| Failed      | Execution failed                                               | Prompt the user to retry or contact support |

{% hint style="info" %}
A **same-chain** swap is atomic: it either fully succeeds or reverts. A **cross-chain** transfer can have the bridge succeed while the destination step fails, in that case the bridged asset is refunded on the destination chain.
{% endhint %}

To resume tracking an interrupted transfer (for example, after a page reload), the SDK can pick up an in-progress route again. See the official [Execute Routes](https://docs.li.fi/sdk/execute-routes) guide. For server-side or out-of-band status checks, query the REST [status endpoint](https://docs.li.fi/api-reference/check-the-status-of-a-cross-chain-transfer) directly.

## Step 6: Keep your UI in sync

Use the SDK's query helpers to populate dropdowns and **verify that Somnia routes and tokens are live** before showing them to users.

```typescript
import { ChainId, ChainType, getChains, getTokens, getTools } from '@lifi/sdk';

// Confirm Somnia (5031) is currently supported
const chains = await getChains({ chainTypes: [ChainType.EVM] });
const somniaSupported = chains.some((c) => c.id === 5031);

// Tokens available on Somnia — getTokens returns a map keyed by chain ID
// (cast 5031 to ChainId if your SDK version predates Somnia in the enum)
const { tokens } = await getTokens({ chains: [5031 as ChainId] });
const somniaTokens = tokens[5031] ?? [];

// Available bridges and exchanges
const { bridges, exchanges } = await getTools();
```

{% hint style="warning" %}
Somnia support on LI.FI is live and expanding. Treat `getChains` and `getTokens` as the source of truth at runtime rather than hard-coding lists, new tokens and bridges are added regularly.
{% endhint %}

## Monetization (optional)

Integrators can charge a fee on transactions routed through their app. Configure a fee when requesting a route and withdraw collected fees via LI.FI's Partner Portal.

See the official guide for the current setup: <https://docs.li.fi/sdk/monetize-sdk>.

## Troubleshooting

* **No route found:** Try a different token pair or a larger amount. Very small amounts may not cover bridge/gas costs.
* **Slippage / minimum-received error:** Increase the `slippage` parameter (default `0.005` = 0.5%) when requesting the quote.
* **Rate limited (`429`):** Without an API key you're limited to 200 requests / 2 hours. [Request a key](https://li.fi/) for production and back off with retries.
* **Insufficient balance:** Ensure the user holds enough of the source token **plus** native gas on the source chain.
* **Network switch fails:** Confirm Somnia (`5031`) is added to the user's wallet. See [Connect Your Wallet to Mainnet](/get-started/connect-your-wallet-to-mainnet).

## Need Help?

* LI.FI SDK docs (source of truth): <https://docs.li.fi/sdk/overview>
* Somnia Discord: <https://discord.gg/somnia>
* Somnia Developer Telegram: <https://t.me/+s_oRMnGpOyQ3ODQ0>

## Disclaimers

* LI.FI is a third-party provider. Always integrate from the official packages (`@lifi/sdk`) and verify chain/token data against the live API.
* Bridging and swapping involve smart-contract and market risk. Test thoroughly on a small amount before enabling production flows.


# Account Abstraction

Account Abstraction (AA) revolutionizes how users interact with blockchain applications by making wallets **smarter, simpler, and more programmable**.

In this section, you’ll explore how to implement **Smart Contract Accounts (SCAs)** on Somnia using modern tooling like **Thirdweb** and **Privy**, and learn how to enable **gasless transactions** and **session keys** for better UX.

You’ll learn how to:

* Create and manage smart contract wallets
* Implement user operations via ERC-4337-style flows
* Enable sponsored and gasless transactions
* Simplify onboarding through smart wallets and relayers

> Account Abstraction bridges the gap between Web2 simplicity and Web3 ownership — empowering developers to build dApps users actually love to use.


# Gasless Transactions with Thirdweb

This tutorial demonstrates how to build a gasless NFT minting application on Somnia using Thirdweb's Account Abstraction infrastructure. Users can mint NFTs without holding STT tokens by using **Smart Accounts** with **sponsored transactions**.

## Prerequisites

* Basic knowledge of React and Next.js
* A Thirdweb account and API key
* A deployed ERC721 NFT Smart Contract on Somnia

### What You'll Build

A web application where users can:

* Connect using email or social accounts (via in-app wallets)
* Mint NFTs without paying gas fees
* View their NFT balance
* Experience seamless Web3 interactions

## Create a Next.js Project

```bash
npx create-next-app@latest somnia-gasless-nft --typescript --tailwind --app
cd somnia-gasless-nft
```

## Install Thirdweb SDK

```bash
npm install thirdweb
```

## Set Up Environment Variables

Create a .env.local file in your project root:

```bash
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id_here
```

Get your Client ID from the[ Thirdweb Dashboard](https://thirdweb.com/dashboard/settings).

## Deploy a Simple NFT Contract

If you haven't already, deploy this simple ERC721 contract on Somnia.

<details>

<summary>SimpleNFT.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";

contract SimpleNFT is ERC721 {
    uint256 private _tokenIdCounter;
    
    constructor() ERC721("SimpleNFT", "SNFT") {}
    
    function mint(address to) public {
        uint256 tokenId = _tokenIdCounter;
        _tokenIdCounter++;
        _safeMint(to, tokenId);
    }
    
    function totalSupply() public view returns (uint256) {
        return _tokenIdCounter;
    }
}
```

</details>

## Create Constants Configuration

Now we need to set up our configuration file that will handle all the blockchain connections and smart account setup. This file will:

* Initialize the Thirdweb client with your API key
* Set up both traditional wallet and in-app wallet options
* Define the smart account infrastructure

Create a new folder called constants in your project root, then create `constants/index.ts.`

<details>

<summary>constants</summary>

```typescript
import { createThirdwebClient, getContract } from 'thirdweb';
import { SmartWalletOptions, inAppWallet } from 'thirdweb/wallets';
import { somniaTestnet } from 'thirdweb/chains';

// Validate environment variables
const clientId = process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID;

if (!clientId) {
  throw new Error('No client ID provided');
}

// Initialize Thirdweb client
export const client = createThirdwebClient({
  clientId: clientId,
});

// Use Somnia testnet
export const chain = somniaTestnet;

// Your deployed NFT contract address
export const nftContractAddress = '0x...'; // UPDATE with your contract address

// Get contract instance
export const nftContract = getContract({
  address: nftContractAddress,
  chain,
  client,
});

// Account Abstraction configuration for standard wallet connection
export const accountAbstraction: SmartWalletOptions = {
  chain,
  sponsorGas: true, // Enable gasless transactions
};

// Smart Account infrastructure addresses
const FACTORY_ADDRESS = '0x4be0ddfebca9a5a4a617dee4dece99e7c862dceb'; // Thirdweb Account Factory

// In-app wallet configuration with smart accounts
export const wallets = [
  inAppWallet({
    smartAccount: {
      chain: somniaTestnet,
      sponsorGas: true,
      factoryAddress: FACTORY_ADDRESS,
    },
  }),
];
```

</details>

## Create the Main Minting Page

This is the main component where users will interact with your NFT minting application. We'll break this into three parts: imports and setup, the component logic, and the UI rendering.

### Imports and Component Setup

First, let's set up our imports and initialize the component. Create `app/page.tsx`.

<details>

<summary>page.tsx</summary>

```typescript
'use client';
import { useState } from 'react';
import { balanceOf, totalSupply } from 'thirdweb/extensions/erc721';
import {
  ConnectButton,
  TransactionButton,
  useActiveAccount,
  useReadContract,
} from 'thirdweb/react';
import { prepareContractCall } from 'thirdweb';
import {
  accountAbstraction,
  client,
  nftContract,
  wallets,
} from '../constants';
import Link from 'next/link';
```

</details>

These imports provide:

* `ERC721 Extensions`: Functions to read NFT data (balance and total supply)
* `React Components`: Pre-built UI components for wallet connection and transactions
* `Hooks`: To access the connected account and read blockchain data
* All our constants from the previous step

### Component Logic and Data Fetching

Now let's add the component logic that handles wallet connections and reads blockchain data:

<details>

<summary>page.tsx</summary>

```typescript
export default function Home() {
  // Get the currently connected account (either smart account or regular wallet)
  const account = useActiveAccount();
  
  // State for showing transaction progress
  const [txStatus, setTxStatus] = useState<string>('');

  // Read the total number of NFTs minted from the contract
  const { data: totalMinted } = useReadContract(totalSupply, {
    contract: nftContract,
  });

  // Read how many NFTs the connected user owns
  const { data: userBalance } = useReadContract(balanceOf, {
    contract: nftContract,
    owner: account?.address!,
    queryOptions: { enabled: !!account }, // Only fetch when account is connected
  });
```

</details>

* `useActiveAccount()`: Gets the connected wallet/smart account
* `useState`: Manages transaction status messages
* `useReadContract`: Automatically fetches and updates blockchain data

The queries will re-fetch automatically when accounts change or transactions complete

## User Interface

Now let's build the complete UI that users will interact with

<details>

<summary>page.tsx</summary>

```typescript
'use client';
import { useState } from 'react';
import { balanceOf, totalSupply } from 'thirdweb/extensions/erc721';
import {
  ConnectButton,
  TransactionButton,
  useActiveAccount,
  useReadContract,
} from 'thirdweb/react';
import { prepareContractCall } from 'thirdweb';
import {
  accountAbstraction,
  client,
  nftContract,
  wallets,
} from '../../constants';
import Link from 'next/link';

const GaslessHome: React.FC = () => {
  const account = useActiveAccount();
  const [txStatus, setTxStatus] = useState<string>('');

  // Get total supply
  const { data: totalMinted } = useReadContract(totalSupply, {
    contract: nftContract,
  });

  // Get user's balance
  const { data: userBalance } = useReadContract(balanceOf, {
    contract: nftContract,
    owner: account?.address!,
    queryOptions: { enabled: !!account },
  });

  return (
    <div className='flex flex-col items-center min-h-screen p-8'>
      {/* Main Title */}
      <h1 className='text-2xl md:text-6xl font-semibold md:font-bold tracking-tighter mb-12 text-zinc-100'>
        Gasless NFT Minting on Somnia
      </h1>

      {/* Wallet Connection Button */}
      <ConnectButton
        client={client}
        wallets={wallets}                    // Enables in-app wallet options
        accountAbstraction={accountAbstraction} // Enables smart accounts for regular wallets
        connectModal={{
          size: 'wide',
          title: 'Choose Your Login Method',
          welcomeScreen: {
            title: 'Gasless NFT Minting',
            subtitle: 'Sign in to mint NFTs without gas fees',
          },
        }}
        appMetadata={{
          name: 'Somnia NFT Minter',
          url: 'https://somnia.network',
        }}
      />

      {/* NFT Display and Minting Section */}
      <div className='flex flex-col mt-8 items-center'>
        {/* Stats Card */}
        <div className='mb-8 p-8 bg-zinc-900 rounded-2xl shadow-xl'>
          <div className='text-center mb-6'>
            <p className='text-3xl font-bold text-white mb-2'>
              {totalMinted?.toString() || '0'}
            </p>
            <p className='text-sm text-zinc-400'>Total NFTs Minted</p>
          </div>

          {/* NFT Visual Representation */}
          <div className='flex items-center justify-center'>
            <div className='w-48 h-48 bg-gradient-to-br from-purple-600 to-blue-600 rounded-xl flex items-center justify-center shadow-lg'>
              <div className='text-white text-center'>
                <p className='text-6xl font-bold mb-2'>NFT</p>
                <p className='text-sm opacity-80'>SimpleNFT on Somnia</p>
              </div>
            </div>
          </div>
        </div>

        {/* Conditional Rendering: Connected vs Not Connected */}
        {account ? (
          <div className='flex flex-col items-center gap-4'>
            {/* User Stats */}
            <div className='text-center'>
              <p className='font-semibold text-lg'>
                You own{' '}
                <span className='text-green-400'>
                  {userBalance?.toString() || '0'}
                </span>{' '}
                NFTs
              </p>
              <p className='text-sm text-zinc-400 mt-1'>
                Wallet: {account.address.slice(0, 6)}...{account.address.slice(-4)}
              </p>
            </div>

            {/* Transaction Status */}
            {txStatus && (
              <p className='text-sm text-yellow-400 mb-2'>{txStatus}</p>
            )}

            {/* Mint Button */}
            <TransactionButton
              transaction={() =>
                prepareContractCall({
                  contract: nftContract,
                  method: 'function mint(address to)',
                  params: [account.address],
                })
              }
              onError={(error) => {
                console.error('Transaction error:', error);
                setTxStatus('');
                
                // User-friendly error messages
                let errorMessage = 'Transaction failed';
                
                if (error.message?.includes('insufficient funds')) {
                  errorMessage = 'Insufficient funds for gas';
                } else if (error.message?.includes('rejected')) {
                  errorMessage = 'Transaction rejected by user';
                } else if (error.message?.includes('500')) {
                  errorMessage = 'Service temporarily unavailable';
                }
                
                alert(`Error: ${errorMessage}`);
              }}
              onTransactionSent={(result) => {
                console.log('Transaction sent:', result.transactionHash);
                setTxStatus('Transaction submitted! Waiting for confirmation...');
              }}
              onTransactionConfirmed={async (receipt) => {
                console.log('Transaction confirmed:', receipt);
                setTxStatus('');
                alert('NFT minted successfully!');
              }}
              className='px-8 py-4 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-700 hover:to-blue-700 rounded-lg font-semibold transition-all transform hover:scale-105 shadow-lg'
            >
              Mint NFT (Gasless)
            </TransactionButton>
          </div>
        ) : (
          {/* Not Connected State */}
          <p className='text-center w-full mt-10 text-zinc-400'>
            Connect your wallet to mint NFTs without gas fees!
          </p>
        )}
      </div>

      {/* Navigation - Removed since this is now the home page */}
    </div>
  );
};
```

</details>

### Understanding the UI Components

`ConnectButton`: This component handles both connection methods:

* `In-App Wallets`: Users can sign in with email, Google, Apple, etc.
* `Traditional Wallets`: Users can connect MetaMask, WalletConnect, etc.

Both methods automatically create smart accounts for gasless transactions

`TransactionButton`: This component prepares and sends the transaction automatically. Handling all wallet interactions and confirmations, and provides callbacks for different transaction states. It also shows loading states automatically

Error Handling: The component provides user-friendly error messages instead of technical blockchain errors, improving the user experience.

## Update the Layout

The layout file wraps your entire application and is where we set up the Thirdweb provider. This provider is essential as it:

* Manages wallet connections across your app
* Handles blockchain interactions
* Provides React hooks for reading blockchain data
* Manages transaction states

Update `app/layout.tsx`:

<details>

<summary>layout.tsx</summary>

```typescript
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { ThirdwebProvider } from "thirdweb/react";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Gasless NFT Minting on Somnia",
  description: "Mint NFTs without gas fees using Account Abstraction",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={inter.className}>
        <ThirdwebProvider>
          <main className="min-h-screen bg-zinc-950 text-white">
            {children}
          </main>
        </ThirdwebProvider>
      </body>
    </html>
  );
}
```

</details>

## Conclusion

You've successfully built a gasless NFT minting application on Somnia! This implementation leverages Thirdweb's Account Abstraction to provide a seamless Web3 experience where users can interact with blockchain without holding native tokens.

For more advanced features and updates on Somnia support, check:

* [Thirdweb Account Abstraction Guide](https://portal.thirdweb.com/connect/account-abstraction)

### Resources

* [Live Demo](https://nextjs-thirdweb-somnia.vercel.app/)


# Smart Wallet App with Thirdweb

The Somnia mission is to enable the development of mass-consumer real-time applications. The Somnia Network allows developers to build a unique experience by implementing Smart Contract Wallets with gasless transactions via Account Abstraction (ERC-4337). In this tutorial, we'll use the [Thirdweb React SDK](https://portal.thirdweb.com/react/v5) to:

* Connect Smart Wallets (Account Abstraction)
* Read Wallet Balance
* Send STT Tokens

## Pre-requisites

Before we start, ensure you have:

* Basic knowledge of React
* A [Thirdweb](https://thirdweb.com/) account & Client ID
* Node.js & npm installed

## Install Dependencies

Run the following command to set up your project:

```bash
npx create-next-app@latest somnia-thirdweb
cd somnia-thirdweb
npm install thirdweb ethers viem dotenv
```

This installs:

`thirdweb` → The Thirdweb React SDK.

`ethers` → To interact with blockchain transactions.

`dotenv` → To securely store API keys.

## Create the Thirdweb Client

The Thirdweb client allows the app to communicate with the blockchain. Create a **`client.ts`** file and add:

```
import { createThirdwebClient } from "thirdweb";
export const client = createThirdwebClient({
  clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, // Replace with your actual Client ID
});
```

{% hint style="info" %}
Get your Client ID: Register at thirdweb.com/dashboard. 💡
{% endhint %}

## Add Environment Variables

Store API keys in a .env.local file:

```
NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your-client-id-here
NEXT_PUBLIC_SOMNIA_RPC_URL=https://dream-rpc.somnia.network/
```

Restart Next.js after modifying .env.local

## Build the Account Abstraction App

To ensure Thirdweb Components are available throughout the app, wrap the children's components inside ThirdwebProvider. Modify **`layout.ts`**:

```typescript
import { ThirdwebProvider } from 'thirdweb/react';

<body>
    <ThirdwebProvider>
        {children}
    </ThirdwebProvider>
 </body>

```

## Create & Connect Smart Contract Wallet

The **`useActiveAccount`** hook allows us to detect the connected Smart Wallet Account. We use **`ConnectButton`** to handle authentication and connection to the blockchain.

```typescript
import { useActiveAccount } from "thirdweb/react";

const smartAccount = useActiveAccount();

<ConnectButton
  client={client}
  appMetadata={{
    name: "Example App",
    url: "https://example.com",
  }}
/>
```

This button will connect the user's Smart Contract Wallet and authenticate the user with Thirdweb. After connection, it will also display the wallet address.

<div align="left"><figure><img src="/files/PghcZYsqSj1FufKnEVdg" alt=""><figcaption><p>click Connect</p></figcaption></figure></div>

<div align="right"><figure><img src="/files/BvOClvE8DKOKtHMFrFzr" alt=""><figcaption><p>Connected Wallet</p></figcaption></figure></div>

To show the connected address, we add the following UI component:

```typescript
{smartAccount ? (
  <div className="mt-6 p-4 bg-white rounded-lg shadow">
    <p className="text-lg font-semibold text-gray-700">
      Connected as: {smartAccount.address}
    </p>
    {message && <p className="mt-2 text-green-600">{message}</p>}
  </div>
) : (
  <p className="text-lg text-red-600 text-center">
    Please connect your wallet.
  </p>
)}
```

## Token Transfer

The **`useSendTransaction`** hook is used to send STT tokens to another address. The function **`sendTokens`** will check that the Smart Account is connected and then send 0.01 STT tokens to a recipient address. First, copy your Smart Wallet Address and request for Tokens on Discord in the **dev-chat,** you can also Transfer some from your EOA.

Log transaction success or failure messages to the console.

```
import { useSendTransaction } from "thirdweb/react";
import { ethers } from "ethers";

const { mutate: sendTransaction, isPending } = useSendTransaction();

const sendTokens = async () => {
  if (!smartAccount) {
    setMessage("No smart account connected.");
    return;
  }
  console.log("Sending 0.01 STT from:", smartAccount.address);
  sendTransaction(
    {
      to: "0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03",
      value: ethers.parseUnits("0.01", 18),
      chain: somniaTestnet,
      client,
    },
    {
      onSuccess: (receipt) => {
        console.log("Transaction Success:", receipt);
        setMessage(`Sent 0.01 STT! TX: ${receipt.transactionHash}`);
      },
      onError: (error) => {
        console.error("Transaction Failed:", error);
        setMessage("Transaction failed! Check console.");
      },
    }
  );
};
```

## Button Integration

The button UI provides a clear interaction for sending STT tokens. The button shows a loading state (Sending...) when the transaction is pending and displays a success or failure message once the transaction is complete.

```typescript
const [message, setMessage] = useState<string>('');

<button
  onClick={sendTokens}
  disabled={isPending}
  className={`mt-4 px-6 py-2 rounded-lg ${
    isPending ? 'bg-gray-400 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 text-white'
  }`}
>
  {isPending ? 'Sending...' : 'Send 0.01 STT'}
</button>
{message && <p className='mt-2 text-green-600'>{message}</p>}
```

<details>

<summary>Complete Code</summary>

```typescript
'use client';

import { useState } from 'react';
import {
  ConnectButton,
  useActiveAccount,
  useSendTransaction,
} from 'thirdweb/react';
import { ethers } from 'ethers';
import { client } from './client';
import { somniaTestnet } from 'viem/chain';

export default function Home() {
  const [message, setMessage] = useState<string>('');
  const smartAccount = useActiveAccount(); // Get connected account
  const { mutate: sendTransaction, isPending } = useSendTransaction();

  const sendTokens = async () => {
    if (!smartAccount) {
      setMessage('No smart account connected.');
      return;
    }

    console.log('🚀 Sending 0.01 STT from:', smartAccount.address);

    sendTransaction(
      {
        to: '0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03', // Replace
        value: ethers.parseUnits('0.01', 18),
        chain: somniaTestnet,
        client,
      },
      {
        onSuccess: (receipt) => {
          console.log('Transaction Success:', receipt);
          setMessage(`Sent 0.01 STT! TX: ${receipt.transactionHash}`);
        },
        onError: (error) => {
          console.error('Transaction Failed:', error);
          setMessage('Transaction failed! Check console.');
        },
      }
    );
  };

  return (
    <main className='p-4 pb-10 min-h-[100vh] flex items-center justify-center container max-w-screen-lg mx-auto'>
      <div className='py-20'>
        <div className='flex justify-center mb-10'>
          <ConnectButton
            client={client}
            appMetadata={{
              name: 'Example App',
              url: 'https://example.com',
            }}
          />
        </div>

        {smartAccount ? (
          <div className='mt-6 p-4 bg-white rounded-lg shadow'>
            <p className='text-lg font-semibold text-gray-700'>
              Connected as: {smartAccount.address}
            </p>
            <button
              onClick={sendTokens}
              disabled={isPending}
              className={`mt-4 px-6 py-2 rounded-lg ${
                isPending
                  ? 'bg-gray-400 cursor-not-allowed'
                  : 'bg-blue-600 hover:bg-blue-700 text-white'
              }`}
            >
              {isPending ? 'Sending...' : 'Send 0.01 STT'}
            </button>
            {message && <p className='mt-2 text-green-600'>{message}</p>}
          </div>
        ) : (
          <p className='text-lg text-white-600 text-center'>
            Please connect your wallet.
          </p>
        )}
      </div>
    </main>
  );
}
```

</details>

The full implementation includes wallet connection, balance retrieval, and token transfer.

<figure><img src="/files/O4AggPLYkpfZ7LS27qZd" alt=""><figcaption><p>Smart Contract Wallet</p></figcaption></figure>

## Conclusion

Congratulations! 🎉 You have successfully connected a smart contract wallet using Thirdweb. Read wallet balances and Transferred STT tokens on Somnia Testnet. You can explore additional features such as gasless transactions, NFT integration, and DeFi applications.

<br>


# Data Indexing and Querying

Subgraphs allow your dApp to efficiently index and query onchain data from the Somnia Network. Whether you're building dashboards, tracking events, or querying user actions, Subgraphs provide a scalable way to make your smart contract data accessible and searchable.

In this section, you’ll learn:

* [How to create and deploy subgraphs on Somnia using **ORMI**](/developer/building-dapps/data-indexing-and-querying/ormi-subgraph)
* [How to build with **Protofire’s subgraph infrastructure**](/developer/building-dapps/data-indexing-and-querying/protofire-subgraph)
* [Frontend integrations for building responsive UIs from your subgraph data](/developer/building-dapps/data-indexing-and-querying/building-subgraph-uis-nextjs-fetch)

> Subgraphs are essential for any developer building real-time dApps, analytics dashboards, or event-driven logic on Somnia.


# Ormi Subgraph

The Graph is a decentralized indexing protocol that allows developers to query blockchain data using GraphQL. Instead of parsing complex raw logs and events directly from smart contracts, developers can build subgraphs that transform onchain activity into structured, queryable datasets.

This tutorial demonstrates how to deploy a subgraph on the Somnia Testnet using [Ormi](https://ormilabs.com), a powerful gateway that simplifies subgraph deployment through a hosted Graph Node and IPFS infrastructure.

{% embed url="<https://www.youtube.com/watch?v=yYKbvJAEX80>" %}

## Prerequisites

* GraphQL is installed and set up on your local machine.
* A verified smart contract address deployed on Somnia.
* An Ormi account and Private Key.

## Install Graph CLI globally

```bash
npm install -g @graphprotocol/graph-cli
```

## Initialize Your Subgraph

The Graph services rely on the existence of a deployed Smart Contract with onchain activity. The subgraph will be created based on indexing the events emitted from the Smart Contract. To set up a subgraph service for an example contract called **`MyToken`** run the following command to scaffold a new subgraph project:

```bash
graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken
```

**`mytoken`** is the folder that contains the subgraph files. Replace `0xYourTokenAddress` with your actual deployed Smart Contract address on Somnia.

This command will generate the following files:

* `subgraph.yaml`Defines the data sources and events to index
* `schema.graphql` Structure of your data
* `src/mytoken.ts`TypeScript logic to handle events

## Define the Subgraph Schema <a href="#define-the-subgraph-schema" id="define-the-subgraph-schema"></a>

For the example`MyToken` Contract, which is an ERC20 Token, Edit `schema.graphql` to index all the Transfer events emitted from the Smart Contract.

```typescript
type Transfer @entity(immutable: true) {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}
```

## Build the Subgraph

After customizing your schema and mapping logic, build the subgraph by running the command:

```
graph codegen && graph build
```

This will generate the necessary `artifacts` for deployment.

## Deploy Using Ormi

Open the Somnia Ormi website <https://subgraph.somnia.network/> and create an account.

<figure><img src="/files/UstnN1TYkxi7Hx7OgQ2Y" alt=""><figcaption></figcaption></figure>

On the left navigation menu, click the "key" icon to access your `privateKey.`

<figure><img src="/files/nBXgxDlYoeu3CXhrXMza" alt=""><figcaption></figcaption></figure>

Deploy your subgraph to Ormi’s hosted infrastructure with the following command:

```bash
graph deploy mytoken --node https://api.subgraph.somnia.network/deploy --ipfs https://api.subgraph.somnia.network/ipfs --deploy-key yourORMIPrivateKey
```

Replace yourPrivateKey with your Somnia Ormi account private key.

Once deployed, Ormi will return a GraphQL endpoint where you can begin querying your subgraph.

Return to the dashboard to find your list of deployed subgraphs.

<figure><img src="/files/2IvGhTlhLq9iVhbuoO55" alt=""><figcaption></figcaption></figure>

Open the deployed subgraph in the explorer to interact with it:

<figure><img src="/files/IQovfUqg7AQDkhjamnln" alt=""><figcaption></figcaption></figure>

## Conclusion

You have successfully deployed a subgraph to index events emitted from your Smart Contract. To challenge yourself even further, you can extend your build:

* Expand your schema and mapping logic to cover more events.
* Connect your subgraph to a frontend UI or analytics dashboard.<br>

For more information, visit the Ormi [docs](https://docs.ormilabs.com/dedicated-env/somnia/subgraphs/overview).


# Protofire Subgraph

The blockchain is an ever-growing database of transactions and Smart Contract events. Developers use subgraphs, an indexing solution provided by the Graph protocol, to retrieve and analyze this data efficiently.

A graph in this context represents the structure of blockchain data, including token transfers, contract events, and user interactions. A subgraph is a customized indexing service that listens to blockchain transactions and structures them in a way that can be easily queried using GraphQL.

{% embed url="<https://www.youtube.com/watch?v=Dbk7KHSyt_I>" %}

## Prerequisites

* This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
* GraphQL is installed and set up on your local machine.

```bash
npm install -g @graphprotocol/graph-cli
```

## Deploy a Simple ERC20 Token on Somnia

We will deploy a basic ERC20 token on the Somnia network using Hardhat. Ensure you have Hardhat, OpenZeppelin, and dotenv installed:

```bash
npm install --save-dev hardhat @nomicfoundation/hardhat-ignition-ethers @openzeppelin/contracts dotenv ethers
```

## Create an ERC20 Token Contract

Create a new Solidity file: `contracts/MyToken.sol` and update it

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10**decimals());
    }
function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}
```

## Create a Deployment Script

Create a new file in `ignition/modules/MyTokenModule.ts`

```typescript
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

export default buildModule("MyTokenModule", (m) => {
    const initialSupply = m.getParameter("initialSupply", 1000000n * 10n ** 18n);
    
    const myToken = m.contract("MyToken", [initialSupply]);
    return { myToken };
});
```

## Deploy the Smart Contract

Open the hardhat.config.js file and update the network information by adding Somnia Network to the list of networks. Copy your Wallet Address Private Key from MetaMask, and add it to the accounts section. Ensure there are enough STT Token in the Wallet Address to pay for Gas. You can get some from the Somnia Faucet.<br>

```javascript
module.exports = {
  // ...
  networks: {
    somniaTestnet: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};

```

Open a new terminal and deploy the smart contract to the Somnia Network. Run the command:

```bash
npx hardhat ignition deploy ./ignition/modules/MyTokenModule.ts --network somniaTestnet

```

This will deploy the ERC20 contract to the Somnia network and return the deployed contract address.

## Simulate On-Chain Activity

Once deployed, we will create a script to generate multiple transactions on the blockchain.

Create a new file `scripts/interact.js`

<details>

<summary><code>interact.js</code></summary>

```javascript
require("dotenv").config();
const { ethers } = require("hardhat");

async function main() {
  // Connect to Somnia RPC
  const provider = new ethers.JsonRpcProvider(process.env.SOMNIA_RPC_URL);

  // Load wallets from .env
  const deployer = new ethers.Wallet(process.env.PRIVATE_KEY_1, provider);
  const user1 = new ethers.Wallet(process.env.PRIVATE_KEY_2, provider);
  const user2 = new ethers.Wallet(process.env.PRIVATE_KEY_3, provider);
  const user3 = new ethers.Wallet(process.env.PRIVATE_KEY_4, provider);
  const user4 = new ethers.Wallet(process.env.PRIVATE_KEY_5, provider);

  const contractAddress = "0xBF9516ADc5263d277E2505d4e141F7159B103d33"; // Replace with your deployed contract address
  const abi = [
    "function transfer(address to, uint256 amount) external returns (bool)",
    "function mint(address to, uint256 amount) external",
    "function burn(uint256 amount) external",
  ];

  // Attach to the deployed ERC20 contract
  const token = new ethers.Contract(contractAddress, abi, provider);

  console.log("🏁 Starting Token Transactions Simulation on Somnia...");

  // Simulate Transfers
  const transfers = [
    { from: deployer, to: user1.address, amount: "1000" },
    { from: deployer, to: user2.address, amount: "1000" },
    { from: user1, to: user2.address, amount: "50" },
    { from: user2, to: user3.address, amount: "30" },
    { from: user3, to: user4.address, amount: "10" },
    { from: user4, to: deployer.address, amount: "5" },
    { from: deployer, to: user2.address, amount: "100" },
    { from: user1, to: user3.address, amount: "70" },
    { from: user2, to: user4.address, amount: "40" },
  ];

  for (const tx of transfers) {
    const { from, to, amount } = tx;
    const txResponse = await token.connect(from).transfer(to, ethers.parseUnits(amount, 18));
    await txResponse.wait();
    console.log(`✅ ${from.address} sent ${amount} MTK to ${to}`);
  }

  // Simulate Minting
  const mintAmount1 = ethers.parseUnits("500", 18);
  const mintTx1 = await token.connect(deployer).mint(user1.address, mintAmount1);
  await mintTx1.wait();
  console.log(`✅ Minted ${ethers.formatUnits(mintAmount1, 18)} MTK to User1!`);

  const mintAmount2 = ethers.parseUnits("300", 18);
  const mintTx2 = await token.connect(deployer).mint(user2.address, mintAmount2);
  await mintTx2.wait();
  console.log(`✅ Minted ${ethers.formatUnits(mintAmount2, 18)} MTK to User2!`);

  // Simulate Burning
  const burnAmount1 = ethers.parseUnits("50", 18);
  const burnTx1 = await token.connect(user1).burn(burnAmount1);
  await burnTx1.wait();
  console.log(`🔥 User1 burned ${ethers.formatUnits(burnAmount1, 18)} MTK!`);

  const burnAmount2 = ethers.parseUnits("100", 18);
  const burnTx2 = await token.connect(user2).burn(burnAmount2);
  await burnTx2.wait();
  console.log(`🔥 User2 burned ${ethers.formatUnits(burnAmount2, 18)} MTK!`);

  console.log("🏁 Simulation Complete on Somnia!");
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
```

</details>

Create an `.env` file to hold sensitive informations such as the private keys

```properties
SOMNIA_RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY_1=0x...
PRIVATE_KEY_2=0x...
PRIVATE_KEY_3=0x...
PRIVATE_KEY_4=0x...
PRIVATE_KEY_5=0x...
```

#### Run the Script

```bash
node scripts/interact.js
```

This will generate several on-chain transactions for our subgraph to index.

## Deploy a Subgraph on Somnia

Go to <https://somnia.chain.love/> and connect your Wallet.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcOA43Zg4tecvciwgPQxb-dlcBwhPkTGY0POkIQ8JaUdKcaheg-AwetWTR1yFdYSGw_A2c_fm-xuQba6mVDitI_rD7wuazgEsjBjUjxT8-0ELLl9CW0JFlzNuPLHlZYXKy7VP16_A?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>

First, you need to create a private key for deploying subgraphs. To do so, please go to [Somnia Protofire Service](https://somnia.chain.love) and create an Account.

You are now able to create subgraphs. Click the create button and enter the required details.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdbWSqj7ddwv0yFj8FlHdDGGlJmeqmRakTRUNX2LUCUHlqPjcK2eSBdRwnwwZ5pq8T-3op10MJwdknOn-KfCDbsJ4nuI7Uu39obkcb8gYfVTlRhrt1s4IJ3U7VNgEnMW0mMN7hK6w?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXex3AIr44-pbEiLznhnQE0xhOYHgtOmwRyu8HwXjFWsTQLM9JtyFD10YPZdDxY0qFNVC2JUqt_29q8VtSmV0emS8VVOZrF4VfYUUxmqQM3yF-_c29E9vQaRfvVoV7Guu6AkI8mgUw?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>

After initialising the subgraph on <https://somnia.chain.love/> the next step is to create and deploy the subgraph via the terminal.

## Initialize the Subgraph

```bash
graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken
```

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeFtlgKbMC4sCBfwGl1xI76iHiOxsc7jKZtg9BUeMGiTsormp5HRYnreVlo4cpuZe23eESQT9pgbCdf9dQ4lKw_-fjsBwPLd-DZd-o_Gc3BA9VxjE95i05yuXDacPmNBbwsEvvvbA?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>

Then, update networks.json to use Somnia’s RPC

```bash
{
  "somnia-testnet": {
    "network": "Somnia Testnet",
    "rpc": "https://dream-rpc.somnia.network",
    "startBlock": 12345678
  }
}
```

## Define the Subgraph Schema

📁 Edit schema.graphql

```bash
type Transfer @entity(immutable: true) {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}
```

## Build the Subgraph

```bash
graph codegen
graph build
```

## Deploy the Subgraph

```bash
graph deploy --node https://proxy.somnia.chain.love/graph/somnia-testnet --version-label 0.0.1 somnia-testnet/test-mytoken 
--access-token=your_token_from_somnia_chain_love
```

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfc-Mu-wBk_mvxbW4Rz6ODZK_RtWPtTxDjKcldwtudapplfGjxVMZTGS_xzX73VOp_XcRCiicQsA-q7yXG0x8Tz6vyCeTU1Lw08Kmi8rbZlWapYgskmfWXgHo3t2Rgypy3x3cfd0A?key=ZDOrGANolV8eE-n5bJ-_qrWy" alt=""><figcaption></figcaption></figure>

## Query the Subgraph

Once your subgraph is deployed and indexing blockchain data on Somnia, you can retrieve information using GraphQL queries. These queries allow you to efficiently access structured data such as token transfers, approvals, and contract interactions without having to scan the blockchain manually.

Developers can query indexed blockchain data in real time using the Graph Explorer or a GraphQL client. This enables DApps, analytics dashboards, and automated systems to interact more efficiently with blockchain events.

This section demonstrates how to write and execute GraphQL queries to fetch blockchain data indexed by the subgraph. Go to <https://somnia.chain.love/graph/17>

### Fetch Latest Transfers

```json
{
  transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
    id
    from
    to
    value
    blockTimestamp
    transactionHash
  }
}
```

### Get Transfers by Address

```json
{
  transfers(where: { from: "0xUserWalletAddress" }) {
    id
    from
    to
    value
  }
}
```

### Get Transfers in a Time Range

```
{
  transfers(where: { blockTimestamp_gte: "1700000000", blockTimestamp_lte: "1710000000" }) {
    id
    from
    to
    value
  }
}
```

## Conclusion

This tutorial provides a complete pipeline for indexing blockchain data on Somnia using The Graph! 🔥

<br>


# Building Subgraph UIs (NextJS/Fetch)

[Subgraphs](https://somnia.chain.love) allow developers to efficiently query Somnia blockchain data using GraphQL, making it easy to index and retrieve real-time blockchain activity. In this tutorial, you’ll learn how to:

* Fetch blockchain data from a Subgraph API
* Fix CORS errors using a NextJS API route
* Display token transfers in a real-time UI

By the end of this guide, you'll have a fully functional UI that fetches and displays token transfers from Somnia’s Subgraph API.

## Prerequisites

* Basic knowledge of React & Next.js.
* A deployed Subgraph API on Somnia ([or use an existing one](https://somnia.chain.love/graph)).
* Account on <https://somnia.chain.love> see [guide](/developer/building-dapps/data-indexing-and-querying/protofire-subgraph).

## Create a NextJS Project

Start by creating a new Next.js app.

```bash
npx create-next-app@latest somnia-subgraph-ui
cd somnia-subgraph-ui
```

Then, install required dependencies.

```bash
npm install thirdweb react-query graphql
```

## Define the Subgraph API in Environment Variables

Create a .env.local file in the root folder.

```
NEXT_PUBLIC_SUBGRAPH_URL=https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/test-mytoken
NEXT_PUBLIC_SUBGRAPH_CLIENT_ID=YOUR_CLIENT_ID
```

{% hint style="info" %}
💡 Note: Restart your development server after modifying .env.local:
{% endhint %}

```bash
npm run dev
```

## Create a NextJS API Route for the Subgraph

Since the Somnia Subgraph API has CORS restrictions, we’ll use a NextJS API route to act as a proxy.

Inside the app directory create the folder paths **`api/proxy`** and add a file **`route.ts`** Update the **`route.ts`** file with the following code:

```typescript
import { NextResponse } from "next/server";

const SUBGRAPH_URL = process.env.NEXT_PUBLIC_SUBGRAPH_URL as string;
const CLIENT_ID = process.env.NEXT_PUBLIC_SUBGRAPH_CLIENT_ID as string;

export async function POST(req: Request) {
  try {
    const body = await req.json();


    const response = await fetch(SUBGRAPH_URL, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "Client-ID": CLIENT_ID, // ✅ Pass the Subgraph Client ID
      },
      body: JSON.stringify(body),
    });


    const data = await response.json();
    return NextResponse.json(data);
  } catch (error) {
    console.error("Proxy Error:", error);
    return NextResponse.json({ error: "Failed to fetch from Subgraph" }, { status: 500 });
  }
}
```

This code allows your frontend to make requests without triggering CORS errors.

## Fetch and Display Token Transfers in the UI

Now that we have the API set up, let’s build a React component that:

* Sends the GraphQL query using fetch()
* Stores the fetched data using useState()
* Displays the token transfers in a simple UI

To fetch the latest 10 token transfers, we’ll use this GraphQL query:

```graphql
{
  transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
    id
    from
    to
    value
    blockTimestamp
    transactionHash
  }
}
```

This code fetches the last 10 transfers (first: 10) and orders them by timestamp (latest first). It retrieves wallet addresses (from, to) and the amount transferred (value) and includes the transaction hash (used to generate an explorer link).

## Fetch and Display Data in a React Component

Now, let’s integrate the query into our NextJS frontend. Create a folder **`components`** and add a file **`TokenTransfer.ts`**

We use **`useState()`** to store transfer data and **`useEffect()`** to fetch it when the component loads.

```typescript
"use client";
import { useEffect, useState } from "react";

export default function TokenTransfers() {
  // Store transfers in state
  const [transfers, setTransfers] = useState<any[]>([]);
  
  // Track loading state
  const [loading, setLoading] = useState(true);


Next, we fetch the token transfer data when the component loads.
 useEffect(() => {
    async function fetchTransfers() {
      setLoading(true); // Show loading state


      const query = `
        {
          transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
            id
            from
            to
            value
            blockTimestamp
            transactionHash
          }
        }
      `;


      const response = await fetch("/api/proxy", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ query }),
      });


      const { data } = await response.json();
      setTransfers(data.transfers || []); // Store results in state
      setLoading(false); // Hide loading state
    }


    fetchTransfers();
  }, []);
```

Once data is fetched, we render it inside the UI.

```typescript
return (
    <div className="p-4">
      <h2 className="text-xl font-semibold mb-4">Latest Token Transfers</h2>
      
      {loading ? (
        <p>Loading transfers...</p>
      ) : (
        <ul>
          {transfers.map((transfer) => (
            <li key={transfer.id} className="mb-2 p-2 border rounded-lg">
              <p><strong>From:</strong> {transfer.from}</p>
              <p><strong>To:</strong> {transfer.to}</p>
              <p><strong>Value:</strong> {parseFloat(transfer.value) / 1e18} STT</p>
              <p>
                <strong>TX:</strong>{" "}
                <a
                  href={`https://shannon-explorer.somnia.network/tx/${transfer.transactionHash}`}
                  target="_blank"
                  className="text-blue-600 underline"
                >
                  View Transaction
                </a>
              </p>
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

The UI shows a loading message while data is being fetched. When data is ready, it displays the latest 10 token transfers. It formats transaction values (value / 1e18) to show the correct STT amount and provides a link to view each transaction on Somnia Explorer.

## Add the Component to the NextJS Page

Update the page.tsx file:

```typescript
"use client";
import TokenTransfers from "../components/TokenTransfers";

export default function Home() {
  return (
    <main className="min-h-screen p-8">
      <h1 className="text-2xl font-bold">Welcome to MyToken Dashboard</h1>
      <TokenTransfers />
    </main>
  );
}
```

Restart the development server:

```
npm run dev
```

<figure><img src="/files/5dJZgcHeqtX3Dp1U1DS0" alt=""><figcaption></figcaption></figure>

Now your NextJS UI dynamically fetches and displays token transfers from Somnia’s Subgraph! 🔥

<br>


# Building Subgraph UIs (Apollo Client)

This guide will teach you how to create a minimal, functional UI that queries blockchain data from a Somnia subgraph using Next.js, Apollo Client, and GraphQL.

## Prerequisites

* Basic knowledge of React and Next.js
* Node.js 20+
* A deployed subgraph on Somnia (we'll use [SomFlip](https://shannon-explorer.somnia.network/address/0x014F851965F281d6112FC7F6dfe8c331C413Eb9b) as an example)

## What You'll Build

A clean, minimal interface that:

* Displays all coin flip results with pagination
* Shows a live feed that auto-refreshes every 5 seconds

## Create a Next.js Project

Start by creating a new Next.js application with TypeScript and TailwindCSS:

```bash
npx create-next-app@latest somnia-subgraph-ui --typescript --tailwind --app
cd somnia-subgraph-ui
```

Install the required GraphQL dependencies:

```bash
npm install @apollo/client graphql
```

## Understand the Architecture

Before we code, let's understand how the pieces fit together:

```
User Interface (React Components)
        ↓
Apollo Client (GraphQL Client)
        ↓
GraphQL Queries
        ↓
Somnia Subgraph API
        ↓
Blockchain Data
```

## Set Up Apollo Client

Apollo Client is a comprehensive GraphQL client that manages data fetching, caching, and state management. Create a `lib` directory and create a file `apollo-client.ts`

```typescript
import { ApolloClient, InMemoryCache } from '@apollo/client';
const client = new ApolloClient({
  // The URI of your subgraph endpoint
  uri: 'https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/SomFlip',
  
  // Apollo's caching layer - stores query results
  cache: new InMemoryCache(),
});

export default client;
```

The `URI` is the endpoint where your subgraph is hosted, and `InMemoryCache` will store the query results in memory for fast access

## Create the Apollo Provider Wrapper

React components need access to the Apollo Client. We'll create a wrapper component that provides this access to the entire app. Create a `components` directory and create a file `ApolloWrapper.tsx`.

```typescript
'use client';  // Next.js 13+ directive for client-side components

import { ApolloProvider } from '@apollo/client';
import client from '@/lib/apollo-client';

// This component wraps your app with Apollo's context provider
export default function ApolloWrapper({ 
  children 
}: { 
  children: React.ReactNode 
}) {
  return (
    <ApolloProvider client={client}>
      {children}
    </ApolloProvider>
  );
}
```

ApolloProvider: Makes the Apollo Client available to all child components

## Update app/layout.tsx

```typescript
import ApolloWrapper from '@/components/ApolloWrapper';
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <ApolloWrapper>
          {children}
        </ApolloWrapper>
      </body>
    </html>
  );
}
```

## Create GraphQL Queries

GraphQL queries define exactly what data you want from the subgraph. Let's create queries for our two main features. In the `lib` directory create a `queries.ts` file.

<details>

<summary>queries.ts</summary>

```
import { gql } from '@apollo/client';
// Query for paginated flip results
export const GET_FLIP_RESULTS = gql`
  query GetFlipResults(
    $first: Int!,        # Number of results to fetch
    $skip: Int!,         # Number of results to skip (for pagination)
    $orderBy: String!,   # Field to sort by
    $orderDirection: String!  # 'asc' or 'desc'
  ) {
    flipResults(
      first: $first
      skip: $skip
      orderBy: $orderBy
      orderDirection: $orderDirection
    ) {
      id                 # Unique identifier
      player             # Wallet address of player
      betAmount          # Amount bet (in wei)
      choice             # Player's choice: HEADS or TAILS
      result             # Actual result: HEADS or TAILS
      payout             # Amount won (0 if lost)
      blockNumber        # Block when flip occurred
      blockTimestamp     # Unix timestamp
      transactionHash    # Transaction hash on blockchain
    }
  }
`;


// Query for recent flips (live feed)
export const GET_RECENT_FLIPS = gql`
  query GetRecentFlips($first: Int!) {
    flipResults(
      first: $first
      orderBy: blockTimestamp
      orderDirection: desc  # Most recent first
    ) {
      id
      player
      betAmount
      choice
      result
      payout
      blockTimestamp
      transactionHash
    }
  }
`;
```

</details>

Note the following:

* `gql` is the template literal tag that parses GraphQL queries
* Variables start with $ and have types (Int!, String!, etc.)
* ! means the field is required (non-nullable)

## Build the All Flips Component

Let's build the All Flips component step by step, understanding each part in detail.

### Set Up the Component File

In the `components` directory create `AllFlips.tsx` file and add the following imports.

```
'use client';  
import { useState } from 'react';
import { useQuery } from '@apollo/client';
import { GET_FLIP_RESULTS } from '@/lib/queries';
```

### Create Utility Functions

Add these helper functions at the top of your component:

```
/ Shortens long blockchain addresses for display
// Example: "0x1234567890abcdef" becomes "0x1234...cdef"
const truncateHash = (hash: string) => {
  return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
};

// Converts wei (smallest unit) to ether (display unit)
// 1 ether = 1,000,000,000,000,000,000 wei (10^18)
const formatEther = (wei: string) => {
  const ether = parseFloat(wei) / 1e18;
  return ether.toFixed(4);  // Show 4 decimal places
};

// Converts Unix timestamp to readable date
// Blockchain stores time as seconds since Jan 1, 1970
const formatTime = (timestamp: string) => {
  const milliseconds = parseInt(timestamp) * 1000;
  const date = new Date(milliseconds);
  return date.toLocaleString();
};
```

### Component Function and State Management

```
export default function AllFlips() {
  // Track which page of results we're viewing
  const [page, setPage] = useState(0);
  const itemsPerPage = 30;
```

`the number ofpage` tracks the current page number (starting at 0), which is used to calculate the number of results to skip. It updates when the user clicks the Previous/Next button.

### Execute the GraphQL Query

```
const { loading, error, data } = useQuery(GET_FLIP_RESULTS, {
    variables: {
      first: itemsPerPage,              // How many results to fetch
      skip: page * itemsPerPage,        // How many to skip
      orderBy: 'blockTimestamp',        // Sort by time
      orderDirection: 'desc',           // Newest first
    },
  });
```

The `useQuery` function is set to `loading: true` while fetching `data` and the `data` contains the query results when successful.

### Handle Query States

```
// Show loading spinner while fetching
  if (loading) {
    return <div className="text-center py-8 text-gray-500">Loading...</div>;
  }
  
  // Show error message if query failed
  if (error) {
    return (
      <div className="text-center py-8 text-red-500">
        Error: {error.message}
      </div>
    );
  }
  
  // Check if we have results
  if (!data?.flipResults?.length) {
    return <div className="text-center py-8 text-gray-500">No flips found</div>;
  }
```

This prevents rendering errors and andles edge cases gracefully

### Render the Table View

<details>

<summary>AllFlips.tsx - Table View</summary>

```
return (
    <div className="space-y-4">
      <div className="overflow-x-auto">  {/* Makes table scrollable on mobile */}
        <table className="w-full text-sm">
          <thead>
            <tr className="border-b">
              <th className="text-left py-2">Player</th>
              <th className="text-left py-2">Bet</th>
              <th className="text-left py-2">Choice</th>
              <th className="text-left py-2">Result</th>
              <th className="text-left py-2">Payout</th>
              <th className="text-left py-2">Time</th>
            </tr>
          </thead>
          <tbody>
            {data.flipResults.map((flip: any) => (
              <tr key={flip.id} className="border-b">
                {/* Player address - truncated for readability */}
                <td className="py-2 font-mono text-xs">
                  {truncateHash(flip.player)}
                </td>
                
                {/* Bet amount - converted from wei to ether */}
                <td className="py-2">
                  {formatEther(flip.betAmount)} STT
                </td>
                
                {/* Player's choice - color coded */}
                <td className="py-2">
                  <span className={
                    flip.choice === 'HEADS' 
                      ? 'text-blue-600'    // Blue for heads
                      : 'text-purple-600'  // Purple for tails
                  }>
                    {flip.choice}
                  </span>
                </td>
                
                {/* Actual result - same color coding */}
                <td className="py-2">
                  <span className={
                    flip.result === 'HEADS' 
                      ? 'text-blue-600' 
                      : 'text-purple-600'
                  }>
                    {flip.result}
                  </span>
                </td>
                
                {/* Payout - green if won, gray if lost */}
                <td className="py-2">
                  <span className={
                    flip.payout !== '0' 
                      ? 'text-green-600'   // Won
                      : 'text-gray-400'    // Lost
                  }>
                    {flip.payout !== '0' 
                      ? `+${formatEther(flip.payout)}` 
                      : '0'
                    } STT
                  </span>
                </td>
                
                {/* Timestamp - converted to readable date */}
                <td className="py-2 text-xs text-gray-500">
                  {formatTime(flip.blockTimestamp)}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
      <div className="flex justify-between">
        {/* Previous button */}
        <button
          onClick={() => setPage(Math.max(0, page - 1))}
          disabled={page === 0}
          className="px-4 py-2 text-sm bg-gray-100 rounded disabled:opacity-50"
        >
          Previous
        </button>
        
        {/* Current page indicator */}
        <span className="py-2 text-sm text-gray-600">
          Page {page + 1}
        </span>
        
        {/* Next button */}
        <button
          onClick={() => setPage(page + 1)}
          disabled={data.flipResults.length < itemsPerPage}
          className="px-4 py-2 text-sm bg-gray-100 rounded disabled:opacity-50"
        >
          Next
        </button>
      </div>
    </div>
  );
}
```

</details>

## Build the Live Feed Component

Now let's build the Live Feed component that automatically refreshes to show new flips.

### Set Up the Component

In the `components` directory create a `LiveFeed.tsx` file and update the imports.

```
'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';
```

### Create Utility Functions

```
const truncateHash = (hash: string) => {
  return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
};

const formatEther = (wei: string) => {
  return (parseFloat(wei) / 1e18).toFixed(4);
};
```

### Component Function with Auto-Refresh

```
export default function LiveFeed() {
  // Execute query with automatic polling
  const { loading, error, data } = useQuery(GET_RECENT_FLIPS, {
    variables: { 
      first: 10  // Get 10 most recent flips
    },
    pollInterval: 5000,  // Refresh every 5 seconds (5000ms)
  });
```

The `pollInterval` automatically re-executes the query every 5 seconds. New flips appear without user interaction with Apollo Client handling the refresh logic. You can set to 0 or remove to disable auto-refresh

### Handle Query States

```
// Same loading/error handling as AllFlips
  if (loading) {
    return <div className="text-center py-8 text-gray-500">Loading...</div>;
  }
  
  if (error) {
    return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
  }
  
  if (!data?.flipResults?.length) {
    return <div className="text-center py-8 text-gray-500">No recent flips</div>;
  }

```

### Complete Live Feed Component

<details>

<summary>LiveFeed.tsx</summary>

```
'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';

const truncateHash = (hash: string) => `${hash.slice(0, 6)}...${hash.slice(-4)}`;
const formatEther = (wei: string) => (parseFloat(wei) / 1e18).toFixed(4);

export default function LiveFeed() {
  const { loading, error, data } = useQuery(GET_RECENT_FLIPS, {
    variables: { first: 10 },
    pollInterval: 5000,
  });

  if (loading) return <div className="text-center py-8 text-gray-500">Loading...</div>;
  if (error) return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
  if (!data?.flipResults?.length) return <div className="text-center py-8 text-gray-500">No recent flips</div>;

  return (
    <div className="space-y-2">
      {data.flipResults.map((flip: any) => {
        const won = flip.payout !== '0';
        return (
          <div key={flip.id} className={`p-3 rounded border ${won ? 'border-green-200 bg-green-50' : 'border-gray-200'}`}>
            <div className="flex justify-between items-center">
              <div>
                <span className="font-mono text-sm">{truncateHash(flip.player)}</span>
                <span className="text-sm text-gray-500 ml-2">bet {formatEther(flip.betAmount)} STT</span>
              </div>
              <div className="text-right">
                <div className="text-sm">
                  <span className={flip.choice === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
                    {flip.choice}
                  </span>
                  <span className="mx-1">→</span>
                  <span className={flip.result === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
                    {flip.result}
                  </span>
                </div>
                <div className={`text-sm font-semibold ${won ? 'text-green-600' : 'text-gray-400'}`}>
                  {won ? `Won ${formatEther(flip.payout)} STT` : 'Lost'}
                </div>
              </div>
            </div>
          </div>
        );
      })}
      <p className="text-center text-xs text-gray-500 pt-2">Auto-refreshing every 5 seconds</p>
    </div>
  );
}
```

</details>

The key differences from the AllFlips page are that there is no pagination (shows most recent only), and it auto-refreshes with pollInterval, with a visual emphasis on win/loss status.

## Update the Main Page.tsx

<details>

<summary>page.tsx</summary>

```
'use client';
import { useState } from 'react';
import AllFlips from '@/components/AllFlips';
import LiveFeed from '@/components/LiveFeed';

export default function Home() {
  const [activeTab, setActiveTab] = useState('allFlips');
  return (
    <div className="max-w-4xl mx-auto p-4">
      <h1 className="text-2xl font-bold mb-6">SomFlip</h1>
      {/* Tab Navigation */}
      <div className="flex gap-4 mb-6 border-b">
        <button
          onClick={() => setActiveTab('allFlips')}
          className={`pb-2 px-1 ${
            activeTab === 'allFlips'
              ? 'border-b-2 border-black font-semibold'
              : 'text-gray-500'
          }`}
        >
          All Flips
        </button>
        <button
          onClick={() => setActiveTab('liveFeed')}
          className={`pb-2 px-1 ${
            activeTab === 'liveFeed'
              ? 'border-b-2 border-black font-semibold'
              : 'text-gray-500'
          }`}
        >
          Live Feed
        </button>
      </div>
      {/* Conditional Rendering Based on Active Tab */}
      {activeTab === 'allFlips' ? <AllFlips /> : <LiveFeed />}
    </div>
  );
}
```

</details>

## Run Your Application

```
npm run dev
```

Visit `http://localhost:3000` to see your UI in action.

<br>


# Using Data APIs (Ormi)

The Somnia mission is to enable the building of mass-consumer real-time applications. As a Developer, you need to understand how to interact with onchain data to build UIs. This guide will teach you how to build a Token Balance dApp that fetches and displays ERC20 token balances from the Somnia Network using Next.js and the Ormi Data APIs.

## Prerequisites

To complete this guide, you will need:

* Basic understanding of React and TypeScript
* An Ormi API key. Get one at[ ](https://ormi.xyz/)<https://subgraph.somnia.network/dashboard/api>.

{% embed url="<https://www.youtube.com/watch?v=6fuKQilbh2c>" %}

## What is Ormi Data API?

[Ormi](https://docs.ormilabs.com/dedicated-env/somnia/data-apis/overview) provides a unified crypto data infrastructure for live and historical blockchain data. The Data APIs allow developers to query blockchain data without running their own nodes, making it easy to build data-rich applications on the Somnia Network.

#### API Base URL

The Ormi Data API for Somnia Network uses the following base URL:

```
https://api.subgraph.somnia.network/public_api/data_api
```

#### API Endpoints

The API follows a RESTful structure. For fetching ERC20 token balances, the endpoint structure is:

```
/somnia/v1/address/{walletAddress}/balance/erc20
```

Where:

* `somnia` - The network identifier
* `v1` - API version
* `{walletAddress}` - The wallet address you want to query
* `balance/erc20` - Specifies that you want ERC-20 token balances

#### Authentication

The Ormi API requires authentication using a Bearer token. Every request must include an Authorization header:

```
Authorization: Bearer YOUR_API_KEY
```

**Important**: Never expose your API key in client-side code. Always make API calls from a server-side route to keep your key secure.

#### Example API Request

Here's an example of how to make a direct API call using curl:

```bash
curl -X GET "https://api.subgraph.somnia.network/public_api/data_api/somnia/v1/address/0xYOUR_WALLET_ADDRESS/balance/erc20" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json"
```

## Set up the Project

Create a new Next.js application with TypeScript and Tailwind CSS:

```bash
npx create-next-app@latest somnia-balance-demo --typescript --tailwind --app
cd somnia-balance-demo
```

## Create the Type Definitions

First, we need to define TypeScript interfaces for the API response. Update `app/page.tsx`:

```typescript
'use client'
import { useState, FormEvent } from 'react'
// Type definitions for the API response
interface TokenBalance {
  balance: string
  contract: {
    address: string
    decimals: number
    erc_type: string
    logoUri: string | null
    name: string
    symbol: string
  }
  raw_balance: string
}

interface BalanceResponse {
  erc20TokenBalances: TokenBalance[]
  resultCount: number
}
```

## Build the User Interface

Now, let's create the main component with an input field and button. Update your `app/page.tsx`:

```typescript
export default function Home() {
  const [walletAddress, setWalletAddress] = useState<string>('')
  const [loading, setLoading] = useState<boolean>(false)
  const [data, setData] = useState<BalanceResponse | null>(null)
  const [error, setError] = useState<string>('')

  return (
    <main className="min-h-screen bg-white p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">Somnia Network Balance Demo</h1>
        
        <form className="mb-8">
          <div className="flex gap-4">
            <input
              type="text"
              value={walletAddress}
              onChange={(e) => setWalletAddress(e.target.value)}
              placeholder="Enter wallet address (0x...)"
              className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              type="submit"
              disabled={loading}
              className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
            >
              {loading ? 'Loading...' : 'Fetch Balance'}
            </button>
          </div>
        </form>
      </div>
    </main>
  )
}
```

## Create a `.env` file

The `.env` is for keeping secrets such as the Ormi API KEY. Add the API KEY:

```bash
PRIVATE_KEY=YOUR_API_KEY_HERE
```

## Create the API Route

To avoid CORS issues and keep your API key secure, we'll create an API route. Create a directory and a new file `app/api/balance/route.ts`:

```typescript
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const walletAddress = searchParams.get('address');

    if (!walletAddress) {
      return NextResponse.json(
        { error: 'Wallet address is required' },
        { status: 400 }
      )
    }

    const apiKey = process.env.PRIVATE_KEY 
    const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api'

    const response = await fetch(
      `${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      }
    )

    const data = await response.json()

    if (!response.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch data from Ormi API', details: data },
        { status: response.status }
      )
    }
    
    return NextResponse.json(data)
  } catch (error) {
    console.error('API Error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}
```

Important: Replace `YOUR_API_KEY_HERE` with your actual Ormi API key.

## Implement the Fetch Function

Add the fetch function to handle form submission. Update your `app/page.tsx`:

```typescript
const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  
  if (!walletAddress) {
    setError('Please enter a wallet address')
    return
  }

  setLoading(true)
  setError('')
  setData(null)

  try {
    const response = await fetch(`/api/balance?address=${walletAddress}`, {
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ walletAddress }),
    })

    const result = await response.json()

    if (!response.ok) {
      throw new Error(result.error || 'Failed to fetch balance')
    }

    setData(result)
  } catch (err) {
    setError(err instanceof Error ? err.message : 'An error occurred')
  } finally {
    setLoading(false)
  }
}
```

Don't forget to add the onSubmit handler to your form:

```typescript
<form onSubmit={fetchBalance} className="mb-8">
```

## Display the Results

Add error handling and a table to display the token balances. Add this code after your form in `app/page.tsx`:

```typescript
{error && (
  <div className="p-4 mb-4 bg-red-50 border border-red-200 rounded-md">
    <p className="text-red-600">{error}</p>
  </div>
)}

{data && data.erc20TokenBalances.length > 0 && (
  <div className="bg-white rounded-lg shadow overflow-hidden">
    <div className="px-6 py-4 bg-gray-50 border-b">
      <h2 className="text-xl font-semibold">Token Balances ({data.resultCount} tokens)</h2>
    </div>
    <div className="overflow-x-auto">
      <table className="min-w-full divide-y divide-gray-200">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Name
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Symbol
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Balance
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Contract Address
            </th>
          </tr>
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {data.erc20TokenBalances.map((token, index) => (
            <tr key={index} className="hover:bg-gray-50">
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {token.contract.name || 'Unknown'}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {token.contract.symbol || '-'}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {parseFloat(token.balance).toLocaleString()}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm">
                <a
                  href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-blue-600 hover:text-blue-800 font-mono"
                >
                  {token.contract.address.slice(0, 6)}...{token.contract.address.slice(-4)}
                </a>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  </div>
)}

{data && data.erc20TokenBalances.length === 0 && (
  <div className="bg-gray-50 p-6 rounded-md text-center">
    <p className="text-gray-600">No ERC-20 tokens found for this address</p>
  </div>
)}
```

### Test Your dApp

Start the development server:

```bash
npm run dev
```

Open[ http://localhost:3000](http://localhost:3000/) in your browser. Enter a wallet address that has tokens on Somnia Network.

Example test address: `0xC4890Bc98273424a18626772F266C35bf57FA56A`

Look at the browser for the response and the displayed token balances. You can click on any contract address to view it in the Shannon Explorer.

## Complete Code

<details>

<summary>page.tsx</summary>

```typescript
'use client';

import { useState, FormEvent } from 'react';

// Type definitions for the API response
interface TokenBalance {
  balance: string;
  contract: {
    address: string;
    decimals: number;
    erc_type: string;
    logoUri: string | null;
    name: string;
    symbol: string;
  };
  raw_balance: string;
}

interface BalanceResponse {
  erc20TokenBalances: TokenBalance[];
  resultCount: number;
}

export default function Home() {
  const [walletAddress, setWalletAddress] = useState<string>('');
  const [loading, setLoading] = useState<boolean>(false);
  const [data, setData] = useState<BalanceResponse | null>(null);
  const [error, setError] = useState<string>('');

  const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();

    if (!walletAddress) {
      setError('Please enter a wallet address');
      return;
    }

    setLoading(true);
    setError('');
    setData(null);

    try {
      const response = await fetch(`/api/balance?address=${walletAddress}`, {
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ walletAddress }),
      });

      const result = await response.json();

      if (!response.ok) {
        throw new Error(result.error || 'Failed to fetch balance');
      }

      setData(result);
    } catch (err) {
      setError(err instanceof Error ? err.message : 'An error occurred');
    } finally {
      setLoading(false);
    }
  };

  return (
    <main className='min-h-screen bg-white p-8'>
      <div className='max-w-6xl mx-auto'>
        <h1 className='text-3xl font-bold mb-8 text-gray-900'>
          Somnia Network Balance Demo
        </h1>

        <form onSubmit={fetchBalance} className='mb-8'>
          <div className='flex gap-4'>
            <input
              type='text'
              value={walletAddress}
              onChange={(e) => setWalletAddress(e.target.value)}
              placeholder='Enter wallet address (0x...)'
              className='flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-500'
            />
            <button
              type='submit'
              disabled={loading}
              className='px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed'
            >
              {loading ? 'Loading...' : 'Fetch Balance'}
            </button>
          </div>
        </form>

        {error && (
          <div className='p-4 mb-4 bg-red-50 border border-red-200 rounded-md'>
            <p className='text-red-600'>{error}</p>
          </div>
        )}

        {data && data.erc20TokenBalances.length > 0 && (
          <div className='bg-white rounded-lg shadow overflow-hidden'>
            <div className='px-6 py-4 bg-gray-50 border-b'>
              <h2 className='text-xl font-semibold text-gray-900'>
                Token Balances ({data.resultCount} tokens)
              </h2>
            </div>
            <div className='overflow-x-auto'>
              <table className='min-w-full divide-y divide-gray-200'>
                <thead className='bg-gray-50'>
                  <tr>
                    <th className='px-6 py-3 text-left text-xs font-medium text-gray-900 uppercase tracking-wider'>
                      Name
                    </th>
                    <th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
                      Symbol
                    </th>
                    <th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
                      Balance
                    </th>
                    <th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
                      Contract Address
                    </th>
                  </tr>
                </thead>
                <tbody className='bg-white divide-y divide-gray-200'>
                  {data.erc20TokenBalances.map((token, index) => (
                    <tr key={index} className='hover:bg-gray-50'>
                      <td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
                        {token.contract.name || 'Unknown'}
                      </td>
                      <td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
                        {token.contract.symbol || '-'}
                      </td>
                      <td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
                        {parseFloat(token.balance).toLocaleString()}
                      </td>
                      <td className='px-6 py-4 whitespace-nowrap text-sm'>
                        <a
                          href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
                          target='_blank'
                          rel='noopener noreferrer'
                          className='text-blue-600 hover:text-blue-800 font-mono'
                        >
                          {token.contract.address.slice(0, 6)}...
                          {token.contract.address.slice(-4)}
                        </a>
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        )}

        {data && data.erc20TokenBalances.length === 0 && (
          <div className='bg-gray-50 p-6 rounded-md text-center'>
            <p className='text-gray-600'>
              No ERC-20 tokens found for this address
            </p>
          </div>
        )}
      </div>
    </main>
  );
}

```

</details>

<details>

<summary>route.ts</summary>

```typescript
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const walletAddress = searchParams.get('address');

    if (!walletAddress) {
      return NextResponse.json(
        { error: 'Wallet address is required' },
        { status: 400 }
      );
    }

    const apiKey = process.env.PRIVATE_KEY;
    const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api';

    const response = await fetch(
      `${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
      {
        headers: {
          Authorization: `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          Accept: 'application/json',
        },
      }
    );

    const data = await response.json();

    if (!response.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch data from Ormi API', details: data },
        { status: response.status }
      );
    }

    return NextResponse.json(data);
  } catch (error) {
    console.error('API Error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

```

</details>

## Congratulations

You have built your first API enabled dApp on the Somnia Network!

Now that you have a working Token Balance dApp, you can extend it by using other Ormi API [endpoints](https://subgraphs.somnia.network/).


# Listening to Blockchain Events (WebSocket)

This guide teaches developers how to create WebSocket connections to listen for smart contract events on the Somnia network in real-time. We'll use a simple Greeting contract as an example to demonstrate the core concepts.

## Resources

| Somnia Mainnet WebSocket | <wss://api.infra.mainnet.somnia.network/ws> |
| ------------------------ | ------------------------------------------- |
| Somnia Testnet WebSocket | <wss://api.infra.testnet.somnia.network/ws> |

For the underlying subscription and log-filtering RPC methods, see the [JSON-RPC API Reference](/developer/json-rpc-api).

## Example Script

<details>

<summary>websocket-listener.js</summary>

```javascript
// const { ethers } = require("ethers");
import { ethers } from "ethers";
// Configuration
const wsUrl = "wss://dream-rpc.somnia.network/ws";
const contractAddress = "0xADA7b2953E7d670092644d37b6a39BAE3237beD7"; // Replace with your contract address

// Contract ABI
const abi = [
  {
    anonymous: false,
    inputs: [
      { indexed: true, internalType: "string", name: "oldGreeting", type: "string" },
      { indexed: true, internalType: "string", name: "newGreeting", type: "string" },
    ],
    name: "GreetingSet",
    type: "event",
  },
  {
    inputs: [],
    name: "getGreeting",
    outputs: [{ internalType: "string", name: "", type: "string" }],
    stateMutability: "view",
    type: "function",
  },
];

async function listen() {
  // Create WebSocket provider and contract
  const provider = new ethers.WebSocketProvider(wsUrl);
  await provider._waitUntilReady();
  const contract = new ethers.Contract(contractAddress, abi, provider);

  console.log("Listening for events...\n");

  // Event filter
  const filter = {
    address: contractAddress,
    topics: [ethers.id("GreetingSet(string,string)")],
  };

  // Listen for events
  provider.on(filter, async (log) => {
    try {
      const greeting = await contract.getGreeting();
      console.log(`New greeting: "${greeting}"`);
    } catch (error) {
      console.error("Error:", error.message);
    }
  });

  // Keep connection alive
  setInterval(async () => {
    try {
      await provider.getBlockNumber();
    } catch (error) {
      console.error("Connection error");
    }
  }, 30000);

  // Handle shutdown
  process.on("SIGINT", () => {
    provider.destroy();
    process.exit(0);
  });
}

// Start listening
listen().catch(console.error);

```

</details>

## Prerequisites

* Node.js 20+
* Basic understanding of JavaScript
* A deployed smart contract on Somnia network

## What are WebSockets?

WebSockets are a communication protocol that provides bidirectional communication channels over a single TCP connection. Unlike traditional HTTP requests, WebSockets maintain a persistent connection between the client and server.

### **How WebSockets Work**

WebSockets begin with a connection establishment phase where the client initiates a WebSocket handshake through an HTTP upgrade request. Once established, the connection stays open as a persistent channel between client and server. This enables bidirectional communication where both client and server can send messages at any time without waiting for requests. The protocol maintains low latency since there's no need to establish new connections for each message exchange. This design is highly efficient with minimal protocol overhead compared to traditional HTTP polling approaches.

**WebSocket Connection Lifecycle**

```bash
Client                    Server
  |                         |
  |----Connection Request-->|
  |<---Connection Accept----|
  |                         |
  |<===Open Connection====> |
  |                         |
  |----Send Message-------->|
  |<---Receive Message------|
  |<---Push Notification----|
  |----Send Message-------->|
  |                         |
  |<===Open Connection====> |
  |                         |
  |----Close Connection---->|
```

### WebSocket vs HTTP Polling

#### **HTTP Polling Approach**

```javascript
// Inefficient: Constantly asking "Any updates?"
setInterval(async () => {
    const response = await fetch('https://api.example.com/events');
    const data = await response.json();
    if (data.hasNewEvents) {
        console.log('New event:', data.events);
    }
}, 5000); // Check every 5 seconds
```

The problem with polling is that Polling wastes bandwidth by constantly checking for updates even when none exist, leading to unnecessary network traffic. This approach introduces delays of up to the polling interval (5 seconds in our example), meaning users might wait several seconds to see new events that have already occurred. Additionally, the server must process these unnecessary requests repeatedly, increasing computational load and infrastructure costs. For blockchain applications, this translates to higher costs for RPC providers who often charge based on the number of requests made.

#### **WebSocket Approach**

```javascript
// Efficient: Server pushes updates immediately
const ws = new WebSocket('wss://api.example.com/events');
ws.on('message', (data) => {
    console.log('New event:', data); // Instant notification
});
```

WebSockets provide real-time updates measured in milliseconds rather than seconds, ensuring users receive notifications instantly when events occur. This eliminates wasted requests since the server only sends data when there are actual updates, significantly reducing bandwidth usage. The reduced request frequency leads to lower server load and better resource utilization. For users, this translates to a superior experience with immediate feedback, while developers benefit from reduced infrastructure costs.

### Blockchain Events and WebSockets

Smart contracts emit events when important state changes occur. These events are included in transaction receipts and stored in blockchain logs.

The event flow begins when a user calls a Smart Contract function through a Transaction. During execution, the contract updates its Internal State and emits Events containing relevant data about the changes. These Transactions and their associated Events are then included in a new block by Validators. Once the Block is finalized, Nodes across the network broadcast it to their peers. WebSocket connections instantly notify connected clients about these new events, enabling real-time reactions to Blockchain state changes.

### **Example Use Cases for WebSocket Event Listening**

| DeFi Applications   | <ul><li>Monitor price updates on DEX swaps</li></ul> |
| ------------------- | ---------------------------------------------------- |
| NFT Marketplaces    | <ul><li>Live bidding updates in auctions</li></ul>   |
| Gaming DApps        | <ul><li>Real-time game state updates</li></ul>       |
| DAOs and Governance | <ul><li>Live voting updates</li></ul>                |
| Supply Chain        | <ul><li>Product status updates</li></ul>             |

### Indexed Parameters in Events

When you mark an event parameter as `indexed` in Solidity, it becomes part of the event's topics rather than the data section. This enables efficient filtering but changes how you access the data.

Non-Indexed String (accessible directly):

```solidity
event MessageSent(string message); // Can read 'message' directly from logs
```

Indexed String (hashed for filtering):

```solidity
event MessageSent(string indexed message); // 'message' is hashed, cannot read directly
```

**Why Use Indexed Parameters?**

`Indexed` parameters enable efficient filtering by allowing nodes to quickly find specific events without scanning through all logs in a block. They provide gas optimization since topics are more gas-efficient for filtering operations compared to parsing event data. Additionally, nodes can index and search these parameters significantly faster, improving overall query performance when applications need to find specific events based on parameter values.

**The Trade-off**

For strings and bytes, indexing means:

* Can filter events by this parameter efficiently
* Cannot retrieve the actual value from the event log
* Must query contract state to get the current value

This is why, in our example, when we receive a `GreetingSet` event with indexed string parameters, we call contract.getGreeting() to retrieve the actual greeting text.

> The pattern for listening to blockchain events via WebSocket follows these principles:
>
> The process begins by establishing a connection to the blockchain node's WebSocket endpoint, ensuring a persistent communication channel. Once connected, you create a filter that defines which events from which contracts to monitor, allowing precise event targeting. Next, you set up listener functions that register callbacks to execute when specific events occur. When events are received, your handler functions process the event data according to your application's needs. Throughout the connection lifetime, you must maintain the connection with periodic activity to prevent timeouts. Finally, when your application terminates, ensure a clean shutdown by properly closing all connections and removing event listeners.

## Code Breakdown

### Connect to Somnia WebSocket

```javascript
const wsUrl = 'wss://api.infra.testnet.somnia.network/ws'; //change url for Mainnet
const provider = new ethers.WebSocketProvider(wsUrl);
await provider._waitUntilReady();
```

The WebSocket URL for Somnia mainnet is <wss://api.infra.mainnet.somnia.network/ws>. This creates a persistent connection to the network.

#### Create Contract Instance

```javascript
const contract = new ethers.Contract(contractAddress, abi, provider);
```

You need:

* `Contract address`: The deployed address on Somnia.
* `ABI`: At minimum, include the events you want to listen for.
* `Provider`: The WebSocket connection.

### Define Event Filter

```javascript
const filter = {
    address: contractAddress,
    topics: [ethers.id("GreetingSet(string,string)")]
};
```

The filter specifies:

* Which contract to monitor
* Which event signature to listen for

### Set Up Event Listener

```javascript
provider.on(filter, async (log) => {
    // Handle the event
    const greeting = await contract.getGreeting();
    console.log(`New greeting: "${greeting}"`);
});
```

When an event is detected:

1. The callback receives the log data
2. Query the contract for the current state
3. Process/display the data as needed

### Maintain Connection

```javascript
setInterval(async () => {
    await provider.getBlockNumber();
}, 30000);
```

This is because WebSocket connections can timeout. Send periodic requests to keep the connection alive.

### Update the ABI

Include only the events and functions you need:

```javascript
const abi = [
    // Your event definition
    {
        "anonymous": false,
        "inputs": [
            // Your event parameters
        ],
        "name": "YourEventName",
        "type": "event"
    },
    // Any read functions you need
    {
        "inputs": [],
        "name": "yourReadFunction",
        "outputs": [/* outputs */],
        "stateMutability": "view",
        "type": "function"
    }
];
```

### Update the Event Filter

Change the event signature to match your event:

```javascript
const filter = {
    address: contractAddress,
    topics: [ethers.id("YourEventName(type1,type2)")]
};
```

### Handle Event Data

Process the event based on your needs:

```javascript
provider.on(filter, async (log) => {
    // For non-indexed parameters, you can parse the log
    const parsedLog = contract.interface.parseLog(log);
    
    // For indexed strings, query the contract state
    const currentState = await contract.yourReadFunction();
    
    // Process your data
    console.log('Event detected:', currentState);
});
```

## Common Patterns

### Multiple Events

Listen for multiple events from the same contract:

```javascript
// Listen for Event1
provider.on({
    address: contractAddress,
    topics: [ethers.id("Event1(...)")]
}, handleEvent1);


// Listen for Event2
provider.on({
    address: contractAddress,
    topics: [ethers.id("Event2(...)")]
}, handleEvent2);
```

### Error Recovery

Add reconnection logic for production applications:

```javascript
async function connectWithRetry() {
    let retries = 0;
    while (retries < 5) {
        try {
            await listen();
            break;
        } catch (error) {
            console.log(`Retry ${++retries}/5...`);
            await new Promise(r => setTimeout(r, 5000));
        }
    }
}
```

### Event History

Get recent events on startup:

```javascript
// Get last 100 blocks of events
const currentBlock = await provider.getBlockNumber();
const events = await contract.queryFilter('YourEventName', currentBlock - 100, currentBlock);
events.forEach(event => {
    console.log('Historical event:', event);
});
```

## Test Your WebSocket Connection

1. Deploy your Smart Contract to Somnia network.
2. Run the listener in one terminal:

```bash
node websocket-listener.js
```

3. Trigger events from another script or dApp
4. Observe real-time updates in your listener

### Conclusion

WebSocket connections provide real-time event monitoring for smart contracts on Somnia. This guide demonstrated:

1. Connecting to Somnia's WebSocket endpoint
2. Listening for specific contract events
3. Handling indexed parameters correctly
4. Maintaining stable connections
5. Adapting the pattern for any smart contract

With this foundation, you can build responsive dApps that react instantly to blockchain events without polling.

<br>


# Oracles

Oracles bridge the gap between onchain and offchain worlds. They bring external data like prices, randomness, and real-world events directly into your smart contracts.

In this section, you’ll explore:

* [How to integrate **DIA price feeds** on Somnia](/developer/building-dapps/oracles/dia-price-feeds)
* [How to implement **Protofire Price Feeds**](/developer/building-dapps/oracles/protofire-price-feeds)
* [How to implement **Verifiable Randomness (VRF)** via **Protofire + Chainlink**](/developer/building-dapps/oracles/using-verifiable-randomness-vrf)
* Use cases for oracles in DeFi, gaming, prediction markets, and more

> If your dApp depends on real-world data, randomness, or secure external inputs — this is where to start.


# DIA Price Feeds

## **Overview**

[DIA](https://docs.diadata.org/) Oracles provide **secure, customizable, and decentralized price feeds** that can be integrated into **smart contracts on the Somnia Testnet**. This guide will walk you through how to access **on-chain price data**, understand the oracle’s functionality, and integrate it into your **Solidity Smart Contracts**.

{% embed url="<https://www.youtube.com/watch?v=Fumsv0y4rNU>" %}

## **Oracle Details**

### **Contracts on Somnia**

<table><thead><tr><th width="117.1484375">Network</th><th>Contract Address</th></tr></thead><tbody><tr><td>Mainnet</td><td><a href="https://explorer.somnia.network/address/0xbA0E0750A56e995506CA458b2BdD752754CF39C4">0xbA0E0750A56e995506CA458b2BdD752754CF39C4</a></td></tr><tr><td>Testnet</td><td><a href="https://shannon-explorer.somnia.network/address/0x9206296Ea3aEE3E6bdC07F7AaeF14DfCf33d865D">0x9206296Ea3aEE3E6bdC07F7AaeF14DfCf33d865D</a><br></td></tr></tbody></table>

### Gas Wallets

The gas wallet is used for pushing data to your contracts. To ensure uninterrupted Oracle operation, please maintain sufficient funds in the gas wallet. You can monitor the wallets below to ensure they remain adequately funded at all times.

<table data-header-hidden><thead><tr><th width="167.76171875">Network</th><th>Contract Address</th></tr></thead><tbody><tr><td>Mainnet</td><td><a href="https://explorer.somnia.network/address/0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a">0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a</a></td></tr><tr><td>Testnet</td><td><a href="https://shannon-explorer.somnia.network/address/0x24384e1c60547b0D5403B21eD9B6FB9457Fb573F">0x24384e1c60547b0d5403b21ed9b6fb9457fb573f</a></td></tr></tbody></table>

### **Oracle Configuration**

* **Pricing Methodology:** MAIR
* **Deviation Threshold:** 0.5% (Triggers price update if exceeded)
* **Refresh Frequency:** Every 120 seconds
* **Heartbeat:** Forced price update every 24 hours

## **Supported Asset Feeds**

### **Mainnet**

<table data-header-hidden><thead><tr><th width="118.98046875"></th><th width="421.53515625"></th><th></th></tr></thead><tbody><tr><td>Asset Ticker</td><td>Adapter Address</td><td>Asset Markets Overview</td></tr><tr><td>USDT</td><td><a href="https://explorer.somnia.network/address/0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267">0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7/">USDT markets</a></td></tr><tr><td>USDC</td><td><a href="https://explorer.somnia.network/address/0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93">0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/">USDC markets</a></td></tr><tr><td>BTC</td><td><a href="https://explorer.somnia.network/address/0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE">0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE</a></td><td><a href="https://www.diadata.org/app/price/asset/Bitcoin/0x0000000000000000000000000000000000000000/">BTC markets</a></td></tr><tr><td>ARB</td><td><a href="https://explorer.somnia.network/address/0x6a96a0232402c2BC027a12C73f763b604c9F77a6">0x6a96a0232402c2BC027a12C73f763b604c9F77a6</a></td><td><a href="https://www.diadata.org/app/price/asset/Arbitrum/0x912CE59144191C1204E64559FE8253a0e49E6548/">ARB markets</a></td></tr><tr><td>SOL</td><td><a href="https://explorer.somnia.network/address/0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234">0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234</a></td><td><a href="https://www.diadata.org/app/price/asset/Solana/0x0000000000000000000000000000000000000000/">SOL markets</a></td></tr><tr><td>WETH</td><td><a href="https://explorer.somnia.network/address/0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52">0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/">WETH markets</a></td></tr><tr><td>SOMI</td><td><a href="https://explorer.somnia.network/address/0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27">0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27</a></td><td><a href="https://www.diadata.org/app/price/asset/Somnia/0x0000000000000000000000000000000000000000/">SOMI markets</a></td></tr></tbody></table>

### **Testnet**

<table data-header-hidden><thead><tr><th width="134.197265625">Asset Ticker</th><th width="407.466796875">Adapter Address</th><th>Asset Markets Overview</th></tr></thead><tbody><tr><td>USDT</td><td><a href="https://shannon-explorer.somnia.network/address/0x67d2C2a87A17b7267a6DBb1A59575C0E9A1D1c3e">0x67d2C2a87A17b7267a6DBb1A59575C0E9A1D1c3e</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xdAC17F958D2ee523a2206206994597C13D831ec7/">USDT markets</a></td></tr><tr><td>USDC</td><td><a href="https://shannon-explorer.somnia.network/address/0x235266D5ca6f19F134421C49834C108b32C2124e">0x235266D5ca6f19F134421C49834C108b32C2124e</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48/">USDC markets</a></td></tr><tr><td>BTC</td><td><a href="https://shannon-explorer.somnia.network/address/0x4803db1ca3A1DA49c3DB991e1c390321c20e1f21">0x4803db1ca3A1DA49c3DB991e1c390321c20e1f21</a></td><td><a href="https://www.diadata.org/app/price/asset/Bitcoin/0x0000000000000000000000000000000000000000/">BTC markets</a></td></tr><tr><td>ARB</td><td><a href="https://shannon-explorer.somnia.network/address/0x74952812B6a9e4f826b2969C6D189c4425CBc19B">0x74952812B6a9e4f826b2969C6D189c4425CBc19B</a></td><td><a href="https://www.diadata.org/app/price/asset/Arbitrum/0x912CE59144191C1204E64559FE8253a0e49E6548/">ARB markets</a></td></tr><tr><td>SOL</td><td><a href="https://shannon-explorer.somnia.network/address/0xD5Ea6C434582F827303423dA21729bEa4F87D519">0xD5Ea6C434582F827303423dA21729bEa4F87D519</a></td><td><a href="https://www.diadata.org/app/price/asset/Solana/0x0000000000000000000000000000000000000000/">SOL markets</a></td></tr><tr><td>WETH</td><td><a href="https://shannon-explorer.somnia.network/address/0x786c7893F8c26b80d42088749562eDb50Ba9601E">0x786c7893F8c26b80d42088749562eDb50Ba9601E</a></td><td><a href="https://www.diadata.org/app/price/asset/Ethereum/0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2/">WETH markets</a></td></tr><tr><td>SOMI</td><td><a href="https://shannon-explorer.somnia.network/address/0xaEAa92c38939775d3be39fFA832A92611f7D6aDe">0xaEAa92c38939775d3be39fFA832A92611f7D6aDe</a></td><td><a href="https://www.diadata.org/app/price/asset/Somnia/0x0000000000000000000000000000000000000000/">SOMI markets</a></td></tr></tbody></table>

## **How the Oracle Works**

DIA oracles continuously fetch and push asset prices **on-chain** using an **oracleUpdater**, which operates within the `DIAOracleV2` contract. The oracle uses **predefined update intervals** and **deviation thresholds** to determine when price updates are necessary.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXelAe-nl93fR4uUB8OHaecQRpe5DuDvy7k-1aMyk_8B1DHX2OzmpuZ00anBlexvcuGcg7oilXmYzTBxTDAeGwdytZmbZicu9yKYhz9rgYPh8SCbuEzia98yvw8F77FUVGWdr7vMJg?key=lW9QgbGCuFAGhXIdiB9dWgLT" alt=""><figcaption></figcaption></figure>

Each asset price feed has an adapter contract, allowing access through the AggregatorV3Interface. You can use the methods `getRoundData` and `latestRoundData` to fetch pricing information. Learn more [here](https://nexus.diadata.org/how-to-guides/migrate-to-dia).

## **Using the Solidity Library**

DIA has a dedicated Solidity library to facilitate the integration of DIA oracles in your own contracts. The library consists of two functions, `getPrice` and `getPriceIfNotOlderThan`.

### Access the library <a href="#access-the-library" id="access-the-library"></a>

```
import { DIAOracleLib } from "./libraries/DIAOracleLib.sol";
```

### `getPrice`

```
function getPrice(
        address oracle,
        string memory key
        )
        public
        view
        returns (uint128 latestPrice, uint128 timestampOflatestPrice);
```

**Returns the price of a specified asset along with the update timestamp**.

### **`getPriceIfNotOlderThan`**

```
function getPriceIfNotOlderThan(
        address oracle,
        string memory key,
        uint128 maxTimePassed
        )
        public
        view
        returns (uint128 price, bool inTime)
    {
```

**Checks if the Oracle price is older than `maxTimePassed`**

## Using DIAOracleV2 Interface

The following contract provides an integration example of retrieving prices and verifying price age.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IDIAOracleV2 {
    function getValue(string memory) external view returns (uint128, 
             uint128);
}

contract DIAOracleSample {

    address diaOracle;

    constructor(address _oracle) {
        diaOracle = _oracle;
    }

    function getPrice(string memory key) 
    external 
    view
    returns (
        uint128 latestPrice, 
        uint128 timestampOflatestPrice
    ) {
        (latestPrice, timestampOflatestPrice) =   
                 IDIAOracleV2(diaOracle).getValue(key); 
    }
}
```

[**Full Example on DIA Docs**](https://www.diadata.org/docs/nexus/how-to-guides/fetch-price-data/solidity)

## Adapter contracts

To consume price data from DIA Oracle, you can use the adapter Smart Contract located at the [adapter address](https://docs.google.com/document/d/1tqK1Va0dOrgZcOjqwLg2fh7hDOK6pQWDfF3ZrljDN-w/edit?tab=t.0#heading=h.iyqc22917lsc) for each asset. This will allow you to access the same methods on the `AggregatorV3Interface` such as `getRoundData` & `latestRoundData`. You can learn more [here](https://www.diadata.org/docs/nexus/how-to-guides/migrate-to-dia).

## **Glossary**

| Term                  | Definition                                                            |
| --------------------- | --------------------------------------------------------------------- |
| **Deviation**         | Percentage threshold that triggers a price update when exceeded.      |
| **Refresh Frequency** | Time interval for checking and updating prices if conditions are met. |
| **Trade Window**      | Time interval used to aggregate trades for price calculation.         |
| **Heartbeat**         | Forced price update at a fixed interval.                              |

## **Support**

If you need further assistance integrating DIA Oracles, reach out through DIA’s[ official documentation](https://docs.diadata.org/) and ask your questions in the #dev-support channel on [Discord](https://discord.com/invite/somnia).

Developers can build secure, real-time, and on-chain financial applications with reliable pricing data by integrating DIA Oracles on Somnia.


# Protofire Price Feeds

## Somnia Mainnet Price Feeds

| **Asset Pair** | **OCR Aggregator**                                                                                                               | **Proxy (read‑only)**                                                                                                            |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| USDC / USD     | [0x4b74EcA574Ce996448b485100e4FFf84866911dF](https://explorer.somnia.network/address/0x4b74EcA574Ce996448b485100e4FFf84866911dF) | [0x843B6812E9Aa67b3773675d2836646BCbd216642](https://explorer.somnia.network/address/0x843B6812E9Aa67b3773675d2836646BCbd216642) |
| ETH / USD      | [0xa3060dd6Bb56EdfB2E0d78c88ef63A974a392D36](https://explorer.somnia.network/address/0xa3060dd6Bb56EdfB2E0d78c88ef63A974a392D36) | [0xeC25a820A6F194118ef8274216a7F225Da019526](https://explorer.somnia.network/address/0xeC25a820A6F194118ef8274216a7F225Da019526) |
| BTC / USD      | [0x3cBdF7F02956c8e946192Bff64bb2Dd470dd589C](https://explorer.somnia.network/address/0x3cBdF7F02956c8e946192Bff64bb2Dd470dd589C) | [0xa57d637618252669fD859B1F4C7bE6F52Bef67ed](https://explorer.somnia.network/address/0xa57d637618252669fD859B1F4C7bE6F52Bef67ed) |

## Somnia Testnet Price Feeds

<table data-header-hidden><thead><tr><th></th><th width="456.400390625"></th></tr></thead><tbody><tr><td><strong>Token Pair</strong></td><td><strong>Contract Address</strong></td></tr><tr><td>USDC/USD</td><td><a href="http://shannon-explorer.somnia.network/address/0xa2515C9480e62B510065917136B08F3f7ad743B4">0xa2515C9480e62B510065917136B08F3f7ad743B4</a></td></tr><tr><td>ETH/USD</td><td><a href="http://shannon-explorer.somnia.network/address/0xd9132c1d762D432672493F640a63B758891B449e">0xd9132c1d762D432672493F640a63B758891B449e</a></td></tr><tr><td>BTC/USD</td><td><a href="http://shannon-explorer.somnia.network/address/0x8CeE6c58b8CbD8afdEaF14e6fCA0876765e161fE">0x8CeE6c58b8CbD8afdEaF14e6fCA0876765e161fE</a></td></tr></tbody></table>

Use any of these when deploying the PriceConsumer Smart Contract for your dApp.

{% embed url="<https://www.youtube.com/watch?v=sH3gxoT4rTM>" %}

## What Are Oracles and Why Do They Matter

This service is powered by Protofire, an infrastructure provider that integrates decentralized oracle networks for Somnia. Learn more at[ protofire.io](https://protofire.io/services/solution/oracle-integration).

Oracles are critical infrastructure in the blockchain ecosystem that enable Smart Contracts to interact with real-world data. Blockchains are deterministic by design and cannot fetch off-chain information directly. This creates a need for oracles, which are trusted data feeds that can push external data, like market prices, sports scores, or weather conditions, into Smart Contracts.

Chainlink is the most widely adopted decentralized oracle network. It allows developers to access reliable data feeds that are resistant to manipulation and downtime. In this tutorial, we focus on Protofire Chainlink Price Feeds, which provide real-time market prices for assets like ETH, BTC, and USDC on Somnia

Smart Contracts that rely on accurate pricing (e.g., lending, trading, insurance) benefit immensely from using decentralized oracles like Protofire. Oracles unlock use cases that were previously impossible due to blockchain isolation.

In this guide, we will build a live crypto price tracker that displays BTC/USD, ETH/USD, and USDC/USD using Protofire Chainlink Price Feeds deployed on the Somnia Testnet.

## Prerequisites

1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
2. Basic knowledge of React & Next.js.

### Solidity Contract (Chainlink Oracle Consumer)

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
    AggregatorV3Interface internal priceFeed;
    constructor(address _priceFeed) {
    priceFeed = AggregatorV3Interface(_priceFeed);
    }
    
     /**
     * Returns the latest price
     */
    function getLatestPrice() public view returns (int256) {
        (
        /* uint80 roundID */,
            int256 price,
            /* uint startedAt */,
            /* uint timeStamp */,
            /* uint80 answeredInRound */
        ) = priceFeed.latestRoundData();
        return price;
}

 /**
     * Returns price decimals
     */
    function getDecimals() public view returns (uint8) {
        return priceFeed.decimals();
    }
}
```

### Code Breakdown

```solidity
import `@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol
```

Pulls in the AggregatorV3Interface from the Chainlink library. This interface allows interaction with Chainlink oracle contracts for Price Feeds.

```solidity
constructor(address _priceFeed) { }
```

The contract's constructor runs once when the contract is deployed. It takes the address of the Protofire Chainlink Price Feed contract and stores it.

```solidity
priceFeed = AggregatorV3Interface(_priceFeed);
```

Instantiates the interface with the provided address, enabling function calls to the external oracle.

```solidity
function getLatestPrice() public view returns (int256) {
        (
            /* uint80 roundID */,
            int256 price,
            /* uint startedAt */,
            /* uint timeStamp */,
            /* uint80 answeredInRound */
        ) = priceFeed.latestRoundData();
        return price;
}
```

`getLatestPrice()` is a public function which is callable from outside the contract. It does not modify blockchain state and returns the latest price from the Chainlink feed. It calls the Chainlink function **`latestRoundData()`** which returns a 5-value tuple:

```solidity
(uint80 roundId, int256 price, uint startedAt, uint timeStamp, uint80 answeredInRound)
```

This function extracts only the price and ignores the rest using commas. It returns the latest price as an int256.

```solidity
function getDecimals() public view returns (uint8) {
        return priceFeed.decimals();
    }
```

`getDecimals()` is a helper function to return the number of decimals used by the price feed. Ensures consumers of the contract know how to scale the price properly.

## How Price Oracle latestRoundData() works

It's helpful to understand how the Price Feed data is structured.

The function `latestRoundData()` from the Chainlink Aggregator interface returns the following tuple:

```solidity
(uint80 roundId, int256 answer, uint startedAt, uint timeStamp, uint80 answeredInRound)
```

* `roundId`: The current round number for the feed
* `price`: The actual price value (e.g. ETH/USD = 1900.42 × 10^8)
* `startedAt`: Timestamp when this round started
* `timeStamp`: When the answer was last updated
* `answeredInRound`: The round in which the answer was submitted<br>

Most price consumer contracts use only `price`, but for more robust designs, `timeStamp` can be checked to ensure the price is recent.

Additionally, Protofire Chainlink Price Feeds often return prices with 8 decimals. This means the raw price value needs to be normalized by dividing it by 10 \*\* decimals. This is essential because Solidity doesn't support floating-point math. Prices are represented using fixed-point math.

For example, if ETH/USD is $1,940.82 and the feed uses 8 decimals, the returned price would be 194082000000. You must divide by 10 \*\* 8 to display $1940.82 in your UI.

Always use the `getDecimals()` method provided by the Aggregator interface to dynamically adjust for different feeds that may use 6, 8, or 18 decimals depending on the asset.

## Deploy with Hardhat

Update ignition/modules/deploy.js:

```typescript
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const PriceConsumerModule = buildModule("PriceConsumerModule", (m) => {
  // Replace this with the correct feed address for your chosen pair
  const feedAddress = m.getParameter(
    "feedAddress",
    "0xd9132c1d762D432672493F640a63B758891B449e" // Example: ETH/USD on Somnia
  );

  const priceConsumer = m.contract("PriceConsumer", [feedAddress]);

  return { priceConsumer };
});

export default PriceConsumerModule;
```

Deploy using:

```bash
npx hardhat ignition deploy ./ignition/modules/Lock.js --network somnia
```

## Building the UI

Now that we’ve deployed the contract and confirmed it fetches live price data from Somnia’s Protofire Oracles, let’s bring it to life with a clean and responsive UI. We’ll use React and Viem.js to build a real-time dashboard that displays crypto prices with auto-refresh and token selection.

Start by creating a new Next.js app.

```bash
npx create-next-app@latest somnia-protofire-example
cd somnia-protofire-example
```

Then, install required dependencies.

```bash
npm install viem
```

Add imports to the `index.js` file

```typescript
import { useEffect, useState } from 'react';
import { createPublicClient, http, parseAbi, formatUnits } from 'viem';
import { somniaTestnet } from 'viem/chains';
```

* useEffect, useState: React hooks for lifecycle and state management.
* createPublicClient: Creates a read-only client to interact with the blockchain.
* http: Defines the transport layer for the client (uses Somnia RPC).
* parseAbi: Parses the contract's ABI.
* formatUnits: Converts big numbers (like token prices) to a human readable format.s.

#### Create a Viem Client for Somnia

```typescript
const client = createPublicClient({
  chain: somniaTestnet,
  transport: http(),
});
```

Creates a blockchain client configured for Somnia Testnet using its RPC URL and allows reading smart contract data without needing a wallet or signer.

#### Declare a variable for the deployed Smart Contracts for your Price Feed Addresses

```typescript
const FEEDS = { //Testnet Price Feeds
  ETH: '0x604CF5063eC760A78d1C089AA55dFf29B90937f9',
  BTC: '0x3dF17dbaa3BA861D03772b501ADB343B4326C676',
  USDC: '0xA4a08Eb26f85A53d40E3f908B406b2a69B1A2441',
};
```

This will map token pairs to their corresponding Chainlink oracle contract addresses deployed on Somnia.<br>

Parse the ABI

```typescript
const abi = parseAbi([
  'function getLatestPrice() view returns (int256)',
  'function getDecimals() view returns (uint8)',
]);
```

#### Set up the State

```typescript
export default function PriceWidget() {
  const [price, setPrice] = useState('');
  const [selectedToken, setSelectedToken] = useState<'ETH' | 'BTC' | 'USDC'>('ETH');
```

The `price` state will store the formatted token price and `selectedToken` will track which token is selected from the dropdown (default: ETH).

#### Fetch the Latest Price

```typescript
const fetchPrice = async () => {
    const contractAddress = FEEDS[selectedToken];
    const [rawPrice, decimals] = await Promise.all([
      client.readContract({ address: contractAddress, abi, functionName: 'getLatestPrice' }),
      client.readContract({ address: contractAddress, abi, functionName: 'getDecimals' }),
    ]);

    const normalized = formatUnits(rawPrice, decimals);
    setPrice(parseFloat(normalized).toFixed(2));
  };
```

Reads the price and its decimal precision from the Chainlink oracle contract. The function declaration uses `Promise.all()` to optimize performance by fetching both at once and formats the price to 2 decimal places for display.

#### Fetch Price on Load & Every 10 Seconds

```typescript
useEffect(() => {
    fetchPrice();
    const interval = setInterval(fetchPrice, 10000);
    return () => clearInterval(interval);
  }, [selectedToken]);
```

The `useEffect` hook runs `fetchPrice()`, once on component mount and every time `selectedToken` changes. The hook also refreshes price data every 10 seconds

Live Price Display in the `return` statement.

```typescript
return (
        ...
        <p>${price}</p>
        ...
        )
```

## Complete Code

<details>

<summary>index.js</summary>

```typescript
import { useEffect, useState } from 'react';
import { createPublicClient, http, parseAbi, formatUnits } from 'viem';
import { somniaTestnet } from 'viem/chains';

const client = createPublicClient({
  chain: somniaTestnet,
  transport: http(),
});

const FEEDS = {
  ETH: '0x604CF5063eC760A78d1C089AA55dFf29B90937f9',
  BTC: '0x3dF17dbaa3BA861D03772b501ADB343B4326C676',
  USDC: '0xA4a08Eb26f85A53d40E3f908B406b2a69B1A2441',
};

const abi = parseAbi([
  'function getLatestPrice() view returns (int256)',
  'function getDecimals() view returns (uint8)',
]);

export default function PriceWidget() {
  const [price, setPrice] = useState('');
  const [selectedToken, setSelectedToken] = useState<'ETH' | 'BTC' | 'USDC'>(
    'ETH'
  );

  const fetchPrice = async () => {
    const contractAddress = FEEDS[selectedToken];
    const [rawPrice, decimals] = await Promise.all([
      client.readContract({
        address: contractAddress,
        abi,
        functionName: 'getLatestPrice',
      }),
      client.readContract({
        address: contractAddress,
        abi,
        functionName: 'getDecimals',
      }),
    ]);

    const normalized = formatUnits(rawPrice, decimals);
    setPrice(parseFloat(normalized).toFixed(2));
  };

  useEffect(() => {
    fetchPrice();
    const interval = setInterval(fetchPrice, 10000);
    return () => clearInterval(interval);
  }, [selectedToken]);

  return (
    <div className='min-h-screen flex items-center justify-center bg-gray-50'>
      <div className='text-center p-6 border border-gray-200 rounded-lg shadow-lg bg-white max-w-sm w-full'>
        <h3 className='text-2xl font-bold mb-4 text-gray-800'>
          {selectedToken}/USD on Somnia
        </h3>
        <select
          value={selectedToken}
          onChange={(e) =>
            setSelectedToken(e.target.value as 'ETH' | 'BTC' | 'USDC')
          }
          className='mb-6 px-4 py-2 border border-gray-300 rounded-md w-full text-gray-700 focus:outline-none focus:ring-2 focus:ring-blue-500'
        >
          <option value='ETH'>ETH/USD</option>
          <option value='BTC'>BTC/USD</option>
          <option value='USDC'>USDC/USD</option>
        </select>
        <p className='text-4xl font-semibold text-blue-600'>${price}</p>
      </div>
    </div>
  );
}
```

</details>

## Conclusion

The Protofire Oracle integration on Somnia provides developers with reliable, on-chain price feeds for key assets like ETH, BTC, and USDC. Using verified oracles and standardized data formats enables accurate, real-time pricing essential for building GaemFi, DeFi, trading, and financial applications.

<br>


# Using Verifiable Randomness (VRF)

Protofire Chainlink’s Verifiable Random Function (VRF) allows developers to securely request random numbers in a tamper-proof and auditable way. It is ideal for gaming, NFT mints, and lotteries. This tutorial walks you through integrating Protofire's Chainlink VRF v2.5 on Somnia Network, using native STT (Somnia Token) as the payment currency.

## Mainnet VRF Smart Contracts

<table><thead><tr><th width="261.458984375">Contract</th><th>Address</th></tr></thead><tbody><tr><td>VRFV2PlusWrapper</td><td><a href="https://explorer.somnia.network/address/0x606b2B36516AB7479D1445Ec14B6B39B44901bf8">0x606b2B36516AB7479D1445Ec14B6B39B44901bf8</a></td></tr><tr><td>LINK Token</td><td><a href="https://explorer.somnia.network/address/0x0a4Db7035284566F6f676991ED418140dC01A2aa">0x0a4Db7035284566F6f676991ED418140dC01A2aa</a></td></tr><tr><td>LINK/NATIVE oracle</td><td><a href="https://explorer.somnia.network/address/0xEBD41881413dD76F42DF2902ee865099af9099B4">0xEBD41881413dD76F42DF2902ee865099af9099B4</a></td></tr></tbody></table>

## Testnet VRF Smart Contracts

<table><thead><tr><th width="261.458984375">Contract</th><th>Address</th></tr></thead><tbody><tr><td>VRFV2PlusWrapper</td><td><a href="https://shannon-explorer.somnia.network/address/0x763cC914d5CA79B04dC4787aC14CcAd780a16BD2">0x763cC914d5CA79B04dC4787aC14CcAd780a16BD2</a></td></tr><tr><td>LINK Token</td><td><a href="https://shannon-explorer.somnia.network/address/0x30C75a2badF9b12733e831fcb5315C8f54e96f6d">0x30C75a2badF9b12733e831fcb5315C8f54e96f6d</a></td></tr><tr><td>LINK/NATIVE oracle</td><td><a href="https://shannon-explorer.somnia.network/address/0xEc00df0e834AB878135b6554bb7438A2Ff66563b">0xEc00df0e834AB878135b6554bb7438A2Ff66563b</a></td></tr></tbody></table>

{% embed url="<https://www.youtube.com/watch?v=7Kn-YcAN0gU>" %}

## Understanding VRF and Why It Matters

Randomness is essential for many blockchain applications, such as Games, Lotteries, Raffles, and NFT drops, but blockchains are deterministic by nature. This means every node must produce the same output given the same inputs. If you try to use on-chain data like `block.timestamp` or `blockhash` as a random source, miners/validators can manipulate these values to influence the outcome. This is where VRF comes in.

### What is VRF?

A Verifiable Random Function (VRF) is a cryptographic method of generating random numbers along with a proof that the result was not tampered with. When using Protofire Chainlink VRF:

1. You request a random number from the VRF service.
2. Protofire Chainlink’s decentralized oracle network generates a random value off-chain along with a cryptographic proof.
3. The proof is verified on-chain before the value is returned to your contract.

This ensures tamper-proof randomness and publicly verifiable results. Where the outcomes are fair.

### Why is VRF important on blockchain?

Without VRF, randomness in blockchain apps can be gamed. With VRF:

* No single party can manipulate the results
* Users can independently verify the randomness
* Applications gain trust from players, participants, and investors

Requesting VRF Data using Protofire Chainlink services relies on two methods: Subscription and Direct Funding

In the Subscription method, Chainlink VRF requests receive funding from subscription accounts. The [Subscription Manager](https://vrf.chain.link/) lets you create an account and pre-pay for your use of Chainlink VRF requests. You can learn more about the subscription method by referencing the Chainlink [documentation](https://docs.chain.link/vrf/v2-5/overview/subscription).\
\
The Direct Funding method doesn't require a subscription and is optimal for one-off requests for randomness. This method also works best for applications where your end-users must pay the fees for VRF because the cost of the request is determined at request time. [Learn more](https://docs.chain.link/vrf/v2-5/overview/direct-funding).<br>

In this guide, we will build a Smart Contract called `RandomNumberConsumer` that:

* Inherits the Protofire Chainlink VRF Wrapper.
* Requests 3 secure random numbers
* Pays for randomness using native STT (no LINK subscription required)
* Emits events and exposes functions to retrieve the randomness
* Handles overpayments and pending request checks

## Prerequisites

Before getting started:

* You are familiar with Solidity (v0.8+)
* You have the VRF Wrapper address for [Protofire ChainLink VRF Wrapper](https://shannon-explorer.somnia.network/address/0x763cC914d5CA79B04dC4787aC14CcAd780a16BD2)

## TL;DR

1. Owner calls `requestRandomNumber()` and sends enough STT `(msg.value)` to cover the fee.
2. Contract uses **VRF Wrapper** to request 3 random words (in native STT).
3. When VRF is ready, the wrapper calls `fulfillRandomWords`, the contract:
   1. verifies the request,
   2. stores the 3 words,
   3. toggles `fulfilled = true`,
   4. emits RandomNumberFulfilled.
4. User Interfaces and Scripts can read `getLatestRandomWord()` or poll `getRequestStatus()`.

<details>

<summary>EXAMPLE - RandomNumberConsumer.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import {VRFConsumerBaseV2Plus} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
import {VRFV2PlusClient} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
import {VRFV2PlusWrapperConsumerBase} from "@chainlink/contracts@1.4.0/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
import {ConfirmedOwner} from "@chainlink/contracts@1.4.0/src/v0.8/shared/access/ConfirmedOwner.sol";

contract RandomNumberConsumer is VRFV2PlusWrapperConsumerBase, ConfirmedOwner {
    uint256 public latestRequestId;
    uint256[] public latestRandomWord;
    bool public fulfilled;

    uint32 public constant CALLBACK_GAS_LIMIT = 2_100_000;
    uint16 public constant REQUEST_CONFIRMATIONS = 3;
    uint32 public constant NUM_WORDS = 3;

    event RandomNumberRequested(uint256 indexed requestId, address indexed requester, uint256 paid);
    event RandomNumberFulfilled(uint256 indexed requestId, uint256[] randomWord);

    error InsufficientPayment(uint256 required, uint256 sent);
    error RequestAlreadyPending();

    constructor(address wrapper) 
        ConfirmedOwner(msg.sender)
        VRFV2PlusWrapperConsumerBase(wrapper) 
    {}

    function requestRandomNumber() external payable onlyOwner {
        // Check if there's already a pending request
        if (latestRequestId != 0 && !fulfilled) {
            revert RequestAlreadyPending();
        }
        
        // Calculate the required payment
        uint256 requestPrice = getRequestPrice();
        if (msg.value < requestPrice) {
            revert InsufficientPayment(requestPrice, msg.value);
        }
        
        // Prepare the extra arguments for native payment
        VRFV2PlusClient.ExtraArgsV1 memory extraArgs = VRFV2PlusClient.ExtraArgsV1({
            nativePayment: true
        });
        bytes memory args = VRFV2PlusClient._argsToBytes(extraArgs);

        // Request randomness
        (uint256 requestId, uint256 paid) = requestRandomnessPayInNative(
            CALLBACK_GAS_LIMIT, 
            REQUEST_CONFIRMATIONS, 
            NUM_WORDS, 
            args
        );

        latestRequestId = requestId;
        fulfilled = false;
        
        emit RandomNumberRequested(requestId, msg.sender, paid);
        
        // Refund excess payment
        if (msg.value > paid) {
            (bool success, ) = msg.sender.call{value: msg.value - paid}("");
            require(success, "Refund failed");
        }
    }

    // This will be called by the VRF Wrapper
    function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
    require(randomWords.length > 0, "No random word returned");
    require(requestId == latestRequestId, "Unexpected request ID");
    latestRandomWord = randomWords;
    fulfilled = true;

      emit RandomNumberFulfilled(requestId, randomWords);
    }

    function getRequestStatus() external view returns (
        uint256 requestId,
        bool isPending,
        bool isFulfilled
    ) {
        return (
            latestRequestId,
            latestRequestId != 0 && !fulfilled,
            fulfilled
        );
    }

    function getLatestRandomWord() external view returns (uint256[] memory) {
        require(fulfilled, "No fulfilled request yet");
        return latestRandomWord;
    }

    /**
     * @notice Get the current price for a VRF request in native tokens
     * @return The price in wei for requesting random numbers
     */
    function getRequestPrice() public view returns (uint256) {
        return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
    }

    /**
     * @notice Withdraw any excess native tokens from the contract
     * @dev Only callable by owner, useful for recovering overpayments
     */
    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        require(balance > 0, "No balance to withdraw");
        
        (bool success, ) = owner().call{value: balance}("");
        require(success, "Withdrawal failed");
    }

    // Allow contract to receive STT for native payment
    receive() external payable {}
}
```

</details>

## Code Breakdown

```solidity
contract RandomNumberConsumer
  is VRFV2PlusWrapperConsumerBase, ConfirmedOwner
```

`VRFV2PlusWrapperConsumerBase` gives you the glue code for requesting randomness from the VRF Wrapper and receiving the callback `(fulfillRandomWords)`. It also exposes the wrapper instance `i_vrfV2PlusWrapper`.

`ConfirmedOwner` is a lightweight ownership module; it lets you restrict actions to the contract owner via onlyOwner.

## State Variables

```solidity
uint256 public latestRequestId;
uint256[] public latestRandomWord;
bool public fulfilled;
```

`latestRequestId` tracks the most recent VRF request ID. Used to make sure the fulfillment we receive matches the last request.\
\
`latestRandomWord` stores the three random words returned by VRF for the latest request.\
\
`fulfilled` marks whether the latest request has finished (prevents overlapping requests and makes UI/state checks easy).

## VRF Request Parameters (constants)

```solidity
uint32 public constant CALLBACK_GAS_LIMIT = 2_100_000;
uint16 public constant REQUEST_CONFIRMATIONS = 3;
uint32 public constant NUM_WORDS = 3;
```

`CALLBACK_GAS_LIMIT` is the max gas VRF can use when calling your `fulfillRandomWords`. Must be large enough for your logic. This example uses headroom for 3 words.\
\
`REQUEST_CONFIRMATIONS` is how many blocks to wait before fulfillment (trade-off between speed and reorg safety).\
\
`NUM_WORDS` This is how many random numbers you want per request. Here, it’s 3.

## Events

```solidity
event RandomNumberRequested(uint256 indexed requestId, address indexed requester, uint256 paid);
event RandomNumberFulfilled(uint256 indexed requestId, uint256[] randomWord);
```

`RandomNumberRequested` is emitted right after submitting a VRF request. Includes the paid cost in native STT.\
\
`RandomNumberFulfilled` is emitted when VRF returns the result, with the three random words.

Events make it easy to monitor behavior from explorers, indexers, or frontends.

## Custom errors

```solidity
error InsufficientPayment(uint256 required, uint256 sent);
error RequestAlreadyPending();
```

`InsufficientPayment` is thrown if msg.value doesn’t cover the VRF native fee at request time.

`RequestAlreadyPending` is thrown if you try to request again while the previous request hasn’t been fulfilled.

Errors are cheaper than require("string") and clearer to reason about.

## Constructor

```solidity
constructor(address wrapper) ConfirmedOwner(msg.sender) VRFV2PlusWrapperConsumerBase(wrapper) {}
```

Takes the VRF V2+ Wrapper address (the on-chain contract that mediates VRF requests) and initializes ownership to the deployer.

## Function Requesting Randomness

```solidity
function requestRandomNumber() external payable onlyOwner {
  // 1) block overlapping requests
  if (latestRequestId != 0 && !fulfilled) revert RequestAlreadyPending();
  
  // 2) compute required fee and validate payment
  uint256 requestPrice = getRequestPrice();
  if (msg.value < requestPrice) revert InsufficientPayment(requestPrice, msg.value);

  // 3) signal native payment to the wrapper
  bytes memory args = VRFV2PlusClient._argsToBytes(
    VRFV2PlusClient.ExtraArgsV1({ nativePayment: true })
  );
  
  // 4) submit request (uses native STT)
  (uint256 requestId, uint256 paid) = requestRandomnessPayInNative(
    CALLBACK_GAS_LIMIT,
    REQUEST_CONFIRMATIONS,
    NUM_WORDS,
    args
  );
  latestRequestId = requestId;
  fulfilled = false;
  emit RandomNumberRequested(requestId, msg.sender, paid);
  
  // 5) refund any excess back to caller
  if (msg.value > paid) {
    (bool ok, ) = msg.sender.call{ value: msg.value - paid }("");
    require(ok, "Refund failed");
  }
}
```

The `requestRandomNumber()` function implements safeguards and processes to ensure reliable VRF operation. First, it enforces safety by preventing spam or overlapping requests, which ensures a predictable user experience and maintains simpler state management. The function then calculates the exact payment required by calling `getRequestPrice()` to determine how much STT the wrapper currently needs, rejecting any transaction with insufficient payment. To specify the payment method, it encodes `nativePayment: true` in the request parameters, instructing the wrapper to charge in native STT tokens rather than LINK.

Once validated, the function submits the request through `requestRandomnessPayInNative()`, which initiates the VRF request to Chainlink while storing the returned requestId and marking the fulfilled status as false to track the pending request.

Finally, the function implements automatic refund logic that returns any excess funds to the user if they overpaid, ensuring users never lose funds due to price variations.

## READ OPERATIONS

### VRF callback (fulfillment)

```solidity
function fulfillRandomWords(
  uint256 requestId,
  uint256[] memory randomWords
) internal override {
  require(randomWords.length > 0, "No random word returned");
  require(requestId == latestRequestId, "Unexpected request ID");


  latestRandomWord = randomWords; // stores 3 words
  fulfilled = true;


  emit RandomNumberFulfilled(requestId, randomWords);
}
```

Called by the VRF Wrapper (not by you) and validates that we actually received words, and the `requestId` matches the latest request (guards against stale/foreign callbacks). It then stores the 3 words and flips `fulfilled = true` and emits a completion event. If you need game logic, derive from these random words inside this function or store and consume later.

### getRequestStatus()

```solidity
function getRequestStatus()
  external
  view
  returns (uint256 requestId, bool isPending, bool isFulfilled)
{
  return (latestRequestId, latestRequestId != 0 && !fulfilled, fulfilled);
}
```

For frontends/monitoring: see the last `request ID`, whether it’s still `pending`, and whether it was `fulfilled`.

### getLatestRandomWord()

```solidity
function getLatestRandomWord() external view returns (uint256[] memory) {
  require(fulfilled, "No fulfilled request yet");
  return latestRandomWord;
}
```

Returns the three words from the most recent fulfilled request, which is actually a string on numbers for example: `93869141573160465677701763703933181905260360385351294458479680637737009096153`

### Pricing Helper

```solidity
function getRequestPrice() public view returns (uint256) {
  return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
}
```

Asks the wrapper how much STT (in wei) you need right now for a request with your chosen `CALLBACK_GAS_LIMIT` and `NUM_WORDS`. Use this in your UI or scripts to fill `msg.value`.

## Conclusion

You've successfully built a secure random number generator on Somnia using Chainlink VRF v2.5. Your `RandomNumberConsumer` Smart Contract provides tamper proof randomness with native STT payment, automatic refunds, and proper request management, everything needed for production use.

### Real-World VRF Use Cases

VRF powers a wide range of blockchain applications where fairness is critical. In gaming, it enables trustworthy dice rolls, loot drops, critical hit calculations, and procedurally generated maps. For NFT collections, VRF ensures unbiased trait assignment during minting, metadata reveals, and rarity distribution. This is crucial when traits can be worth thousands.

Lotteries and raffles benefit from transparent winner selection, whether for small community giveaways or million dollar prize pools. DeFi protocols use VRF for random liquidator selection, fair distribution, and variable reward mechanisms, while DAO governance applications include jury selection for disputes, randomized proposal ordering, and representative sampling for surveys.

With VRF integrated, you're ready to build applications where fairness is cryptographically guaranteed, not just promised. Whether for games, NFTs, or DeFi protocols, your users can verify that randomness is truly random and build trust through mathematics, not faith.

<br>


# Example Applications

This section showcases **hands-on examples** of real applications built on Somnia, from small experimental dApps to full scale and production ready projects.

Each example walks you through architecture, deployment, and integration patterns, helping you understand **how all the pieces fit together:** contracts, subgraphs, oracles, APIs, and SDKs.

You’ll find examples like:

* Token swap dApps (DEX)
* DAO Smart Contract
* DAO User Interface

> These examples are meant to inspire and guide you. Use them as blueprints, remix them, or extend them — and build the next great dApp on Somnia.


# DAO Smart Contract

Decentralized Autonomous Organizations (DAOs) are an innovative way to organize communities where decisions are made collectively without centralized authority. In this tutorial, we’ll explore a simple DAO implemented in Solidity. By the end, you’ll understand how to deploy and interact with this contract.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

## Example Use Case: DAO Implementation in Gaming

DAOs can be particularly impactful in gaming environments. Imagine a massive multiplayer online game (MMO) with a shared in-game economy. A DAO can be used to manage a treasury funded by player contributions, allowing players to propose and vote on game updates, community events, or rewards.

For example:

1. In-Game Treasury Management: Players deposit some of their in-game earnings into a DAO treasury. Proposals for using these funds—such as hosting tournaments or funding new content—are created and voted on.
2. Player-Driven Governance: Gamers vote on new features like maps, characters, or weapons, giving them a direct say in the game's evolution.
3. Community Rewards: DAOs could allocate funds to reward top-performing players or teams, enhancing engagement and competition.

This decentralized approach ensures that game updates align with player interests, creating a more engaging and community-driven gaming experience.

## Prerequisites

Before starting, ensure you have:

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to Connect Your Wallet.
3. You can deploy the Smart Contracts using our Hardhat or Foundry guides.

## Overview of the DAO Contract

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdJhAoATXHidSpCiF3u9C42iFnofuVqoMNCc3QyfzWhFAlxRCtwFtPbxfDexiWAdKTjUprX56sA5e3YtxI-jwfmVsTPkDoucVvkCCpQKLJHPTSD6uGa1xkioMbOfbSjy0IO_ERmeQ?key=96mDMjIvPGrXt0Tzk68UXspT" alt=""><figcaption></figcaption></figure>

The provided DAO contract allows users to:

1. Deposit funds to gain voting power.
2. Create proposals.
3. Vote on proposals.
4. Execute proposals if they pass.

The key features of the contract include:

* Proposal Struct: Stores details of proposals.
* Voting Mechanism: Allows weighted voting based on deposited funds.
* Execution Logic: Ensures proposals are executed only if approved.

## Setting Up the Development Environment

Follow the [Hardhat](/developer/development-frameworks/deploy-with-hardhat) or [Foundry](/developer/development-frameworks/deploy-with-foundry) guides.

## Create the Smart Contract

Create a new file named DAO.sol in the contracts folder and copy the provided contract code.

<details>

<summary>DAO.sol</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract DAO {
    struct Proposal {
        string description; // Proposal details
        uint256 deadline;   // Voting deadline
        uint256 yesVotes;   // Votes in favor
        uint256 noVotes;    // Votes against
        bool executed;      // Whether the proposal has been executed
        address proposer;   // Address of the proposer
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(address => uint256) public votingPower;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    uint256 public totalProposals;
    uint256 public votingDuration = 10 minutes;
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }

    constructor() {
        owner = msg.sender;
    }

    function deposit() external payable {
        require(msg.value == 0.001 ether, "Must deposit STT");
        votingPower[msg.sender] += msg.value;
    }

    function createProposal(string calldata description) external {
        require(votingPower[msg.sender] > 0, "No voting power");

        proposals[totalProposals] = Proposal({
            description: description,
            deadline: block.timestamp + votingDuration,
            yesVotes: 0,
            noVotes: 0,
            executed: false,
            proposer: msg.sender
        });

        totalProposals++;
    }

    function vote(uint256 proposalId, bool support) external {
        Proposal storage proposal = proposals[proposalId];

        require(block.timestamp < proposal.deadline, "Voting has ended");
        require(!hasVoted[proposalId][msg.sender], "Already voted");
        require(votingPower[msg.sender] > 0, "No voting power");

        hasVoted[proposalId][msg.sender] = true;

        if (support) {
            proposal.yesVotes += votingPower[msg.sender];
        } else {
            proposal.noVotes += votingPower[msg.sender];
        }
    }

    function executeProposal(uint256 proposalId) external {
        Proposal storage proposal = proposals[proposalId];

        require(block.timestamp >= proposal.deadline, "Voting still active");
        require(!proposal.executed, "Proposal already executed");
        require(proposal.yesVotes > proposal.noVotes, "Proposal did not pass");

        proposal.executed = true;

        // Logic for proposal execution
        // Example: transfer STT to proposer as a reward for successful vote pass
        payable(proposal.proposer).transfer(0.001 ether);
    }
}

```

</details>

Let’s break down the contract into its main components:

#### Mappings

Mappings are used to store structured data efficiently:

1. **`proposals`**

```solidity
mapping(uint256 => Proposal) public proposals;
```

* Stores all proposals created in the DAO.
* It represents the Proposal `struct` containing details like description, deadline, votes, and proposer.

```solidity
struct Proposal {
        string description; // Proposal details
        uint256 deadline;   // Voting deadline
        uint256 yesVotes;   // Votes in favor
        uint256 noVotes;    // Votes against
        bool executed;      // Whether the proposal has been executed
        address proposer;   // Address of the proposer
    }
```

2. **`votingPower`**

```solidity
mapping(address => uint256) public votingPower;
```

* Tracks the voting power of each address.
* Voting power increases when users deposit funds into the DAO.

3. **`hasVoted`**

```solidity
mapping(uint256 => mapping(address => bool)) public hasVoted;
```

* Tracks whether a specific address has voted on a specific proposal.
* Prevents double voting.

#### Functions

The contract includes several key functions:

1. **`Constructor`**

```solidity
constructor() {
    owner = msg.sender;
}
```

* Sets the deployer as the owner of the contract.\ <br>

2. **`deposit`**

```solidity
function deposit() external payable {
    require(msg.value >= 0.001 ether, "Minimum deposit is 0.001 STT");
    votingPower[msg.sender] += msg.value;
}
```

* Allows users to deposit STT Tokens to gain voting power.
* Increases their votingPower by the amount deposited.

3. **`createProposal`**

```solidity
function createProposal(string calldata description) external {
    require(votingPower[msg.sender] > 0, "No voting power");
    proposals[totalProposals] = Proposal({
        description: description,
        deadline: block.timestamp + votingDuration,
        yesVotes: 0,
        noVotes: 0,
        executed: false,
        proposer: msg.sender
    });
    totalProposals++;
}    
```

* Allows users with voting power to create new proposals.
* Adds the proposal to the proposals mapping.

4. **`vote`**

```solidity
function vote(uint256 proposalId, bool support) external {
    Proposal storage proposal = proposals[proposalId];


    require(block.timestamp < proposal.deadline, "Voting has ended");
    require(!hasVoted[proposalId][msg.sender], "Already voted");
    require(votingPower[msg.sender] > 0, "No voting power");


    hasVoted[proposalId][msg.sender] = true;


    if (support) {
        proposal.yesVotes += votingPower[msg.sender];
    } else {
        proposal.noVotes += votingPower[msg.sender];
    }
}
```

* Allows users to cast a vote on a proposal.
* Updates the **`yesVotes`** or **`noVotes`** in the Proposal struct based on the user's choice.
* Prevents double voting by using the **`hasVoted`** mapping.

5. **`executeProposal`**

```solidity
function executeProposal(uint256 proposalId) external {
    Proposal storage proposal = proposals[proposalId];

    require(block.timestamp >= proposal.deadline, "Voting still active");
    require(!proposal.executed, "Proposal already executed");
    require(proposal.yesVotes > proposal.noVotes, "Proposal did not pass");
    
    proposal.executed = true;
    payable(proposal.proposer).transfer(0.001 ether);
}
```

* Executes a proposal if it passes (more yes votes than no votes).
* Transfers a fixed amount of ETH to the proposer as an example of execution logic.
* Ensures proposals cannot be executed multiple times.

#### Key Variables

1. **`totalProposals`**

```solidity
uint256 public totalProposals;
```

* Tracks the total number of proposals created.

2. **`votingDuration`**

```solidity
uint256 public votingDuration = 10 minutes;
```

* Sets the default duration for voting on proposals.

3. **`owner`**

```solidity
address public owner;
```

* Stores the address of the contract owner.
* Used for functions that require administrative control.

Understanding these components shows how the DAO enables decentralized governance while maintaining transparency and fairness.

## Deploy the Smart Contract to Somnia

Follow the [Hardhat](/developer/development-frameworks/deploy-with-hardhat) or [Foundry](/developer/development-frameworks/deploy-with-foundry) guides. First, compile the Smart Contract to Bytecode by running the Hardhat or Foundry compile instructions.

This is an example deployment script using Hardhat. Create a file in the **`/ignition/module`** folder and name it **`deploy.js`**

```javascript
import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const dao = buildModule("DAO", (m) => {
  const contract = m.contract("DAO");
  return { contract };
});

module.exports = dao;
```

Before running the deploy command, add Somnia Network to the **`hardhat.config.js`** file:

```javascript
const config = {
  solidity: "0.8.28",
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["YOUR_PRIVATE_KEY"],
    },
  },
};
```

Ensure that the deploying address has enough STT Tokens. You can get STT Tokens from the [Faucet](https://devnet.somnia.network/).

Run the deployment script:

```bash
npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia
```

Congratulations. :tada: You have successfully deployed the DAO Smart Contract. :tada:

## Interacting with the Contract

Use the Hardhat console or scripts to interact with the contract.

#### 1. Deposit Funds

Call the deposit function to gain voting power:

```javascript
await dao.deposit({ value: ethers.utils.parseEther("0.001") });
```

#### 2. Create a Proposal

Create a new proposal by calling createProposal:

```javascript
await dao.createProposal("Fund development of new feature");
```

#### 3. Vote on a Proposal

Vote on a proposal by specifying its ID and your support (true for yes, false for no):

```javascript
await dao.vote(0, true); // Vote ‘yes’ on proposal 0
```

#### 4. Execute a Proposal

After the voting deadline, execute the proposal if it has majority votes:

```javascript
await dao.executeProposal(0);
```

## Testing the Contract

#### Writing Tests

Create a test file DAO.test.js in the test folder.

```javascript
const { expect } = require("chai");
const { ethers } = require("hardhat");


describe("DAO", function () {
  let dao;
  let owner, addr1;


  beforeEach(async function () {
    const DAO = await ethers.getContractFactory("DAO");
    dao = await DAO.deploy();
    [owner, addr1] = await ethers.getSigners();
  });


  it("Should allow deposits and update voting power", async function () {
    await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
    expect(await dao.votingPower(addr1.address)).to.equal(ethers.utils.parseEther("0.001"));
  });


  it("Should allow proposal creation", async function () {
    await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
    await dao.connect(addr1).createProposal("Test Proposal");
    const proposal = await dao.proposals(0);
    expect(proposal.description).to.equal("Test Proposal");
  });
});
```

Run the tests:

```bash
npx hardhat test
```

## Enhance the DAO

You can expand this DAO contract by:

1. Adding Governance Tokens: Reward participants with tokens for voting or executing proposals. Follow the ERC20 Token Guide here.
2. Implementing Quorums: Require a minimum number of votes for proposals to pass.
3. Flexible Voting Power: Allow dynamic voting power allocation.

## Conclusion

This tutorial provided a foundational understanding of building and deploying a simple DAO on Somnia. Experiment with enhancements to create more complex governance structures. DAOs are a powerful tool for decentralized decision-making, and the possibilities for innovation are limitless!

<br>


# DAO UI Tutorial p1

Somnia empowers developers to build applications for mass adoption. Smart Contracts deployed on the Somnia Blockchain will sometimes require building a User Interface. This guide will teach you how to build a user interface for a [DAO Smart Contract](/developer/building-dapps/example-applications/dao-smart-contract) using Next.js and React Context. It is divided into three parts. At the end of this guide, you’ll learn how to:

1. Initialize a Next.js project.
2. Set up a global state using the Context API ([**`useContext`**](https://react.dev/reference/react/useContext) hook).
3. Add a global NavBar in **`_app.js`** so it appears on every page.

You will have a basic skeleton of a DApp, ready for **READ/WRITE** operations and UI components —topics we’ll cover in the subsequent articles.

***

## Pre-requisites: <a href="#pre-requisites" id="pre-requisites"></a>

1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to [Connect Your Wallet](https://codex.somnia.network/get-started/connect-your-wallet).

## Create Your Next.js Project

To create a NextJS project, run the command:

```bash
npx create-next-app my-dapp-ui
```

Accept the prompts and change directory into the folder after the build is completed.

This gives you a minimal Next.js setup with a pages folder (holding your routes), a public folder (for static assets), and config files.

#### (Optional) Add Tailwind CSS

If you plan to style your app with Tailwind, install and configure it now:

```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

Then edit your tailwind.config.js:

```javascript
module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Finally, include Tailwind in styles/globals.css:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

***

## Setting Up a React Context for Global State

In many DApps, including this one, developers will manage **Wallet connection** and **State** globally so each page and component in the project can access it without repetitive code. This can be achieved by a [Context](https://react.dev/learn/passing-data-deeply-with-context) following React patterns.

* Create a folder and name it **`contexts`** at the project root or inside **`pages`** directory.
* Inside it, create a file called **`walletcontext.js`** add the following code to the file:

<details>

<summary>walletcontext.js</summary>

```javascript
import { createContext, useContext, useState } from "react";

const WalletContext = createContext();

export function WalletProvider({ children }) {
  const [connected, setConnected] = useState(false);
  const [address, setAddress] = useState("");
  
  async function connectToMetaMask() {
    if (typeof window !== "undefined" && window.ethereum) {
      try {
        await window.ethereum.request({ method: "eth_requestAccounts" });
        // For simplicity, get the first address
        const [userAddress] = window.ethereum.selectedAddress
          ? [window.ethereum.selectedAddress]
          : [];
        setAddress(userAddress);
         setConnected(true);
      } catch (err) {
        console.error("User denied account access:", err);
      }
    } else {
      console.log("MetaMask is not installed!");
    }
  }

  function disconnectWallet() {
    setConnected(false);
    setAddress("");
  }

  // Return the context provider
  return (
<WalletContext.Provider
      value={{
        connected,
        address,
        connectToMetaMask,
        disconnectWallet,
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}


export function useWallet() {
  return useContext(WalletContext);
}

```

</details>

We parse three class methods from React: **`createContext`**, **`useContext`**, an&#x64;**`useState`**

`createContext` is used to create a [context](https://react.dev/learn/passing-data-deeply-with-context) that components can provide or read. In this example, we assign `createContext` to the **`WalletContext`** variable and call the **`Provider`** method on each State and Function to make them available throughout the application.

**`useWallet()`** is a custom hook, so any page or component can do:

```javascript
const { connected, ... } = useWallet()
```

to access the global wallet state i.e. any of the **`Wallet.Provider`** methods

**`connectToMetaMask()`** triggers the MetaMask connection flow.

**`WalletProvider`** manages **State** and **Methods** in the application.

***

## Creating a Global NavBar in \_app.js

Next.js automatically uses **`pages/_app.js`** to initialize every page. We will wrap the entire app in the **`WalletProvider`** inside `_app.js` and inject a **`NavBar`** menu that appears site-wide in the application

#### Create the `_app.js` and add the code:

```javascript
import "../styles/globals.css";
import { WalletProvider } from "../contexts/walletcontext";
import NavBar from "../components/navbar";

function MyApp({ Component, pageProps }) {
  return (
    <WalletProvider>
      <NavBar />
      <main className="pt-16">
        <Component {...pageProps} />
      </main>
    </WalletProvider>
  );
}

export default MyApp;
```

**`<WalletProvider>`** wraps the entire **`<Component />`** tree so that every page can share the same wallet state.

**`<NavBar />`** is placed above **`<main>`**, so it’s visible on all pages. We give **`<main>`** a pt-16 to avoid content hiding behind a fixed navbar.

## NavBar

Create a sub directory **components** and add a file called **`navbar.js`** Add the code:

```javascript
import { useWallet } from "../contexts/walletcontext";
import Link from "next/link";

export default function NavBar() {
  const { connected, address, disconnectWallet } = useWallet();


  return (
    <nav className="fixed w-full bg-white shadow z-50">
      <div className="mx-auto max-w-7xl px-4 flex h-16 items-center justify-between">
        <Link href="/">
          <h1 className="text-xl font-bold text-blue-600">MyDAO</h1>
        </Link>
<div>
          {connected ? (
            <div className="flex items-center space-x-4 text-blue-500">
              <span>{address.slice(0, 6)}...{address.slice(-4)}</span>
              <button onClick={disconnectWallet} className="px-4 py-2 bg-red-500 text-white rounded">
                Logout
              </button>
            </div>
          ) : (
            <span className="text-gray-500">Not connected</span>
          )}
        </div>
      </div>
    </nav>
  );
}
```

It uses **`useWallet()`** to read the global `connected` and `address`states, and the **`disconnectWallet`** function. The truncated `address` is displayed if a user is logged into the App or “Not connected” otherwise. A **Logout** button calls **`disconnectWallet()`** to reset the global state.

***

## Test Your Setup

Start the dev server:

```bash
npm run dev
```

Open **<http://localhost:3000>** in a Web Browser.\
\
You should see your NavBar at the top.

<figure><img src="/files/IcMEfKtYvYYFVcgu1MGc" alt=""><figcaption><p>Not Logged In</p></figcaption></figure>

<figure><img src="/files/91YinQY2WppAlj7r0zCz" alt=""><figcaption><p>Logged In</p></figcaption></figure>

Because we haven’t built any advanced pages yet, you will see a blank home page. The important part is that your **WalletContext** and global **NavBar** are in place and ready for the next steps.\ <br>

***

### 5. Next Steps

* Article 2 shows you how to implement READ/WRITE operations (e.g., deposit, create proposals, vote, etc.) across different Next.js pages—using the same WalletContext to handle contract calls.
* Article 3 will focus on UI components, like forms, buttons, and event handling, tying it all together into a polished user interface.

Congratulations! You have a clean foundation, a Next.js project configured with Tailwind, a global context to manage wallet states, and a NavBar appearing across all routes. This sets the stage for adding contract interactions and advanced UI flows in the subsequent articles. Happy building!

<br>


# DAO UI Tutorial p2

This guide will focus exclusively on implementing Read Operations, which fetches data from your deployed [DAO Smart Contract](/developer/building-dapps/example-applications/dao-smart-contract). By the end of this article, you’ll be able to:

1. Understand how to read data from your smart contract using [viem](/developer/development-frameworks/using-the-viem-library).
2. Implement functions to fetch the total number of proposals and specific proposal details.
3. Integrate these READ operations into your Next.js pages to display dynamic data.

Prerequisite:\
Ensure you’ve completed [Part 1](/developer/building-dapps/example-applications/dao-ui-tutorial-p1) of this series, where you initialized a Next.js project, set up a **`WalletContext`** for global state management, and added a global **`NavBar`**.

***

## Understand READ Operations

In decentralized applications (dApps), Read Operations involve fetching data from the blockchain without altering its state. In the example DAO Smart Contract, this is crucial for displaying dynamic information such as:

* **Total Number of Proposals**: How many proposals have been created.
* **Proposal Details**: Information about a specific proposal, including its description, votes, and execution status.

These operations are read-only and do not require the user to sign any transactions, making them free of gas costs.

We’ll use the viem library to interact with our smart contract and perform these READ operations.

***

## Expand walletcontext.js for Read Operations

**`walletcontext.js`** is the central hub for managing wallet connections and interacting with your Smart Contract. We’ll add two primary READ functions:

1. **`fetchTotalProposals()`**: Retrieves the total number of proposals created.
2. **`fetchProposal(proposalId)`**: Fetches details of a specific proposal by its ID.

### Fetch Total Proposals

Functionality: This function calls the **`totalProposals`** method in your smart contract to determine how many proposals have been created so far.

<details>

<summary>contexts/walletcontext.js</summary>

```javascript
import { createContext, useContext, useState } from "react";
import {
  defineChain,
  createPublicClient,
  createWalletClient,
  http,
  custom,
  parseEther,
} from "viem";
import { ABI } from "../../abi"; // Adjust the path as necessary
// Define Somnia Chain
const SOMNIA = defineChain({
  id: 50312,
  name: "Somnia Testnet",
  nativeCurrency: {
    decimals: 18,
    name: "Ether",
    symbol: "STT",
  },
  rpcUrls: {
    default: {
      http: ["https://dream-rpc.somnia.network"],
    },
  },
  blockExplorers: {
    default: { name: "Explorer", url: "https://somnia-devnet.socialscan.io" },
  },
});
// Create a public client for read operations
const publicClient = createPublicClient({
  chain: SOMNIA,
  transport: http(),
});

const WalletContext = createContext();

export function WalletProvider({ children }) {
  // ---------- STATE ------------
  const [connected, setConnected] = useState(false);
  const [address, setAddress] = useState("");
  const [client, setClient] = useState(null);
  
   // Fetch Total Proposals
  async function fetchTotalProposals() {
    try {
      const result = await publicClient.readContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4",
        abi: ABI,
        functionName: "totalProposals",
      });
      return result; // Returns a BigInt
    } catch (error) {
      console.error("Error fetching totalProposals:", error);
      throw error;
    }
  }


  // Fetch Proposal Details
  async function fetchProposal(proposalId) {
    try {
      const result = await publicClient.readContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4",
        abi: ABI,
        functionName: "proposals",
        args: [parseInt(proposalId)],
      });
      console.log(result);
      return result; // Returns the Proposal struct
    } catch (error) {
      console.error("Error fetching proposal:", error);
      throw error;
    }
  }


  // Provider's value
  return (
    <WalletContext.Provider
      value={{
        connected,
        address,
        client,
        connectToMetaMask,
        disconnectWallet,
        fetchTotalProposals,
        fetchProposal,
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}

// Custom hook to consume context
export function useWallet() {
  return useContext(WalletContext);
}
```

</details>

**`fetchTotalProposals()`** uses **`publicClient.readContract`** to call the **`totalProposals`** function in the Smart Contract. This function returns a `BigInt` representing the total number of proposals.\
\
\&#xNAN;**`fetchProposal(proposalId)`** calls the proposals mapping in your contract to retrieve details of a specific proposal by its `ID`. It returns a struct containing the proposal's **description**, **deadline**, **votes**, **execution status**, and **proposer**.

***

## Integrate Read Operations into Pages

With the read functions in place, let’s integrate them into Next.js pages to display dynamic data.

### Home Page

Update the `index.js` page to show the total number of proposals created in your DAO on the home page.

<details>

<summary>pages/index.js</summary>

```javascript
import { useState, useEffect } from "react";
import ConnectButton from "../components/connectbutton";
import { useWallet } from "../contexts/walletcontext";

export default function Home() {
  const { fetchTotalProposals } = useWallet();
  const [totalProposals, setTotalProposals] = useState(null);
  
    useEffect(() => {
    async function loadData() {
      try {
        const count = await fetchTotalProposals();
        setTotalProposals(count);
      } catch (error) {
        console.error("Failed to fetch total proposals:", error);
      }
    }
    loadData();
  }, [fetchTotalProposals]);
  return (
    <div
      className={`${geistSans.variable} ${geistMono.variable} 
        grid grid-rows-[20px_1fr_20px] items-center justify-items-center 
        min-h-screen p-8 pb-20 gap-16 sm:p-20 
        font-[family-name:var(--font-geist-sans)]`}
    >
      {/* The NavBar is already rendered in _app.js */}
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <h1 className="text-3xl font-bold">Welcome to MyDAO</h1>


        {totalProposals !== null ? (
          <p className="text-lg">
            Total proposals created: {totalProposals.toString()}
          </p>
        ) : (
          <p>Loading total proposals...</p>
        )}

        <ConnectButton />
      </main>
    </div>
  );
}
```

</details>

Here we set the `totalProposals` state variable to store the fetched total number of proposals.\
The **`ConnectButton`** implements the MetaMask authentication. The useWallet hook parse the function from **`WalletContext.`**

The **`useEffect`** Hook is applied on component mount, **`fetchTotalProposals()`** is then called to retrieve the total number of proposals from the Smart Contract.

The page displays a loading message until totalProposals is fetched. Once fetched, it displays the total number of proposals. Users will have to click the **ConnectButton** to connect their wallets for WRITE operations. See part 3.

***

### Fetch-Proposal Page

This page allow users to input a proposal ID, fetch its details, and display them. Additionally, on the page users are provided options for voting on or executing the proposal.

Implementation:

<details>

<summary>pages/fetch-proposal.js</summary>

```javascript
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import { useWallet } from "../contexts/walletcontext";
import { Button, Card, Label, TextInput } from "flowbite-react"; // Optional Flowbite imports

export default function FetchProposalPage() {
  const [proposalId, setProposalId] = useState("");
  const [proposalData, setProposalData] = useState(null);
  const [error, setError] = useState("");
  
  const { connected, fetchProposal, voteOnProposal, executeProposal } = useWallet();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setError(""); // Clear previous errors

    if (!connected) {
      alert("You must connect your wallet first!");
      return;
    }
    if (!proposalId.trim()) {
      setError("Please enter a proposal ID.");
      return;
    }

    try {
      // Fetch the proposal from the contract
      const result = await fetchProposal(proposalId);
      console.log("Fetched Proposal:", result);
      setProposalData(result);
    } catch (err) {
      console.error("Error fetching proposal:", err);
      setError("Failed to fetch proposal. Check console for details.");
    }
  };
  
  useEffect(() => {
    if (proposalData !== null) {
      console.log("Updated Proposal Data:", proposalData);
    }
  }, [proposalData]);

  return (
    <div className="max-w-2xl mx-auto mt-20 p-4">
      <h1 className="text-2xl font-bold mb-4">Fetch a Proposal</h1>

      {/* Form to input Proposal ID */}
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <Label htmlFor="proposal-id" value="Proposal ID" />
          <TextInput
            id="proposal-id"
            type="number"
            placeholder="Enter proposal ID"
            value={proposalId}
            onChange={(e) => setProposalId(e.target.value)}
            required
          />
        </div>

        <Button type="submit" color="blue">
          Fetch
        </Button>
      </form>

      {/* Display Errors */}
      {error && <div className="mt-4 text-red-600">{error}</div>}

      {/* Display Proposal Details */}
      {proposalData && (
        <Card className="mt-8">
          <h2 className="text-xl font-bold mb-2">Proposal #{proposalId}</h2>
          <ul className="list-disc list-inside space-y-1">
            <li>
              <strong>Description:</strong> {proposalData[0]}
            </li>
            <li>
              <strong>Deadline:</strong> {new Date(proposalData[1] * 1000).toLocaleString()}
            </li>
            <li>
              <strong>Yes Votes:</strong> {proposalData[2].toString()}
            </li>
            <li>
              <strong>No Votes:</strong> {proposalData[3].toString()}
            </li>
            <li>
              <strong>Executed:</strong> {proposalData[4] ? "Yes" : "No"}
            </li>
            <li>
              <strong>Proposer:</strong> {proposalData[5]}
            </li>
          </ul>
          </div>
        </Card>
      )}
    </div>
  );
}
```

</details>

The following React states are implemented

* `proposalId`: Stores the user-inputted proposal ID.
* `proposalData`: Stores the fetched proposal details.
* `error`: Captures any errors during fetch, vote, or execute operations.

The **`handleSubmit`** function is used to validate the input and connection status. It then calls the **`fetchProposal(proposalId)`** to retrieve proposal details.

We use a **Form** element for users to input a proposal ID and fetch its details. The **Error** Display is implemented to show any errors that occur during operations. The **Proposal Details** displays the fetched proposal information in a styled card.

The card contains **Vote** and **Execute b**uttons for users to vote **YES/NO** or execute the proposal if eligible.

***

## Edge Cases and Errors

For better UX, consider adding loading indicators while fetching data or awaiting transaction confirmations.

Example:

<details>

<summary>pages/fetch-proposal.js</summary>

```javascript
const [loading, setLoading] = useState(false);

// In handleSubmit
const handleSubmit = async (e) => {
  e.preventDefault();
  setError("");
  setLoading(true);
  // ... rest of the code
  setLoading(false);
};


// In the button
<Button type="submit" color="blue" disabled={loading}>
  {loading ? "Fetching..." : "Fetch"}
</Button>
```

</details>

***

## Test Read Operations

### Populate Some Data

Before testing read operations, make sure there are some proposals created:

1. Load your Smart Contract on the [Remix IDE](/developer/building-dapps/tokens-and-nfts/create-erc20-tokens).
2. Deposit 0.001 ETH to gain voting power.
3. Create one or more proposals via the Create Proposal page.

### Verify Read Operations

Run your application using the command:

```
npm run dev
```

Your application will be running on **`localhost:3000`** in your web browser. Check for the following in the User Interface:

1. Total Proposals: On the Home page, verify that the total number of proposals matches the number you’ve created via Remix IDE.
2. Fetch Proposal Details:\
   \- Navigate to the Fetch-Proposal page.\
   \- Input a valid proposalId (e.g., 0 for the first proposal).\
   \- Verify that all proposal details are accurately displayed.

Monitor the browser console for any errors or logs that can help in debugging.

***

## Conclusion and Next Steps

In Part 2, you successfully implemented Read Operations in your DAO front end:

* **`fetchTotalProposals()`**: Displayed the total number of proposals on the Home page.
* **fetchProposal(proposalId)**: Retrieved and displayed specific proposal details on the Fetch-Proposal page.

#### What's Next?

Stay tuned for Part 3 of this series, where we’ll dive into building UI Components—crafting forms, buttons, and enhancing event handling to create a more polished and user-friendly interface for your DAO dApp.

***

Congratulations! You’ve now built a robust foundation for reading data from your DAO smart contract within your Next.js front end. Keep experimenting and enhancing your dApp’s capabilities in the upcoming sections!

<br>


# DAO UI Tutorial p3

This guide focuses exclusively on implementing Write Operations—interacting with your smart contract to perform actions such as depositing funds, creating proposals, voting, and executing proposals for the [DAO Smart Contract](/developer/building-dapps/example-applications/dao-smart-contract). By the end of this article, you’ll be able to:

1. Understand the Write Operations necessary for the DAO.
2. Implement these operations within the existing [WalletContext](/developer/building-dapps/example-applications/dao-ui-tutorial-p1).
3. Integrate these operations into your Next.js pages with intuitive UI components.
4. Handle transaction states and provide user feedback.

Prerequisite:\
Ensure you’ve completed [Part 2](/developer/building-dapps/example-applications/dao-ui-tutorial-p2) of this series, where you set up the WalletContext for global state management and added a global NavBar.

{% embed url="<https://www.youtube.com/watch?v=sWYfpt7g_p4>" %}

***

## Overview of Write Operations

Write Operations in a DAO involve actions that modify the blockchain state. These include:

* Depositing Funds: Adding 0.001 STT to the DAO to gain voting power.
* Creating Proposals: Submitting new proposals for the DAO to consider.
* Voting on Proposals: Casting votes (Yes/No) on existing proposals.
* Executing Proposals: Finalizing and implementing approved proposals.

These operations require users to sign transactions, incurring gas fees. Proper handling of these interactions is crucial for a smooth user experience.

***

## Expand WalletContext with Write Functions

We’ll enhance the existing WalletContext by adding functions to handle the aforementioned write operations. This centralized approach ensures that all blockchain interactions are managed consistently.

### Implement deposit

Allows users to deposit a fixed amount of ETH (e.g., 0.001 ETH) into the DAO contract to gain voting power.

<details>

<summary>contexts/walletContext.js</summary>

```javascript
import { parseEther } from "viem";

export function WalletProvider({ children }) {
  // ...existing state and actions
  
  // Deposit Function
  const deposit = async () => {
    if (!client || !address) {
      alert("Please connect your wallet first!");
      return;
    }
    try {
      const tx = await client.writeContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
        abi: ABI,
        functionName: "deposit",
        value: parseEther("0.001"), // 0.001 STT
      });
      console.log("Deposit Transaction:", tx);
      alert("Deposit successful! Transaction hash: " + tx.hash);
    } catch (error) {
      console.error("Deposit failed:", error);
      alert("Deposit failed. Check console for details.");
    }
  };
  // ...other functions
  return (
    <WalletContext.Provider
      value={{
        // ...existing values
        deposit,
        // ...other write functions
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}
```

</details>

**`parseEther("0.001")`**: Converts 0.001 STT to Wei, the smallest denomination of Ether.

**`writeContract`**: Sends a transaction to call the deposit function on the DAO contract, transferring 0.001 STT.

### Implement createProposal

Allows users to create a new proposal by submitting a description.

<details>

<summary>contexts/walletContext.js</summary>

```javascript
export function WalletProvider({ children }) {
  // ...existing state and actions
  
  // Create Proposal Function
  const createProposal = async (description) => {
    if (!client || !address) {
      alert("Please connect your wallet first!");
      return;
    }
    try {
      const tx = await client.writeContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
        abi: ABI,
        functionName: "createProposal",
        args: [description],
      });
      console.log("Create Proposal Transaction:", tx);
      alert("Proposal created! Transaction hash: " + tx.hash);
    } catch (error) {
      console.error("Create Proposal failed:", error);
      alert("Failed to create proposal. Check console for details.");
    }
  };
  // ...other functions
  return (
    <WalletContext.Provider
      value={{
        // ...existing values
        createProposal,
        // ...other write functions
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}
```

</details>

**`createProposal(description)`**: Takes a proposal description as an argument and sends a transaction to the DAO contract to create the proposal.

### Implement voteOnProposal

Allows users to vote on a specific proposal by its ID, supporting either a Yes or No vote.

<details>

<summary>contexts/walletContext.js</summary>

```javascript
export function WalletProvider({ children }) {
// ...existing state and actions
  // Vote on Proposal Function
  const voteOnProposal = async (proposalId, support) => {
    if (!client || !address) {
      alert("Please connect your wallet first!");
      return;
    }
    try {
      const tx = await client.writeContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
        abi: ABI,
        functionName: "vote",
        args: [parseInt(proposalId), support],
      });
      console.log("Vote Transaction:", tx);
      alert(`Voted ${support ? "YES" : "NO"} on proposal #${proposalId}! Transaction hash: ${tx.hash}`);
    } catch (error) {
      console.error("Vote failed:", error);
      alert("Voting failed. Check console for details.");
    }
  };
  // ...other functions
  return (
    <WalletContext.Provider
      value={{
        // ...existing values
        voteOnProposal,
        // ...other write functions
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}
```

</details>

**`voteOnProposal(proposalId, support)`**: Takes a proposal ID and a boolean indicating support (**`true for Yes, false for No`**). Sends a transaction to cast the vote.

### Implement executeProposal

Allows users to execute a proposal if it meets the necessary conditions (e.g., quorum reached).

<details>

<summary>contexts/walletContext.js</summary>

```javascript
export function WalletProvider({ children }) {
  // ...existing state and actions
  // Execute Proposal Function
  const executeProposal = async (proposalId) => {
    if (!client || !address) {
      alert("Please connect your wallet first!");
      return;
    }
    try {
      const tx = await client.writeContract({
        address: "0x7be249A360DB86E2Cf538A6893f37aFd89C70Ab4", // Your DAO contract address
        abi: ABI,
        functionName: "executeProposal",
        args: [parseInt(proposalId)],
      });
      console.log("Execute Proposal Transaction:", tx);
      alert(`Proposal #${proposalId} executed! Transaction hash: ${tx.hash}`);
    } catch (error) {
      console.error("Execute Proposal failed:", error);
      alert("Execution failed. Check console for details.");
    }
  };
  // ...other functions
  return (
    <WalletContext.Provider
      value={{
        // ...existing values
        executeProposal,
        // ...other write functions
      }}
    >
      {children}
    </WalletContext.Provider>
  );
}
```

</details>

**`executeProposal(proposalId)`**: Takes a proposal ID and sends a transaction to execute the proposal.

***

## Integrate Write Operations

With the write functions added to WalletContext, the next step is to integrate these operations into your Next.js pages, providing users with interactive UI components to perform actions.

### Create-Proposal Page

Allow users to submit new proposals by entering a description.

<details>

<summary>pages/create-proposal.js</summary>

```javascript
import { useState } from "react";
import { useRouter } from "next/router";
import { useWallet } from "../contexts/walletContext";
import { Label, TextInput, Button, Alert } from "flowbite-react";

export default function CreateProposalPage() {
  const [description, setDescription] = useState("");
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState("");
  const [error, setError] = useState("");
  
  const { connected, createProposal } = useWallet();
  const router = useRouter();

const handleSubmit = async (e) => {
    e.preventDefault();
    setError("");
    setSuccess("");
    if (!connected) {
      setError("You must connect your wallet first!");
      return;
    }
    if (!description.trim()) {
      setError("Proposal description cannot be empty!");
      return;
    }
    setLoading(true);
    try {
      await createProposal(description.trim());
      setSuccess("Proposal created successfully!");
      setDescription("");
      // Optionally redirect to home or another page
      // router.push("/");
    } catch (err) {
      console.error("Error creating proposal:", err);
      setError("Failed to create proposal. Check console for details.");
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div className="max-w-2xl mx-auto mt-20 p-4">
      <h1 className="text-2xl font-bold mb-4">Create Proposal</h1>
      
      {error && (
        <Alert color="failure" className="mb-4">
          <span>
            <span className="font-medium">Error!</span> {error}
          </span>
        </Alert>
      )}
      
      {success && (
        <Alert color="success" className="mb-4">
          <span>
            <span className="font-medium">Success!</span> {success}
          </span>
        </Alert>
      )}
      
      <form onSubmit={handleSubmit} className="space-y-4">
        <div>
          <Label htmlFor="proposal-description" value="Proposal Description" />
          <TextInput
            id="proposal-description"
            type="text"
            placeholder="Enter proposal description..."
            value={description}
            onChange={(e) => setDescription(e.target.value)}
            required
          />
        </div>
        <Button type="submit" color="purple" disabled={loading}>
          {loading ? "Submitting..." : "Submit Proposal"}
        </Button>
      </form>
    </div>
  );
}
```

</details>

State Variables:

* **description**: Stores the user's input for the proposal description.
* **loading**: Indicates whether the submission is in progress.
* **success** **& error**: Handle user feedback messages.

The **`handleSubmit`** function undergoes validation, ensuring that the user is connected and has entered a description. It then calls the **`createProposal`** from **`WalletContext`**. It displays success or error messages based on the outcome.

The return statement contains the UI Components:

* Label & TextInput: For user input.
* Button: Triggers the submission. Disabled and shows a loading state when processing.
* Alert: Provides visual feedback for success and error messages.

### Fetch-Proposal Page: Vote and Execution

Allow users to fetch proposal details, vote on them, and execute if eligible.

<details>

<summary>pages/fetch-proposal.js</summary>

```javascript
import { useState } from "react";
import { useWallet } from "../contexts/walletContext";
import { Button, Card, Label, TextInput, Spinner, Alert } from "flowbite-react";

export default function FetchProposalPage() {
  const [proposalId, setProposalId] = useState("");
  const [proposalData, setProposalData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [voting, setVoting] = useState(false);
  const [executing, setExecuting] = useState(false);
  const [error, setError] = useState("");
  const [success, setSuccess] = useState("");
  
  const { connected, fetchProposal, voteOnProposal, executeProposal } = useWallet();
  
  const handleFetch = async (e) => {
    e.preventDefault();
    setError("");
    setSuccess("");
    setProposalData(null);
    if (!connected) {
      setError("You must connect your wallet first!");
      return;
    }
    if (!proposalId.trim()) {
      setError("Please enter a proposal ID.");
      return;
    }
    setLoading(true);
    try {
      const data = await fetchProposal(proposalId);
      setProposalData(data);
    } catch (err) {
      console.error("Error fetching proposal:", err);
      setError("Failed to fetch proposal. Check console for details.");
    } finally {
      setLoading(false);
    }
  };
  const handleVote = async (support) => {
    setError("");
    setSuccess("");
    setVoting(true);
    try {
      await voteOnProposal(proposalId, support);
      setSuccess(`Successfully voted ${support ? "YES" : "NO"} on proposal #${proposalId}.`);
      // Optionally, refresh the proposal data
      const updatedData = await fetchProposal(proposalId);
      setProposalData(updatedData);
    } catch (err) {
      console.error("Error voting:", err);
      setError("Voting failed. Check console for details.");
    } finally {
      setVoting(false);
    }
  };
  const handleExecute = async () => {
    setError("");
    setSuccess("");
    setExecuting(true);
    try {
      await executeProposal(proposalId);
      setSuccess(`Proposal #${proposalId} executed successfully.`);
      // Optionally, refresh the proposal data
      const updatedData = await fetchProposal(proposalId);
      setProposalData(updatedData);
    } catch (err) {
      console.error("Error executing proposal:", err);
      setError("Execution failed. Check console for details.");
    } finally {
      setExecuting(false);
    }
  };
  return (
    <div className="max-w-2xl mx-auto mt-20 p-4">
      <h1 className="text-2xl font-bold mb-4">Fetch a Proposal</h1>
      {/* Form to input Proposal ID */}
      <form onSubmit={handleFetch} className="space-y-4">
        <div>
          <Label htmlFor="proposal-id" value="Proposal ID" />
          <TextInput
            id="proposal-id"
            type="number"
            placeholder="Enter proposal ID"
            value={proposalId}
            onChange={(e) => setProposalId(e.target.value)}
            required
          />
        </div>
        <Button type="submit" color="blue" disabled={loading}>
          {loading ? <Spinner aria-label="Loading" /> : "Fetch Proposal"}
        </Button>
      </form>
      {/* Display Errors */}
      {error && (
        <Alert color="failure" className="mt-4">
          <span>
            <span className="font-medium">Error!</span> {error}
          </span>
        </Alert>
      )}
      {/* Display Success Messages */}
      {success && (
        <Alert color="success" className="mt-4">
          <span>
            <span className="font-medium">Success!</span> {success}
          </span>
        </Alert>
      )}
      {/* Display Proposal Details */}
      {proposalData && (
        <Card className="mt-8">
          <h2 className="text-xl font-bold mb-2">Proposal #{proposalId}</h2>
          <ul className="list-disc list-inside space-y-1">
            <li>
              <strong>Description:</strong> {proposalData[0]}
            </li>
            <li>
              <strong>Deadline:</strong> {new Date(proposalData[1] * 1000).toLocaleString()}
            </li>
            <li>
              <strong>Yes Votes:</strong> {proposalData[2].toString()}
            </li>
            <li>
              <strong>No Votes:</strong> {proposalData[3].toString()}
            </li>
            <li>
              <strong>Executed:</strong> {proposalData[4] ? "Yes" : "No"}
            </li>
            <li>
              <strong>Proposer:</strong> {proposalData[5]}
            </li>
          </ul>
          {/* Voting Buttons */}
          <div className="mt-4 flex space-x-4">
            <Button
              color="green"
              onClick={() => handleVote(true)}
              disabled={voting || executing}
            >
              {voting ? <Spinner aria-label="Loading" size="sm" /> : "Vote YES"}
            </Button>
            <Button
              color="red"
              onClick={() => handleVote(false)}
              disabled={voting || executing}
            >
              {voting ? <Spinner aria-label="Loading" size="sm" /> : "Vote NO"}
            </Button>
          </div>
          {/* Execute Button */}
          {!proposalData[4] && (
            <div className="mt-4">
              <Button
                color="purple"
                onClick={handleExecute}
                disabled={executing || voting}
              >
                {executing ? <Spinner aria-label="Loading" size="sm" /> : "Execute Proposal"}
              </Button>
            </div>
          )}
        </Card>
      )}
    </div>
  );
}
```

</details>

State Variables:

* **proposalId**: User input for the proposal ID.
* **proposalData**: Stores fetched proposal details.
* **loading, voting, executing**: Manage the loading states for different operations.
* **error & success**: Handle feedback messages.

The **`handleFetch`** function ensures the user is connected and has entered a valid proposal ID. It calls **`fetchProposal`** to retrieve proposal details, and displays error messages if fetching fails.

The **`handleVote`** function has the parameters for indicating the Voter support (**true** for **Yes**, **false** for **No**). The function processes Vote, by calling **`voteOnProposal`** with the provided **`proposalId`** and **`support`**&#x70;arameter`.` It returns success or error messages based on the outcome. It re-fetches the proposal to reflect updated vote counts.

The **`handleExecute`** function processes execution by calling **`executeProposal`** with the provided ~~**proposalId**~~. It returns success or error messages based on the outcome, and re-fetches the proposal to reflect execution status.

The return statement contains the UI Components:

* Label & TextInput: For inputting the proposal ID.
* Button: Triggers fetching, voting, and executing actions. Disabled and shows a spinner during processing.
* Alert: Provides visual feedback for success and error messages.
* Card: Displays the fetched proposal details in a structured format.
* Voting & Execution Buttons: Allow users to interact with the proposal directly from the details view.

***

## Transaction States and User Feedback

Clear feedback during and after transactions enhances user experience and trust in your dApp. Consider using libraries like[ react-toastify](https://github.com/fkhadra/react-toastify) for non-intrusive notifications. Example with Toast Notifications:

Install react-toastify

```bash
npm install react-toastify
```

Inside the `_app.js`

```javascript
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
function MyApp({ Component, pageProps }) {
  return (
    <WalletProvider>
      <NavBar />
      <main className="pt-16">
        <Component {...pageProps} />
        <ToastContainer />
      </main>
    </WalletProvider>
  );
}
export default MyApp;
```

In your WalletContext or Pages

```javascript
import { toast } from 'react-toastify';
// Replace alert with toast
toast.success("Deposit successful! Transaction hash: " + tx.hash);
toast.error("Deposit failed. Check console for details.");
```

The benefits of React Toastify are that it is **non-intrusive** and modal alerts don't block users. It is also customizable, which allows developers to style and position as needed.

***

## Test Write Operations

Thorough testing ensures the reliability and trustworthiness of your dApp. Here's how to effectively test your write operations:

#### Connect to a Test Network

Run your application using the command:

```
npm run dev
```

Your application will be running on **`localhost:3000`** in your web browser.

#### Obtain STT from the [Faucet.](https://devnet.somnia.network/)

#### Perform Write Operations

Deposit Funds:

* Navigate to the Home page.
* Click the Deposit button.
* Confirm the transaction in MetaMask.
* Verify that the deposit is reflected in the contract's state.

Create a Proposal:

* Go to the Create Proposal page.
* Enter a proposal description and submit.
* Confirm the transaction in MetaMask.
* Check that the proposal count increments and the new proposal is retrievable.

Vote on a Proposal:

* Access the Fetch-Proposal page.
* Enter a valid proposal ID and fetch details.
* Click Vote YES or Vote NO.
* Confirm the transaction in MetaMask.
* Verify that vote counts update accordingly.

Execute a Proposal:

* After a proposal meets the execution deadline, execute it.
* Confirm the transaction in MetaMask.
* Ensure that the proposal's execution status is updated.
* Monitor the browser console for any errors or logs that aid in debugging.

***

## Conclusion and Next Steps

In Part 3, you successfully implemented Write Operations in your DAO front end:

* **`deposit`**: Allowed users to deposit ETH into the DAO.
* **`createProposal`**: Enabled users to submit new proposals.
* **`voteOnProposal`**: Provided functionality to cast votes on proposals.
* **`executeProposal`**: Facilitated the execution of approved proposals.

***

Congratulations! Using Next.js and React Context, you’ve built a fully functional set of Write Operations for your DAO’s front end. This foundation empowers users to interact with your DAO seamlessly, fostering a decentralized, community-driven governance model.

Continue refining and expanding your dApp to cater to your community’s evolving needs.

<br>


# Building a Simple DEX on Somnia

This tutorial will guide you through building a simple Decentralized Exchange (DEX) on Somnia, inspired by Uniswap V2's core mechanics. We'll implement the essential components: Liquidity Pools, Automated Market Maker (AMM) logic, and Token Swapping functionality.

{% hint style="success" %}
Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the [guide](/get-started/getting-started-for-mainnet) on Moving from Testnet to Mainnet.
{% endhint %}

## Prerequisites

1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.
2. You can deploy the Smart Contracts using our [Hardhat](/developer/development-frameworks/deploy-with-hardhat) or [Foundry](/developer/development-frameworks/deploy-with-foundry) guides.

## Core Concepts

### Automated Market Maker (AMM)

At the core of an AMM is a Liquidity Pool, a Smart Contract that holds reserves of two (or more) tokens (e.g., STT and USDC). Users trade directly against this pool instead of with other users.

Most AMMs (like Uniswap v2) use the constant product formula:

x⋅y=kx \cdot y = kx⋅y=k

* `x` = amount of Token A in the pool
* `y` = amount of Token B in the pool
* `k` = constant (must remain unchanged)

This ensures price adjustment based on supply and demand.

If a user wants to buy Token A with Token B:

* They send Token B into the Pool
* The Smart Contract calculates how much Token A to send out to maintain `x * y = k`
* As more Token A is withdrawn, its price increases (slippage)

Anyone can deposit an equal value of both tokens into the pool to become a Liquidity Provider (LP) and earn a share of the trading fees (e.g., 0.3%).

> **AMMs are fully decentralized with no need for counterparties and are Open and permissionless to use and contribute liquidity.**

### Liquidity Pools

Pairs of tokens locked in Smart Contracts that facilitate trading without traditional order books.

## Smart Contract Architecture

We'll build three main contracts:

1. `SomniaFactory`: Creates and manages pair contracts
2. `SomniaPair`: Individual liquidity pool for token pairs
3. `SomniaRouter`: User-facing contract for swaps and liquidity management

## Implementation

### ERC-20 Interface

First, let's define the ERC-20 interface we'll use.

<details>

<summary>SomniaPair.sol</summary>

```solidity
// IERC20.sol
pragma solidity ^0.8.0;

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

```

</details>

### SomniaPair Contract

The pair contract manages individual Liquidity Pools.

<details>

<summary>SomniaPair.sol</summary>

```solidity
// SomniaPair.sol
pragma solidity ^0.8.0;

import "./IERC20.sol";

contract SomniaPair is IERC20 {
    uint256 public constant MINIMUM_LIQUIDITY = 10**3;
    
    address public factory;
    address public token0;
    address public token1;
    
    uint112 private reserve0;
    uint112 private reserve1;
    uint32 private blockTimestampLast;
    
    uint256 public kLast;
    
    uint256 private unlocked = 1;
    modifier lock() {
        require(unlocked == 1, 'LOCKED');
        unlocked = 0;
        _;
        unlocked = 1;
    }
    
    // ERC-20 Implementation
    string public constant name = "Somnia LP Token";
    string public constant symbol = "SLP";
    uint8 public constant decimals = 18;
    uint256 public totalSupply;
    mapping(address => uint256) public balanceOf;
    mapping(address => mapping(address => uint256)) public allowance;
    
    event Mint(address indexed sender, uint256 amount0, uint256 amount1);
    event Burn(address indexed sender, uint256 amount0, uint256 amount1, address indexed to);
    event Swap(
        address indexed sender,
        uint256 amount0In,
        uint256 amount1In,
        uint256 amount0Out,
        uint256 amount1Out,
        address indexed to
    );
    event Sync(uint112 reserve0, uint112 reserve1);
    
    constructor() {
        factory = msg.sender;
    }
    
    function initialize(address _token0, address _token1) external {
        require(msg.sender == factory, 'FORBIDDEN');
        token0 = _token0;
        token1 = _token1;
    }
    
    function getReserves() public view returns (uint112 _reserve0, uint112 _reserve1, uint32 _blockTimestampLast) {
        _reserve0 = reserve0;
        _reserve1 = reserve1;
        _blockTimestampLast = blockTimestampLast;
    }
    
    function _safeTransfer(address token, address to, uint256 value) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FAILED');
    }
    
    function _update(uint256 balance0, uint256 balance1, uint112 _reserve0, uint112 _reserve1) private {
        require(balance0 <= type(uint112).max && balance1 <= type(uint112).max, 'OVERFLOW');
        uint32 blockTimestamp = uint32(block.timestamp % 2**32);
        reserve0 = uint112(balance0);
        reserve1 = uint112(balance1);
        blockTimestampLast = blockTimestamp;
        emit Sync(reserve0, reserve1);
    }
    
    function mint(address to) external lock returns (uint256 liquidity) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        uint256 balance0 = IERC20(token0).balanceOf(address(this));
        uint256 balance1 = IERC20(token1).balanceOf(address(this));
        uint256 amount0 = balance0 - _reserve0;
        uint256 amount1 = balance1 - _reserve1;
        
        uint256 _totalSupply = totalSupply;
        if (_totalSupply == 0) {
            liquidity = sqrt(amount0 * amount1) - MINIMUM_LIQUIDITY;
            _mint(address(0), MINIMUM_LIQUIDITY); // permanently lock the first MINIMUM_LIQUIDITY tokens
        } else {
            liquidity = min(amount0 * _totalSupply / _reserve0, amount1 * _totalSupply / _reserve1);
        }
        require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED');
        _mint(to, liquidity);
        
        _update(balance0, balance1, _reserve0, _reserve1);
        kLast = uint256(reserve0) * reserve1;
        emit Mint(msg.sender, amount0, amount1);
    }
    
    function burn(address to) external lock returns (uint256 amount0, uint256 amount1) {
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        address _token0 = token0;
        address _token1 = token1;
        uint256 balance0 = IERC20(_token0).balanceOf(address(this));
        uint256 balance1 = IERC20(_token1).balanceOf(address(this));
        uint256 liquidity = balanceOf[address(this)];
        
        uint256 _totalSupply = totalSupply;
        amount0 = liquidity * balance0 / _totalSupply;
        amount1 = liquidity * balance1 / _totalSupply;
        require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED');
        _burn(address(this), liquidity);
        _safeTransfer(_token0, to, amount0);
        _safeTransfer(_token1, to, amount1);
        balance0 = IERC20(_token0).balanceOf(address(this));
        balance1 = IERC20(_token1).balanceOf(address(this));
        
        _update(balance0, balance1, _reserve0, _reserve1);
        kLast = uint256(reserve0) * reserve1;
        emit Burn(msg.sender, amount0, amount1, to);
    }
    
    function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data) external lock {
        require(amount0Out > 0 || amount1Out > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
        (uint112 _reserve0, uint112 _reserve1,) = getReserves();
        require(amount0Out < _reserve0 && amount1Out < _reserve1, 'INSUFFICIENT_LIQUIDITY');
        
        uint256 balance0;
        uint256 balance1;
        {
            address _token0 = token0;
            address _token1 = token1;
            require(to != _token0 && to != _token1, 'INVALID_TO');
            if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out);
            if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out);
            balance0 = IERC20(_token0).balanceOf(address(this));
            balance1 = IERC20(_token1).balanceOf(address(this));
        }
        uint256 amount0In = balance0 > _reserve0 - amount0Out ? balance0 - (_reserve0 - amount0Out) : 0;
        uint256 amount1In = balance1 > _reserve1 - amount1Out ? balance1 - (_reserve1 - amount1Out) : 0;
        require(amount0In > 0 || amount1In > 0, 'INSUFFICIENT_INPUT_AMOUNT');
        {
            uint256 balance0Adjusted = balance0 * 1000 - amount0In * 3;
            uint256 balance1Adjusted = balance1 * 1000 - amount1In * 3;
            require(balance0Adjusted * balance1Adjusted >= uint256(_reserve0) * _reserve1 * 1000**2, 'K');
        }
        
        _update(balance0, balance1, _reserve0, _reserve1);
        emit Swap(msg.sender, amount0In, amount1In, amount0Out, amount1Out, to);
    }
    
    // Helper functions
    function sqrt(uint256 y) internal pure returns (uint256 z) {
        if (y > 3) {
            z = y;
            uint256 x = y / 2 + 1;
            while (x < z) {
                z = x;
                x = (y / x + x) / 2;
            }
        } else if (y != 0) {
            z = 1;
        }
    }
    
    function min(uint256 x, uint256 y) internal pure returns (uint256 z) {
        z = x < y ? x : y;
    }
    
    // ERC-20 functions
    function _mint(address to, uint256 value) internal {
        totalSupply += value;
        balanceOf[to] += value;
        emit Transfer(address(0), to, value);
    }
    
    function _burn(address from, uint256 value) internal {
        balanceOf[from] -= value;
        totalSupply -= value;
        emit Transfer(from, address(0), value);
    }
    
    function approve(address spender, uint256 value) external returns (bool) {
        allowance[msg.sender][spender] = value;
        emit Approval(msg.sender, spender, value);
        return true;
    }
    
    function transfer(address to, uint256 value) external returns (bool) {
        balanceOf[msg.sender] -= value;
        balanceOf[to] += value;
        emit Transfer(msg.sender, to, value);
        return true;
    }
    
    function transferFrom(address from, address to, uint256 value) external returns (bool) {
        if (allowance[from][msg.sender] != type(uint256).max) {
            allowance[from][msg.sender] -= value;
        }
        balanceOf[from] -= value;
        balanceOf[to] += value;
        emit Transfer(from, to, value);
        return true;
    }
}

```

</details>

### SomniaFactory Contract

The factory creates and tracks all pair contracts.

<details>

<summary>SomniaFactory.sol</summary>

```solidity
// SomniaFactory.sol
pragma solidity ^0.8.0;

import "./SomniaPair.sol";

contract SomniaFactory {
    mapping(address => mapping(address => address)) public getPair;
    address[] public allPairs;
    
    event PairCreated(address indexed token0, address indexed token1, address pair, uint256);
    
    function allPairsLength() external view returns (uint256) {
        return allPairs.length;
    }
    
    function createPair(address tokenA, address tokenB) external returns (address pair) {
        require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
        (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'ZERO_ADDRESS');
        require(getPair[token0][token1] == address(0), 'PAIR_EXISTS');
        
        bytes memory bytecode = type(SomniaPair).creationCode;
        bytes32 salt = keccak256(abi.encodePacked(token0, token1));
        assembly {
            pair := create2(0, add(bytecode, 32), mload(bytecode), salt)
        }
        
        SomniaPair(pair).initialize(token0, token1);
        getPair[token0][token1] = pair;
        getPair[token1][token0] = pair;
        allPairs.push(pair);
        emit PairCreated(token0, token1, pair, allPairs.length);
    }
}

```

</details>

### SomniaRouter Contract

The router provides user-friendly functions for swapping and liquidity management.

<details>

<summary>SomniaRouter.sol</summary>

```solidity
// SomniaRouter.sol
pragma solidity ^0.8.0;

import "./IERC20.sol";
import "./SomniaPair.sol";
import "./SomniaFactory.sol";

contract SomniaRouter {
    address public immutable factory;
    
    modifier ensure(uint256 deadline) {
        require(deadline >= block.timestamp, 'EXPIRED');
        _;
    }
    
    constructor(address _factory) {
        factory = _factory;
    }
    
    // Add liquidity
    function addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256 amountA, uint256 amountB, uint256 liquidity) {
        (amountA, amountB) = _addLiquidity(tokenA, tokenB, amountADesired, amountBDesired, amountAMin, amountBMin);
        address pair = pairFor(tokenA, tokenB);
        _safeTransferFrom(tokenA, msg.sender, pair, amountA);
        _safeTransferFrom(tokenB, msg.sender, pair, amountB);
        liquidity = SomniaPair(pair).mint(to);
    }
    
    function _addLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountADesired,
        uint256 amountBDesired,
        uint256 amountAMin,
        uint256 amountBMin
    ) internal returns (uint256 amountA, uint256 amountB) {
        if (SomniaFactory(factory).getPair(tokenA, tokenB) == address(0)) {
            SomniaFactory(factory).createPair(tokenA, tokenB);
        }
        (uint256 reserveA, uint256 reserveB) = getReserves(tokenA, tokenB);
        if (reserveA == 0 && reserveB == 0) {
            (amountA, amountB) = (amountADesired, amountBDesired);
        } else {
            uint256 amountBOptimal = quote(amountADesired, reserveA, reserveB);
            if (amountBOptimal <= amountBDesired) {
                require(amountBOptimal >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
                (amountA, amountB) = (amountADesired, amountBOptimal);
            } else {
                uint256 amountAOptimal = quote(amountBDesired, reserveB, reserveA);
                assert(amountAOptimal <= amountADesired);
                require(amountAOptimal >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
                (amountA, amountB) = (amountAOptimal, amountBDesired);
            }
        }
    }
    
    // Remove liquidity
    function removeLiquidity(
        address tokenA,
        address tokenB,
        uint256 liquidity,
        uint256 amountAMin,
        uint256 amountBMin,
        address to,
        uint256 deadline
    ) public ensure(deadline) returns (uint256 amountA, uint256 amountB) {
        address pair = pairFor(tokenA, tokenB);
        SomniaPair(pair).transferFrom(msg.sender, pair, liquidity);
        (uint256 amount0, uint256 amount1) = SomniaPair(pair).burn(to);
        (address token0,) = sortTokens(tokenA, tokenB);
        (amountA, amountB) = tokenA == token0 ? (amount0, amount1) : (amount1, amount0);
        require(amountA >= amountAMin, 'INSUFFICIENT_A_AMOUNT');
        require(amountB >= amountBMin, 'INSUFFICIENT_B_AMOUNT');
    }
    
    // Swap functions
    function swapExactTokensForTokens(
        uint256 amountIn,
        uint256 amountOutMin,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256[] memory amounts) {
        amounts = getAmountsOut(amountIn, path);
        require(amounts[amounts.length - 1] >= amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT');
        _safeTransferFrom(
            path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
    
    function swapTokensForExactTokens(
        uint256 amountOut,
        uint256 amountInMax,
        address[] calldata path,
        address to,
        uint256 deadline
    ) external ensure(deadline) returns (uint256[] memory amounts) {
        amounts = getAmountsIn(amountOut, path);
        require(amounts[0] <= amountInMax, 'EXCESSIVE_INPUT_AMOUNT');
        _safeTransferFrom(
            path[0], msg.sender, pairFor(path[0], path[1]), amounts[0]
        );
        _swap(amounts, path, to);
    }
    
    // Internal functions
    function _swap(uint256[] memory amounts, address[] memory path, address _to) internal {
        for (uint256 i; i < path.length - 1; i++) {
            (address input, address output) = (path[i], path[i + 1]);
            (address token0,) = sortTokens(input, output);
            uint256 amountOut = amounts[i + 1];
            (uint256 amount0Out, uint256 amount1Out) = input == token0 ? (uint256(0), amountOut) : (amountOut, uint256(0));
            address to = i < path.length - 2 ? pairFor(output, path[i + 2]) : _to;
            SomniaPair(pairFor(input, output)).swap(
                amount0Out, amount1Out, to, new bytes(0)
            );
        }
    }
    
    // Library functions
    function sortTokens(address tokenA, address tokenB) internal pure returns (address token0, address token1) {
        require(tokenA != tokenB, 'IDENTICAL_ADDRESSES');
        (token0, token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA);
        require(token0 != address(0), 'ZERO_ADDRESS');
    }
    
    function pairFor(address tokenA, address tokenB) internal view returns (address pair) {
        (address token0, address token1) = sortTokens(tokenA, tokenB);
        pair = SomniaFactory(factory).getPair(token0, token1);
        require(pair != address(0), 'PAIR_DOES_NOT_EXIST');
    }
    
    function getReserves(address tokenA, address tokenB) internal view returns (uint256 reserveA, uint256 reserveB) {
        (address token0,) = sortTokens(tokenA, tokenB);
        (uint256 reserve0, uint256 reserve1,) = SomniaPair(pairFor(tokenA, tokenB)).getReserves();
        (reserveA, reserveB) = tokenA == token0 ? (reserve0, reserve1) : (reserve1, reserve0);
    }
    
    function quote(uint256 amountA, uint256 reserveA, uint256 reserveB) internal pure returns (uint256 amountB) {
        require(amountA > 0, 'INSUFFICIENT_AMOUNT');
        require(reserveA > 0 && reserveB > 0, 'INSUFFICIENT_LIQUIDITY');
        amountB = amountA * reserveB / reserveA;
    }
    
    function getAmountOut(uint256 amountIn, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountOut) {
        require(amountIn > 0, 'INSUFFICIENT_INPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
        uint256 amountInWithFee = amountIn * 997;
        uint256 numerator = amountInWithFee * reserveOut;
        uint256 denominator = reserveIn * 1000 + amountInWithFee;
        amountOut = numerator / denominator;
    }
    
    function getAmountIn(uint256 amountOut, uint256 reserveIn, uint256 reserveOut) internal pure returns (uint256 amountIn) {
        require(amountOut > 0, 'INSUFFICIENT_OUTPUT_AMOUNT');
        require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY');
        uint256 numerator = reserveIn * amountOut * 1000;
        uint256 denominator = (reserveOut - amountOut) * 997;
        amountIn = (numerator / denominator) + 1;
    }
    
    function getAmountsOut(uint256 amountIn, address[] memory path) public view returns (uint256[] memory amounts) {
        require(path.length >= 2, 'INVALID_PATH');
        amounts = new uint256[](path.length);
        amounts[0] = amountIn;
        for (uint256 i; i < path.length - 1; i++) {
            (uint256 reserveIn, uint256 reserveOut) = getReserves(path[i], path[i + 1]);
            amounts[i + 1] = getAmountOut(amounts[i], reserveIn, reserveOut);
        }
    }
    
    function getAmountsIn(uint256 amountOut, address[] memory path) public view returns (uint256[] memory amounts) {
        require(path.length >= 2, 'INVALID_PATH');
        amounts = new uint256[](path.length);
        amounts[amounts.length - 1] = amountOut;
        for (uint256 i = path.length - 1; i > 0; i--) {
            (uint256 reserveIn, uint256 reserveOut) = getReserves(path[i - 1], path[i]);
            amounts[i - 1] = getAmountIn(amounts[i], reserveIn, reserveOut);
        }
    }
    
    function _safeTransferFrom(address token, address from, address to, uint256 value) private {
        (bool success, bytes memory data) = token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, value));
        require(success && (data.length == 0 || abi.decode(data, (bool))), 'TRANSFER_FROM_FAILED');
    }
}

```

</details>

You can deploy the Smart Contracts in the order they have been created above. Create `Token A` and `Token B` or, for example, use `wSTT` and `USDC` Token Pairs.\
\
The next step will be to create a Pool Pair for `Token A` and `Token B`. Deploy the Pair and Add Liquidity. Then users will be able to create `SWAP` using the `Router` Smart Contract.

### Usage Example

<details>

<summary>Add Liquidity</summary>

```solidity
// Approve router to spend tokens
tokenA.approve(router.address, amountA);
tokenB.approve(router.address, amountB);

// Add liquidity
router.addLiquidity(
    tokenA.address,
    tokenB.address,
    amountA,
    amountB,
    minAmountA,
    minAmountB,
    userAddress,
    deadline
);
```

</details>

<details>

<summary>Swap Tokens</summary>

```solidity
// Approve router to spend input token
tokenA.approve(router.address, amountIn);

// Swap exact tokens for tokens
address[] memory path = new address[](2);
path[0] = tokenA.address;
path[1] = tokenB.address;

router.swapExactTokensForTokens(
    amountIn,
    minAmountOut,
    path,
    userAddress,
    deadline
);
```

</details>

<details>

<summary>Remove Liquidity</summary>

```solidity
// Approve router to spend LP tokens
pair.approve(router.address, lpTokenAmount);

// Remove liquidity
router.removeLiquidity(
    tokenA.address,
    tokenB.address,
    lpTokenAmount,
    minAmountA,
    minAmountB,
    userAddress,
    deadline
);
```

</details>

### Test Your DEX

Create a test script to verify functionality:

<details>

<summary>TestDEX.sol</summary>

```solidity
pragma solidity ^0.8.0;

import "./SomniaFactory.sol";
import "./SomniaRouter.sol";
import "./IERC20.sol";

contract TestDEX {
    SomniaFactory public factory;
    SomniaRouter public router;
    
    constructor() {
        factory = new SomniaFactory();
        router = new SomniaRouter(address(factory));
    }
    
    function testCreatePair(address tokenA, address tokenB) external returns (address) {
        return factory.createPair(tokenA, tokenB);
    }
    
    function testAddLiquidity(
        address tokenA,
        address tokenB,
        uint256 amountA,
        uint256 amountB
    ) external {
        IERC20(tokenA).transferFrom(msg.sender, address(this), amountA);
        IERC20(tokenB).transferFrom(msg.sender, address(this), amountB);
        
        IERC20(tokenA).approve(address(router), amountA);
        IERC20(tokenB).approve(address(router), amountB);
        
        router.addLiquidity(
            tokenA,
            tokenB,
            amountA,
            amountB,
            0,
            0,
            msg.sender,
            block.timestamp + 3600
        );
    }
}
```

</details>

## Conclusion

This tutorial has walked you through implementing a fully functional decentralized exchange (DEX)on Somnia, demonstrating the core Smart Contract architecture that powers Automated Market Makers - AMM. By building these contracts from scratch, you've gained hands-on experience with the fundamental mechanics of DEXs:\
1\. How Liquidity Pools maintain token reserves.\
2\. How the Constant Product Formula enables permissionless trading.\
3\. How router contracts abstract complex operations into user-friendly interfaces.\
\
The implementation covers essential features including liquidity provision with LP token minting, atomic swaps with built-in slippage protection, and multi-hop routing for indirect trading pairs. While this represents a complete Smart Contract foundation, production deployments would benefit from additional features such as concentrated liquidity (as seen in Uniswap V3), dynamic fee tiers, flash loan functionality, and comprehensive governance mechanisms. The modular architecture we've implemented makes these enhancements straightforward to integrate.\
\
As you continue developing on Somnia, remember that the Smart Contracts presented here are just one layer of a complete DEX ecosystem; You'll need to consider frontend interfaces, liquidity incentives, and integration with other DeFi protocols to create a thriving exchange. Most importantly, ensure thorough testing on Somnia's testnet and seek professional security audits before deploying any contracts handling real value. This foundation provides you with the knowledge and code necessary to contribute to Somnia's DeFi ecosystem, whether by deploying your own DEX or building innovative features on top of existing protocols.


# Security


# Smart Contract Security 101

Smart contract vulnerabilities can lead to devastating financial losses and compromise user trust. This guide examines three critical vulnerability categories with hands-on examples that demonstrate both vulnerable patterns and secure implementations.

Learn to identify and prevent the most critical security vulnerabilities in smart contracts through practical examples. This section covers real attack vectors with vulnerable and secure code implementations, along with comprehensive prevention strategies.

What you'll achieve: Recognize vulnerable code patterns, understand attack mechanisms, and learn to implement secure alternatives.

## Prerequisites

✅ Required:

* Understanding of Solidity function execution
* Basic knowledge of EVM call mechanics

✅ Recommended:

* Access to Remix IDE for testing examples
* Somnia Testnet setup for deployment testing

## Vulnerability Overview

The following table categorizes vulnerabilities by severity and implementation difficulty:

| Vulnerability Type | Severity | Frequency | Detection Difficulty | Financial Impact |
| ------------------ | -------- | --------- | -------------------- | ---------------- |
| Reentrancy         | Critical | High      | Medium               | Very High        |
| Access Control     | Critical | Medium    | Low                  | High             |
| Integer Overflow   | High     | Low       | Low                  | Medium           |

***

## 1. Reentrancy Vulnerabilities

#### Understanding the Attack

Reentrancy occurs when a contract calls an external contract before updating its internal state, allowing the external contract to call back and exploit the inconsistent state.

#### Vulnerable Implementation Analysis

<details>

<summary>ReentrancyVulnerable Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

/**
 * @title ReentrancyVulnerable
 * @notice Classic vulnerable withdraw pattern for tutorial exploitation in Remix.
 * @dev Demonstrates why updating state after external calls is dangerous.
 *
 * WARNING: Use only in local/Remix test environments. Do NOT deploy vulnerable contracts with real funds on mainnet.
 */
contract ReentrancyVulnerable {
    mapping(address => uint256) public deposits;

    event Deposited(address indexed who, uint256 amount);
    event Withdrawn(address indexed who, uint256 amount);

    function deposit() external payable {
        require(msg.value > 0, "zero deposit");
        deposits[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external {
        require(deposits[msg.sender] >= amount, "insufficient balance");

        // INTERACTION before EFFECTS -> vulnerable to reentrancy
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "transfer failed");

        // EFFECTS: update balance after external call -> attacker can re-enter here
        deposits[msg.sender] -= amount;
        emit Withdrawn(msg.sender, amount);
    }

    function contractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
```

</details>

Why This Contract Is Vulnerable:

* Interaction (external call) occurs before updating internal state.
* No reentrancy guard or checks-effects-interactions.
* Attackers can re-enter withdraw() during the external call and drain funds.

#### Attack Mechanism

{% stepper %}
{% step %}
**Attack contract & setup**

Attacker contract:

```solidity
contract Attacker {
    ReentrancyVulnerable target;
    uint256 public constant ATTACK_AMOUNT = 1 ether;

    constructor(address _target) {
        target = ReentrancyVulnerable(_target);
    }

    function attack() external payable {
        require(msg.value >= ATTACK_AMOUNT, "need at least 1 ETH");
        target.deposit{value: ATTACK_AMOUNT}();
        target.withdraw(ATTACK_AMOUNT);
    }

    receive() external payable {
        if (address(target).balance >= ATTACK_AMOUNT) {
            target.withdraw(ATTACK_AMOUNT);
        }
    }
}
```

Initial state example:

* Vulnerable contract has 10 ETH from other users
* Attacker deposits 1 ETH
  {% endstep %}

{% step %}
**First withdraw and reentry**

* Attacker calls `target.withdraw(1 ether)`.
* Contract sends 1 ETH via `call` to attacker -> triggers attacker's `receive()`.
* Because deposits were not updated yet, `deposits[attacker]` still equals 1 ETH.
* In `receive()`, attacker calls `target.withdraw(1 ether)` again.
  {% endstep %}

{% step %}
**Recursive drain and final impact**

* Reentrancy repeats until contract balance < attack amount.
* Attacker drains nearly all ETH while only depositing 1 ETH.
* Final state: contract drained, attacker profit huge, other users lose funds.
  {% endstep %}
  {% endstepper %}

#### Secure Implementation

<details>

<summary>Secure Reentrancy Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract ReentrancySecure {
    mapping(address => uint256) private _deposits;

    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status = _NOT_ENTERED;

    event Deposited(address indexed who, uint256 amount);
    event Withdrawn(address indexed who, uint256 amount);

    modifier nonReentrant() {
        require(_status == _NOT_ENTERED, "reentrant");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }

    function deposit() external payable {
        require(msg.value > 0, "zero deposit");
        _deposits[msg.sender] += msg.value;
        emit Deposited(msg.sender, msg.value);
    }

    function withdraw(uint256 amount) external nonReentrant {
        uint256 bal = _deposits[msg.sender];
        require(bal >= amount, "insufficient balance");

        // EFFECTS
        _deposits[msg.sender] = bal - amount;

        // INTERACTIONS
        (bool sent, ) = msg.sender.call{value: amount}("");
        require(sent, "transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    function depositOf(address who) external view returns (uint256) {
        return _deposits[who];
    }

    function contractBalance() external view returns (uint256) {
        return address(this).balance;
    }
}
```

</details>

This secure contract uses a reentrancy guard to prevent recursive calls:

**Key Components:**

1. **Reentrancy Guard Variables:**
   * `_NOT_ENTERED = 1` and `_ENTERED = 2`: Lock states
   * `_status`: Tracks if function is currently executing
2. **nonReentrant Modifier:**
   * Checks if function is already running (`_status == _NOT_ENTERED`)
   * Sets lock before execution (`_status = _ENTERED`)
   * Releases lock after completion (`_status = _NOT_ENTERED`)
3. **Secure withdraw() Function:**
   * Uses `nonReentrant` modifier to block recursive calls
   * Updates balance BEFORE making external call (checks-effects-interactions pattern)
   * External call cannot trigger another withdrawal due to the guard

**How It Prevents Attacks:**

* First call sets `_status = _ENTERED`
* Any reentrant call fails the `require(_status == _NOT_ENTERED)` check
* Transaction reverts with "reentrant" error
* Only one withdrawal per transaction is possible

**Security Features:**

* Reentrancy protection through state locking
* State updates before external calls
* Automatic revert on attack attempts

Security fixes:

* Checks-Effects-Interactions: state updated before external calls.
* Reentrancy guard (mutex) via nonReentrant modifier.
* Double protection: both CEI and guard prevent recursive drains.

#### Prevention Strategies (summary)

| Strategy                     | Implementation                     | Effectiveness | Gas Cost |
| ---------------------------- | ---------------------------------- | ------------- | -------- |
| CEI Pattern                  | Update state before external calls | High          | Low      |
| Reentrancy Guard             | Mutex-style protection             | Very High     | Medium   |
| Pull Payment                 | Users withdraw instead of push     | High          | Low      |
| OpenZeppelin ReentrancyGuard | Battle-tested implementation       | Very High     | Medium   |

***

## 2. Access Control Vulnerabilities

#### Understanding the Flaw

Poor access control allows unauthorized users to execute privileged functions, leading to complete contract compromise.

#### Vulnerable Implementation Analysis

<details>

<summary>Access Control Vulnerable Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract AccessControlVulnerable {
    mapping(bytes32 => mapping(address => bool)) public roles;

    bytes32 public constant ADMIN = keccak256("ADMIN");
    bytes32 public constant WRITER = keccak256("WRITER");

    string public data;

    constructor() {
        roles[ADMIN][msg.sender] = true;
        roles[WRITER][msg.sender] = true;
    }

    function grantRole(bytes32 role, address account) external {
        require(account != address(0), "zero account");
        require(roles[role][msg.sender], "only role-holder");
        roles[role][account] = true;
    }

    function revokeRole(bytes32 role, address account) external {
        require(roles[role][msg.sender], "only role-holder");
        roles[role][account] = false;
    }

    function write(string calldata newData) external {
        require(roles[WRITER][msg.sender], "not writer");
        data = newData;
    }

    function emergencyReset() external {
        require(roles[ADMIN][msg.sender], "not admin");
        data = "";
    }

    function hasRole(bytes32 role, address account) external view returns (bool) {
        return roles[role][account];
    }
}solid
```

</details>

Critical issues:

* Any role-holder can grant the same role to others → role escalation.
* No owner/admin separation.
* No events emitted for role changes (no audit trail).

#### Attack Mechanism

{% stepper %}
{% step %}
**Setup & initial compromise**

Initial state:

* Deployer has ADMIN and WRITER.
* Legitimate user Alice has WRITER.
* Attacker Bob has no roles.

Attacker compromises Alice or Alice becomes malicious.
{% endstep %}

{% step %}
**Role escalation sequence**

* Alice calls `grantRole(WRITER, bob)` → Bob becomes WRITER.
* Bob (now a role-holder) calls `grantRole(ADMIN, bob)` if the model allows it.
* Because the contract lets role-holders grant roles, escalation to ADMIN can occur.
  {% endstep %}

{% step %}
**Final state & impact**

* Bob gains ADMIN powers: can grant/revoke roles and perform privileged actions.
* No events: attack may be undetected.
* Complete contract compromise possible.
  {% endstep %}
  {% endstepper %}

#### Secure Implementation

<details>

<summary>Secure Access Control Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract AccessControlSecure {
    mapping(bytes32 => mapping(address => bool)) private _roles;

    bytes32 public constant ADMIN = keccak256("ADMIN");
    bytes32 public constant WRITER = keccak256("WRITER");

    address public immutable owner;

    string public data;

    event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
    event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
    event DataWritten(address indexed sender, string newData);

    constructor() {
        owner = msg.sender;
        _roles[ADMIN][msg.sender] = true;
        emit RoleGranted(ADMIN, msg.sender, msg.sender);
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "only owner");
        _;
    }

    modifier onlyAdmin() {
        require(_roles[ADMIN][msg.sender], "only admin");
        _;
    }

    function grantRole(bytes32 role, address account) external {
        require(account != address(0), "zero address");
        if (role == ADMIN) {
            require(msg.sender == owner, "only owner can grant admin");
        } else {
            require(_roles[ADMIN][msg.sender], "only admin can grant");
        }
        if (!_roles[role][account]) {
            _roles[role][account] = true;
            emit RoleGranted(role, account, msg.sender);
        }
    }

    function revokeRole(bytes32 role, address account) external {
        require(account != address(0), "zero address");
        if (role == ADMIN) {
            require(msg.sender == owner, "only owner can revoke admin");
        } else {
            require(_roles[ADMIN][msg.sender], "only admin can revoke");
        }
        if (_roles[role][account]) {
            _roles[role][account] = false;
            emit RoleRevoked(role, account, msg.sender);
        }
    }

    function write(string calldata newData) external {
        require(_roles[WRITER][msg.sender], "not writer");
        data = newData;
        emit DataWritten(msg.sender, newData);
    }

    function hasRole(bytes32 role, address account) external view returns (bool) {
        return _roles[role][account];
    }
}
```

</details>

This secure contract implements proper role hierarchy:

**Key Security Features:**

1. **Immutable Owner:**
   * Owner address cannot be changed after deployment
   * Only owner can grant/revoke ADMIN roles
2. **Role Hierarchy:**
   * Owner > Admin > Writer
   * Only ADMIN can grant WRITER roles
   * WRITER cannot grant any roles
3. **Secure Role Management:**
   * Different permissions for granting ADMIN vs other roles
   * Input validation prevents zero address assignments
   * Events log all role changes for audit trail

**How it prevents attacks:**

* WRITER cannot escalate to ADMIN (only owner can grant ADMIN)
* Clear separation of permissions
* Complete audit trail of all role changes

**How Security Works:**

**Attack Prevention:**

* Alice (WRITER) tries to grant ADMIN to Bob → Fails (only owner can grant ADMIN)
* Bob tries to grant himself ADMIN → Fails (only owner can grant ADMIN)
* Role escalation is impossible

**Legitimate Operations:**

* Owner can grant ADMIN roles
* ADMIN can grant WRITER roles
* All changes are logged with events

**Result:**

* Clear hierarchy prevents unauthorized escalation
* Complete audit trail of all role changes
* Attack is blocked by proper permission checks

#### Best Practices (summary)

| Practice           | Purpose                      | Implementation           |
| ------------------ | ---------------------------- | ------------------------ |
| Role Hierarchy     | Prevent privilege escalation | Owner > Admin > User     |
| Immutable Owner    | Prevent ownership takeover   | Set in constructor       |
| Event Logging      | Enable audit trails          | Emit on all role changes |
| Zero Address Check | Prevent accidental locks     | Validate addresses       |

***

## 3. Integer Overflow/Underflow

#### Understanding the Issue

Integer overflow/underflow occurs when arithmetic operations exceed the maximum or minimum values for the data type, potentially causing unexpected behavior or security vulnerabilities in financial calculations.

#### Vulnerable Implementation Analysis

<details>

<summary>Integer Overflow Vulnerable Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;

contract IntegerOverflowVulnerable {
    mapping(address => uint256) public balance;
    uint256 public totalSupply;

    function mint(address to, uint256 amount) external {
        require(to != address(0), "zero address");
        balance[to] += amount;
        totalSupply += amount;
    }

    function transfer(address from, address to, uint256 amount) external {
        require(to != address(0), "zero address");
        // Missing check: require(balance[from] >= amount)
        balance[from] -= amount; // may revert in 0.8+; would underflow silently pre-0.8
        balance[to] += amount;
    }

    function batchAdd(address[] calldata recipients, uint256[] calldata amounts) external {
        require(recipients.length == amounts.length, "length mismatch");
        for (uint256 i = 0; i < recipients.length; i++) {
            require(recipients[i] != address(0), "zero addr in batch");
            balance[recipients[i]] += amounts[i];
            totalSupply += amounts[i];
        }
    }

    function balanceOf(address who) external view returns (uint256) {
        return balance[who];
    }
}
```

</details>

Critical vulnerabilities:

* Unchecked addition (overflow) on balances and totalSupply.
* Unchecked subtraction (underflow) in transfer.
* Batch operations amplify overflow risks.
* No access control on minting.

#### Attack Mechanisms

{% stepper %}
{% step %}
**Overflow via mint()**

Initial state:

* totalSupply = expected supply
* Attacker computes amount = max\_uint256 - totalSupply + 1
* Calling mint(attacker, amount) causes totalSupply and balance to wrap to 0 (pre-0.8 behavior).
* State becomes corrupted.
  {% endstep %}

{% step %}
**Underflow via transfer() (pre-0.8 behavior)**

* Attacker with 0 balance attempts to transfer a huge amount.
* balance\[attacker] -= amount underflows to near max uint256.
* Attacker ends up with immense balance.
  {% endstep %}

{% step %}
**Batch amplification**

* Attacker uses many entries with large but individually safe amounts.
* Cumulative additions overflow totalSupply or balances.
* Economic model breaks.
  {% endstep %}
  {% endstepper %}

#### Secure Implementation

<details>

<summary>Secure Integer Overflow Contract</summary>

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract IntegerOverflowSecure {
    mapping(address => uint256) public balances;
    address public owner;
    uint256 public totalSupply;
    uint256 public constant MAX_SUPPLY = type(uint256).max; // example

    constructor() {
        owner = msg.sender;
    }

    modifier onlyOwner() {
        require(msg.sender == owner, "Not owner");
        _;
    }

    function mint(address to, uint256 amount) external onlyOwner {
        require(to != address(0), "Zero address");
        require(amount > 0, "Zero amount");

        require(balances[to] + amount >= balances[to], "Balance overflow");
        require(totalSupply + amount >= totalSupply, "Total supply overflow");
        require(totalSupply + amount <= MAX_SUPPLY, "exceeds max supply");

        balances[to] += amount;
        totalSupply += amount;
    }

    function transfer(address to, uint256 amount) external {
        require(to != address(0), "Zero address");
        require(amount > 0, "Zero amount");

        require(balances[msg.sender] >= amount, "Insufficient balance");
        require(balances[to] + amount >= balances[to], "Balance overflow");

        balances[msg.sender] -= amount;
        balances[to] += amount;
    }

    function batchAdd(uint256[] calldata amounts) external {
        uint256 currentBalance = balances[msg.sender];

        for (uint256 i = 0; i < amounts.length; i++) {
            require(currentBalance + amounts[i] >= currentBalance, "Overflow in batch");
            currentBalance += amounts[i];
        }

        balances[msg.sender] = currentBalance;
    }

    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }
}
```

</details>

This secure contract prevents overflow/underflow attacks:

**Key Security Features:**

1. **Overflow/Underflow Checks:**
   * `require(a + b >= a, "overflow")` - detects addition overflow
   * `require(a >= b, "underflow")` - prevents subtraction underflow
   * Checks happen BEFORE arithmetic operations
2. **Supply Cap Enforcement:**
   * Actually enforces `MAX_SUPPLY` limit
   * Prevents unlimited token creation
3. **Access Control:**
   * Only owner can mint tokens
   * Prevents unauthorized token creation
4. **Input Validation:**
   * Checks for zero addresses and amounts
   * Batch size limits prevent gas attacks

**How it prevents attacks:**

* All arithmetic operations are validated before execution
* Supply limits prevent economic manipulation
* Access controls prevent unauthorized minting
* Batch limits prevent gas-based attacks

**How Security Works:**

**Attack Prevention:**

* Overflow/underflow checks prevent arithmetic attacks
* Supply cap enforcement prevents unlimited token creation
* Access control restricts minting to owner only
* Input validation prevents edge cases

**Result:**

* All arithmetic operations are safe
* Economic model is protected
* Attacks are blocked before execution

#### Protection Methods (summary)

| Method              | Solidity Version | Effectiveness | Gas Cost |
| ------------------- | ---------------- | ------------- | -------- |
| Built-in Protection | 0.8+             | Very High     | Low      |
| SafeMath Library    | <0.8             | High          | Medium   |
| Manual Checks       | Any              | High          | Low      |
| Unchecked Blocks    | 0.8+ (when safe) | N/A           | Very Low |

***

## Prevention Strategies (Consolidated)

1. Reentrancy Prevention
   * Primary: Checks-Effects-Interactions pattern
   * Secondary: Reentrancy guards (e.g., OpenZeppelin ReentrancyGuard)
   * Additional: Pull payments, limit gas forwarded, prefer transfer() for simple ETH sends
2. Access Control Best Practices
   * Use battle-tested libraries: OpenZeppelin AccessControl, Ownable
   * Implement clear role hierarchy and immutable owner
   * Emit events for role changes and use multisig for critical roles
3. Integer Overflow Protection
   * Use Solidity 0.8+ (automatic checks)
   * Add explicit pre-operation checks where appropriate
   * Enforce supply caps and input limits
   * For legacy code, use SafeMath
4. General Principles
   * Defense in depth: stack protections
   * Fail securely: default to safe state on errors
   * Principle of least privilege: minimal necessary permissions
   * Thorough testing and professional audits before mainnet deployment

***

## Testing Vulnerabilities

Use these commands and steps to test examples in Remix (manual steps):

```bash
# Deploy vulnerable contract in Remix
# Deploy attacker contract and execute attack
# Observe behavior and balances
# Deploy secure version and verify attack fails
```

Verification steps:

1. Deploy vulnerable contract on testnet (Somnia Testnet recommended).
2. Attempt exploit using attacker contract.
3. Observe vulnerability in action.
4. Deploy secure version with protections.
5. Verify exploit fails against secure implementation.

You can successfully identify and exploit vulnerabilities in a controlled test environment and verify mitigations on secure contracts.

***

## Common Vulnerability Patterns

Red flags in code review:

* External calls before state updates
* Missing access control modifiers
* Unchecked arithmetic operations
* Missing input validation
* No event emissions for critical actions
* Hardcoded addresses or values
* Complex inheritance hierarchies
* Missing reentrancy protection

Security scanning tools:

| Tool      | Type               | Effectiveness | Cost |
| --------- | ------------------ | ------------- | ---- |
| Slither   | Static Analysis    | High          | Free |
| MythX     | Comprehensive      | Very High     | Paid |
| Securify  | Academic           | Medium        | Free |
| Manticore | Symbolic Execution | High          | Free |

***

## Additional Resources

* [SWC Registry](https://swcregistry.io/) - Smart Contract Weakness Classification
* [Consensys Security Best Practices](https://consensys.github.io/smart-contract-best-practices/)

***

✅ Verification: You can identify vulnerable patterns and understand how attacks work.

🎉 Congratulations! You've mastered the most critical smart contract vulnerabilities, prevention strategies and secure coding patterns.


# Audit Checklist

This Self-Review Audit Checklist is the mandatory, internal quality assurance process that every smart contract must undergo before deployment to any public or private blockchain environment. Its purpose is to catch common, critical, and complex security vulnerabilities early, significantly reducing the risk of exploits, financial loss, and costly post-deployment fixes.

This process consists of two primary phases: a Manual Pre-Deployment Checklist and Automated Static Analysis Tooling.

## Phase 1: Manual Pre-Deployment Checklist

The development team must manually review and verify that the contract adheres to the following security, logic, and best-practice requirements.

**1.1. Security Vulnerability Checks**

| Item                           | Requirement                                                                                                                                                  | Verification Steps                                                                                                                               |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Reentrancy Protection**      | All functions that send Ether or tokens to external addresses must follow the Checks-Effects-Interactions pattern.                                           | Verify that state variables are updated before any external calls are made. Use `transfer`/`send` methods or reentrancy guards where necessary.  |
| **Access Control**             | Critical state-changing functions (e.g., `setOwner`, `pause`, `upgrade`, `mint`) must be guarded by proper access modifiers (e.g., `onlyOwner`, `onlyRole`). | Check that function visibility is correctly set (e.g., `internal`, `external`, `private`).                                                       |
| **Integer Overflow/Underflow** | All arithmetic operations, especially those based on user input, must be safe.                                                                               | For Solidity >= 0.8.0, verify the compiler's default overflow checks are not disabled. For older versions, ensure the use of SafeMath libraries. |
| **Denial-of-Service (DoS)**    | Operations must not iterate over unbounded arrays or map sizes that could be arbitrarily inflated by a malicious user, leading to excessive gas costs.       | Check all loops to ensure iteration counts are fixed or restricted.                                                                              |
| **External Call Security**     | All interactions with unknown or untrusted external contracts are handled safely.                                                                            | Ensure that results of external calls are checked and fail gracefully if necessary. Use call wrappers to limit reentrancy risk.                  |
| **Visibility**                 | State variables and functions intended for internal use must be declared `private` or `internal`.                                                            | Review all function and variable declarations for accidental public exposure.                                                                    |

**1.2. Logic and Functional Checks**

| Item                         | Requirement                                                                                                                                         | Verification Steps                                                                                                                    |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Functional Specification** | The contract logic precisely matches the intended business logic and all requirements documented in the functional specification.                   | Verify contract against all use cases and edge cases defined in the project scope.                                                    |
| **State Transitions**        | The contract's state (e.g., token balances, operational phase) transitions correctly and predictably.                                               | Trace critical functions (`transfer`, `claim`, `lock`) to ensure state variables update correctly.                                    |
| **Error Handling**           | All potential failure points are handled gracefully with clear, descriptive error messages.                                                         | Ensure that `require()` or `revert()` statements are used everywhere necessary and that custom error codes are defined and leveraged. |
| **Event Emission**           | All critical state changes and value transfers must emit an appropriate `Event` to allow for off-chain monitoring, indexing, and UI responsiveness. | Verify that an `Event` is emitted for every action that changes a user-facing state.                                                  |

## Phase 2: Automated Static Analysis Tools

Static analysis is a mandatory, automated review step that must be completed using approved tools before deployment.

**2.1. Mandatory Tooling**

The following static analysis tools must be executed against the final version of the smart contract code:

| Tool                                                         | Purpose                                                                                                                                         | Output Review Requirement                                                                 |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| [**Slither**](https://github.com/crytic/slither)             | Detects various security vulnerabilities (e.g., reentrancy, unprotected calls, misuses of `msg.sender`) and code optimization issues.           | Must be executed with all security and efficiency detectors enabled.                      |
| [**Mythril**](https://github.com/ConsenSysDiligence/mythril) | Performs symbolic execution to find potential execution paths that could lead to vulnerabilities (e.g., integer overflows, assertion failures). | Must be run using its security analysis modes (e.g., full scan, execution path analysis). |
| [**Solhint**](https://github.com/protofire/solhint)          | Enforces code style and security best practices, ensuring clean and maintainable code.                                                          | Must pass all configured security and style rulesets without warnings.                    |

**2.2. Warning Triage and Sign-off**

<details>

<summary>High/Critical Severity Warnings</summary>

Any issue categorized as High or Critical by the static analysis tools must be fixed immediately. The deployment process cannot proceed until these are resolved and the tools run clean.

</details>

<details>

<summary>Medium/Low Severity Warnings</summary>

These must be reviewed by a lead developer. They must either be:

* Fixed: If the issue is a genuine vulnerability or best practice deviation.
* Justified: If the tool's warning is a false positive or the logic is intentionally implemented in that manner, a clear, documented justification must be added to a dedicated "Audit Waivers" log.

</details>

<details>

<summary>Report Retention</summary>

The final, clean static analysis reports must be saved and archived as part of the pre-deployment documentation.

</details>

{% hint style="success" %}
**Congratulations!** You've mastered the audit checklist for both manual and automatic process. Continue to learn prevention strategies and secure coding patterns.
{% endhint %}


# Node/Infra Security

## Secure RPC Key Management and Environment Configuration for Somnia Developers

This comprehensive guide teaches developers how to securely manage RPC keys, private keys, and environment variables when building applications on the Somnia blockchain. If you're deploying smart contracts, building dApps, or integrating with Somnia. Proper security practices are essential to protect your assets and maintain service reliability. By following this tutorial, you'll implement industry-standard security measures with practical code examples that seamlessly integrate into your development workflow.

## Prerequisites

Before starting this guide, ensure you have:

* Basic knowledge of blockchain development and EVM concepts
* Node.js (20+) installed
* A code editor (VS Code recommended)
* A Somnia wallet with Somnia Token (STT) for testing
* Familiarity with environment variables and package managers
* Basic understanding of Git and version control

## RPC Key Security Fundamentals

RPC (Remote Procedure Call) keys and endpoints allow your application to interact with the blockchain. Using them securely is paramount.

A publicly accessible key, especially one with write permissions, can be exploited by an attacker to drain wallets or cause network congestion.

#### Using Ankr Provider

❌ **Bad Practice:**

```javascript
// Do not call your api Provider directly in your script with the api-keys!

// Setup provider AnkrProvider
const provider = new AnkrProvider('https://rpc.ankr.com/somnia_testnet/your-private-key');
```

✅ **Good Practice:**

```javascript
// Create a .env file
SOMNIA_ANKR_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-private-key

// Use environment variables
// Setup provider AnkrProvider
const provider = new AnkrProvider(process.env.SOMNIA_ANKR_RPC_URL);
```

## Environment Variable Management

#### Private RPC Endpoints

While public endpoints are convenient for basic queries, they are prone to unreliability and congestion during high-traffic events. **Private RPCs are premium services and perform significantly better than the public RPC**, offering more speed and reliability through dedicated connections.

```javascript
// Example configuration for private endpoint
const config = {
  testnet: {
    url: process.env.SOMNIA_TESTNET_RPC_URL,
    accounts: [process.env.TESTNET_PRIVATE_KEY]
  }
};
```

#### Environment Variable Best Practices

A .env file is a standard way to manage environment-specific configuration:

```bash
# .env file
SOMNIA_TESTNET_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-private-key
TESTNET_PRIVATE_KEY=your_private_key_here
NODE_ENV=development
```

#### Never Commit .env Files

The .env file should be added to your .gitignore file.

```bash
# .gitignore
.env
.env.local
.env.*.local
node_modules/
dist/
```

#### Create Separate Environment Files

Use separate configuration files for different environments.

```bash
# Project structure
├── .env.example          # Template file (safe to commit)
├── .env.development      # Development secrets
├── .env.test            # Test environment
├── .env.staging         # Staging environment
└── .env.production      # Production secrets (never commit)
```

```javascript
// config/environment.js
const dotenv = require('dotenv');
const path = require('path');

const environment = process.env.NODE_ENV || 'development';
const envFile = `.env.${environment}`;

dotenv.config({ path: path.resolve(process.cwd(), envFile) });

module.exports = {
  rpcUrl: process.env.SOMNIA_RPC_URL,
  privateKey: process.env.PRIVATE_KEY,
  environment
};
```

#### Reference Keys in Code

Always reference environment variables rather than hardcoding sensitive keys.

```javascript
// utils/blockchain.js
const { ethers } = require('ethers');
const config = require('../config/environment');

class BlockchainService {
  constructor() {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.wallet = new ethers.Wallet(config.privateKey, this.provider);
  }
  
  async getBalance(address) {
    return await this.provider.getBalance(address);
  }
  
  async sendTransaction(to, value) {
    const tx = {
      to,
      value: ethers.parseEther(value.toString())
    };
    
    return await this.wallet.sendTransaction(tx);
  }
}

module.exports = BlockchainService;
```

#### Environment Variable Testing for Applications

**Note:** This testing approach is designed for application projects only, not system-wide configurations.

```javascript
// test-env.js - For application projects only
require('dotenv').config();

const testEnvironmentVariables = () => {
  const requiredVars = [
    'SOMNIA_TESTNET_RPC_URL',
    'TESTNET_PRIVATE_KEY'
  ];
  
  const missing = requiredVars.filter(varName => !process.env[varName]);
  
  if (missing.length > 0) {
    console.error('Missing required environment variables:', missing);
    process.exit(1);
  }
  
  console.log('All required environment variables are loaded');
};

testEnvironmentVariables();
```

## Implementation Examples

#### Complete Project Setup

```bash
# 1. Initialize project
npm init -y
npm install ethers dotenv
npm install -D nodemon

# 2. Create environment template
echo "SOMNIA_RPC_URL=https://rpc.ankr.com/somnia_testnet/your-key-here" > .env.example
echo "PRIVATE_KEY=your-private-key-here" >> .env.example
echo "CONTRACT_ADDRESS=0x..." >> .env.example

# 3. Add to .gitignore
echo ".env*" >> .gitignore
echo "!.env.example" >> .gitignore
```

#### Secure Contract Interaction

```javascript
// contracts/SomniaContract.js
const { ethers } = require('ethers');
const config = require('../config/environment');

class SomniaContract {
  constructor(contractAddress, abi) {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.wallet = new ethers.Wallet(config.privateKey, this.provider);
    this.contract = new ethers.Contract(contractAddress, abi, this.wallet);
  }
  
  async safeCall(methodName, ...args) {
    try {
      // Estimate gas first
      const gasEstimate = await this.contract[methodName].estimateGas(...args);
      
      // Add 20% buffer
      const gasLimit = gasEstimate * 120n / 100n;
      
      const tx = await this.contract[methodName](...args, { gasLimit });
      console.log(`Transaction sent: ${tx.hash}`);
      
      const receipt = await tx.wait();
      console.log(`Transaction confirmed: ${receipt.transactionHash}`);
      
      return receipt;
    } catch (error) {
      console.error('Transaction failed:', error.message);
      throw error;
    }
  }
}

module.exports = SomniaContract;
```

## RPC Key Management

#### IP Whitelisting

If your RPC provider supports it, restrict access to your API key by creating an allowlist of trusted IP addresses.

```bash
# Example: Configure IP allowlist in your provider dashboard
# Allowed IPs: 203.0.113.1, 203.0.113.2
# This ensures only requests from your servers can use the key
```

#### Key Rotation and Expiration

Regularly rotate your RPC keys and immediately revoke any that are no longer in use.

<details>

<summary>Manual key rotation implementation</summary>

```javascript
// Manual key rotation implementation
// Note: RPC providers typically require manual key generation through their dashboard
// This implementation helps manage the rotation process once you have new keys

const updateEnvironmentVariable = async (key, value) => {
  // Update .env file or environment configuration
  const fs = require('fs').promises;
  const envPath = '.env';
  
  try {
    let envContent = await fs.readFile(envPath, 'utf8');
    const regex = new RegExp(`^${key}=.*$`, 'm');
    
    if (regex.test(envContent)) {
      envContent = envContent.replace(regex, `${key}=${value}`);
    } else {
      envContent += `\n${key}=${value}`;
    }
    
    await fs.writeFile(envPath, envContent);
    console.log(`Updated ${key} in environment file`);
  } catch (error) {
    console.error('Failed to update environment variable:', error);
    throw error;
  }
};

// Manual key rotation helper
const rotateApiKey = async (newKey) => {
  try {
    // Validate the new key format
    if (!newKey || typeof newKey !== 'string') {
      throw new Error('Invalid API key provided');
    }
    
    // Store old key for reference
    const oldKey = process.env.SOMNIA_TESTNET_RPC_URL;
    console.log('Rotating API key...');
    
    // Update environment variable
    await updateEnvironmentVariable('SOMNIA_TESTNET_RPC_URL', newKey);
    
    console.log('API key rotated successfully');
    console.log('Please manually revoke the old key in your RPC provider dashboard');
    console.log('Old key (first 10 chars):', oldKey?.substring(0, 10) + '...');
    
  } catch (error) {
    console.error('Key rotation failed:', error);
  }
};

// Key rotation reminder system
const setupRotationReminder = () => {
  const NINETY_DAYS = 90 * 24 * 60 * 60 * 1000;
  
  setInterval(() => {
    console.log('\n🔑 SECURITY REMINDER: Consider rotating your RPC API keys');
    console.log('1. Generate new key in your RPC provider dashboard');
    console.log('2. Call rotateApiKey(newKey) with the new key');
    console.log('3. Manually revoke old key in provider dashboard\n');
  }, NINETY_DAYS);
};

// Usage example:
// rotateApiKey('https://rpc.ankr.com/somnia_testnet/your-new-private-key');
// setupRotationReminder();
```

</details>

#### Secrets Management for Production

For production environments, use a dedicated secrets management platform.

```javascript
// AWS Secrets Manager example
const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

const getRpcKey = async () => {
  const secret = await secretsManager.getSecretValue({
    SecretId: 'somnia-rpc-key'
  }).promise();
  
  return JSON.parse(secret.SecretString).rpcUrl;
};
```

## Private Key Security

Private keys authorize all transactions on a blockchain and should be protected with the utmost vigilance.

#### Secure Key Generation

Use reputable tools that follow industry standards for cryptographically random key generation.

```javascript
// Example: Secure key generation with ethers.js
const { Wallet } = require('ethers');
const { randomBytes } = require('crypto');

// Generate cryptographically secure random wallet
const generateSecureWallet = () => {
  const randomWallet = Wallet.createRandom();
  return {
    address: randomWallet.address,
    privateKey: randomWallet.privateKey,
    mnemonic: randomWallet.mnemonic.phrase
  };
};
```

#### Access Control

Private keys should never be shared. For team access, use multisig wallets or role-based access control.

```javascript
// Example: Role-based access pattern
class SecureWalletManager {
  constructor() {
    this.roles = new Map();
    this.permissions = {
      'admin': ['deploy', 'transfer', 'read'],
      'developer': ['deploy', 'read'],
      'viewer': ['read']
    };
  }
  
  assignRole(address, role) {
    this.roles.set(address, role);
  }
  
  canExecute(address, action) {
    const role = this.roles.get(address);
    return this.permissions[role]?.includes(action) || false;
  }
}
```

## Error Handling and Logging

Proper error handling and logging are crucial for maintaining security and debugging issues in production environments. When implementing logging for blockchain applications, it's essential to balance transparency with security, ensuring that sensitive information like private keys and API secrets are never exposed in logs.

#### Secure Logging Practices

<details>

<summary>Secure logging implementation</summary>

```javascript
// Secure logging implementation
const winston = require('winston');

// Create logger with security considerations
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json(),
    // Custom format to redact sensitive information
    winston.format.printf(({ timestamp, level, message, ...meta }) => {
      // Redact sensitive data
      const sanitized = JSON.stringify(meta).replace(
        /(private_key|api_key|secret)":\s*"[^"]+"/gi,
        '$1": "[REDACTED]"'
      );
      return `${timestamp} [${level}]: ${message} ${sanitized}`;
    })
  ),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Error handling for RPC calls
const safeRpcCall = async (provider, method, params) => {
  try {
    const result = await provider.send(method, params);
    logger.info('RPC call successful', { method, success: true });
    return result;
  } catch (error) {
    // Log error without exposing sensitive information
    logger.error('RPC call failed', {
      method,
      error: error.message,
      code: error.code
    });
    throw new Error(`RPC call failed: ${error.message}`);
  }
};
```

</details>

#### Error Recovery Strategies

```javascript
// Implement retry logic with exponential backoff
const retryRpcCall = async (provider, method, params, maxRetries = 3) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await safeRpcCall(provider, method, params);
    } catch (error) {
      if (attempt === maxRetries) {
        logger.error('Max retries exceeded', { method, attempts: attempt });
        throw error;
      }
      
      const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
      logger.warn('Retrying RPC call', { method, attempt, delay });
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};
```

## Security Checklist

* [ ] Store all sensitive keys in environment variables
* [ ] Use private RPC endpoints for better performance and reliability
* [ ] Implement IP whitelisting where supported
* [ ] Set up regular key rotation schedules
* [ ] Use hardware wallets for high-value operations
* [ ] Implement proper error handling and logging
* [ ] Never commit secrets to version control
* [ ] Use role-based access control for team environments
* [ ] Monitor and audit key usage regularly
* [ ] Test environment variable loading in application context

## Conclusion

By following these security practices, you'll significantly reduce the risk of key compromise and ensure your Somnia blockchain applications operate securely and reliably. Security is an ongoing process, and you should regularly review and update your practices as new threats emerge and best practices evolve.


# Responsible Disclosure Policy

This page serves as the unified communication hub for **Somnia developers, contributors, and security researchers**. It combines two essential areas:

* **Developer Contact and Support:** How to reach the Somnia DevRel and technical teams.
* **Responsible Disclosure:** How to report security vulnerabilities, contribute improvements, and participate in the future bounty ecosystem.

***

## Developer Contact and Support

The Somnia developer community operates across several communication channels to provide quick technical assistance, feedback exchange, and support for integrations or bug reports.

#### Active Support Channels

* **Telegram (DevRel Team):**
  * [@emreyeth](https://t.me/emreyeth)
  * [@PromiseGameFi](https://t.me/PromiseGameFi)
  * [@emmaodia](https://t.me/emmaodia)
* **Discord:**
  * Join the official [Somnia ](https://discord.gg/somnia)server.
  * For technical questions, use the `#dev-support` or `#dev-chat` channel.
  * To report issues privately, open a **support ticket** under “Bug Reports”
* **Email:**\
  Send an email to [**developers@somnia.network**](mailto:developers@somnia.network) for official inquiries, integration help, or collaboration requests.

{% hint style="info" %}
Response time varies based on the request type, but DevRel aims to reply within **24 hours**.
{% endhint %}

#### Types of Support Requests

| Category                | Description                                   | Preferred Channel      |
| ----------------------- | --------------------------------------------- | ---------------------- |
| **Integration Help**    | RPC, SDK, and Smart Contract setup assistance | Discord / Email        |
| **Docs Contribution**   | Reporting outdated or missing developer docs  | GitHub PR / Email      |
| **Bug Report**          | Contract, SDK, or explorer bugs               | Discord Ticket / Email |
| **Partnership Inquiry** | Technical collaborations or integration ideas | Email                  |

***

## Responsible Disclosure

Somnia encourages ethical researchers and contributors to responsibly disclose vulnerabilities or security risks found in the ecosystem. Even though a formal bounty system is not yet live, this framework ensures findings are handled safely and recognized appropriately.

***

## Technical Disclosure Guidelines

All vulnerability reports should follow a clear, reproducible structure for fast triage and validation.

#### **Required Report Template**

{% code title="Vulnerability Report Template" %}

```markdown
# Vulnerability Report — Somnia Network

## Summary
Brief description of the issue.

## Impact
Potential risks if exploited.

## Steps to Reproduce
1. Step-by-step actions.
2. Include RPC endpoint, contract address, and network (Mainnet or Shannon Testnet).

## Expected vs Actual Behavior
Explain the difference in observed vs intended behavior.

## Proof of Concept (PoC)
Include transaction hash, minimal code snippet, or call trace.

## Suggested Fix (Optional)
Provide insights or improvement recommendations.

## Contact
Telegram / Discord handle / Email.
```

{% endcode %}

<details>

<summary>Example</summary>

{% code title="Example Vulnerability Report" %}

```markdown
# Vulnerability Report — Somnia Bridge Contract

## Summary
Bridge contract mishandles token decimals in cross-chain conversion.

## Impact
Potential underflow on tokens with decimals < 18.

## Steps to Reproduce
1. Deploy ERC20 with 6 decimals.
2. Execute `bridgeToSomnia(token, 1000000)`.
3. Observe incorrect amount on destination.

## Expected vs Actual
Expected: normalized 1 token.
Actual: 0.000001 tokens received.

## Suggested Fix
Add decimal normalization logic.

## Proof of Concept
Testnet Tx: `0x92b...4fe1`

## Contact
@emreyeth (Telegram)
```

{% endcode %}

</details>

***

## Contribution Pathways for Developers

Somnia invites developers to contribute beyond bug reporting. Follow these pathways to get involved.

{% stepper %}
{% step %}
**Documentation Contributions**

* Suggest edits or add missing examples in tutorials.
* Create new pages under categories like *Debugging*, *Testing*, or *Security*.
  {% endstep %}

{% step %}
**Testing Best Practices**

* Always test exploits or stress scenarios on **Shannon Testnet**, not on Mainnet.
* Use local forks with Hardhat or Foundry for reproducibility.
  {% endstep %}
  {% endstepper %}

***

## Somnia Report Lifecycle

{% stepper %}
{% step %}
**Submission**

Researcher submits a report via email, Discord, or Telegram.
{% endstep %}

{% step %}
**Verification**

Somnia DevRel reproduces the issue and collects context.
{% endstep %}

{% step %}
**Escalation**

Valid issues are passed to Somnia Core Security.
{% endstep %}

{% step %}
**Patch Deployment**

Fix rolled out to Shannon Testnet, then Mainnet. (Based on where is it.)
{% endstep %}

{% step %}
**Acknowledgment**

Researcher credited publicly in Somnia Docs and Discord.
{% endstep %}
{% endstepper %}

For multi-party vulnerabilities (e.g., involving validators or external oracles), coordinated disclosure will be handled privately.

***

## Ethical Rules

{% hint style="warning" %}

* Do **not** exploit vulnerabilities on Mainnet.
* Do **not** disrupt network services or RPC endpoints.
* Do **not** engage in social engineering or phishing.
* Always disclose vulnerabilities privately and responsibly.

Researchers acting in good faith will **not face any penalties** and will be publicly recognized for their ethical contributions.
{% endhint %}

***

## Summary

* Use **Telegram, Discord, or Email** to reach Somnia’s DevRel and security teams.
* Follow the **Responsible Disclosure template** for structured vulnerability reports.
* Contribute improvements via **Pull Requests** or documentation updates.
* Future bounty and recognition programs will expand as Somnia Mainnet evolves.


# Deployment and Production


# Go-Live Checklist

This page provides a **checklist for deploying Somnia applications to production (Mainnet)**. It ensures that all necessary environment variables, contract addresses, allowlists, and rollback strategies are correctly configured before launch.

{% hint style="warning" %}
Never commit secrets to git. Use encrypted secret storage or CI secret managers for production deployments.
{% endhint %}

***

### Environment Variables

* [ ] Store RPC URLs, private keys, and API keys in `.env` files (never commit to git).
* [ ] Define `SOMNIA_RPC_MAINNET` and `SOMNIA_RPC_TESTNET` for switching between environments.
* [ ] Configure block explorer API keys (for contract verification).
* [ ] Separate `.env.production` vs `.env.development`.
* [ ] Double-check secrets with `printenv | grep SOMNIA`.

Example `.env.production`:

{% code title=".env.production" %}

```bash
SOMNIA_RPC_MAINNET=https://api.infra.mainnet.somnia.network/
PRIVATE_KEY=0xabc123...
EXPLORER_API_KEY=...
ALLOWED_ORIGIN=https://yourapp.com
```

{% endcode %}

***

### Contract Addresses

* [ ] Verify all **core Somnia system contracts** (e.g., wrapped SOMI, multicall, registry).
* [ ] Update deployed contract addresses in `.env` or config files.
* [ ] Confirm that addresses match **Mainnet deployments** (not testnet).
* [ ] Cross-check with block explorer for correct bytecode & verification.

Example `config/addresses.json`:

{% code title="config/addresses.json" %}

```json
{
  "network": "mainnet",
  "dao": "0x1234...",
  "token": "0xabcd...",
  "subgraph": "https://subgraph.somnia.network/dashboard/subgraph/..."
}
```

{% endcode %}

***

### Allowlists

* [ ] Maintain allowlists for admin roles, multisigs, and privileged addresses.
* [ ] Use multisig wallets ([Safe](https://safe.somnia.network/welcome)) for critical roles (owner, pauser, upgrader).
* [ ] Double-check allowlist in contracts (no dev/test keys).
* [ ] Store allowlist in version control (JSON/YAML).

Example `allowlist.json`:

{% code title="allowlist.json" %}

```json
{
  "admins": ["0xAdmin1...", "0xAdmin2..."],
  "oracles": ["0xOracle1..."],
  "relayers": ["0xRelayer1..."]
}
```

{% endcode %}

***

### Rollback Plan

Having a clear rollback strategy is essential if something goes wrong during or after deployment. This should include both **technical measures** and **operational procedures**.

#### Technical Rollback Steps

* [ ] **Pause contracts** – ensure critical contracts implement a `pause()` function for emergencies.
* [ ] **Feature flags** – enable/disable risky features without redeployment.
* [ ] **Upgradable contracts** – if using proxies, keep previous implementation verified and ready.
* [ ] **Role revocation** – remove compromised keys or revoke admin privileges quickly.
* [ ] **Migration scripts** – maintain scripts to redeploy or roll forward to a safe version.

#### Operational Rollback Steps

* [ ] **Communication plan** – inform users immediately via Discord, Twitter, or status page.
* [ ] **Validator coordination** – notify Somnia infra providers/validators in case of critical incidents.
* [ ] **Emergency access** – keep a secure list of multisig signers available for urgent transactions.
* [ ] **Data backups** – keep copies of config files, [subgraph](https://subgraph.somnia.network/) schemas, and off-chain DBs.
* [ ] **Post-mortem process** – document the issue, resolution, and future prevention steps.

***

### Best Practices

* Always test on **Shannon Testnet** before mainnet deployment.
* Pin dependencies & compiler versions for reproducibility.
* Run **audit checklist** (see Smart Contract Security 101).
* Monitor deployed contracts with logging & health checks.
* Communicate launch windows to your community in advance.


# Explorer API Health and Monitoring

A comprehensive guide for integrating Somnia Network explorer APIs with production-ready logging and health monitoring systems.

This guide demonstrates how to build robust applications on Somnia Network using Blockscout's explorer APIs, implement enterprise-grade logging, and set up comprehensive health monitoring for your dApps.

**What You'll Learn**

* Somnia Network explorer API integration
* Production logging with Winston
* Health monitoring systems
* Error handling and debugging
* Performance optimization

## **Prerequisites**

* Node.js 20+
* TypeScript/JavaScript knowledge
* Blockchain/EVM concepts
* Funded Somnia wallet
* REST API experience

## **Explorer Endpoints**

Somnia Network uses Blockscout as its blockchain explorer infrastructure:

| Network     | Explorer URL                              | API Endpoint |
| ----------- | ----------------------------------------- | ------------ |
| **Testnet** | <https://shannon-explorer.somnia.network> | `/api`       |
| **Mainnet** | <https://explorer.somnia.network>         | `/api`       |

### **Available APIs**

Blockscout provides multiple API interfaces:

* **REST API** - Primary interface for UI operations
* **RPC API** - Etherscan-compatible endpoints
* **ETH RPC API** - Standard [JSON-RPC methods](/developer/json-rpc-api)
* **GraphQL** - Advanced querying capabilities

## Quick Start

Initialize your Somnia integration project:

```bash
mkdir somnia-explorer-integration
cd somnia-explorer-integration
npm init -y
```

### **Dependencies**

Install required packages:

```bash
# Core dependencies
npm install axios winston express helmet cors dotenv ethers

# Development dependencies
npm install --save-dev @types/node typescript ts-node nodemon @types/express @types/cors
```

### **Core Dependencies:**

* `axios` - Promise-based HTTP client used for making API requests to Somnia Network's Blockscout explorer APIs with automatic request/response logging and error handling
* `winston` - Comprehensive logging library used for creating structured JSON logs with timestamps, component-specific loggers (API, explorer, health), and automatic log rotation
* `express` - Minimal web framework used for creating health monitoring endpoints and serving the application
* `helmet` - Security middleware used for setting HTTP security headers to protect against XSS, clickjacking, and other common web vulnerabilities
* `cors` - Cross-Origin Resource Sharing middleware used for enabling secure cross-origin requests from frontend applications
* `dotenv` - Environment variable loader used for managing configuration settings and sensitive information like API keys and network settings
* `ethers` - Ethereum library used for blockchain interactions, RPC connections, and retrieving network information

### **Project Structure**

```bash
mkdir src/{config,services,utils} logs
```

### **Package Configuration**

Update `package.json` with build scripts:

```json
{
  "name": "somnia-explorer-integration",
  "version": "1.0.0",
  "description": "Somnia Network explorer integration with logging and health checks",
  "main": "dist/app.js",
  "scripts": {
    "build": "tsc",
    "start": "node dist/app.js",
    "dev": "nodemon --exec ts-node src/app.ts",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["somnia", "blockchain", "explorer", "blockscout"],
  "author": "Your Name",
  "license": "MIT"
}
```

### Configuration

**Network Settings**

Create a configuration file for Somnia Network settings to:

* Consolidate all Somnia Network endpoints (testnet/mainnet) in one location
* Enable automatic environment-based network switching
* Provide TypeScript interfaces for type safety
* Make it easy to update endpoints without changing multiple files

```typescript
// src/config/network.ts
export const SOMNIA_CONFIG = {
  testnet: {
    name: 'Somnia Testnet',
    rpcUrl: 'https://dream-rpc.somnia.network',
    chainId: 50312,
    explorerUrl: 'https://shannon-explorer.somnia.network',
    explorerApiUrl: 'https://shannon-explorer.somnia.network/api'
  },
  mainnet: {
    name: 'Somnia Mainnet',
    rpcUrl: 'https://somnia-json-rpc.stakely.io',
    chainId: 5031,
    explorerUrl: 'https://explorer.somnia.network',
    explorerApiUrl: 'https://explorer.somnia.network/api'
  }
};

export const getCurrentNetwork = () => {
  return process.env.NODE_ENV === 'production' 
    ? SOMNIA_CONFIG.mainnet 
    : SOMNIA_CONFIG.testnet;
};
```

### **Environment Variables**

Why create a .env file:

* Keeps sensitive information out of source code
* Useful when using third-party RPC API Keys
* Enables different configurations for development/production environments
* Controls which Somnia network to use (testnet/mainnet) via `NODE_ENV`
* Allows easy modification of settings without code changes

```bash
# .env
NODE_ENV=development
PORT=3000
LOG_LEVEL=info
```

####

## API Integration

### **Explorer Service**

This service provides comprehensive logging for all API interactions and follows Blockscout's REST API patterns.

<details>

<summary>src/services/somniaExplorerService.ts</summary>

```javascript
// src/services/somniaExplorerService.ts
import axios, { AxiosInstance } from 'axios';
import { getCurrentNetwork } from '../config/network';
import { logger } from '../utils/logger';

export interface TransactionData {
  hash: string;
  blockNumber: number;
  from: string;
  to: string;
  value: string;
  gasUsed: string;
  status: string;
  timestamp: string;
}

export interface BlockData {
  number: number;
  hash: string;
  timestamp: number;
  transactions: string[];
  gasUsed: string;
  gasLimit: string;
  miner: string;
}

export class SomniaExplorerService {
  private api: AxiosInstance;
  private network = getCurrentNetwork();

  constructor() {
    this.api = axios.create({
      baseURL: this.network.explorerApiUrl,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json',
        'User-Agent': 'Somnia-Explorer-Client/1.0',
        'Cache-Control': 'no-cache' // Ensure fresh data for health checks
      }
    });

    // Add request interceptor for comprehensive logging
    this.api.interceptors.request.use(
      (config) => {
        logger.info('Somnia Explorer API Request', {
          method: config.method?.toUpperCase(),
          url: config.url,
          baseURL: config.baseURL,
          timestamp: new Date().toISOString(),
          network: this.network.name
        });
        return config;
      },
      (error) => {
        logger.error('Somnia Explorer API Request Error', {
          error: error.message,
          timestamp: new Date().toISOString(),
          network: this.network.name
        });
        return Promise.reject(error);
      }
    );

    // Add response interceptor for logging
    this.api.interceptors.response.use(
      (response) => {
        logger.info('Somnia Explorer API Response', {
          status: response.status,
          url: response.config.url,
          responseTime: response.headers['x-response-time'] || 'unknown',
          timestamp: new Date().toISOString(),
          network: this.network.name
        });
        return response;
      },
      (error) => {
        logger.error('Somnia Explorer API Response Error', {
          status: error.response?.status,
          statusText: error.response?.statusText,
          url: error.config?.url,
          message: error.message,
          timestamp: new Date().toISOString(),
          network: this.network.name
        });
        return Promise.reject(error);
      }
    );
  }

  async getTransaction(txHash: string): Promise<TransactionData | null> {
    try {
      const response = await this.api.get(`/v2/transactions/${txHash}`);
      logger.info('Successfully retrieved transaction', {
        txHash,
        blockNumber: response.data.block,
        network: this.network.name
      });
      return response.data;
    } catch (error: any) {
      logger.error('Failed to fetch transaction', {
        txHash,
        error: error.message,
        network: this.network.name
      });
      return null;
    }
  }

  async getBlock(blockNumber: number): Promise<BlockData | null> {
    try {
      const response = await this.api.get(`/v2/blocks/${blockNumber}`);
      logger.info('Successfully retrieved block', {
        blockNumber,
        hash: response.data.hash,
        txCount: response.data.tx_count,
        network: this.network.name
      });
      return response.data;
    } catch (error: any) {
      logger.error('Failed to fetch block', {
        blockNumber,
        error: error.message,
        network: this.network.name
      });
      return null;
    }
  }

  async getAddressTransactions(address: string, page = 1, limit = 20): Promise<TransactionData[]> {
    try {
      const response = await this.api.get(`/v2/addresses/${address}/transactions`, {
        params: { page, limit }
      });
      logger.info('Retrieved address transactions', {
        address,
        count: response.data.items?.length || 0,
        page,
        limit,
        network: this.network.name
      });
      return response.data.items || [];
    } catch (error: any) {
      logger.error('Failed to fetch address transactions', {
        address,
        error: error.message,
        network: this.network.name
      });
      return [];
    }
  }
}
```

</details>

This `SomniaExplorerService` class provides comprehensive logging for all API interactions and follows Blockscout's REST API patterns. The service includes:

* **Type-safe interfaces** for transaction and block data
* **Axios interceptors** for automatic request/response logging
* **Error handling** with detailed logging for debugging
* **Network-aware configuration** that adapts to different Somnia environments

Tip: The service automatically logs all API requests and responses, making it easy to debug issues and monitor performance.

### Logging System

<details>

<summary>src/utils/logger.ts</summary>

```javascript
// src/utils/logger.ts
import winston from 'winston';
import path from 'path';

// Custom log format for Somnia applications
const somniaLogFormat = winston.format.combine(
  winston.format.timestamp({
    format: 'YYYY-MM-DD HH:mm:ss'
  }),
  winston.format.errors({ stack: true }),
  winston.format.json(),
  winston.format.printf(({ timestamp, level, message, ...meta }) => {
    return JSON.stringify({
      timestamp,
      level,
      message,
      service: 'somnia-explorer-client',
      network: process.env.NODE_ENV === 'production' ? 'mainnet' : 'testnet',
      ...meta
    });
  })
);

// Create logger instance with Somnia-specific configuration
export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: somniaLogFormat,
  defaultMeta: {
    service: 'somnia-explorer-integration',
    version: '1.0.0',
    chain: 'somnia'
  },
  transports: [
    // Console transport for development
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    
    // File transport for all logs
    new winston.transports.File({
      filename: path.join('logs', 'somnia-explorer.log'),
      maxsize: 5242880, // 5MB
      maxFiles: 5
    }),
    
    // Separate file for API-specific logs
    new winston.transports.File({
      filename: path.join('logs', 'api-requests.log'),
      level: 'info',
      maxsize: 5242880,
      maxFiles: 3,
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
      )
    }),
    
    // Separate file for errors
    new winston.transports.File({
      filename: path.join('logs', 'error.log'),
      level: 'error',
      maxsize: 5242880,
      maxFiles: 3
    })
  ],
  
  // Handle uncaught exceptions
  exceptionHandlers: [
    new winston.transports.File({
      filename: path.join('logs', 'exceptions.log')
    })
  ],
  
  // Handle unhandled promise rejections
  rejectionHandlers: [
    new winston.transports.File({
      filename: path.join('logs', 'rejections.log')
    })
  ]
});

// Create specialized loggers for different components
export const apiLogger = logger.child({ component: 'api' });
export const explorerLogger = logger.child({ component: 'explorer' });
export const healthLogger = logger.child({ component: 'health' });
```

</details>

**Winston Configuration**

This Winston configuration provides several enterprise-grade features that make it production-ready:

* **Structured JSON logs**: logs are formatted as JSON objects with consistent fields like `timestamp`, `level`, `message`, and `metadata`. This makes it easy to parse logs programmatically, search for specific events, and integrate with log analysis tools like ELK Stack or Splunk.
* **File rotation**: Automatically manages log file sizes by creating new files when they reach 5MB and keeping only the 5 most recent files.
* **Component-specific loggers**: Creates separate logger instances for different parts of your application (API, Explorer, Health) using `logger.child()`.
* **Error tracking with stack traces**: Captures complete error information including stack traces using `winston.format.errors({ stack: true })`.
* **Performance metrics**: Logs response times and request metadata, allowing monitoring of API performance.
* **Separate log files by purpose**: Different files for general logs, API requests, errors, exceptions, and promise rejections.

{% hint style="danger" %}
**Warning**: Ensure the `logs` directory exists before starting your application, or Winston will fail to write log files.
{% endhint %}

### Usage Examples

**Structured Logging Output**

```json
{
  "timestamp": "2024-01-15 10:30:45",
  "level": "info",
  "message": "Somnia Explorer API Request",
  "service": "somnia-explorer-integration",
  "network": "testnet",
  "component": "explorer",
  "method": "GET",
  "url": "/v2/transactions/0x1234...",
  "baseURL": "https://shannon-explorer.somnia.network/api",
  "chain": "somnia"
}
```

**API Service Usage**

```typescript
// Initialize the explorer service
const explorerService = new SomniaExplorerService();

// Get transaction data
const txData = await explorerService.getTransaction('0x1234...');
if (txData) {
  console.log('Transaction found:', txData.hash);
}

// Get block data
const blockData = await explorerService.getBlock(12345);
if (blockData) {
  console.log('Block transactions:', blockData.transactions.length);
}

// Get address transactions
const addressTxs = await explorerService.getAddressTransactions(
  '0xabcd...',
  1, // page
  10 // limit
);
console.log('Address transactions:', addressTxs.length);
```

## Troubleshooting

<details>

<summary>If API requests fail</summary>

* Verify Somnia Network explorer endpoints are accessible
* Check network connectivity to shannon-explorer.somnia.network or explorer.somnia.network
* Ensure correct API endpoint format matches Blockscout v2 schema

</details>

<details>

<summary>If logging doesn't work</summary>

* Ensure the logs directory exists: `mkdir logs`
* Check file permissions for log files
* Verify Winston configuration matches your environment

</details>

You've successfully implemented:

* **Comprehensive API integration** with the Somnia Explorer using type-safe interfaces
* **Enterprise-grade logging** with Winston, including structured JSON output and file rotation
* **Error handling and monitoring** for production-ready applications
* **Performance tracking** with request/response logging and timing metrics

Your application now has robust blockchain data access with proper logging infrastructure that will help you monitor, debug, and maintain your Somnia-based applications in production.

{% hint style="success" %}
[Sample Project](https://github.com/PromiseGameFi/Somnia-sample-project/tree/main/somnia-explorer-integration)
{% endhint %}


# Somnia Gas Differences To Ethereum

Somnia's gas costs work very differently to Ethereum's.

For background: EVM gas fees are designed to approximate the load placed on the blockchain. They are used to charge users in proportion to the system resources they consume. A transaction's gas fee (in ETH or SOMI) is calculated as `number of units of gas * price per unit of gas`. The number of units of gas is just the sum of the gas costs of the transaction's operations.

Because the price of a unit of gas is variable, the absolute cost of an operation in units of gas does not ultimately matter. What matters is the relative difference between operations' gas costs. In other words, if you doubled the gas costs for all operations, and halved the price per unit of gas, there would be no functional change.

In regards to determining gas costs, there are two fundamental differences between Ethereum and Somnia:

1. Somnia's architecture and implementation has drastically reduced the real-world performance cost of most EVM operations, but not all. A few operations, such as third-party library precompiles, are just as performant on Ethereum as on Somnia - both use the same code. This relative difference requires us to adjust the relative gas costs of these operations. We could either reduce the gas costs of all operations that Somnia has improved, or increase the gas costs of all operations that Somnia has *not* improved - the result is equivalent. Somnia does the latter, as it fits better into the Ethereum developer ecosystem.
2. Ethereum charges a flat gas cost to read any existing value from storage for the first time in a transaction. This ends up hitting a database whose latency for this read can vary by multiple orders of magnitude. This gas cost inaccuracy is a big problem when scaling these blockchains, as it requires them to leave massive amounts of unused buffer in their execution budget to account for these unpredictable latencies. Somnia has built a new database, IceDb, that is designed to fix this problem. Only the (rare) reads which actually take a long time have a high gas cost; reading from cached state is far cheaper.

These two factors mean that Somnia requires different gas semantics to Ethereum. The differences are detailed in this document.

This document covers the number of units of gas each operation costs. Somnia also differs from Ethereum in the *price* charged per unit of gas: heavy, sustained users of the network automatically pay a discounted base gas price, as described in [Sustained Use Gas Discounts](/developer/deployment-and-production/sustained-use-gas-discounts).

## Precompiles

These precompiles have flat multiples applied to [the same gas calculation that Ethereum uses](https://www.evm.codes/precompiled).

* `ecRecover (0x01)` costs **50** times the Ethereum calculated gas usage. (i.e. **150,000** gas instead of **3000** gas.)
* `SHA2-256 (0x02)` costs **50** times the Ethereum calculated gas usage.
* `RIPEMD-160 (0x03)` costs **10** times the Ethereum calculated gas usage.
* `modexp (0x05)` costs **10** times the Ethereum calculated gas usage.
* `ecAdd (0x06)` costs **50** times the Ethereum calculated gas usage.
* `ecMul (0x07)` costs **10** times the Ethereum calculated gas usage.
* `ecPairing (0x08)` costs **250** times the Ethereum calculated gas usage.
* `blake2f (0x09)` costs **10** times the Ethereum calculated gas usage.
* `point evaluation (0x0a)` costs **50** times the Ethereum calculated gas usage.

BLS operations:

* `BLS12_G1ADD (0x0b)` costs **220** times the Ethereum calculated gas usage.
* `BLS12_G1MSM (0x0c)` costs **110** times the Ethereum calculated gas usage.
* `BLS12_G2ADD (0x0d)` costs **330** times the Ethereum calculated gas usage.
* `BLS12_G2MSM (0x0e)` costs **50** times the Ethereum calculated gas usage.
* `BLS12_PAIRING_CHECK (0x0f)` costs **50** times the Ethereum calculated gas usage.
* `BLS12_MAP_FP_TO_G1 (0x10)` costs **120** times the Ethereum calculated gas usage.
* `BLS12_MAP_FP2_TO_G2 (0x11)` costs **50** times the Ethereum calculated gas usage.

## Non-storage EVM operations

* `SELFBALANCE` costs **305** gas instead of **5** gas.
* `KECCAK256` costs `1250 + 300 * minimum_word_size` gas instead of `30 + 6 * minimum_word_size` gas.
* `ADDMOD` costs **358** gas instead of **8** gas.
* `MULMOD` costs **358** gas instead of **8** gas.

## Storage EVM operations

Ethereum uses an [access list based gas model](https://www.evm.codes/about#access_list) for all storage operations, to charge extra gas depending on if the value being accessed has already been accessed *in the same transaction*, and if it is creating or deleting that state.

As discussed above, Somnia instead charges extra gas based on a more accurate model of the real world latency it takes to read or write the value in question:

### `SLOAD`

* If the storage slot key is in the set of most recently accessed **128 million** contract slot keys, **no extra gas is charged** (on top of the static op gas fee, which is **100** gas).
* If the key does not exist, the access requires **at least 1,000,000 gas remaining**, but that gas is **not charged**, so there is no extra cost.
* Otherwise, the read costs an additional **1,000,000** gas.

### `SSTORE`

* If the storage slot key is in the set of most recently accessed **128 million** contract slot keys, **no extra gas is charged** (on top of the static op gas fee, which is **100** gas).
* If the key does not exist and is being *written to zero*, the access requires **at least 1,000,000 gas remaining**, but that gas is **not charged**, so there is no extra cost.
* If the key does not exist and is being *written to a non-zero value*, the access requires **at least 1,000,000 gas remaining**, but the caller is charged **200,000** gas.
* Otherwise, the write costs an additional **1,000,000** gas.

### Account operations

The following EVM operations access account state:

* `BALANCE`
* `EXTCODESIZE`
* `EXTCODECOPY`
* `EXTCODEHASH`
* `CALL`
* `CALLCODE`
* `DELEGATECALL`
* `STATICCALL`
* `CREATE`
* `CREATE2`
* `SELFDESTRUCT`

These have an additional storage gas fee (in place of Ethereum's dynamic access list gas fee):

* If the account is in the set of most recently accessed **32 million** accounts, **no extra gas is charged**.
* If the account does not exist and is being *read*, the access requires **at least 1,000,000 gas remaining**, but that gas is **not charged**, so there is no extra cost.
* If the account does not exist and is being *created*, the access requires **at least 1,000,000 gas remaining**, but the caller is charged **400,000** gas for the new account.
* Otherwise, the access costs **1,000,000** gas.

## EVM Logs

Somnia charges an increased amount of gas for EVM logs due to the historical storage requirements.

Ethereum charges: `375 + 375 * topic_count + 8 * size + memory_expansion_cost` gas

Somnia charges: `3200 + 5120 * topic_count + 160 * size + memory_expansion_cost` gas

To illustrate these changes with the five different EVM log operations:

| EVM Op                     | Ethereum gas | Somnia gas |
| -------------------------- | -----------: | ---------: |
| LOG0 with 32 bytes of data |          631 |      8,320 |
| LOG1 with 32 bytes of data |        1,006 |     13,440 |
| LOG2 with 32 bytes of data |        1,381 |     18,560 |
| LOG3 with 32 bytes of data |        1,756 |     23,680 |
| LOG4 with 32 bytes of data |        2,131 |     28,800 |

## Contract bytecode deployment cost

Ethereum charges **200** gas per byte of deployed bytecode. Somnia charges **3125** gas per byte of deployed bytecode.

## EIP-7702 delegation authorisations

Ethereum charges **25,000** gas per authorisation in a [Set Code transaction](https://eips.ethereum.org/EIPS/eip-7702). **12,500** gas is refunded if account creation isn't required.

Somnia charges **1,570,000** gas per authorisation in a Set Code transaction. **400,000** gas is refunded if account creation isn't required.


# Sustained Use Gas Discounts

Somnia automatically reduces the base gas price paid by accounts that consistently consume a lot of gas. The more gas you use over a sustained period, the less you pay for each unit of gas.

There is nothing to opt into, claim, stake or sign. Discounts are computed by the network from observed gas usage and applied automatically when your transactions execute.

## How the discount is applied

A discount is a percentage off the **base gas price only**. Any priority fee you pay is added on top, undiscounted:

```
price charged per gas = base_gas_price * (100 - discount_percent) / 100  +  priority_fee
```

Discounts never reach 100% - gas is never free.

The discount changes the **price** you pay per unit of gas. It never changes how many units of gas an operation costs, and it does not raise any gas limit.

## How usage is measured

Usage is tracked per **sender address** - the `from` of the transaction, which is the account that pays. Gas consumed by contracts you call counts toward your usage, not theirs.

Somnia divides time into **epochs**. On mainnet and testnet an epoch is 3000 ledger blocks, which is roughly **5 minutes** of wall-clock time.

* **Every epoch, the gas used by each account is tracked.** An account is only recorded for that epoch if it consumed more than ten million gas units in that epoch, and only the 1000 heaviest gas consumers of the epoch are recorded.
* **Your discount is derived from your total tracked gas across the most recent 16 epochs** - a rolling window of roughly **80 minutes**. As each epoch passes, the oldest epoch's usage drops out of the window.
* That rolling total is compared against a table of tiers. Each tier names a total amount of gas and the discount earned by reaching it; you get the discount of the highest tier your rolling total reaches. Below the lowest tier, your discount is zero.

A newly earned discount applies to every transaction you send from the **start of the next epoch**, so it takes effect at the next 5-minute boundary and reflects roughly your last 80 minutes of activity.

### Current tiers

The tier table on mainnet:

| Gas units used over the rolling window | Discount |
| -------------------------------------- | -------- |
| 900,000,000                            | 70%      |
| 4,000,000,000                          | 90%      |

At a base gas price of 6 gwei, sustaining those rates costs roughly **100 SOMI** and **430 SOMI** per day on gas at the undiscounted price.

Tiers are network configuration and may be retuned over time; the authoritative table is always the live one returned by `getTiers()` (see [Reading discounts on chain](#reading-discounts-on-chain)).

### Which transactions accrue usage

| Transaction type                                       | Accrues usage                       | Receives a discount |
| ------------------------------------------------------ | ----------------------------------- | ------------------- |
| Ordinary transactions you sign and send                | Yes                                 | Yes                 |
| Transactions that revert or run out of gas             | Yes - you paid for the gas          | Yes                 |
| [Reactivity](/developer/reactivity) handler executions | Yes, to the subscription owner      | Yes                 |
| Reactivity subscription deactivations                  | No - they consume no chargeable gas | n/a                 |
| Protocol-level and zero-gas transactions               | No                                  | No                  |

Because a Reactivity subscription owner may be a contract, contract accounts can accrue usage and earn discounts for the handler executions they pay for.

## Keeping and losing a discount

Your discount is derived from a rolling window, not from a single epoch, so it is stable across quiet periods. If you stop transacting entirely, your usage ages out gradually and your discount only reaches zero after the full window - 16 epochs, roughly 80 minutes - has passed with no activity.

There is no penalty for bursty traffic. Usage accumulates into the window whenever you are busy, and a spike is credited in full.

## What this means for sending transactions

{% hint style="warning" %}
**You must still submit a `gasPrice` at or above the full, undiscounted base fee.** The discount is applied when your transaction executes, not when it is accepted.
{% endhint %}

Concretely:

* **Transaction admission is undiscounted.** A transaction whose `gasPrice` is below the current base fee is rejected from the mempool, whatever your discount. Keep quoting `eth_gasPrice` (or your wallet's estimate) as normal.
* **Balance requirements are undiscounted while pending.** The node reserves `gasPrice * gas` at the full price for in-flight transactions, so you need to be able to cover the undiscounted cost even though you will be charged less.
* **You are charged the discounted price.** The saving shows up as a smaller balance deduction, and in `effectiveGasPrice` on the receipt.

The practical upshot for a dApp is that no change to your submission logic is needed. Fee estimation, `gasPrice` selection and balance checks all continue to work against the undiscounted base fee; your users simply pay less.

## RPC behaviour

No new RPC methods or response fields are involved. Existing fields reflect the discount where - and only where - they describe an actual charge.

| Method / field                                                                     | Reflects the sender's discount?                                                                                            |
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `effectiveGasPrice` on a transaction receipt                                       | **Yes** - the price actually charged. This is the authoritative record.                                                    |
| `gasPrice` on `eth_getTransactionByHash` / `...ByBlock...`                         | **Yes** - note this reports the price *charged*, which for a discounted sender is lower than the `gasPrice` you submitted. |
| `debug_traceTransaction`, `debug_traceBlockByHash`, `debug_traceBlockByNumber`     | **Yes** - replayed with exactly the discount that was charged at the time.                                                 |
| `eth_call`, `eth_estimateGas`, `eth_createAccessList` against `latest` / `pending` | **Yes** - simulated with the `from` account's current discount.                                                            |
| `eth_call` or `debug_traceCall` pinned to a **past** block                         | **No** - historical simulations deliberately run undiscounted.                                                             |
| `eth_gasPrice`                                                                     | **No** - the request has no sender, so it returns the network base price.                                                  |
| `eth_feeHistory`, `block.baseFeePerGas`                                            | **No** - these describe blocks, not senders.                                                                               |
| `eth_maxPriorityFeePerGas`                                                         | Unchanged (priority fees are never discounted).                                                                            |

{% hint style="info" %}
Because receipts record the discount that was applied, a transaction's `effectiveGasPrice` is reproducible forever, even after the sender's discount has changed or expired.
{% endhint %}

## Reading discounts on chain

Discounts live in the enshrined `GasDiscounts` contract, at a per-network address. The contract is a UUPS proxy, so the address is stable across upgrades.

Look the address up for the network you are on, rather than hardcoding it, using the `somnia_getProtocolParameters` method:

```bash
cast rpc somnia_getProtocolParameters '["latest"]' \
  --rpc-url https://api.infra.testnet.somnia.network \
  | jq -r '.protocol_parameters.gas_discounts_contract_address'
```

The useful read-only views are:

| View                                                                                                                                            | Returns                                                                                                                                                                                               |
| ----------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `getTiers() → (uint256 gasThreshold, uint256 discountBps)[]`                                                                                    | The live tier table. `gasThreshold` is gas used **over the whole rolling window**; thresholds are inclusive, and the highest one you reach applies.                                                   |
| `getAccountsWithDiscountsForEpoch() → (address[] accounts, uint256[] discountBps, uint256 hotAccountGasThreshold, uint256 maxReportedAccounts)` | The discount set active for the current epoch - only accounts earning a non-zero discount, in ascending address order - plus the per-epoch minimum gas and the number of accounts recorded per epoch. |
| `getTrackedAccounts() → (address[] accounts, uint256[] discountBps, uint256[] rollingGasUsed)`                                                  | Every account with a live usage window, including those not yet earning a discount, with each window's total.                                                                                         |
| `lastReportedEpoch() → uint256`                                                                                                                 | The epoch of the most recent update.                                                                                                                                                                  |

Discount values in these responses are in basis points, where `10000` is 100% - so a 70% discount reads as `7000`.

For example, to fetch the tier table and the current discount set:

```bash
PROXY=$(cast rpc somnia_getProtocolParameters '["latest"]' \
  --rpc-url $RPC | jq -r '.protocol_parameters.gas_discounts_contract_address')

cast call $PROXY "getTiers()((uint256,uint256)[])" --rpc-url $RPC
cast call $PROXY "getAccountsWithDiscountsForEpoch()(address[],uint256[],uint256,uint256)" --rpc-url $RPC
```

{% hint style="info" %}
There is deliberately no per-account lookup function. To find your own discount, read the full set and filter client-side; both arrays are sorted ascending by address, so a binary search is possible. `rollingGasUsed` from `getTrackedAccounts()` is each window's total **as of the last update**, not aged forward to the current epoch.
{% endhint %}

The simplest way to confirm a discount is being applied to real transactions is to compare `effectiveGasPrice` on one of your receipts against the block's base gas price.

### Events

| Event                                                                                                                     | Emitted                                                                                                                      |
| ------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `DiscountSetComputed(uint256 indexed epochReceivingDiscounts, uint256 indexed setCounter, uint256 numDiscountedAccounts)` | Once per epoch, naming the epoch the new discount set applies to.                                                            |
| `HotAccountsReported(uint256 indexed epoch, address[] accounts, uint256[] gasUsedInEpoch)`                                | Once per epoch, with that epoch's tracked usage. Addresses are in the payload, not indexed, so you cannot filter by address. |
| `TiersUpdated(Tier[] newTiers)`                                                                                           | When the tier table changes.                                                                                                 |
| `ReportingParametersUpdated(uint256 hotAccountGasThreshold, uint256 maxReportedAccounts)`                                 | When the per-epoch minimum gas or the number of accounts recorded per epoch changes.                                         |

Indexing `DiscountSetComputed` and `TiersUpdated` is the cheapest way to keep a dashboard in sync without polling the full set every epoch.

## See also

* [Somnia Gas Differences To Ethereum](/developer/deployment-and-production/somnia-gas-differences-to-ethereum) - how Somnia's gas costs per operation differ from Ethereum's
* [SOMI coin](/developer/network-info/somi-coin) - denominations and the native gas currency


# Ecosystem


# Ecosystem Showcase

Please visit [Somnia Ecoystem](https://somnia.network/ecosystem) to explore projects within the ecosystem.

<figure><img src="/files/wy2Q9ZfgUWTXBViGEzoe" alt=""><figcaption></figcaption></figure>


# Ecosystem Tools

Somnia is built with a robust ecosystem of infrastructure providers that enable developers, projects, and businesses to integrate seamlessly with the network.

With Somnia's ecosystem of infrastructure partners, developers have access to scalable RPCs, smart contract tools, oracles, account abstraction, identity solutions, and analytics.

Each partner provides critical tooling, APIs, and services to power decentralized applications (dApps) on Somnia. Below is an overview of our key Infrastructure Partners and resources to get started.

{% hint style="success" %}
**Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT):**\
\
Please join the [Discord](https://discord.com/invite/somnia). Go to the `#dev-chat` channel, tag the Somnia DevRel, `@emreyeth` and request Test Tokens.\
\
You can also join the [Somnia Developer Telegram](https://t.me/+s_oRMnGpOyQ3ODQ0) or use [Faucet](https://testnet.somnia.network/).\
\
You can also email `developers@somnia.foundation` with a brief description of what you are building and your GitHub profile.
{% endhint %}

| Service                                                                                                   | Provider                                     |
| --------------------------------------------------------------------------------------------------------- | -------------------------------------------- |
| [RPC](/developer/deployment-and-production/ecosystem/ecosystem-tools/rpc)                                 | Ankr, Public Node, Stakely, Validation Cloud |
| [Oracles](/developer/deployment-and-production/ecosystem/ecosystem-tools/oracles)                         | DIA                                          |
| [Subgraphs](/developer/deployment-and-production/ecosystem/ecosystem-tools/subgraphs)                     | Protofire, Ormi                              |
| [Wallet Provider](/developer/deployment-and-production/ecosystem/ecosystem-tools/wallet-providers)        | Privy, Thirdweb                              |
| [Safes](/developer/deployment-and-production/ecosystem/ecosystem-tools/safes)                             | Palmera DAO                                  |
| [Explorers](/developer/deployment-and-production/ecosystem/ecosystem-tools/explorers)                     | Block Scout                                  |
| [SDKs](/developer/deployment-and-production/ecosystem/ecosystem-tools/sdks)                               | Sequence, Thirdweb                           |
| [On Ramps](/developer/deployment-and-production/ecosystem/ecosystem-tools/on-ramps)                       | Banxa                                        |
| [APIs](/developer/deployment-and-production/ecosystem/ecosystem-tools/apis)                               | Ormi                                         |
| [Account Abstraction](/developer/deployment-and-production/ecosystem/ecosystem-tools/account-abstraction) | Pimlico                                      |

<br>


# RPC

## [Ankr](https://ankr.com)

Ankr provides scalable RPC and API services to interact with the Somnia blockchain efficiently. Developers can use Ankr's enterprise-grade infrastructure to enhance dApp performance.

### Resources

* [Ankr RPC for Somnia](https://www.ankr.com/rpc/somnia/)

***

## Public Node

Public Node by All Nodes provides fast, free, and privacy-first RPC endpoints for Somnia Network.

### Resources

* [Public Node RPC for Somnia](https://somnia.publicnode.com/)

***

## Stakely

Stakely is an enterprise-grade infrastructure providerr, trusted by developers and institutions.

### Resources

* [Stakely RPC for Somnia](https://somnia-json-rpc.stakely.io/)

***

## Validation Cloud

Validation Cloud is the world’s fastest node provider according to Compare Nodes. With 50 million compute units available for use without a credit card and a scale tier that never imposes rate limits, Validation Cloud is built to support the most rigorous, low-latency workloads. Somnia Testnet & Somnia Mainnet are supported.

### Resources

* [Link](https://www.validationcloud.io/somnia)


# Oracles

## [DIA](https://www.diadata.org/)

DIA (Decentralized Information Asset) provides real-time on-chain oracles and trusted price feeds for decentralized applications on Somnia. This is critical for DeFi, lending, and trading protocols.

### Resources

* [Somnia Price Oracles](https://docs.diadata.org/use-nexus-product/how-to-dia-nexus-oracles/access-the-oracle/somnia-price-oracles)
* [How to Integrate DIA Price Feeds](/developer/building-dapps/oracles/dia-price-feeds)

***

## [Protofire](http://protofire.io/)

Protofire deploys custom, compatible oracles using the same data providers and node operators, allowing protocols to connect to their network without modifying Smart Contracts.

### Resources

* [Price Feeds](/developer/building-dapps/oracles/protofire-price-feeds)
* [Verifiable Randomness Function](/developer/building-dapps/oracles/using-verifiable-randomness-vrf)




---

[Next Page](/llms-full.txt/1)

