arrow-left

Only this pageAll pages
gitbookPowered by GitBook
triangle-exclamation
Couldn't generate the PDF for 119 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

Main Documentation

Loading...

Get Started

Loading...

Loading...

Loading...

Loading...

Developer

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Getting Started for Mainnet

hashtag
Get SOMI Tokens

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

hashtag
Bridge to Somnia

To have SOMI directly on the mainnet: Relay: To bridge stablecoins from other Networks to Somnia: LayerZero's STARGATE: Orbiter Finance: Welcome to Somnia Mainnet. Below is a checklist for you to confirm the migration from Testnet to Mainnet.

hashtag
For Non-Developers

hashtag
For Developers

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

Gate.ioarrow-up-right

BitMartarrow-up-right

Bitgetarrow-up-right

BingXarrow-up-right

https://relay.link/bridge/somniaarrow-up-right
https://stargate.finance/bridgearrow-up-right
https://www.orbiter.finance/trade/Somniaarrow-up-right
ChainListarrow-up-right
Binancearrow-up-right
Bybitarrow-up-right
module.exports = {
  // ...
  networks: {
    somnia: {
      url: "https://api.infra.mainnet.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};
forge create --rpc-url https://api.infra.mainnet.somnia.network --private-key PRIVATE_KEY src/Example.sol:Example

Tutorials

No, this is not like regular EVM event subscriptions

What eth_subscribe should have been

Think event subscriptions are old news? On Ethereum or other EVM chains, they're just events, no state, and no on-chain reactions. Somnia's push subscriptions deliver state along side event data, something other EVMs cannot offer.

hashtag
Chain Comparison

  • Other Chains: eth_subscribe gives events only—you still pull state separately, risking inconsistency.

  • Somnia: Pushes event + state atomically; invokes Solidity handlers directly.

hashtag
Code Comparison

Ethereum (Pull):

Somnia (Push):

Off-Chain (TypeScript)

Tooling for the typescript ecosystem

hashtag
Overview

Tooling is as follows:

  • TypeScript SDK - Compatible in NodeJS, Browser, JavaScript and Typescript environments

  • React library (future) - Native hooks and react APIs for getting started with subscriptions following React best practives

See the quick start guide for getting started with the SDK

Testnet STT Coin

circle-exclamation

Somnia Testnet is meant for developers to build and deploy the first versions of their applications, and there may be bugs. If you have a question or face an issues, please:

  • Share your feedback at [email protected]

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:

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.

    Quickstartchevron-right

    Leverage ConnectKit for flexible and extensible wallet integrations

    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.

    Integrate Privy for embedded, Web2-friendly wallet flows
    Use RainbowKit to offer a beautiful, customizable wallet connection UI

    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.

    Tutorials

    Hands-on guides to build from scratch

    circle-exclamation

    Somnia Reactivity is currently only available on TESTNET

    Concepts

    Building DApps

    web3.eth.subscribe('logs', { address: '0x...' }, (err, log) => {
      // Now pull state manually
      contract.methods.balanceOf(...).call();
    });
    sdk.subscribe({
      ethCalls: [{
        to: contractAddress,
        data: encodeFunctionData({ abi, functionName: 'balanceOf', args: [userAddress] })
      }],
      onData: (data) => {
        // Event + state (including balanceOf result) delivered atomically with `data`
      }
    });

    Talk to our team in our Discordarrow-up-right

    1. To Proceed with this step, you should connect your wallet with Somnia Testnet

    2. To Request for Somnia Testnet Token, click on the Get STT button

    circle-check

    Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT): Please join the Discordarrow-up-right. Go to the #dev-chat channel, tag the Somnia DevRel, @emreyeth and request Test Tokens. You can also join the Somnia Developer Telegramarrow-up-right or use Faucetarrow-up-right. You can also email [email protected] with a brief description of what you are building and your GitHub profile.

    1. Click on "Get STT"

    1. You should have it in your wallet

    1. You can Select Somnia Testnet and See your tokens

    hashtag
    Try Sending Tokens

    1. Click on Send Tokens

    1. Send STT tokens to your Desired Wallet address or a random address

    1. Check your Activity Section in Metamask

    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.

    hashtag
    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

    hashtag
    Supported Bridges

    hashtag
    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

    Supported Assets: ETH, USDC, USDT, and other major tokens Website:

    hashtag
    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

    Supported Assets: USDC, USDT, ETH, BTC, and LayerZero OFTs Website:

    hashtag
    Using Relay Bridge

    1

    hashtag
    Access Relay Bridge

    Navigate to in your web browser.

    2

    hashtag
    Using Stargate Finance

    1

    hashtag
    Access Stargate Bridge

    Visit and click "Bridge" or "Transfer".

    2

    hashtag
    Troubleshooting Common Issues

    hashtag
    Transaction Stuck or Pending

    Symptoms: Bridge transaction shows "pending" for extended period

    1

    Check Network Status: Verify source and destination chain health

    2

    Gas Price Issues: Increase gas price if transaction is stuck

    3

    Bridge Congestion: Wait for network congestion to clear

    hashtag
    Insufficient Liquidity

    Symptoms: Bridge shows "insufficient liquidity" error

    1

    Reduce Amount: Try bridging smaller amounts

    2

    Wait for Rebalancing: Liquidity pools rebalance automatically

    3

    Alternative Routes: Use different bridge or route

    hashtag
    Wrong Network or Address

    Symptoms: Tokens didn't arrive at expected destination

    1

    Verify Network: Ensure correct destination network selected

    2

    Check Address: Confirm recipient address accuracy

    3

    Network Switch: Switch wallet to destination network

    hashtag
    High Fees or Slippage

    Symptoms: Unexpected high costs or poor exchange rates

    1

    Timing: Bridge during low network congestion periods

    2

    Route Optimization: Compare different bridge options

    3

    Amount Adjustment: Larger amounts often have better rates

    hashtag
    Verification And Testing

    hashtag
    Test Bridge Functionality

    1

    Connect wallet to both source and destination networks

    2

    Bridge small test amount

    3

    Verify tokens arrive within expected timeframe

    hashtag
    Confirm Successful Bridge

    1

    Source Chain: Confirm tokens debited from source wallet

    2

    Bridge Status: Check bridge interface for completion status

    3

    Destination Chain: Verify tokens credited to destination wallet

    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.

    Intersection with Somnia Reactivity

    How to build applications that react to data being streamed to the Somnia chain by creating subscriptions

    hashtag
    Reactivity background

    For detailed information about reactivity please visit the Reactivity docs:

    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:

    Unit
    Wei Value
    Exp
    Ethereum synonym

    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:

    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 implement Verifiable Randomness (VRF) via Protofire + Chainlink

  • 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.

    How to integrate DIA price feeds on Somnia
    How to implement Protofire Price Feeds

    Frontend integrations for building responsive UIs from your subgraph data

    Subgraphs are essential for any developer building real-time dApps, analytics dashboards, or event-driven logic on Somnia.

    How to create and deploy subgraphs on Somnia using ORMI
    How to build with Protofire’s subgraph infrastructure

    Deployment and Production

    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.

    State Consistency Guarantees

    For high performance blockchains, the latency between events being emitted and state updates can cause strange behaviors without Somnia Reactivity

    Somnia ensures notifications deliver events and state that are consistent—sourced from the exact same block. This eliminates race conditions common in pull models.

    hashtag
    How It Works

    • Atomic Delivery: Event + state (via ETH calls) processed in one validator-executed bundle.

    • Guarantees:

      • Non-coalesced: One notification per event.

      • Coalesced: Batched, but state reflects the latest in the batch.

    hashtag
    Example Impact

    In a DeFi app, a "Transfer" event pushes the new balance immediately—no extra balanceOf call needed.

    This makes dApps more reliable and easier to reason about.

    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.

    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.

    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.

    1e18

    Ether (ETH)

    milliSomi

    1,000,000,000,000,000,

    1e15

    microSomi

    1,000,000,000,000

    1e12

    nanoSomi

    1,000,000,000

    1e9

    gWei

    Wei

    1

    1

    Wei

    Somi (SOMI)

    1,000,000,000,000,000,000

    hashtag
    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:

    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.

    Reactivity

    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
    )
    Enterprise Grade: 99.9% uptime with automatic redundancy systems
    LayerZero Powered: Built on robust omnichain infrastructure
    hashtag
    Connect Your Wallet

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

    3

    hashtag
    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

    4

    hashtag
    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.

    5

    hashtag
    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

    6

    hashtag
    Verify Completion

    Check your wallet balance on the destination chain. Relay provides real-time status updates during the bridging process.

    hashtag
    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.)

    3

    hashtag
    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.\

    • Slippage: Set acceptable slippage tolerance (usually 0.1-0.5%)

    4

    hashtag
    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

    5

    hashtag
    Execute Transfer

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

    6

    hashtag
    Monitor Transaction

    Track your transfer through:

    • Stargate Interface: Real-time status updates

    • LayerZero Scan: Cross-chain transaction

    • Destination Explorer: Confirm arrival on target chain

    4

    Contact Support: Reach out to bridge support with transaction hash

    4

    Split Transactions: Break large transfers into smaller ones

    4

    Recovery Process: Contact bridge support for recovery options

    4

    Alternative Bridges: Consider other bridge providers

    4

    Check token balances on both chains

    5

    Test bridge interface responsiveness

    4

    Balance Check: Ensure correct amounts received

    relay.link/bridgearrow-up-right
    stargate.financearrow-up-right
    relay.link/bridgearrow-up-right
    stargate.financearrow-up-right

    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.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the guide on Moving from Testnet to Mainnet.

    hashtag
    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 .

    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.

    hashtag
    Connect to Somnia Testnet

    Ensure you are logged into your MetaMask, connected to the Somnia Testnet, and have some STT Tokens. from the faucet.

    hashtag
    Create the Smart Contract

    Go to the Remix IDE and create a new file. Paste the Smart Contract below:

    hashtag
    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.

    hashtag
    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.

    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.

    Congratulations. 🎉 You have deployed your first Smart Contract to the Somnia Network. 🎉

    Comparison

    Comparing usage of off-chain vs on-chain reactivity

    Somnia Reactivity supports two subscription modes: off-chain (via WebSocket in TypeScript) for flexible, external app integration, and on-chain (via Solidity handlers) for automated, trustless blockchain reactions. Choose based on your dApp's needs—off-chain for UIs/backends, on-chain for DeFi/automation.

    hashtag
    Comparison Table

    Aspect
    Off-Chain (WebSocket/TypeScript)
    On-Chain (Solidity/EVM)

    hashtag
    When to Use Off-Chain Subscriptions

    • Pros: No gas per event, easy integration with web apps, full access to external data/APIs.

    • Cons: Relies on your app being online

    • Example: Real-time dashboard updating on Transfer events without chain writes.

    hashtag
    When to Use On-Chain Subscriptions

    • Pros: Fully decentralized reactions; executes automatically on-chain.

    • Cons: Gas costs accumulate; potential for loops if handlers emit events.

    • Example: Smart contract that auto-swaps tokens on price oracle updates.

    hashtag
    Hybrid Approaches

    Combine both: Use off-chain for monitoring/UI, trigger on-chain actions via transactions when needed.

    On-chain (Solidity)

    The Somnia Reactivity Precompile serves as the backbone for on-chain reactivity

    hashtag
    Somnia Reactivity Precompile

    The Somnia Reactivity Precompile is located at address 0x0100. It provides an interface for managing event subscriptions.

    hashtag
    Interface

    The interface for the precompile is defined in ISomniaReactivityPrecompile.sol:

    circle-info

    Validation rules: At least one filter field must be non-zero — that is, at least one of eventTopics[0..3], origin, or emitter must be specified. A subscription with all wildcard filters will be rejected. The handlerContractAddress must also be non-zero, and gasLimit must be greater than zero.

    Subscriptions: The Core Primitive

    Activating Somnia Reactivity

    Subscriptions are configurable listeners that define what events to watch and how to deliver notifications. They're the foundation of reactivity—create one, and the chain does the rest.

    hashtag
    Key Features

    • Filters: Wildcard (*) for all events, or specify emitters, topics.

    • On-chain

      • Costs: Minimum 32 SOMI balance to cover handler invocation costs on-chain (validators execute handlers) + gas (~210K) to create each subscription

      • Options:

    • Off-chain

      • Costs: Cost of running the Somnia node or paying an RPC provider

    Authenticating with ConnectKit

    In this guide, we'll integrate with the Somnia Network in a Next.js application. This will enable users to connect their wallets seamlessly, facilitating interactions with the Somnia blockchain.

    hashtag
    Prerequisites

    Before we begin, ensure you have the following:

    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.

    hashtag
    Extension in practice (Example 1)

    The typescript code shows how two new schemas re-use the GPS schema in order to append an additional field

    isGuaranteed: Eventual delivery with some block inclusion distance (true/false).
  • isCoalesced: Batch multiple events into one notification within a block.

  • Handler Gas params: priorityFeePerGas, maxFeePerGas, gasLimit

  • Block time (atomic with chain state)

    State Access

    Include ETH view calls in sub; flexible queries

    Limited to handler logic; no external calls;Solidity contract does its own external view calls

    Use Cases

    Front-ends (live UIs), backends (DB updates), integrations

    DeFi (auto-compound), NFTs (reactive mints), oracles

    Reliability Options

    Can specify callback only executed when state changes

    Can specify if you want delivery regardless if block inclusion distance > 0, can also coalece

    Setup Complexity

    Install SDK, subscribe callback

    Deploy handler contract, create/fund sub

    Security

    App-level (e.g., auth WebSockets)

    Blockchain-level (reentrancy risks in handlers)

    Scalability

    Handles high volume off-chain

    Limited by gas/block; use coalescing for batches

    Delivery Mechanism

    WebSocket push to your app/server

    Direct EVM invocation of handler contract

    Execution Environment

    Off-chain (Node.js, browser)

    On-chain (Somnia validators execute)

    Gas / Costs

    None per notification (pay for node or rpc provider)

    Pays gas per invocation (from min 32 SOMI balance)

    Latency

    Near-real-time (block time + network)

    This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.
  • 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.

  • Familiarity with React and Next.js is assumed.

  • hashtag
    Create the Next.js Project

    Open your terminal and run the following commands to set up a new Next.js project:

    Install the required Dependencies, which are wagmi, viem, @tanstack/react-query, and connectkit. Run the following command:

    hashtag
    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:

    In the app directory, locate the layout.tsx file and update it as follows:

    hashtag
    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:

    To run the application, start the Development Server by running the following command:

    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.

    hashtag
    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 Discordarrow-up-right.

    ConnectKitarrow-up-right
    hashtag
    Extension in practice (Example 2)

    Versioned schemas

    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

    interface ISomniaReactivityPrecompile {
        struct SubscriptionData {
            bytes32[4] eventTopics;      // Topic filter (0x0 for wildcard)
            address origin;              // Origin (tx.origin) filter (address(0) for wildcard)
            address caller;              // Reserved for future use (address(0) for wildcard)
            address emitter;             // Contract emitting the event (address(0) for wildcard)
            address handlerContractAddress; // Address of the contract to handle the event
            bytes4 handlerFunctionSelector; // Function selector in the handler contract
            uint64 priorityFeePerGas;    // Extra fee to prioritize handling, in nanoSOMI
            uint64 maxFeePerGas;         // Max fee willing to pay, in nanoSOMI
            uint64 gasLimit;             // Maximum gas that will be provisioned per subscription callback
            bool isGuaranteed;           // If true, moves to next block if current is full
            bool isCoalesced;            // If true, multiple events can be coalesced
        }
    
        // System events
        event BlockTick(uint64 indexed blockNumber);
        event EpochTick(uint64 indexed epochNumber, uint64 indexed blockNumber);
        event Schedule(uint256 indexed timestampMillis);
    
        event SubscriptionCreated(uint64 indexed subscriptionId, address indexed owner);
        event SubscriptionRemoved(uint64 indexed subscriptionId, address indexed owner);
    
        function subscribe(SubscriptionData calldata subscriptionData) external returns (uint256 subscriptionId);
        function unsubscribe(uint256 subscriptionId) external;
        function getSubscriptionInfo(uint256 subscriptionId) external view returns (SubscriptionData memory subscriptionData, address owner);
    }
    npx create-next-app@latest somnia-connectkit
    cd somnia-connectkit
    npm install wagmi viem @tanstack/react-query connectkit
    '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>
      );
    }
    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>
      );
    }
    '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>
      );
    }
    npm run dev
    import { SDK } from "@somnia-chain/streams"
    const sdk = new SDK({
        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
    ])
    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 }
    ])
    Connect Your Wallet
    Remixarrow-up-right
    Get STT Tokens
    explorerarrow-up-right

    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.

    Foundryarrow-up-right 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.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the guide on Moving from Testnet to Mainnet.

    hashtag
    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 .

    3. Foundry is installed and set up on your local machine. See

    hashtag
    Initialise Foundry Project

    To start a new project with Foundry, run the command:

    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.

    hashtag
    Create the Smart Contract

    Create a new file inside the src directory and name it BallotVoting.sol and paste the following code:

    hashtag
    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:

    It will return the response:

    You can learn more about parsing arguments using flags by reading the .

    hashtag
    Deploy Contract.

    Deploying Smart Contracts to the Somnia Network is very straightforward. All you need the RPC URL and the Private Key from an Ethereum address which contains some STT tokens to pay for Gas during deployment. You can get some STT Tokens from the Somnia . Follow this to get your Private Key on MetaMask. To deploy the Smart Contract, run this command in the terminal:

    You will see a status response:

    Copy the Transaction hash and paste it into the Somnia Network . You will find the deployed Smart Contract address. Congratulations. 🎉 You have deployed your “BallotVoting” Smart Contract to the Somnia Network using Foundry. 🎉

    Cron subscriptions via SDK

    Simplified Subscriptions to System Events via the typescript SDK

    Starting from @somnia-chain/[email protected], the typescript SDK introduces two new convenience functions to streamline creating subscriptions for system-generated events: BlockTick and Schedule. These functions reduce boilerplate by handling the underlying SubscriptionData structure and precompile interactions for you, while still allowing customization of gas fees, guarantees, and other parameters.

    For a deeper understanding of how these system events work under the hood (including event signatures and behaviors), refer to the System Events documentation.

    hashtag
    Block Tick Subscription

    The BlockTick event triggers at the end of every block if no specific block number is provided, or at a targeted block if specified.

    hashtag
    Using the SDK

    Use createOnchainBlockTickSubscription to set up the subscription with minimal code:

    This returns the transaction hash on success or an error if the subscription fails.

    hashtag
    Equivalent in Solidity

    For comparison, here's the lower-level Solidity equivalent (as in the core docs):

    hashtag
    Schedule Event (One-Off Cron Job)

    The Schedule event is ideal for one-time future actions. Key notes:

    • Timestamp must be in the future (at least 12 seconds from n ow).

    • It's a one-off subscription—automatically deleted after triggering.

    • Use milliseconds (e.g., via ).

    hashtag
    Using the SDK

    Use scheduleOnchainCronJob for a simple setup:

    This returns the transaction hash on success or an error if the subscription fails.

    hashtag
    Equivalent in Solidity

    For comparison, here's the lower-level Solidity equivalent (as in the core docs):

    circle-info

    These SDK functions default the emitter to SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS and handle event topics automatically, ensuring your handler only responds to genuine system events.

    To get started, update your typescript SDK package to @somnia-chain/[email protected] or later and integrate these functions into your dApp. If you need more advanced customizations (e.g., additional filters like origin or caller), you can still use the full SoliditySubscriptionData type directly with the createSoliditySubscription SDK function (scheduleOnchainCronJob is calling that internally).

    See for a tutorial on how to create a regular subscription to any event emitted by any smart contract.

    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. MetaMaskarrow-up-right is a wallet library that developers can use to build login functionality for applications on the Somnia Network.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the guide on Moving from Testnet to Mainnet.

    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.

    hashtag
    Start a NextJS Project

    Run the command below to start a NextJS project:

    Select Typescript, TailWind CSS, and Page Router in the build options.

    Change the directory into the project folder. Delete the code inside of the <main> tags and replace them with the following:

    hashtag
    Install Viem

    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.

    hashtag
    Import Methods

    The next step is to set up the React State methods and the ViemJS methods that we will require:

    The http is the transport protocol for interacting with the Node of the Somnia Blockchain via RPC. It uses the default Somnia RPC URL: . In the future developers can use RPC providers to avoid rate limiting.

    hashtag
    Import Somnia

    Import Somnia Testnet

    hashtag
    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:

    Now that the States are declared, we can declare a function to handle the MetaMask authentication process on Somnia Network.

    hashtag
    Connect MetaMask Function

    Add the function below inside the export statement:

    hashtag
    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:

    Open your terminal and run the following command to start the app:

    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 . Congratulations, you have successfully connected from MetaMask to Somnia Network. 🎉

    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. is a TypeScript interface for Ethereum that provides low-level stateless primitives for interacting with Ethereum.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the on Moving from Testnet to Mainnet.

    Network Info

    circle-check

    The Somnia Mainnet is LIVE

    circle-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 .

    Somnia Data vs Event Streams

    Serving different purposes, data and event streams can be used independently or together

    hashtag
    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

    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.


    hashtag
    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.

    Quickstart

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

    hashtag
    Pre-requisites

    A typescript environment with and installed

    hashtag

    Push vs Pull: An Architectural Shift

    Simplify your web3 architecture

    Traditional EVM dApps "pull" data via polling (e.g., repeated getLogs or state rpc queries), leading to inefficiency and high rpc costs. Somnia Reactivity's "push" model notifies you proactively, transforming app architecture.

    hashtag
    Highlights

    Aspect
    Pull (Traditional)
    Push (Somnia Reactivity)

    Tooling

    Get started with reactivity tools. Split by environment for clarity.

    circle-exclamation

    Somnia Reactivity is currently only available on TESTNET

    Dive into the following sections to get a better understanding of the Somnia Reactivity tooling

    // 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, "!"));
        }
    }
    Comparisonchevron-right
    Off-Chain (TypeScript)chevron-right
    On-chain (Solidity)chevron-right
    Subscription managementchevron-right
    Connect Your Wallet
    Guidearrow-up-right
    Foundry bookarrow-up-right
    Faucetarrow-up-right
    guidearrow-up-right
    Explorerarrow-up-right
    currentmillis.comarrow-up-right
    Solidity on-chain Reactivity Tutorial
    Viemarrow-up-right
    https://dream-rpc.somnia.networkarrow-up-right
    here

    Event Streams: EVM logsarrow-up-right 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

    hashtag
    TypeScript SDK interface

    Steps

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

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

    hashtag
    2. Compute your unique schema identifier from the schema

    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.

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

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

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

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

    hashtag
    5. Direct data read without reactivity

    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:

    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.

    viemarrow-up-right
    @somnia-chain/streamsarrow-up-right
    forge init BallotVoting
    // 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;
        }
    }
    forge build
    [⠊] Compiling...
    [⠢] Compiling 27 files with Solc 0.8.28
    [⠆] Solc 0.8.28 finished in 2.22s
    Compiler run successful!
    forge create --rpc-url 
    https://dream-rpc.somnia.network
     --private-key PRIVATE_KEY src/BallotVoting.sol:BallotVoting
    [⠊] Compiling...
    No files changed, compilation skipped
    Deployer: 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
    Deployed to: 0x46639fB6Ce28FceC29993Fc0201Cd5B6fb1b7b16
    Transaction hash: 0xb3f8fe0443acae4efdb6d642bbadbb66797ae1dcde2c864d5c00a56302fb9a34
    import { SDK } from '@somnia-chain/reactivity';
    
    // Assuming you have an instance of SomniaReactivity SDK
    const reactivity = new SDK(/* your config */);
    
    async function setupBlockTick() {
      try {
        const txHash = await reactivity.createOnchainBlockTickSubscription({
          // Optional: Specify a future block number; omit for every block
          // blockNumber: BigInt(123456789),
          handlerContractAddress: '0xYourHandlerContractAddress',
          // Optional: Override default handler selector (defaults to onEvent)
          // handlerFunctionSelector: '0xYourSelector',
          priorityFeePerGas: BigInt(1000000000), // 1 nanoSOMI
          maxFeePerGas: BigInt(20000000000), // 20 nanoSOMI
          gasLimit: BigInt(2000000),
          isGuaranteed: true, // Ensure delivery even if delayed
          isCoalesced: false // Handle each event separately
        });
        console.log('Subscription created with tx hash:', txHash);
      } catch (error) {
        console.error('Error creating subscription:', error);
      }
    }
    
    setupBlockTick();
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [BlockTick.selector, bytes32(0), bytes32(0), bytes32(0)], // Or specify blockNumber in topics[1]
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /* Add gas params, isGuaranteed, isCoalesced here */
        });
    // Then call subscribe(subscriptionData) on the precompile
    import { SDK } from '@somnia-chain/reactivity';
    
    // Assuming you have an instance of SomniaReactivity
    const reactivity = new SDK(/* your config */);
    
    async function setupSchedule() {
      try {
        const txHash = await reactivity.scheduleOnchainCronJob({
          timestampMs: 1794395471011, // e.g., Nov 11, 2026, 11:11:11.011
          handlerContractAddress: '0xYourHandlerContractAddress',
          // Optional: Override default handler selector (defaults to onEvent)
          // handlerFunctionSelector: '0xYourSelector',
          priorityFeePerGas: BigInt(1000000000), // 1 nanoSOMI
          maxFeePerGas: BigInt(20000000000), // 20 nanoSOMI
          gasLimit: BigInt(2000000),
          isGuaranteed: true, // Ensure delivery even if delayed
          isCoalesced: false // N/A for one-off, but included for consistency
        });
        console.log('Cron job scheduled with tx hash:', txHash);
      } catch (error) {
        console.error('Error scheduling cron job:', error);
      }
    }
    
    setupSchedule();
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [Schedule.selector, bytes32(uint256(1794395471011)), bytes32(0), bytes32(0)],
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /* Add gas params, isGuaranteed, isCoalesced here */
        });
    // Then call subscribe(subscriptionData) on the precompile
    npx create-next-app metamask-example
     <p>Hello, World!</p>
    npm i viem
    import { useState } from "react";
    import {
      createPublicClient,
      http,
      createWalletClient,
    } from "viem";
    import { somniaTestnet } from "viem/chains";
    const [address, setAddress] = useState<string>("");
    const [connected, setConnected] = useState(false);
    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!"
          );
        }
      };
    {!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>
          )}
    npm run dev
    /**
     * @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>;
    }
    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)
    const sdk = new SDK({
        public: getPublicClient(),
        wallet: getWalletClient(),
    })
    const schemaId = await sdk.streams.computeSchemaId(gpsSchema)
    console.log(`Schema ID ${schemaId}`)
    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" },
    ])
    const publishTxHash = await sdk.streams.set([{
        id: toHex("london", { size: 32 }),
        schemaId: computedGpsSchemaId,
        data: encodedData,
    }])
    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
    }
    const data = await sdk.streams.getByKey(
      computedGpsSchemaId,
      publisherWalletAddress,
      dataKey
    )
    if (data) {
      schemaEncoder.decode(data)
    }
    hashtag
    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.

    hashtag
    Example Smart Contract

    Here is an example `Greeter.sol` Smart Contract:

    hashtag
    Example ABI

    When the Greeter Smart Contract is compiled, below is its ABI:

    chevron-rightABIhashtag

    https://gist.github.com/emmaodia/bdb9b84998b1e4f3f19d0ae27c541e63arrow-up-right

    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:

    hashtag
    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.

    Initialize a project in the directory by running the command:

    Install Viem by running the following command.

    hashtag
    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:

    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.networkarrow-up-right. In the future developers can use RPC providers to avoid rate limiting.

    hashtag
    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.

    hashtag
    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.

    In the index.js we can import the ABI file and start calling methods on the deployed Smart Contract. Import the ABI:

    Set Contract Address

    This is an example Greeter Smart Contract deployed on Somnia Testnet

    Write a Function `interactWithContract`:

    Open your terminal and run the following:

    You will see the response from the Smart Contract logged into the Console!

    Congratulations, you have successfully performed a READ operation on your Smart Contract deployed on Somnia.

    hashtag
    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:

    Then, create a variable walletClient

    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:

    Save the file and run the node command to see your responses logged into the console.

    Congratulations, you have successfully performed a WRITE operation on your Smart Contract deployed on Somnia. 🎉

    Viemarrow-up-right
    guide
    circle-check

    Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT): Please join the Discordarrow-up-right. Go to the #dev-chat channel, tag the Somnia DevRel, @emreyeth and request Test Tokens. You can also join the Somnia Developer Telegramarrow-up-right or use Faucetarrow-up-right. You can also email [email protected] with a brief description of what you are building and your GitHub profile.

    Network
    Mainnet
    Testnet

    Chain ID

    5031

    50312

    Block Explorer

    hashtag
    RPC Providers

    RPCs

    Ankr

    Public Node

    Stakely

    Validation Cloud

    hashtag
    Faucet Providers

    Google Cloud Faucet

    Stakely

    Thirdweb Faucet

    hashtag
    Community

    • Join us on Discordarrow-up-right to chat with devs and raise issues

    • Follow us on Twitterarrow-up-right for updates

    hashtag
    Solidity Resources

    • Ethereum Developer arrow-up-rightby Learn Web3

    • Solidity by Examplearrow-up-right

    • CryptoZombiesarrow-up-right

    hashtag
    Toolkit

    • Hardhat Toolkitarrow-up-right - Solidity development framework paired with a JavaScript testing framework

    • Foundry Toolkitarrow-up-right - Solidity framework for both development and testing.

    • Remixarrow-up-right - Solidity IDE with interactive features

    here
    hashtag
    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.

    hashtag
    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.

    hashtag
    Example


    hashtag
    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.

    hashtag
    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.

    hashtag
    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.

    hashtag
    Example


    hashtag
    Quick Comparison

    Feature
    Mainnet (Production)
    Testnet (Shannon)

    Currency

    SOMI (real value)

    STT (valueless, faucet)

    Purpose

    Production deployments

    Development & testing


    hashtag
    Best Practices

    circle-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.

    circle-check

    Tip: Treat Testnet as your safe playground and Mainnet as your production stage. Every project should pass through Testnet before moving to Mainnet.

    Data Fetch

    Poll RPCs periodically

    Passive notifications

    Latency

    Seconds to minutes (poll interval)

    Near-instant (block time)

    RPC Calls

    High (loops, retries)

    Minimal (one sub setup)

    Complexity

    hashtag
    Why It Matters

    • Simplified Front-Ends: No more setInterval for balances—push updates UIs directly.

    • Efficient Indexers: Push to DBs instead of scanning blocks.

    • Cost Savings: Avoid redundant queries.

    Let the chain push changes to you and build realtime blockchain applications

    Subscription management

    Creating, viewing, and cancelling subscriptions for both off-chain and on-chain scenarios

    circle-exclamation

    Reactivity is currently only available on TESTNET

    Manage your reactivity subscriptions efficiently using the SDK, or do the same via Solidity by directly accessing the Somnia Reactivity Precompile. This covers creation, listing, querying, and cancellation for both off-chain (WebSocket) and on-chain (Solidity) types. Off-chain subscriptions are local to your app; on-chain are chain-managed and require funding (min 32 SOMI).

    hashtag
    Off-Chain (WebSocket) Subscriptions

    Off-chain subs use WebSockets for push notifications. Management is app-side—no chain queries needed.

    Creating a Subscription

    Use sdk.subscribe() to start listening. Returns an object with unsubscribe().

    Unsubscribing

    Call the returned method to stop.

    Tips

    • Track subs in your app state (e.g., array of subscription objects).

    • No listing/querying via SDK—handle locally as they're not persisted on-chain.

    hashtag
    On-Chain (Solidity) Subscriptions

    On-chain subs invoke handlers via EVM. Managed by the chain; owner must fund.

    Subscription Data Structure

    Creating a Subscription

    Returns tx hash on success.

    Getting Subscription Info

    Fetch details by ID.

    Cancelling a Subscription

    Returns txn hash on success. Only owner can cancel.

    hashtag
    Best Practices

    • Funding: Ensure owner has 32+ SOMI; subs pause if low.

    • Error Handling: Always check for Error instances.

    • Monitoring: For on-chain, periodically list and query to monitor status.

    For full SDK reference, see API Reference.

    hashtag
    Solidity Subscription management

    hashtag
    Creating Subscriptions

    Whoever calls the subscribe function becomes the owner of the subscription. The owner can be EOA or a smart contract. In either case, the owner is required to hold a minimum amount of SOMI and is responsible for paying the gas fees associated with handling events.

    The SubscriptionData struct defines the criteria for the event subscription and how it should be handled:

    • eventTopics: An array of 4 bytes32 values representing the event topics to filter by. Use bytes32(0) for wildcards.

    • origin: Filters by the transaction origin (tx.origin). Use address(0) for any origin.

    A subscription can be handled by any smart contract (no special op codes). Additionally, the optional function selector can be used, that is prefixed to the event data when calling the handler contract.

    Handling Events

    When an event matching the subscription criteria is emitted, the Somnia Reactivity Precompile will invoke the specified handler contract and function. The handler contract can implement a function that matches the handlerFunctionSelector specified in the subscription. This function will be called with the event data when a matching event occurs. The owner of the subscription is charged the gas fees specified in the subscription for each event handled. The two fields indicate that the call the to handler has been initialized by the precompile:

    • msg.sender: The Somnia Reactivity Precompile address (0x0100).

    • tx.origin: The owner of the subscription.

    hashtag
    Examples

    1. Fully On-Chain Subscription

    You can create subscriptions directly from another smart contract. This is useful for creating autonomous agents or protocols that react to network activity.

    Key Snippet:

    Authenticating with RainbowKit

    In this guide, we'll integrate RainbowKitarrow-up-right with the Somnia Network in a Next.js application. This will enable users to connect their wallets seamlessly, facilitating interactions with the Somnia blockchain.

    hashtag
    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 .

    3. Familiarity with React and Next.js is assumed.

    hashtag
    Create the Next.js Project

    Open your terminal and run the following commands to set up a new Next.js project:

    Install the required Dependencies, which are wagmi, viem, @tanstack/react-query, and rainbowkit. Run the following command:

    hashtag
    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:

    Every Rainbowkit dApp relies on WalletConnect and needs to obtain a projectId from WalletConnect Cloud. Get one .

    In the app directory, locate the layout.tsx file and update it as follows:

    hashtag
    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:

    hashtag
    Run the Application

    Start the Development Server by running the following command:

    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.

    hashtag
    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.

    Connect Your Wallet To Mainnet

    circle-check

    Somnia Mainnet is LIVE. Visit the Network Information Page for Network details.

    1. You can create your MetaMask Wallet arrow-up-rightfrom here. If you already have a wallet you may skip this step

    2. Visit and Click on "Connect Wallet":

    3. You may use any desired wallet or Metamask (preferred)

    1. Click on "Add to Metamask" to add Somnia Mainnet to your wallet.

    Gas Configuration

    Properly configuring gas parameters is critical for on-chain reactivity subscriptions. If gas values are too low, validators will silently skip your subscription — no error, no warning, just nothing happens.

    triangle-exclamation

    This is the #1 cause of "reactivity not working." Most developers set up their contracts and subscriptions correctly, but use gas values that are too low for validators to process.

    Off-Chain Reactivity: Filtered Subscriptions tutorial

    This tutorial builds on basic off-chain reactivity by adding filters to your WebSocket subscriptions. While wildcard (all events) is great for quick testing and seeing reactivity in action, it's often too verbose for production—flooding logs with irrelevant data. Instead, use filters to target specific emitters or events, making your app more efficient and focused.

    We'll subscribe to Transfer events from a specific ERC20 contract, include a view call (e.g., balanceOf), and enable onlyPushChanges to notify only on state changes. This is ideal for real-time UIs or monitoring without noise.

    hashtag
    Overview

    Deploy with Thirdweb

    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.

    hashtag
    Prerequisites

    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 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

    // 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, "!"));
        }
    }
    {
    "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.
    },
    mkdir viem-example && cd viem-example
    npm init -y
    npm i viem
    import { createPublicClient, createWalletClient, http } from "viem";
    import { somniaTestnet } from "viem/chains"
    const publicClient = createPublicClient({ 
      chain: somniaTestnet, 
      transport: http(), 
    }) 
    export const ABI = [//...ABI here]
    import { ABI } from "./abi.js";
    const CONTRACT_ADDRESS = "0x2e7f682863a9dcb32dd298ccf8724603728d0edd";
    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();
    node index.js
    import { privateKeyToAccount } from "viem/accounts";
    const walletClient = createWalletClient({
      account: privateKeyToAccount($YOUR_PRIVATE_KEY),
      chain: somniaTestnet,
      transport: http(),
    });
       // 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);
    node index.js
    Deploy to Somnia Mainnet
    # Deploying a contract to Somnia Mainnet
    npx hardhat run scripts/deploy.js --network somnia_mainnet
    Deploy to Somnia Testnet (Shannon)
    # Deploying a contract to Somnia Testnet
    npx hardhat run scripts/deploy.js --network somnia_testnet

    Transactions

    Permanent and irreversible

    Experimental and disposable

    Typical Use

    Live dApps, DeFi, staking, NFT

    Prototyping, QA, education

    Risk

    Financial impact possible

    No financial risk

    Manage loops, error handling

    Simple callback/handler

    Use Cases

    Basic event listening

    Real-time reactions, auto-updates

    Symbol

    SOMI

    STT

    RPC

    https://api.infra.mainnet.somnia.network/arrow-up-right wss://api.infra.mainnet.somnia.network/ws

    https://api.infra.testnet.somnia.network/arrow-up-right wss://api.infra.testnet.somnia.network/ws

    MultiCallV3

    0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11arrow-up-right

    0x841b8199E6d3Db3C6f264f6C2bd8848b3cA64223arrow-up-right

    EntryPoint v0.7

    0x0000000071727De22E5E9d8BAf0edAc6f37da032arrow-up-right

    Factory Address

    0x4be0ddfebca9a5a4a617dee4dece99e7c862dcebarrow-up-right

    Alternative Testnet Block Explorer

    https://somnia-testnet.socialscan.io/arrow-up-right

    Testnet Faucet

    https://testnet.somnia.network/arrow-up-right

    Stakely Mainnet Faucet

    https://stakely.io/faucet/somnia-somiarrow-up-right

    CreateX

    0xD13C575ED5378fd18B100Bd87D5765d9A747358B

    0x535822d4b86b2372FBE4fd9d1468318F04A2A640

    https://www.validationcloud.io/somniaarrow-up-right

    Cookbook.devarrow-up-right
    Alchemy Unversityarrow-up-right
    https://explorer.somnia.networkarrow-up-right
    https://shannon-explorer.somnia.network/arrow-up-right
    https://www.ankr.com/rpc/somniaarrow-up-right
    https://somnia.publicnode.comarrow-up-right
    https://somnia-json-rpc.stakely.ioarrow-up-right
    https://cloud.google.com/application/web3/faucet/somnia/shannonarrow-up-right
    https://stakely.io/faucet/somnia-testnet-sttarrow-up-right
    https://thirdweb.com/somnia-shannon-testnetarrow-up-right
    Chainlistarrow-up-right
    Security: Use private keys securely; avoid over-provisioning gas.
    caller: Reserved for future use. Currently not active in event matching. Use address(0).
  • emitter: The address of the contract emitting the event. Use address(0) for any emitter.

  • handlerContractAddress: The address of the contract that will be called when a matching event occurs.

  • handlerFunctionSelector: The 4-byte function selector of the method to call on the handler contract.

  • priorityFeePerGas: Additional gas fee paid to validators to prioritize this event handling. This is expressed in nanoSOMI (gwei equivalent).

  • maxFeePerGas: The maximum total gas fee (base + priority) the subscriber is willing to pay. This is expressed in nanoSOMI (gwei equivalent).

  • gasLimit: The maximum gas that will be provisioned per subscription callback

  • isGuaranteed: If true, the event handling is guaranteed to execute, potentially moving to the next block if the current block is full.

  • isCoalesced: If true, multiple matching events in the same block can be coalesced into a single handler call (implementation dependent).

  • Connect Your Wallet guide
    herearrow-up-right
    Somnia Developer Discordarrow-up-right
    Off-chain subscriptions push filtered events + state via WebSockets to TypeScript apps. Filters reduce volume:
    • eventContractSources: Limit to specific emitter addresses.

    • topicOverrides: Filter by event topics (e.g., keccak256 signatures like Transfer's 0xddf...).

    • onlyPushChanges: Skip notifications if ethCalls results match the previous one.

    • ethCalls: Optional view calls for bundled state.

    • onError: Handle failures gracefully.

    No gas costs; runs off-chain.

    Prerequisites:

    • Same as wildcard tutorial: Node.js, npm i @somnia-chain/reactivity viem.

    • Know your target contract/event (e.g., ERC20 Transfer).

    hashtag
    Key Objectives

    1. Set Up Chain and SDK: Configure viem and initialize SDK.

    2. Define Filters and ETH Calls: Specify emitters, topics, and views.

    3. Create Filtered Subscription: Use params for targeted listening.

    4. Decode and Handle Data: Parse with viem; add error handling.

    5. Run and Optimize: Test with onlyPushChanges.

    hashtag
    Step 1: Install Dependencies

    hashtag
    Step 2: Define the Somnia Chain

    (Reuse from wildcard tutorial.)

    hashtag
    Step 3: Initialize the SDK

    hashtag
    Step 4: Define ETH Calls and Filters

    • ethCalls: Query balanceOf on an ERC20.

    • eventContractSources: Array of emitter addresses (e.g., one ERC20 contract).

    • topicOverrides: Hex for event signatures (e.g., Transfer: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef).

    hashtag
    Step 5: Create the Filtered Subscription

    Include onlyPushChanges: true to notify only on balance changes. Add onError for robustness.

    hashtag
    Step 6: Decode Data in the Callback

    Parse the event (Transfer) and view result (balanceOf).

    hashtag
    Step 7: Put It All Together and Run

    Full script (main.ts):

    Run: ts-node main.ts.

    hashtag
    Testing

    1. Run the script.

    2. Trigger a Transfer on the filtered contract (e.g., send tokens).

    3. See notifications only on relevant events/changes.

    • If too quiet: Remove onlyPushChanges or broaden filters.

    • Prod Tip: Start with wildcard for debugging, then add filters.

    hashtag
    Troubleshooting

    • No Notifications? Verify topics/address (use explorers for keccak). Check WS connection.

    • Errors? Handle in onError; common: Invalid filters or RPC issues.

    • Too Many? Tighten topicOverrides or eventContractSources.

    hashtag
    Next Steps

    • Multi-emitters: Add more to eventContractSources.

    • Custom Events: Compute topics for your ABI.

    • Advanced: Combine with React for live UIs.

    • Compare to On-Chain: On-Chain Tutorial.

    import { SDK, SubscriptionCallback } from '@somnia-chain/reactivity';
    
    const subscription = await sdk.subscribe({
      ethCalls: [], // Optional: ETH view calls
      onData: (data: SubscriptionCallback) => {
        console.log('Event:', data);
      },
      // Other filters: eventTopics, origin, etc.
    });
    
    // Store subscription for later management
    subscription.unsubscribe();
    export type SoliditySubscriptionData = {
      eventTopics?: Hex[]; // Optional filters
      origin?: Address;
      caller?: Address;
      emitter?: Address;
      handlerContractAddress: Address; // Required
      handlerFunctionSelector?: Hex; // Optional override
      priorityFeePerGas: bigint;
      maxFeePerGas: bigint;
      gasLimit: bigint;
      isGuaranteed: boolean;
      isCoalesced: boolean;
    };
    
    export type SoliditySubscriptionInfo = {
      subscriptionData: SoliditySubscriptionData,
      owner: Address
    };
    const subData: SoliditySubscriptionData = {
      handlerContractAddress: '0x123...',
      priorityFeePerGas: parseGwei('2'),
      maxFeePerGas: parseGwei('10'),
      gasLimit: 2_000_000n,
      isGuaranteed: true,
      isCoalesced: false,
      // Add filters as needed
    };
    
    const txHash = await sdk.createSoliditySubscription(subData);
    if (txHash instanceof Error) {
      console.error(txHash.message);
    } else {
      console.log('Created:', txHash);
    }
    const info = await sdk.getSubscriptionInfo(123n); // bigint ID
    if (info instanceof Error) {
      console.error(info.message);
    } else {
      console.log('Info:', info);
    }
    const txHash = await sdk.cancelSoliditySubscription(123n);
    if (txHash instanceof Error) {
      console.error(txHash.message);
    } else {
      console.log('Canceled:', txHash);
    }
    ISomniaReactivityPrecompile.SubscriptionData memory subscriptionData = ISomniaReactivityPrecompile.SubscriptionData({
        eventTopics: [Transfer.selector, bytes32(0), bytes32(0), bytes32(0)],
        origin: address(0),
        caller: address(0),
        emitter: address(tokenAddress),
        handlerContractAddress: address(this),
        handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
        priorityFeePerGas: 2 gwei,       // 2 nanoSOMI
        maxFeePerGas: 10 gwei,           // 10 nanoSOMI
        gasLimit: 2_000_000,             // Sufficient for simple state updates
        isGuaranteed: true,
        isCoalesced: false
    });
    
    uint256 subscriptionId = somniaReactivityPrecompile.subscribe(subscriptionData);
    npx create-next-app@latest somnia-rainbowkit
    cd somnia-rainbowkit
    npm install wagmi viem @tanstack/react-query rainbowkit
    '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>
      );
    }
    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>
      );
    }
    '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>
      );
    }
    npm run dev
    npm i @somnia-chain/reactivity viem
    import { defineChain } from 'viem';
    
    const somniaTestnet = defineChain({
      id: 50312,
      name: 'Somnia Testnet',
      // ... full config as before
    });
    import { SDK } from '@somnia-chain/reactivity';
    import { createPublicClient, webSocket } from 'viem';
    
    const publicClient = createPublicClient({
      chain: somniaTestnet,
      transport: webSocket(),
    });
    
    const sdk = new SDK({ public: publicClient });
    import { encodeFunctionData, erc20Abi, keccak256, toHex } from 'viem';
    
    // Example: Transfer topic (keccak256('Transfer(address,address,uint256)'))
    const transferTopic = keccak256(toHex('Transfer(address,address,uint256)'));
    
    const ethCall = {
      to: '0xExampleERC20Address', // Your target ERC20
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: 'balanceOf',
        args: ['0xYourWalletAddress'], // Monitor this balance
      }),
    };
    
    const filters = {
      eventContractSources: ['0xExampleERC20Address'], // Filter to this emitter
      topicOverrides: [transferTopic], // Only Transfer events
    };
    const subscription = await sdk.subscribe({
      ethCalls: [ethCall], // Bundled state query
      ...filters, // From Step 4
      onlyPushChanges: true, // Efficient: Skip if balance unchanged
      onData: (data) => {
        console.log('Filtered Notification:', data);
        // Decoding here (Step 6)
      },
      onError: (error) => {
        console.error('Subscription Error:', error.message);
        // Retry logic or alert
      },
    });
    
    // Unsubscribe: subscription.unsubscribe();
    import { decodeEventLog, decodeFunctionResult } from 'viem';
    
    // Inside onData:
    const decodedLog = decodeEventLog({
      abi: erc20Abi,
      topics: data.result.topics,
      data: data.result.data,
    });
    
    const decodedBalance = decodeFunctionResult({
      abi: erc20Abi,
      functionName: 'balanceOf',
      data: data.result.simulationResults[0],
    });
    
    console.log('Decoded Transfer:', decodedLog.args); // { from, to, value }
    console.log('New Balance:', decodedBalance);
    // Imports...
    
    async function main() {
      // Chain, client, SDK from Steps 2-3...
    
      // ethCall and filters from Step 4...
    
      const subscription = await sdk.subscribe({
        ethCalls: [ethCall],
        ...filters,
        onlyPushChanges: true,
        onData: (data) => {
          // Decoding from Step 6...
        },
        onError: (error) => console.error(error),
      });
    
      // Run indefinitely or unsubscribe on signal
    }
    
    main().catch(console.error);
    hashtag
    Understanding the Parameters

    On-chain reactivity subscriptions require three gas parameters:

    Parameter
    Description
    Unit

    priorityFeePerGas

    Tip paid to validators to prioritize your handler execution

    nanoSOMI (gwei)

    maxFeePerGas

    Maximum total fee per gas (base fee + priority fee)

    nanoSOMI (gwei)

    hashtag
    How They Work Together

    1

    hashtag
    priorityFeePerGas

    This is essentially the "tip" for validators. The default value is 0 nanoSOMI. Increase it to make sure your handler is executed before others.

    2

    hashtag
    maxFeePerGas

    The ceiling on what you'll pay per gas unit. The minimum is baseFee + priorityFeePerGas , where the base fee in Somnia is 6 nanoSOMI. Setting this too low will cause your handler invocation to fail in peak times. If set to 0, the protocol automatically sets it to the maximum allowed value.

    3

    hashtag
    gasLimit

    How much gas your _onEvent handler is allowed to consume. If your handler runs out of gas, it reverts.

    hashtag
    Recommended Values

    hashtag
    Standard Use Cases

    hashtag
    By Handler Complexity

    Handler Type

    priorityFeePerGas

    maxFeePerGas

    gasLimit

    Example

    Simple (state updates, emit event)

    parseGwei('0')

    parseGwei('10')

    2_000_000n

    Counter, token reward

    hashtag
    Quick Reference Table (Raw BigInt Values)

    If you prefer raw values instead of parseGwei():

    Level

    priorityFeePerGas

    maxFeePerGas

    gasLimit

    Minimum recommended

    0n

    10_000_000_000n

    2_000_000n

    Comfortable

    hashtag
    Common Mistakes

    hashtag
    Using wei instead of nanoSOMI (gwei)

    circle-exclamation

    10n is 10 wei = 0.00000001 gwei. This is 200 million times less than the recommended 2 gwei. Always use parseGwei() to avoid unit confusion. See SOMI coin for details on how this is calculated.

    hashtag
    Computing from gasPrice with too-small divisors

    hashtag
    Setting gasLimit too low

    Somnia operates on a different gas model to Ethereum. One of the key differences is that the 1,000,000 gas reserve is required for any storage operations, see . It is safe to up your gas limit to meet the reserve requirements. If your handler reverts due to out-of-gas, the subscription still charges you but the state change doesn't happen.

    hashtag
    Forgetting to recreate subscription after redeploying

    Subscriptions are tied to specific contract addresses. If you redeploy your contract, you get a new address. The old subscription won't trigger for the new contract.

    hashtag
    Cost Estimation

    The subscription owner pays for each handler invocation. The cost per invocation is:

    Where effectiveGasPrice is at most maxFeePerGas and at least baseFee + priorityFeePerGas.

    hashtag
    Example

    For a simple handler using ~50,000 gas at 10 gwei max fee:

    The subscription owner must maintain at least 32 SOMI balance. This is not spent — it's a minimum holding requirement. Actual costs are deducted per invocation.

    hashtag
    Debugging Gas Issues

    If your subscription was created successfully but the handler is never invoked:

    1

    hashtag
    Check subscription info

    Verify that the fee fields (e.g., priorityFeePerGas, maxFeePerGas) are gwei‑denominated — i.e., multiples of 1 gwei = 1_000_000_000 wei (nanoSOMI‑scale). For example, 2000000000 is 2 gwei; a raw value like 2 is effectively zero even though it is still expressed in wei.

    2

    hashtag
    Test with a CLI script first

    Before debugging frontend issues, confirm reactivity works via a Hardhat script:

    3

    hashtag
    Look for validator transactions

    On-chain reactivity is executed by validators from the address 0x0000000000000000000000000000000000000100. Check the block explorer for transactions from this address to your handler contract after your event was emitted.

    hashtag
    Summary

    Do
    Don't

    Use parseGwei() for fee fields (priorityFeePerGas, maxFeePerGas)

    Use raw small numbers like 10n or 100n for fee values

    Start with parseGwei('10') for maxFeePerGas on testnets

    Compute maxFeePerGas from gasPrice with arbitrary divisors

    Set gasLimit based on handler complexity

    Use a one-size-fits-all low gasLimit

    Recreate subscription after redeploying

    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:

  • hashtag
    Set Up the Project

    First, create a new folder for your project and initialize it.

    hashtag
    Write the Smart Contract

    You can write your Smart Contract using the Remix IDE to ensure it works. Create a file OpenGreeter.sol and add the following code:

    chevron-rightOpenGreeter.solhashtag

    This is a simple Greeter Smart Contract that any address can call the changeName function. The Smart Contract has two functions: 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.

    hashtag
    Deploy the Smart Contract using Thirdweb.

    First, go to Thirdweb and create a profile. After you have completed the onboarding process, create a Project.

    Go to the project settings.

    Copy your secret key, and keep it safe. The secret key will be used to deploy the Smart Contract.

    Go to the terminal and paste the following command to deploy the Smart Contract:

    Select the solc option to be true in the prompts on Terminal.

    Click the link to open the User Interface in your Browser to deploy the Smart Contract.

    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.

    Your Smart Contract is deployed, and you can view it on your Thirdweb Dashboard.

    Visit the Explorer section to simulate interactions with your deployed Smart Contract and carry out actual transactions.

    Congratulations. 🎉 You have deployed your Smart Contract to the Somnia Network using Thirdweb. 🎉

    Thirdwebarrow-up-right

    Complete payment securely

  • Receive SOMI in your wallet


  • hashtag
    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.).


    hashtag
    Step 1 — Visit the Banxa Checkout Page

    Go to https://checkout.banxa.com/arrow-up-right. You’ll see a simple interface to select your fiat currency and cryptocurrency.


    hashtag

    hashtag
    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.


    hashtag
    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.

    triangle-exclamation

    Triple check the address before proceeding. Funds sent to the wrong address cannot be recovered.


    hashtag
    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.


    hashtag
    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.


    hashtag
    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.


    hashtag
    Need Help?

    If your transaction is delayed or you need assistance, you can contact:

    • Banxa Support: support.banxa.comarrow-up-right

    • Somnia Community: Discord → #supportarrow-up-right


    hashtag
    Disclaimers

    • Banxa is a third party provider. Always ensure you are using the official checkout.banxa.comarrow-up-right link.

    • Transaction fees, limits, and verification steps vary by country and payment method.

    • SOMI purchases are non reversible once processed.

    Banxa Checkout Portalarrow-up-right

    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.

    circle-exclamation

    Kindly note that the Native Coin for Somnia Testnet is STT.

    This multi-part guide shows how to use SOMI for:

    • Payments

    • Escrow

    • Donations & Tipping

    • Sponsored gas via Account Abstraction

    hashtag
    Use SOMI for Payments in Smart Contracts

    A simple contract that accepts exact SOMI payments:

    Use msg.value to access the native coin sent in a transaction. No ERC20 functions are needed.

    To withdraw collected SOMI:

    Deploy using Hardhat Ignition or Viem and test with a sendTransaction call.

    chevron-rightExample.solhashtag

    hashtag
    Build an SOMI Escrow Contract

    A secure escrow contract allows a buyer to deposit SOMI and later release or refund:

    Release funds to the seller:

    Deploy with Hardhat Ignition and pass SOMI as value during deployment.

    chevron-rightExample.solhashtag

    hashtag
    SOMI Tip Jar

    Allow any wallet to send tips directly:

    Withdraw all tips:

    chevron-rightExample.solhashtag

    hashtag
    Frontend tip

    hashtag
    Sponsor SOMI Transactions with Account Abstraction

    Using a smart account + relayer (e.g. via Privy, Thirdweb), the dApp can cover gas fees:

    The Smart Contract function can execute mint or other logic as usual. The paymaster or relayer pays SOMI.

    hashtag
    Conclusion

    • SOMI is native and used via msg.value, .transfer(), and payable.

    • There is no contract address for SOMI (STT for Testnet).

    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.

    hashtag
    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

    1.2. Logic and Functional Checks

    Item
    Requirement
    Verification Steps

    hashtag
    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

    2.2. Warning Triage and Sign-off

    chevron-rightHigh/Critical Severity Warningshashtag

    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.

    chevron-rightMedium/Low Severity Warningshashtag

    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.

    chevron-rightReport Retentionhashtag

    The final, clean static analysis reports must be saved and archived as part of the pre-deployment documentation.

    circle-check

    Congratulations! You've mastered the audit checklist for both manual and automatic process. Continue to learn prevention strategies and secure coding patterns.

    Introduction

    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.

    circle-check

    Somnia Mainnet is LIVE. Visit the Network Information Page for Network details.

    circle-check

    Developers who are deploying Smart Contracts and need Somnia Test Tokens (STT): Please join the . Go to the #dev-chat channel, tag the Somnia DevRel, @emreyeth and request Test Tokens. You can also join the or use . You can also email [email protected] with a brief description of what you are building and your GitHub profile.

    Somnia is supported by and . 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.

    Building Subgraph UIs (NextJS/Fetch)

    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

    Streams Case Study: Formula 1

    Streaming data from OpenF1 on-chain and building reactive applications

    hashtag
    Schemas

    Driver schema

    Cartesian 3D coordinates schema

    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.

    FAQs & Troubleshooting

    Get answers to commonly asked questions and issues faced by users

    hashtag
    FAQs

    circle-exclamation

    Somnia Reactivity is currently only available on TESTNET

    Somnia Reactivity

    Blockchain data pushed to applications; available to Solidity and Typescript Somnia developers

    circle-exclamation

    Reactivity is currently only available on TESTNET

    Somnia Reactivity is a native toolkit for building event-driven dApps on the Somnia chain. Events and blockchain state is pushed directly to your TypeScript or Solidity apps in one atomic notification without polling.

    hashtag

    const info = await sdk.getSubscriptionInfo(subscriptionId);
    console.log(JSON.stringify(info, (_, v) => typeof v === 'bigint' ? v.toString() : v, 2));
    import { parseGwei } from 'viem';
    
    await sdk.createSoliditySubscription({
      handlerContractAddress: '0x...',
      emitter: '0x...',
      eventTopics: [eventSignature],
      priorityFeePerGas: parseGwei('0'), // 0 nanoSOMI — typically, no priority is required 
      maxFeePerGas: parseGwei('10'),     // 10 nanoSOMI — comfortable ceiling
      gasLimit: 2_000_000n,              // Sufficient for simple state updates
      isGuaranteed: true,
      isCoalesced: false,
    });
    // WRONG — these are 10 wei and 20 wei, essentially zero
    priorityFeePerGas: 10n,
    maxFeePerGas: 20n,
    
    // CORRECT — these are 2 nanoSOMI and 10 nanoSOMI
    priorityFeePerGas: parseGwei('2'),    // = 2_000_000_000n
    maxFeePerGas: parseGwei('10'),         // = 10_000_000_000n
    // RISKY — gas price fluctuates and dividing by 10 may yield too-low values
    const gasPrice = await publicClient.getGasPrice();
    priorityFeePerGas: gasPrice / 10n,
    
    // SAFER — use fixed proven values
    priorityFeePerGas: parseGwei('2'),
    // TOO LOW for any storage operations
    gasLimit: 100_000n,
    
    // SAFE for most handlers
    gasLimit: 2_000_000n,
    
    // SAFE for complex handlers with external calls
    gasLimit: 10_000_000n,
    Deploy contract → address A
    Create subscription → emitter: A, handler: A  ✅
    
    Redeploy contract → address B
    Old subscription still points to A → ❌ won't work
    Must create NEW subscription → emitter: B, handler: B  ✅
    cost = gasUsed × effectiveGasPrice
    cost ≈ 50,000 × 10 gwei = 500,000 gwei = 0.0005 SOMI per invocation
    // 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, "!"));
        }
    }
    npm thirdweb install
    mkdir somnia-thirdweb-example
    cd somnia-thirdweb-example
    npx thirdweb deploy -k your_secret_key
    hashtag
    How Do I Fetch Historical Data?

    To get event and state data from before an application subscription was created there are a few approaches:

    • Use a traditional indexer or tooling such as a subgraph

    • Build a custom indexer which starts at an earlier block and persists data received from Somnia reactivity into a DB that can be queried from the chain

    • Directly query historical data from the chain from within your application (generally inefficient)

    hashtag
    Troubleshooting

    hashtag
    Issues starting websocket subscriptions

    Two core reasons for this:

    1. The chain definition for the Somnia testnet or mainnet does not contain a wss url

    2. The wss url does not support the reactivity feature set or the rpc provider is having issues

    hashtag
    Too many active websocket subscriptions

    Often seen in React applications where useEffect is not being used correctly or not used at all to start an event subscription leading to attempts to create a subscription every time the page renders

    hashtag
    Solidity handler not being invoked

    Four core reasons for this:

    1. Incorrect gas configuration — this is the most common cause. Using raw values like 10n instead of parseGwei('10') results in fees that are too low for validators to process. See Gas Configuration for recommended values.

    2. Invalid implementation of SomniaEventHandler interface

    3. No active subscription

    4. Insufficient subscription balance

    Key Benefits
    • Real-Time Efficiency: Notifications include events + state from the same block, slashing RPC calls and latency.

    • Cross-Environment: Seamless for off-chain (WebSocket) and on-chain (EVM invocations).

    • Scalable Subscriptions: Customizable for filters, guarantees, and coalescing to fit your app's needs.

    TL;DR: Build reactive dApps that respond instantly to on-chain activity, reducing complexity and costs vs. traditional EVM setups.

    hashtag
    Schema registration and re-use
    uint32 number, string name, string abbreviation, string teamName, string teamColor
    int256 x, int256 y, int256 z
    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)
    }
    You can integrate SOMI into any Solidity or Viem app with no ERC20 logic.
  • Account Abstraction enables gasless dApps using SOMI as sponsor currency.

  • 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.

    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.

    Enforces code style and security best practices, ensuring clean and maintainable code.

    Must pass all configured security and style rulesets without warnings.

    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).

    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.

    Slitherarrow-up-right

    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.

    Mythrilarrow-up-right

    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).

    Integer Overflow/Underflow

    Error Handling

    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.

    hashtag
    Prerequisites

    • Basic knowledge of React & Next.js.

    • A deployed Subgraph API on Somnia (or use an existing onearrow-up-right).

    • Account on https://somnia.chain.lovearrow-up-right see guide.

    hashtag
    Create a NextJS Project

    Start by creating a new Next.js app.

    Then, install required dependencies.

    hashtag
    Define the Subgraph API in Environment Variables

    Create a .env.local file in the root folder.

    circle-info

    💡 Note: Restart your development server after modifying .env.local:

    hashtag
    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:

    This code allows your frontend to make requests without triggering CORS errors.

    hashtag
    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:

    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).

    hashtag
    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.

    Once data is fetched, we render it inside the UI.

    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.

    hashtag
    Add the Component to the NextJS Page

    Update the page.tsx file:

    Restart the development server:

    Now your NextJS UI dynamically fetches and displays token transfers from Somnia’s Subgraph! 🔥

    Subgraphsarrow-up-right

    gasLimit

    Maximum gas provisioned per handler invocation

    gas units

    Medium (cross-contract calls)

    parseGwei('0')

    parseGwei('10')

    3_000_000n

    Game logic with external calls

    Complex (multiple external calls, loops)

    parseGwei('10')

    parseGwei('20')

    10_000_000n

    Settlement, multi-step workflows

    0n

    10_000_000_000n

    3_000_000n

    High priority

    10_000_000_000n

    20_000_000_000n

    10_000_000n

    Assume old subscription works with new contract

    Test via CLI before building frontend

    Debug reactivity issues through the browser

    Discordarrow-up-right
    Somnia Developer Telegramarrow-up-right
    Faucetarrow-up-right
    Improbablearrow-up-right
    MSquaredarrow-up-right
    Cover

    Start developing on Somnia

    Cover

    Connect To Mainnet

    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.

    hashtag
    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.

    hashtag
    Example: Defining a Schema

    This schema tells the Somnia Data Streams system how your data is structured and typed.

    hashtag
    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.

    Example Output:

    triangle-exclamation

    This hash (schemaId) uniquely identifies the schema onchain. If you change even one character in the schema definition, the Schema ID will change.

    The Schema ID is the hash that ensures the same structure is used everywhere, preventing mismatched or corrupted data.

    hashtag
    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:

    id is a string. human human-readable identifierignoreExistingSchemas 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.

    For instance:

    Schema → CREATE TABLE Users (id INT, name TEXT)

    Schema ID → 0x9f3a...a7c (hash of the above definition)

    hashtag
    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.

    hashtag
    Example: Creating a Data ID

    A Data ID can be created by hashing a string, typically by combining context and timestamp.

    Example Output:

    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.

    hashtag
    Example: Writing Data with a Schema and Data ID

    Think of a Data ID like a primary key in a SQL table.

    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

    hashtag
    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.

    hashtag
    Example: Getting a Publisher Address

    If you’re using a connected wallet, your publisher is automatically derived using the createWalletClient from viem:

    Where publisher = wallet.account.address

    When reading data, you can specify which publisher’s records to fetch:

    Example Output:

    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.

    hashtag
    Putting It All Together

    When you publish data on Somnia, three identifiers always work together:

    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.

    hashtag
    Example Use Case: Chat Messages

    Here’s how they interact in a real-world scenario, a decentralized chat room.

    hashtag
    Step 1: Define Schema

    hashtag
    Step 2: Compute Schema ID

    hashtag
    Step 3: Generate Data ID for each message

    hashtag
    Step 4: Publish Message

    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

    hashtag
    Common Pitfalls

    hashtag
    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

    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. Hardhatarrow-up-right 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.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the guide on Moving from Testnet to Mainnet.

    hashtag
    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 .

    3. Hardhat is installed and set up on your local machine. See .

    hashtag
    Initialise Hardhat Project

    Start a new Hardhat project by running the following command in your Terminal:

    This will give you a series of prompts. Select the option to “Create a TypeScript Project (with Viem)”

    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.

    hashtag
    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:

    hashtag
    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:

    It will return the response:

    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:

    hashtag
    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 .

    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 to get your Private Key on MetaMask.

    Open a new terminal and deploy the smart contract to the Somnia Network. Run the command:

    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.

    Congratulations. 🎉 You have deployed your “BuyMeCoffee” Smart Contract to the Somnia Network using Hardhat. 🎉

    hashtag
    Verify Your Smart Contract

    After deploying your contract, you can verify it using the . This allows your source code to be visible and validated on the .

    hashtag
    Update hardhat.config.tsAdd the following to your config file:

    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:

    Example for a contract with one string constructor arg:

    Visit the and search for your contract address. If successful, the source code will appear under the “Contract” tab and show as verified.

    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.

    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.

    hashtag
    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:

    hashtag
    Conceptual Contract Storage

    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.

    hashtag
    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.

    hashtag
    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.

    hashtag
    Project Setup

    We will use the same project setup as the "" 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.

    (No changes needed from the previous tutorial. We just need publicClient.)

    src/lib/schema.ts

    hashtag
    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

    src/scripts/verifyOrigin.ts

    hashtag
    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.

    You will see an output similar to this:

    hashtag
    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.

    System Events

    Beyond smart-contract capabilities

    There are three events that are generated by the system, this is represented in Solidity as:

    You can subscribe to those events as any other. The system will generate those events for every block and match with any subscriptions.

    circle-info

    Remember to set the emitter field to SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS. This will make sure that your handler will only respond to system events.

    hashtag
    Block Tick Event

    If blockNumber is provided then this event will trigger at the specific block. Otherwise this will be triggered at every block, ~10 times per second.

    This example will tick at every single block:

    hashtag
    Epoch Tick Event

    If epochNumber is provided then this event will trigger at the end of the specific epoch (on its last block). Otherwise this will be triggered at every epoch, roughly every ~5 minutes.

    For example, if epochs are 3000 blocks long, epoch 1 covers blocks 0–2999 and the EpochTick for epoch 1 fires on block 2999. Epoch 2 covers blocks 3000–5999 and fires on block 5999, and so on.

    This example will tick at every single epoch:

    This example will tick at the end of epoch 42:

    hashtag
    Schedule Event

    This event is useful for scheduling actions in the future. Few things to remember:

    • The provided timestamp must be strictly greater than the current block timestamp

    • The subscription to Schedule is one-off and will be deleted after triggering

    • The timestamp is expressed in milliseconds (see for handy calculations)

    This example will tick on Nov 11 2026 11:11:11.011 :

    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.

    Local Testing and Forking

    circle-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.


    hashtag

    “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.

    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.


    function payToAccess() external payable {
      require(msg.value == 0.01 ether, "Must send exactly 0.01 SOMI");
    }
    function withdraw() external onlyOwner {
      payable(owner).transfer(address(this).balance);
    }
    // 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);
        }
    }
    constructor(address payable _seller) payable {
      buyer = msg.sender;
      seller = _seller;
      amount = msg.value;
    }
    function release() external onlyBuyer {
      seller.transfer(amount);
    }
    // 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);
        }
    }
    
    receive() external payable {
      emit Tipped(msg.sender, msg.value);
    }
    function withdraw() external onlyOwner {
      payable(owner).transfer(address(this).balance);
    }
    // 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);
        }
    }
    await walletClient.sendTransaction({
      to: '0xTipJarAddress',
      value: parseEther('0.05'),
    });
    await sendTransaction({
      to: contractAddress,
      data: mintFunctionEncoded,
      value: 0n, // user sends no SOMI
    });
    npx create-next-app@latest somnia-subgraph-ui
    cd somnia-subgraph-ui
    npm install thirdweb react-query graphql
    NEXT_PUBLIC_SUBGRAPH_URL=https://proxy.somnia.chain.love/subgraphs/name/somnia-testnet/test-mytoken
    NEXT_PUBLIC_SUBGRAPH_CLIENT_ID=YOUR_CLIENT_ID
    npm run dev
    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 });
      }
    }
    {
      transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
        id
        from
        to
        value
        blockTimestamp
        transactionHash
      }
    }
    "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();
      }, []);
    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>
      );
    }
    "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>
      );
    }
    npm run dev
    // 1. Call your contract function that emits the event
    const tx = await contract.myFunction();
    await tx.wait();
    
    // 2. Poll for state change
    for (let i = 0; i < 15; i++) {
      await new Promise(r => setTimeout(r, 2000));
      const result = await contract.myStateVariable();
      if (result !== previousValue) {
        console.log('Reactivity worked!');
        break;
      }
    }
    event BlockTick(uint64 indexed blockNumber);
    event EpochTick(uint64 indexed epochNumber, uint64 indexed blockNumber);
    event Schedule(uint256 indexed timestampMillis);
    Solhintarrow-up-right

    Security

    Data 2

    Emmanuel’s todos

    Identifies sender

    0x3dC360e038...

    The Publisher records who wrote it.

    Leads to incomplete reads

    Query by the correct publisher address and consider aggregating many publishers under a single contract address

    Concept

    Database Equivalent

    Description

    Schema

    Table Definition

    Defines data fields and types

    Schema ID

    Table Hash

    Uniquely identifies that schema definition

    Data ID (Primary Key)

    username

    bio

    0x1234abcd...

    Emmanuel

    Blockchain Developer

    Publisher (Wallet)

    Schema ID

    Data ID

    Description

    0x123...abc

    Schema A

    Data 1

    Paul’s todos

    0x789...def

    Concept

    Role

    Example

    Schema ID

    Identifies schema hash

    0x5e4bce54...

    Data ID

    Identifies record

    0x75736572...

    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

    Schema A

    Publisher

    Mixing publisher data

    function that fetches data
    only
    for a specific publisher address.
  • Run verification for all addresses and show that data is only returned for the correct publishers.

  • 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.

  • Multi-Publisher Aggregatorarrow-up-right
    src/lib/clients.tsarrow-up-right
    https://currentmillis.com/arrow-up-right
    Prerequisites
    • Node.js 20+

    • Hardhat (or Foundry/Anvil if you prefer)

    • A .env file with Somnia RPCs:

    Do not commit real keys/tokens. Use environment variables.


    hashtag
    Quick Start (Hardhat)

    1
    2

    hashtag
    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.

    hashtag
    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.

    hashtag
    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.


    hashtag
    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.

    Testnet (STT) is ideal for validating flows and cheaper RPC limits; Mainnet (SOMI) reflects real contract/state and gas rules.


    hashtag
    Handy RPC Tricks (Hardhat)

    These RPC methods provided by Hardhat Network allow you to manipulate the blockchain state for advanced testing scenarios.

    hashtag
    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").

    hashtag
    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.

    hashtag
    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.

    hashtag
    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.


    hashtag
    Alternative: Anvil (Foundry)

    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.


    hashtag
    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.


    hashtag
    Common Issues

    chevron-rightInsufficient funds for gashashtag

    Top up native balance with `hardhat_setBalance`.

    chevron-rightNonce too high / replacement underpricedhashtag

    Reset account state (new snapshot) or use a fresh signer.

    chevron-rightContract already deployed at addresshashtag

    On persistent localhost, restart node or change deployer nonce.

    const userSchema = `
      uint64 timestamp,
      string username,
      string bio,
      address owner
    `
    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)
    Computed Schema ID: 0x5e4bce54a39b42b5b8a235b5d9e27e7031e39b65d7a42a6e0ac5e8b2c79e17b0
    
    import { zeroBytes32 } from '@somnia-chain/streams'
    
    const ignoreExistingSchemas = true
    await sdk.streams.registerDataSchemas([
      { schemaName: "MySchema", schema: userSchema, parentSchemaId: zeroBytes32 }
    ], ignoreExistingSchemas)
    import { toHex } from 'viem'
    
    const dataId = toHex(`username-${Date.now()}`, { size: 32 })
    console.log("Data ID:", dataId)
    Data ID: 0x757365726e616d652d31373239303239323435
    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 }
    ])
    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" },
    ]);
    const messages = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherAddress)
    [
      { timestamp: 1729302920, username: "Victory", bio: "Blockchain Developer" }
    ]
    const chatSchema = `
      uint64 timestamp,
      bytes32 roomId,
      string content,
      string senderName,
      address sender
    `
    const schemaId = await sdk.streams.computeSchemaId(chatSchema)
    const dataId = toHex(`${roomName}-${Date.now()}`, { size: 32 })
    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 }])
    // mapping: schemaId => publisherAddress => dataId => data
    mapping(bytes32 => mapping(address => mapping(bytes32 => bytes))) public dsstore;
    export const oraclePriceSchema = 'uint256 price, uint64 timestamp'
    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)
    })
    # Add to package.json
    "verify": "ts-node src/scripts/verifyOrigin.ts"
    
    # Run it
    npm run verify
    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.
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [BlockTick.selector, bytes32(0), bytes32(0), bytes32(0)],
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /*...*/
        });
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [EpochTick.selector, bytes32(0), bytes32(0), bytes32(0)],
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /*...*/
        });
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [EpochTick.selector, bytes32(uint256(42)), bytes32(0), bytes32(0)],
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /*...*/
        });
    ISomniaReactivityPrecompile.SubscriptionData
        memory subscriptionData = ISomniaReactivityPrecompile
            .SubscriptionData({
                eventTopics: [Schedule.selector, 1794395471011, bytes32(0), bytes32(0)],
                emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS,
                handlerContractAddress: address(this),
                handlerFunctionSelector: ISomniaEventHandler.onEvent.selector,
                /*...*/
        });
    Install packages
    npm i -D hardhat @nomicfoundation/hardhat-ethers ethers dotenv
    npx hardhat # if project not initialized yet
    cp .env.example .env || true
    hardhat.config.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;
    hardhat: {
      forking: {
        url: process.env.SOMNIA_RPC_TESTNET!,
        blockNumber: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
      },
    }
    # in-process fork
    npx hardhat test
    
    # persistent node
    npx hardhat node --fork $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET}
    hardhat: {
      forking: {
        url: process.env.SOMNIA_RPC_MAINNET!,
        blockNumber: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
      },
    }
    # in-process fork
    npx hardhat test
    
    # persistent node
    npx hardhat node --fork $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET}
    anvil --fork-url $SOMNIA_RPC_TESTNET ${FORK_BLOCK_TESTNET:+--fork-block-number $FORK_BLOCK_TESTNET} --port 8546
    anvil --fork-url $SOMNIA_RPC_MAINNET ${FORK_BLOCK_MAINNET:+--fork-block-number $FORK_BLOCK_MAINNET} --port 8546
    .env (example)
    # .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=
    contracts/Counter.sol
    // 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);
        }
    }
    test/Counter.test.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);
      });
    });
    # 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
    impersonate.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); });
    await network.provider.send("evm_setNextBlockTimestamp", [Math.floor(Date.now()/1000) + 3600]);
    await network.provider.send("evm_mine");
    const id = await network.provider.send("evm_snapshot");
    // ... run actions ...
    await network.provider.send("evm_revert", [id]);
    await network.provider.request({
      method: "hardhat_reset",
      params: [{ forking: { url: process.env.SOMNIA_RPC_TESTNET!, blockNumber: Number(process.env.FORK_BLOCK_TESTNET||0) || undefined } }]
    });
    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.

  • This guide will teach you how to connect to and deploy your ERC20 Smart Contract to the Somia Network using Hardhat.

    hashtag
    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.

    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.

    hashtag
    ERC721 Smart Contract

    chevron-rightNFTTest.solhashtag
    // SPDX-License-Identifier: MIT
    // Compatible with OpenZeppelin Contracts ^5.4.0
    pragma solidity ^0.8.27;
    

    hashtag
    Code Breakdown

    Below is a breakdown explanation of the code:

    hashtag
    Imports

    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”.

    hashtag
    Contract Declaration

    _nextTokenId is an Internal counter for new token IDs. Starts at 0 by default (since not set), so your first mint is tokenId = 0.

    hashtag
    Constructor

    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.

    hashtag
    Base URI

    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/

    hashtag
    Minting

    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.

    hashtag
    Required overrides

    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.

    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.

    hashtag
    Initialize Hardhat

    Create .env:

    chevron-righthardhat.config.tshashtag

    Add the Smart Contract to the Contract directory

    hashtag
    Deploy with Ignition

    Create ignition/modules/NFTTest.ts:

    Deploy:

    Copy the deployed address from the output.


    hashtag
    Verify Smart Contract

    Ensure compiler version, optimizer, and constructor args match.


    hashtag
    Mint your collection

    We’ll mint by calling safeMint(to, uri) 10 times, matching your uploaded metadata files.

    chevron-rightscripts/mint.tshashtag

    Run the project:


    hashtag
    Inspect Token metadata

    Read a token’s URI (e.g., tokenId 0) and open it in your browser.

    scripts/read-uri.ts:

    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. 🎉

    Data IDs – uniquely identify each record.
  • 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.

    hashtag
    Prerequisites

    Before you begin:

    • Node.js 20+

    • A Somnia Testnet wallet with STT test tokens

    • .env file containing your wallet credentials

    hashtag
    Project Setup

    Create a Project Directory and install dependencies @somnia-chain/streamsarrow-up-right and viemarrow-up-right:

    Now create a .env file to hold your test wallet’s private key:

    hashtag
    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

    hashtag
    Network Configuration

    The file dream-chain.js defines the blockchain network connection.

    This allows both publisher and subscriber scripts to easily reference the same testnet environment.

    hashtag
    Hello World Publisher

    The publisher connects to the blockchain, registers a schema if necessary, and sends a “Hello World” message every few seconds.

    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.


    hashtag
    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.

    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.


    hashtag
    Run the App

    Run both scripts in separate terminals:

    and then

    You’ll see Publisher Output:

    Subscriber Output

    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.


    hashtag
    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.

    hashtag
    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.

    hashtag
    Active Support Channels

    • Telegram (DevRel Team):

      • @emreyetharrow-up-right

      • @PromiseGameFiarrow-up-right

    • Discord:

      • Join the official server.

      • For technical questions, use the #dev-support or #dev-chat channel.

    • Email: Send an email to for official inquiries, integration help, or collaboration requests.

    circle-info

    Response time varies based on the request type, but DevRel aims to reply within 24 hours.

    hashtag
    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


    hashtag
    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.


    hashtag
    Technical Disclosure Guidelines

    All vulnerability reports should follow a clear, reproducible structure for fast triage and validation.

    hashtag
    Required Report Template

    chevron-rightExamplehashtag

    hashtag
    Contribution Pathways for Developers

    Somnia invites developers to contribute beyond bug reporting. Follow these pathways to get involved.

    1

    hashtag
    Documentation Contributions

    • Suggest edits or add missing examples in tutorials.

    • Create new pages under categories like Debugging, Testing, or Security.

    2

    hashtag
    Testing Best Practices

    • Always test exploits or stress scenarios on Shannon Testnet, not on Mainnet.


    hashtag
    Somnia Report Lifecycle

    1

    hashtag
    Submission

    Researcher submits a report via email, Discord, or Telegram.

    2

    hashtag
    Verification

    Somnia DevRel reproduces the issue and collects context.

    3

    hashtag
    Escalation

    Valid issues are passed to Somnia Core Security.

    4

    hashtag
    Patch Deployment

    Fix rolled out to Shannon Testnet, then Mainnet. (Based on where is it.)

    5

    hashtag
    Acknowledgment

    Researcher credited publicly in Somnia Docs and Discord.

    For multi-party vulnerabilities (e.g., involving validators or external oracles), coordinated disclosure will be handled privately.


    hashtag
    Ethical Rules

    circle-exclamation
    • 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.


    hashtag
    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.

    Connect Your Wallet
    Guidearrow-up-right
    Faucetarrow-up-right
    guidearrow-up-right
    Hardhat Verify pluginarrow-up-right
    Somnia Explorerarrow-up-right
    Somnia Explorerarrow-up-right

    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.

    hashtag
    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.

    hashtag
    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:

    hashtag
    Create an ERC20 Token Contract

    Create a new Solidity file: contracts/MyToken.sol and update it

    hashtag
    Create a Deployment Script

    Create a new file in ignition/modules/MyTokenModule.ts

    hashtag
    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.

    Open a new terminal and deploy the smart contract to the Somnia Network. Run the command:

    This will deploy the ERC20 contract to the Somnia network and return the deployed contract address.

    hashtag
    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

    chevron-rightinteract.jshashtag

    Create an .env file to hold sensitive informations such as the private keys

    hashtag
    Run the Script

    This will generate several on-chain transactions for our subgraph to index.

    hashtag
    Deploy a Subgraph on Somnia

    Go to and connect your Wallet.

    First, you need to create a private key for deploying subgraphs. To do so, please go to and create an Account.

    You are now able to create subgraphs. Click the create button and enter the required details.

    After initialising the subgraph on the next step is to create and deploy the subgraph via the terminal.

    hashtag
    Initialize the Subgraph

    Then, update networks.json to use Somnia’s RPC

    hashtag
    Define the Subgraph Schema

    📁 Edit schema.graphql

    hashtag
    Build the Subgraph

    hashtag
    Deploy the Subgraph

    hashtag
    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

    hashtag
    Fetch Latest Transfers

    hashtag
    Get Transfers by Address

    hashtag
    Get Transfers in a Time Range

    hashtag
    Conclusion

    This tutorial provides a complete pipeline for indexing blockchain data on Somnia using The Graph! 🔥

    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.


    hashtag
    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.

    • Custom contract storage is powerful but heavyweight, and you maintain the whole schema logic, CRUD, indexing patterns, and migrations.

    • Off-chain DB and proofs are flexible but brittle; either centralized or require extra machinery.

    Streams solves this by standardizing:

    1. Schemas (the “data ABI”)

    2. Publish/Subscribe primitives (SDK, not boilerplate contracts)

    3. Deterministic IDs (schemaId, dataId) and provenance (publisher address)

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


    hashtag
    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

    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)


    hashtag
    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).

    • Publisher: The signer that writes data. EOA or Smart Contract that writes data under a schema. Readers trust provenance by address.

    • Subscriber: reader that fetches all records for a (schemaId, publisher) pair.

    hashtag
    Data flow

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


    hashtag
    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.).


    hashtag
    Data Writing Patterns

    • Single Publisher One server wallet publishes; User Interaces can read the schema using getByKey , getAtIndex , getAllPublisherDataForSchema.

    • Multi-Publisher Many devices publish under a shared schema. Your API aggregates across a list of publisher addresses.


    hashtag
    Quickstart in 5 Minutes

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

    hashtag
    Install

    hashtag
    Set up Env

    hashtag
    Define Chain

    hashtag
    Define Client

    hashtag
    Schema

    hashtag
    Register Schema (optional but recommended)

    chevron-rightscripts/register.tshashtag

    hashtag
    Publish (Write)

    hashtag
    Read Data

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


    hashtag
    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.

    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 SDKarrow-up-right to:

    • Connect Smart Wallets (Account Abstraction)

    • Read Wallet Balance

    • Send STT Tokens

    hashtag
    Pre-requisites

    Before we start, ensure you have:

    • Basic knowledge of React

    • A account & Client ID

    • Node.js & npm installed

    hashtag
    Install Dependencies

    Run the following command to set up your project:

    This installs:

    thirdweb → The Thirdweb React SDK.

    ethers → To interact with blockchain transactions.

    dotenv → To securely store API keys.

    hashtag
    Create the Thirdweb Client

    The Thirdweb client allows the app to communicate with the blockchain. Create a client.ts file and add:

    circle-info

    Get your Client ID: Register at thirdweb.com/dashboard. 💡

    hashtag
    Add Environment Variables

    Store API keys in a .env.local file:

    Restart Next.js after modifying .env.local

    hashtag
    Build the Account Abstraction App

    To ensure Thirdweb Components are available throughout the app, wrap the children's components inside ThirdwebProvider. Modify layout.ts:

    hashtag
    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.

    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.

    To show the connected address, we add the following UI component:

    hashtag
    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.

    hashtag
    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.

    chevron-rightComplete Codehashtag

    The full implementation includes wallet connection, balance retrieval, and token transfer.

    hashtag
    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.

    Quickstart

    Install the tooling to get up and running

    circle-exclamation

    Reactivity is currently only available on TESTNET

    hashtag
    Off-chain (TypeScript)

    hashtag
    📦 SDK Installation

    hashtag
    🔌 Plugging into the SDK

    You'll need viem installed for the public and or wallet client. Install it with npm i viem.

    hashtag
    📡 Activating Websocket Reactivity Subscriptions

    Use WebSocket subscriptions for real-time updates to contract event and state updates atomically. Define params and subscribe — the SDK handles the rest via WebSockets.

    hashtag
    On-chain (Solidity handlers)

    Developers can build Solidity smart contracts that get invoked when other contracts emit events—allowing smart contracts to "react" to what's happening on-chain.

    In order to achieve this, we need two things:

    1. A Somnia event handler smart contract (standard Solidity syntax).

    2. A valid subscription with funds to pay for Solidity handler invocations. Creators of on-chain subscriptions are required to hold minimum balances (currently 32 SOMI) that pay for handler invocations executed by validators.

    hashtag
    Creating the Handler Smart Contract

    Very basic contract with the @somnia-chain/reactivity-contracts npm package installed

    Once the handler is complete, deploy it using Foundry or Hardhat, and note the address — this will be required for creating a subscription.

    hashtag
    Setting Up an On-Chain Subscription (Using the SDK)

    The following uses the TypeScript SDK to create and pay for a subscription that will invoke a handler contract for events emitted by other smart contracts. Another approach would be for the subscribing smart contract to directly hold the required SOMI balance and have the logic for creating the subscription baked into one place, but that may not always be optimal.

    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.

    Authenticating with Privy

    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 . This guide will integrate Privy with the Somnia Testnet, enabling users to create and connect wallets effortlessly.

    hashtag

    API Reference

    Understanding types and functionality through the API spec, distinguished by environment

    circle-exclamation

    Somnia Reactivity is currently only available on TESTNET

    hashtag
    Off-chain (TypeScript)

    What is Reactivity?

    tldr; Pub/sub, for blockchain dApps regardless of environment including

    Reactivity is Somnia's event-driven paradigm for dApps. It pushes notifications—combining emitted events and related blockchain state—to subscribers in real-time, enabling "reactive" logic without polling.

    circle-exclamation

    Reactivity is currently only available on TESTNET

    hashtag

    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;
    
    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);
    });
    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);
    }
    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
    PRIVATE_KEY=0xYourPrivateKey
    SOMNIA_RPC_HTTPS=https://dream-rpc.somnia.network
    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;
    npx hardhat ignition deploy ignition/modules/NFTTest.ts --network somnia
    npx hardhat verify --network somnia <DEPLOYED_ADDRESS> 0xYourOwnerAddress
    npx hardhat run scripts/mint.ts --network somnia
    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);
    npx hardhat run scripts/read-uri.ts --network somnia
    npm i @somnia-chain/streams viem dotenv
    PRIVATE_KEY=0xYOUR_PRIVATE_KEY
    PUBLIC_KEY=0xYOUR_PUBLIC_ADDRESS
    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 };
    
    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()
    
    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();
    npm run publisher
    npm run subscriber
    Schema ID: 0x27c30fa6547c34518f2de6a268b29ac3b54e51c98f8d0ef6018bbec9153e9742
    ⚠️ Schema already registered. Continuing...
    ✅ Published: Hello World #1 (Tx: 0xf21ad71a6c7aa54c171ad38b79ef417e8488fd750ce00c1357918b7c7fa5c951)
    ✅ Published: Hello World #2 (Tx: 0xe999b0381ba9d937d85eb558fefe214fa4e572767c4e698c6e31588ff0e68f0a)
    🆕 Hello World #2 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:04 PM
    🆕 Hello World #3 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:07 PM
    Example Vulnerability Report
    # 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)
    Vulnerability Report Template
    # 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.
    npx hardhat init
    // 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;
        }
    }
    npx hardhat compile
    Compiling...
    Compiled 1 contract successfully
    import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
    
    const BuyMeCoffee = buildModule("BuyMeCoffee", (m) => {
      const contract = m.contract("BuyMeCoffee");
      return { contract };
    });
    
    module.exports = BuyMeCoffee;
    module.exports = {
      // ...
      networks: {
        somnia: {
          url: "https://dream-rpc.somnia.network",
          accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
        },
       },
      // ...
    };
    npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia
    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",
            },
          },
        ],
      },
    };
    npx hardhat verify --network somnia DEPLOYED_CONTRACT_ADDRESS "ConstructorArgument1" ...
    npx hardhat verify --network somnia 0xYourContractAddress "YourDeployerWalletAddress"
    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);
    }
    }
    Core Concepts
    • Events: Triggers from smart contracts (e.g., Transfer, Approval).

    • State: View calls for contract data fetched at the event's block height.

    • Push Delivery: Chain validators / nodes handle notifications, invoking handlers or WebSocket callbacks directly.

    • Subscribers: Off-chain apps (TypeScript) or on-chain contracts (Solidity).

    This shifts dApps from reactive querying to proactive responses, like a pub/sub system baked into the blockchain.

    Handles dependencies and npm scripts

    To report issues privately, open a support ticket under “Bug Reports”

    Use local forks with Hardhat or Foundry for reproducibility.

    Bug Report

    Contract, SDK, or explorer bugs

    Discord Ticket / Email

    Partnership Inquiry

    Technical collaborations or integration ideas

    Email

    @emmaodiaarrow-up-right
    Somnia arrow-up-right
    [email protected]envelope
    Storage EVM operations
    Oracles are useful for external data, but not a generic modeling layer for app-originated data.
    You need instant push to clients (Streams also works well with polling; you can add WS if desired)

    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 })

  • Encoder: converts typed values ⇄ bytes according to the schema.

  • schemaId: computed from the schema string. Treat it like a contract address for data shape.

  • Use string for human-readable info (names, messages), but keep it short for gas efficiency.
    Derived Views Build REST endpoints that query Streams and derive higher-level views (e.g., “latest per room”).
    Thirdwebarrow-up-right
    click Connect
    Connected Wallet
    Smart Contract Wallet
    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.

    hashtag
    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.

    hashtag
    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.

    hashtag
    Project Setup

    Create a new directory for your application and install the necessary packages.

    Create a tsconfig.json file in your project root:

    hashtag
    Configure Environment Variables

    Create a .env file in your project root. For this tutorial, we will need two different private keys.

    circle-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.

    hashtag
    Chain and Client Configuration

    Create a folder named src/lib and set up your chain.ts and clients.ts files.

    src/lib/chain.ts

    src/lib/clients.ts

    hashtag
    Define the Shared Schema

    Let's define the common schema that all publishers will use.

    src/lib/schema.ts

    hashtag
    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

    To run this script:

    Add the following scripts to your package.json file:

    Now, open two different terminals and send data from each:

    Repeat this a few times to build up a dataset from both publishers.

    hashtag
    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

    To run this script:

    Add the script to your package.json file:

    And run it:

    hashtag
    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.

    hashtag
    WebSocket Subscription Initialization Params

    An object of type WebsocketSubscriptionInitParams is the only required argument to the subscribe function required to create a subscription to a Somnia node to become notified about event + state changes that take place on-chain

    Example:

    hashtag
    Solidity Subscription Creation from SDK

    Example:

    hashtag
    Solidity subscription info query from SDK

    Example:

    +-------------+        publishData(payload)         +--------------------+
    |  Publisher  |  -------------------------------->  | Somnia Streams L1  |
    |  (wallet)   |                                     |   (on-chain data)  |
    +-------------+                                     +--------------------+
           ^                                                     |
           |                                     getAllPublisherDataForSchema
           |                                                     v
    +-------------+                                        +-----------+
    | Subscriber  |  <------------------------------------ |  Reader   |
    | (frontend)  |                                        | (SDK)     |
    +-------------+                                        +-----------+
    npm i @somnia-chain/streams viem
    RPC_URL=https://dream-rpc.somnia.network
    PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
    // 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)
    // 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) })
    // lib/schema.ts
    export const chatSchema =
      'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
    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()
    // 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()
    // 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()
    
    npx create-next-app@latest somnia-thirdweb
    cd somnia-thirdweb
    npm install thirdweb ethers viem dotenv
    import { createThirdwebClient } from "thirdweb";
    export const client = createThirdwebClient({
      clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, // Replace with your actual Client ID
    });
    NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your-client-id-here
    NEXT_PUBLIC_SOMNIA_RPC_URL=https://dream-rpc.somnia.network/
    import { ThirdwebProvider } from 'thirdweb/react';
    
    <body>
        <ThirdwebProvider>
            {children}
        </ThirdwebProvider>
     </body>
    
    import { useActiveAccount } from "thirdweb/react";
    
    const smartAccount = useActiveAccount();
    
    <ConnectButton
      client={client}
      appMetadata={{
        name: "Example App",
        url: "https://example.com",
      }}
    />
    {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>
    )}
    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.");
          },
        }
      );
    };
    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>}
    '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>
      );
    }
    npm i @somnia-chain/reactivity
    import { createPublicClient, createWalletClient, http, defineChain } from 'viem'
    import { SDK } from '@somnia-chain/reactivity'
    
    // Example: Public client (required for reading data)
    const chain = defineChain() // see viem docs for defining a chain
    const publicClient = createPublicClient({
      chain, 
      transport: http(),
    })
    
    // Optional: Wallet client for writes
    const walletClient = createWalletClient({
      account,
      chain,
      transport: http(),
    })
    
    const sdk = new SDK({
      public: publicClient,
      wallet: walletClient, // Omit if not executing transactions on-chain
    })
    import { SDK, WebsocketSubscriptionInitParams, SubscriptionCallback } from '@somnia-chain/reactivity'
    
    const initParams: WebsocketSubscriptionInitParams = {
      ethCalls: [], // State to read when events are emitted
      onData: (data: SubscriptionCallback) => console.log('Received:', data),
    }
    
    const subscription = await sdk.subscribe(initParams)
    pragma solidity ^0.8.20;
    
    import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
    
    contract ExampleEventHandler is SomniaEventHandler {
    
        function _onEvent(
            address emitter,
            bytes32[] calldata eventTopics,
            bytes calldata data
        ) internal override {
            // Execute your logic here
            // Be careful about emitting events to avoid infinite loops
        }
    
    }
    import { SDK } from '@somnia-chain/reactivity';
    import { parseGwei } from 'viem';
    
    // Initialize the SDK
    const sdk = new SDK({
      public: publicClient,
      wallet: walletClient,
    })
    
    // Create a Solidity subscription
    // This is an example of a wildcard subscription to all events
    // We do not need to supply SOMI—the chain ensures min balance
    await sdk.createSoliditySubscription({
      handlerContractAddress: '0x123...',
      priorityFeePerGas: parseGwei('2'),   // 2 gwei — minimum recommended for validators to process
      maxFeePerGas: parseGwei('10'),       // 10 gwei — max you're willing to pay (base + priority)
      gasLimit: 2_000_000n,                // Minimum recommended for state changes, increase for complex logic
      isGuaranteed: true,
      isCoalesced: false,
    });
    mkdir somnia-aggregator
    cd somnia-aggregator
    npm init -y
    npm i @somnia-chain/streams viem dotenv
    npm i -D @types/node typescript ts-node
    {
      "compilerOptions": {
        "target": "ES2020",
        "module": "commonjs",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "outDir": "./dist"
      },
      "include": ["src/**/*"]
    }
    # .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
    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)
    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')),
    })
    // This schema will be used by multiple devices
    export const telemetrySchema = 
      'uint64 timestamp, string deviceId, int32 x, int32 y, uint32 speed'
    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)
    })
    "scripts": {
      "publish:p1": "ts-node src/scripts/publishData.ts p1",
      "publish:p2": "ts-node src/scripts/publishData.ts p2"
    }
    # Terminal 1
    npm run publish:p1
    # Terminal 2
    npm run publish:p2
    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)
    })
    "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"
    }
    npm run aggregate
    /**
     * @property The notification result data containing information about the event topic, data and view results
     */
    export type SubscriptionCallback = {
      result: {
        topics: Hex[],
        data: Hex,
        simulationResults: Hex[]
      }
    }
    
    /**
     * @property ethCalls Fixed set of ETH calls that must be executed before onData callback is triggered. Multicall3 is recommended. Can be an empty array
     * @property context Event sourced selectors to be added to the data field of ETH calls, possible values: topic0, topic1, topic2, topic3, data and address
     * @property onData Callback for a successful reactivity notification
     * @property onError Callback for a failed attempt 
     * @property eventContractSources Alternative contract event source(s) (any on somnia) that will be emitting the logs specified by topicOverrides
     * @property topicOverrides Optional filter for specifying topics of interest, otherwise wildcard filter is applied (all events watched)
     * @property onlyPushChanges Whether the data should be pushed to the subscriber only if eth_call results are different from the previous
     */
    export type WebsocketSubscriptionInitParams = {
        ethCalls: EthCall[]
        context?: string
        onData: (data: SubscriptionCallback) => void
        onError?: (error: Error) => void
        eventContractSources?: Address[]
        topicOverrides?: Hex[]
        onlyPushChanges?: boolean
    }
    const subscription = await sdk.subscribe({
      ethCalls: [], // State to read when events are emitted
      onData: (data: SubscriptionCallback) => console.log('Received:', data),
    })
    /**
     * @property eventTopics Optional event filters
     * @property origin Optional tx.origin filter
     * @property caller Reserved for future use (not currently active in event matching)
     * @property emitter Optional contract event emitter filter
     * @property handlerContractAddress Contract that will handle subscription callback
     * @property handlerFunctionSelector Optional override for specifying callback handler function
     * @property priorityFeePerGas Additional priority fee that will be paid per gas consumed by callback
     * @property maxFeePerGas Maximum fee per gas the subscriber is willing to pay (base fee + priority fee)
     * @property gasLimit Maximum gas that will be provisioned per subscription callback
     * @property isGuaranteed Whether event notification must be delivered regardless of block inclusion distance from emission
     * @property isCoalesced Whether multiple events can be coalesced into a single handling call per block
     */
    export type SoliditySubscriptionData = {
        eventTopics?: Hex[];
        origin?: Address;
        caller?: Address;
        emitter?: Address;
        handlerContractAddress: Address;
        handlerFunctionSelector?: Hex;
        priorityFeePerGas: bigint;      
        maxFeePerGas: bigint;
        gasLimit: bigint;
        isGuaranteed: boolean;
        isCoalesced: boolean;
    }
    import { parseGwei } from 'viem';
    
    await sdk.createSoliditySubscription({
      handlerContractAddress: '0x123...',
      priorityFeePerGas: parseGwei('2'),   // 2 nanoSOMI — use parseGwei, not raw values
      maxFeePerGas: parseGwei('10'),       // 10 nanoSOMI
      gasLimit: 2_000_000n,                // Minimum recommended for state changes
      isGuaranteed: true,
      isCoalesced: false,
    });
    export type SoliditySubscriptionInfo = {
      subscriptionData: SoliditySubscriptionData,
      owner: Address
    }
    const subscriptionId = 1n;
    const subscriptionInfo: SoliditySubscriptionInfo = await sdk.getSubscriptionInfo(
        subscriptionId
    );
    Prerequisites
    • This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.

    • To complete this guide, sign up for Privyarrow-up-right and get an AppID and get the Somnia Provider AppID.

    • Familiarity with React and Next.js is assumed.

    hashtag
    Installation

    hashtag
    Create the Next.js Project

    Open your terminal and run the following commands to set up a new Next.js project:

    Install the necessary packages

    hashtag
    Set Up PrivyProvider

    Go to https://dashboard.privy.io/arrow-up-right to set up an account.

    Click "New App" to create a new application that will connect to the Somnia Provider AppID.

    Open the newly created app and in the left side navigation menu navigate to:

    User Management >>>> Global Wallet >>>> Integrations

    Click the toggle to turn ON the Somnia Provider App.

    Wrap your application layout.ts file with PrivyProvider and supply your PrivateKey from Privy and the Somnia Provider App ID to the loginMethods:

    Add your environment variable in .env.local:

    hashtag
    Privy Hooks

    These hooks make it easy to authenticate users, manage wallets, and interact with the Somnia Network using Privy Global Wallet

    hashtag
    Authenticate

    Use the provided hooks to authenticate users and access their wallets.

    chevron-rightpage.tsxhashtag

    hashtag
    Send Transactions

    Once authenticated, use the useSendTransaction hook from useCrossAppAccount method to interact with Somnia Testnet:

    hashtag
    Complete Code

    chevron-rightComplete page.tsx codehashtag

    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.

    Privyarrow-up-right
    herearrow-up-right
    https://somnia.chain.love/arrow-up-right
    Somnia Protofire Servicearrow-up-right
    https://somnia.chain.love/arrow-up-right
    https://somnia.chain.love/graph/17arrow-up-right

    DIA Price Feeds

    hashtag
    Overview

    DIAarrow-up-right 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.

    hashtag
    Oracle Details

    hashtag
    Contracts on Somnia

    Network
    Contract Address

    hashtag
    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.

    hashtag
    Oracle Configuration

    • Pricing Methodology: MAIR

    • Deviation Threshold: 0.5% (Triggers price update if exceeded)

    • Refresh Frequency: Every 120 seconds

    hashtag
    Supported Asset Feeds

    hashtag
    Mainnet

    hashtag
    Testnet

    hashtag
    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.

    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 .

    hashtag
    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.

    hashtag
    Access the library

    hashtag
    getPrice

    Returns the price of a specified asset along with the update timestamp.

    hashtag
    getPriceIfNotOlderThan

    Checks if the Oracle price is older than maxTimePassed

    hashtag
    Using DIAOracleV2 Interface

    The following contract provides an integration example of retrieving prices and verifying price age.

    hashtag
    Adapter contracts

    To consume price data from DIA Oracle, you can use the adapter Smart Contract located at the for each asset. This will allow you to access the same methods on the AggregatorV3Interface such as getRoundData & latestRoundData. You can learn more .

    hashtag
    Glossary

    Term
    Definition

    hashtag
    Support

    If you need further assistance integrating DIA Oracles, reach out through DIA’s and ask your questions in the #dev-support channel on .

    Developers can build secure, real-time, and on-chain financial applications with reliable pricing data by integrating DIA Oracles on Somnia.

    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.

    hashtag
    Prerequisites

    • Basic knowledge of React and Next.js

    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.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the on Moving from Testnet to Mainnet.

    The DApp Publisher Proxy Pattern

    In the "" 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

    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 .

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the on Moving from Testnet to Mainnet.

    Verifying via Explorer

    This document explains how to verify smart contracts on Somnia Mainnet using Blockscout. It replaces the old explorer-based verification flow.

    hashtag
    How It Works

    Somnia Mainnet Explorer is Blockscout-based and uses Blockscout verification endpoints.

    There are 4 primary verification methods:

    Wildcard Off-Chain Reactivity Tutorial

    Example of how to subscribe to all events and fetch state on Somnia using WebSockets off-chain

    This tutorial shows how to set up an off-chain subscription in TypeScript to listen for all events emitted on the Somnia blockchain (wildcard mode). Notifications will push event data plus results from Solidity view calls (e.g., querying contract state like balances) in a single atomic payload. This reduces RPC roundtrips compared to traditional event listening + separate state fetches.

    We'll use the Reactivity SDK for WebSocket subscriptions and viem for chain setup, ABI handling, and decoding. For familiarity, we'll subscribe to all events but decode a common one (ERC20 Transfer) in the callback.

    Whilst most developers are unlikely to use reactivity in this way in production, there are scenarios where this will be useful:

    • Testing reactivity before applying more filters (see our other tutorials)

    export default function Home() {
    
      const { loginWithCrossAppAccount } = useCrossAppAccounts();
      const { ready, authenticated, user, logout } = usePrivy();
      const disableLogin = !ready || (ready && authenticated);
      
      const [loginError, setLoginError] = useState<string | null>(null);
      const [walletAddress, setWalletAddress] = useState<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 ? (
              <p>Loading...</p>
            ) : authenticated ? (
                {walletAddress ? (
                  <p>Connected as: {walletAddress}</p>
                ) : (
                  <p className='text-gray-600'>No wallet address found.</p>
                )}
                <button
                  onClick={logout}
                  className='bg-red-600 text-white px-4 py-2 rounded'
                >
                  Logout
                </button>
              </div>
              
            ) : (
              <>
                <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>}
              </>          </div>
            )}
            
    }
    '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>
      );
    }
    npx create-next-app@latest somnia-privy
    cd somnia-privy
    npm install @privy-io/react-auth viem
    '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>
      );
    }
    NEXT_PUBLIC_PRIVY_APP_ID=your-privy-app-id
    import { useCrossAppAccounts, usePrivy } from '@privy-io/react-auth';
     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>
    npm install -g @graphprotocol/graph-cli
    npm install --save-dev hardhat @nomicfoundation/hardhat-ignition-ethers @openzeppelin/contracts dotenv ethers
    // 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);
        }
    }
    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 };
    });
    module.exports = {
      // ...
      networks: {
        somniaTestnet: {
          url: "https://dream-rpc.somnia.network",
          accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
        },
       },
      // ...
    };
    
    npx hardhat ignition deploy ./ignition/modules/MyTokenModule.ts --network somniaTestnet
    
    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);
      });
    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...
    node scripts/interact.js
    graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken
    {
      "somnia-testnet": {
        "network": "Somnia Testnet",
        "rpc": "https://dream-rpc.somnia.network",
        "startBlock": 12345678
      }
    }
    type Transfer @entity(immutable: true) {
      id: Bytes!
      from: Bytes!
      to: Bytes!
      value: BigInt!
      blockNumber: BigInt!
      blockTimestamp: BigInt!
      transactionHash: Bytes!
    }
    graph codegen
    graph build
    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
    {
      transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
        id
        from
        to
        value
        blockTimestamp
        transactionHash
      }
    }
    {
      transfers(where: { from: "0xUserWalletAddress" }) {
        id
        from
        to
        value
      }
    }
    {
      transfers(where: { blockTimestamp_gte: "1700000000", blockTimestamp_lte: "1710000000" }) {
        id
        from
        to
        value
      }
    }

    A Thirdweb account and API key

  • A deployed ERC721 NFT Smart Contract on Somnia

  • hashtag
    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

    hashtag
    Create a Next.js Project

    hashtag
    Install Thirdweb SDK

    hashtag
    Set Up Environment Variables

    Create a .env.local file in your project root:

    Get your Client ID from the Thirdweb Dashboardarrow-up-right.

    hashtag
    Deploy a Simple NFT Contract

    If you haven't already, deploy this simple ERC721 contract on Somnia.

    chevron-rightSimpleNFT.solhashtag

    hashtag
    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.

    chevron-rightconstantshashtag

    hashtag
    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.

    hashtag
    Imports and Component Setup

    First, let's set up our imports and initialize the component. Create app/page.tsx.

    chevron-rightpage.tsxhashtag

    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

    hashtag
    Component Logic and Data Fetching

    Now let's add the component logic that handles wallet connections and reads blockchain data:

    chevron-rightpage.tsxhashtag
    • 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

    hashtag
    User Interface

    Now let's build the complete UI that users will interact with

    chevron-rightpage.tsxhashtag

    hashtag
    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.

    hashtag
    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:

    chevron-rightlayout.tsxhashtag

    hashtag
    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 Guidearrow-up-right

    hashtag
    Resources

    • Live Demoarrow-up-right

    Building an indexer which scrapes required information from the chain into a secondary database that would serve other applications that want large volumes of historical chain data

    hashtag
    Overview

    Off-chain reactivity uses WebSockets to push notifications to your TypeScript app. Key features:

    • Wildcard Listening: Catch every event without filters.

    • Bundled State: Include ETH view calls executed at the event's block height.

    • Real-Time: Low-latency updates for UIs, backends, or scripts.

    • No Gas Costs: Off-chain, so free per notification (after setup).

    This enables reactive apps like live dashboards or automated alerts.

    Prerequisites:

    • Node.js 20+

    • Somnia testnet access (RPC: https://dream-rpc.somnia.network)

    • Install dependencies: npm i @somnia-chain/reactivity viem

    hashtag
    Key Objectives

    1. Set Up the Chain and SDK: Configure viem for Somnia Testnet and initialize the SDK.

    2. Define ETH Calls: Specify view functions to run on events (e.g., balanceOf).

    3. Create the Subscription: Start a wildcard WebSocket sub with a data callback.

    4. Decode Notifications: Use viem to parse event logs and function results.

    5. Run and Test: Handle incoming data in real-time.

    hashtag
    Step 1: Install Dependencies

    hashtag
    Step 2: Define the Somnia Chain

    Use viem's defineChain to configure the testnet.

    hashtag
    Step 3: Initialize the SDK

    Create a public client with WebSocket transport and pass it to the SDK.

    hashtag
    Step 4: Define ETH Calls

    Specify view calls to execute when events emit. Here, we query an ERC721 balanceOf (adjust addresses as needed).

    hashtag
    Step 5: Create the Wildcard Subscription

    Subscribe with ethCalls and an onData callback. Omit filters for wildcard (all events).

    • Unsubscribe Later: subscription.unsubscribe();

    hashtag
    Step 6: Decode Data in the Callback

    Use viem to decode the event log and function results. For example, assuming an ERC20 Transfer event (use erc20Abi from viem).

    • Notes: data.result contains topics, data (event payload), and simulationResults (view call outputs). Handle errors if decoding fails (e.g., non-matching ABI).

    hashtag
    Step 7: Put It All Together and Run

    Full script (main.ts):

    Run: ts-node main.ts (install ts-node if needed).

    hashtag
    Testing

    1. Run the script.

    2. Trigger events on Somnia Testnet (e.g., transfer ERC20 tokens via a wallet).

    3. Watch console for decoded notifications.

    • If no events: Deploy a test contract and emit manually.

    hashtag
    Troubleshooting

    • No Data? Ensure WebSocket RPC is connected; check filters (none for wildcard).

    • Decoding Errors? Verify ABI matches the event/contract.

    • Connection Issues? Use HTTP fallback if WS fails, but prefer WS for reactivity.

    hashtag
    Next Steps

    • Add filters (e.g., eventTopics: ['0xddf...'] for Transfer keccak).

    • Integrate with React (future hooks).

    • Handle multiple ethCalls/decodes.

    • Explore on-chain version: On-Chain Tutorial.

    // 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;
        }
    }
    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,
        },
      }),
    ];
    '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';
    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
      });
    '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>
      );
    };
    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>
      );
    }
    npx create-next-app@latest somnia-gasless-nft --typescript --tailwind --app
    cd somnia-gasless-nft
    npm install thirdweb
    NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your_client_id_here
    npm i @somnia-chain/reactivity viem
    import { defineChain } from 'viem';
    
    const somniaTestnet = defineChain({
      id: 50312,
      name: 'Somnia Testnet',
      network: 'testnet',
      nativeCurrency: {
        decimals: 18,
        name: 'STT',
        symbol: 'STT',
      },
      rpcUrls: {
        default: {
          http: ['https://dream-rpc.somnia.network'],
          webSocket: ['ws://api.infra.testnet.somnia.network/ws'],
        },
        public: {
          http: ['https://dream-rpc.somnia.network'],
          webSocket: ['ws://api.infra.testnet.somnia.network/ws'],
        },
      },
    });
    import { SDK } from '@somnia-chain/reactivity';
    import { createPublicClient, webSocket } from 'viem';
    
    const publicClient = createPublicClient({
      chain: somniaTestnet,
      transport: webSocket(),
    });
    
    const sdk = new SDK({ public: publicClient });
    import { encodeFunctionData, erc721Abi } from 'viem';
    
    const ethCall = {
      to: '0x23B66B772AE29708a884cca2f9dec0e0c278bA2c', // Example Somnia ERC721 contract
      data: encodeFunctionData({
        abi: erc721Abi,
        functionName: 'balanceOf',
        args: ['0x3dC360e0389683cA0341a11Fc3bC26252b5AF9bA'], // Example owner address
      }),
    };
    const subscription = await sdk.subscribe({
      ethCalls: [ethCall], // Array of calls; add more if needed
      onData: (data) => {
        console.log('Raw Notification:', data);
        // Decoding happens here (Step 6)
      },
    });
    import { decodeEventLog, decodeFunctionResult, erc20Abi } from 'viem';
    
    // Inside onData:
    const decodedLog = decodeEventLog({
      abi: erc20Abi, // Or your custom ABI
      topics: data.result.topics,
      data: data.result.data,
    });
    
    const decodedFunctionResult = decodeFunctionResult({
      abi: erc721Abi, // Match the call's ABI
      functionName: 'balanceOf',
      data: data.result.simulationResults[0], // First call's result
    });
    
    console.log('Decoded Event:', decodedLog); // e.g., { eventName: 'Transfer', args: { from, to, value } }
    console.log('Decoded Balance:', decodedFunctionResult); // e.g., 42n
    // Imports from above...
    
    async function main() {
      // Chain, client, SDK setup from Steps 2-3...
    
      // EthCall from Step 4...
    
      const subscription = await sdk.subscribe({
        ethCalls: [ethCall],
        onData: (data) => {
          // Decoding from Step 6...
        },
      });
    
      // Keep running (e.g., for a server) or unsubscribe after testing
    }
    
    main().catch(console.error);
    Heartbeat: Forced price update every 24 hours

    ARB

    SOL

    WETH

    SOMI

    SOL

    WETH

    SOMI

    Mainnet

    0xbA0E0750A56e995506CA458b2BdD752754CF39C4arrow-up-right

    Testnet

    0x9206296Ea3aEE3E6bdC07F7AaeF14DfCf33d865Darrow-up-right

    Mainnet

    0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04aarrow-up-right

    Testnet

    0x24384e1c60547b0d5403b21ed9b6fb9457fb573farrow-up-right

    Asset Ticker

    Adapter Address

    Asset Markets Overview

    USDT

    0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267arrow-up-right

    USDT marketsarrow-up-right

    USDC

    0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93arrow-up-right

    USDC marketsarrow-up-right

    USDT

    0x67d2C2a87A17b7267a6DBb1A59575C0E9A1D1c3earrow-up-right

    USDT marketsarrow-up-right

    USDC

    0x235266D5ca6f19F134421C49834C108b32C2124earrow-up-right

    USDC marketsarrow-up-right

    BTC

    0x4803db1ca3A1DA49c3DB991e1c390321c20e1f21arrow-up-right

    BTC marketsarrow-up-right

    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

    herearrow-up-right
    Full Example on DIA Docsarrow-up-right
    adapter addressarrow-up-right
    herearrow-up-right
    official documentationarrow-up-right
    Discordarrow-up-right

    BTC

    ARB

    Forced price update at a fixed interval.

    hashtag
    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.

    hashtag
    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.

    hashtag
    Overview of the DAO Contract

    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.

    hashtag
    Setting Up the Development Environment

    Follow the Hardhat or Foundry guides.

    hashtag
    Create the Smart Contract

    Create a new file named DAO.sol in the contracts folder and copy the provided contract code.

    chevron-rightDAO.solhashtag

    Let’s break down the contract into its main components:

    hashtag
    Mappings

    Mappings are used to store structured data efficiently:

    1. proposals

    • Stores all proposals created in the DAO.

    • It represents the Proposal struct containing details like description, deadline, votes, and proposer.

    1. votingPower

    • Tracks the voting power of each address.

    • Voting power increases when users deposit funds into the DAO.

    1. hasVoted

    • Tracks whether a specific address has voted on a specific proposal.

    • Prevents double voting.

    hashtag
    Functions

    The contract includes several key functions:

    1. Constructor

    • Sets the deployer as the owner of the contract.

    1. deposit

    • Allows users to deposit STT Tokens to gain voting power.

    • Increases their votingPower by the amount deposited.

    1. createProposal

    • Allows users with voting power to create new proposals.

    • Adds the proposal to the proposals mapping.

    1. vote

    • 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.

    1. executeProposal

    • 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.

    hashtag
    Key Variables

    1. totalProposals

    • Tracks the total number of proposals created.

    1. votingDuration

    • Sets the default duration for voting on proposals.

    1. 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.

    hashtag
    Deploy the Smart Contract to Somnia

    Follow the Hardhat or 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

    Before running the deploy command, add Somnia Network to the hardhat.config.js file:

    Ensure that the deploying address has enough STT Tokens. You can get STT Tokens from the Faucetarrow-up-right.

    Run the deployment script:

    Congratulations. 🎉 You have successfully deployed the DAO Smart Contract. 🎉

    hashtag
    Interacting with the Contract

    Use the Hardhat console or scripts to interact with the contract.

    hashtag
    1. Deposit Funds

    Call the deposit function to gain voting power:

    hashtag
    2. Create a Proposal

    Create a new proposal by calling createProposal:

    hashtag
    3. Vote on a Proposal

    Vote on a proposal by specifying its ID and your support (true for yes, false for no):

    hashtag
    4. Execute a Proposal

    After the voting deadline, execute the proposal if it has majority votes:

    hashtag
    Testing the Contract

    hashtag
    Writing Tests

    Create a test file DAO.test.js in the test folder.

    Run the tests:

    hashtag
    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.

    hashtag
    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!

    guide
    sdk.streams.getAllPublisherDataForSchema()
    for each address.
  • 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?

    hashtag
    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.

    hashtag
    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.

    hashtag
    Tutorial: Building a GameLeaderboard Proxy

    Let's build a conceptual example of this pattern.

    hashtag
    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.

    hashtag
    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

    hashtag
    The Proxy Smart Contract (Solidity)

    This is a new smart contract you would write and deploy for your DApp. It acts as the gatekeeper.

    circle-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.

    src/contracts/GameLeaderboard.sol

    hashtag
    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

    hashtag
    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

    hashtag
    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.

    hashtag
    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.

    Working with Multiple Publishersarrow-up-right
    hashtag
    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.

    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 standardarrow-up-right 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.

    hashtag
    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

    hashtag
    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.

    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).

    hashtag
    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.

    hashtag
    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).

    hashtag
    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.

    hashtag
    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.

    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.

    Congratulations. 🎉 You have deployed an ERC20 Smart Contract to the Somnia Network. 🎉


    hashtag
    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:

    The process for deploying this Smart Contract implementation built with OpenZeppelin is the same as Steps 3 and 4 above.

    Remix IDEarrow-up-right
    guide

    Verification via Explorer UI

  • Verification via Blockscout Smart Contract Verification API

  • Verification via Hardhat plugin

  • Verification via Foundry (forge)

  • circle-info

    Recommendation: For production contracts, use Standard JSON Input whenever possible. It includes full compiler metadata and gives the most reliable results.

    hashtag
    1. Explorer UI

    1

    hashtag
    Go to the explorer

    Go to https://explorer.somnia.network/arrow-up-right .

    2

    hashtag
    Open the verification page

    Open Other -> Verify Contract.

    3

    hashtag
    Enter contract details

    Enter your contract address and select a verification method.

    4

    hashtag
    Match compilation parameters

    Match all compilation parameters exactly with your deployment settings:

    5

    hashtag
    Verify and publish

    Click Verify and Publish to complete the process.

    hashtag
    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)

    hashtag
    2. API Verification (Blockscout v2)

    hashtag
    Base URL

    https://mainnet.somnia.w3us.site/api/v2

    hashtag
    Service health check

    To check whether the verification service is active:

    If healthy, it returns 200.

    hashtag
    2.1 Flattened Solidity verification

    hashtag
    2.2 Standard JSON Input verification

    hashtag
    2.3 Multi-part Solidity verification

    hashtag
    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

    hashtag
    3. Hardhat Verification

    For Somnia Blockscout, use the @nomicfoundation/hardhat-verify plugin.

    hashtag
    Installation

    hashtag
    Example hardhat.config.ts

    hashtag
    Verification command

    hashtag
    4. Foundry Verification

    Use Blockscout verifier in Foundry as follows:

    To verify at deployment time:

    hashtag
    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.

    hashtag
    Useful Links

    • Blockscout Verification API docs: https://docs.blockscout.com/devs/verification/blockscout-smart-contract-verification-apiarrow-up-right

    • Blockscout UI docs: https://docs.blockscout.com/devs/verification/blockscout-uiarrow-up-right

    • Hardhat verify plugin (Blockscout): https://docs.blockscout.com/devs/verification/hardhat-verification-pluginarrow-up-right

    • Foundry verification (Blockscout):

    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


    hashtag
    Prerequisites

    • Node.js 20+

    • A funded Somnia Testnet wallet. Kindly get some from the

    • Basic familiarity with TypeScript and Next.js


    hashtag
    Project Setup

    Initialize a new Next.js app and install dependencies. Create the app by creating a directory where the app will live

    Install the and ViemJS dependencies

    Create a .env.local file for storing secrets and environmental variables

    triangle-exclamation

    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.


    hashtag
    Define Tap Schema

    Create a file lib/schema.ts:

    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.


    hashtag
    Setup Clients

    Create lib/serverClient.ts for server-side reads:

    Create lib/clients.ts for client-side access:


    hashtag
    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.


    hashtag
    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.

    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)


    hashtag
    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.

    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.


    hashtag
    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).

    This gives you full read/write access to the Somnia Streams contract on Somnia Testnet.


    hashtag
    Compute the Schema ID

    Schemas define how the onchain data is structured. In this case, the tap schema looks like this:

    Before writing data, we must compute its unique Schema ID:

    This produces a deterministic ID derived from the schema text, ensuring that any app using the same schema can read or decode your data.


    hashtag
    Register the Schema

    If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.

    hashtag
    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.

    This converts your JavaScript values into the precise binary format that can be stored onchain and later decoded.


    hashtag
    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.

    This ensures no two taps collide, even if the same player taps rapidly.


    hashtag
    Store the Tap Onchain

    Finally, we push the structured data to the blockchain using:

    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


    hashtag
    Manage Cooldowns and Feedback

    After the tap is sent, we apply a 1-second cooldown to avoid flooding transactions and reset the pending state.

    This gives players a smooth UX while maintaining blockchain transaction integrity.


    hashtag
    Putting It All Together

    Here’s the complete sendTap() method with all steps combined:

    hashtag
    Complete `page.tsx` Code

    chevron-rightpage.tsxhashtag

    hashtag
    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:

    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.


    Creat a api route to retrieve Leaderboard score. Create the file app/api/leaderboard/route.ts

    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.

    The page.tsx fetches /api/leaderboard every few seconds to stay updated.


    Every tap executes a real blockchain transaction:

    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.


    hashtag
    Run the App

    Open and connect your MetaMask wallet. Click 🖱️ Tap to send onchain transactions, and watch your leaderboard update live.


    hashtag
    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.

    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.

    hashtag
    Resources

    Somnia Mainnet WebSocket

    hashtag
    Example Script

    chevron-rightwebsocket-listener.jshashtag

    hashtag
    Prerequisites

    • Node.js 20+

    • Basic understanding of JavaScript

    • A deployed smart contract on Somnia network

    hashtag
    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.

    hashtag
    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

    hashtag
    WebSocket vs HTTP Polling

    hashtag
    HTTP Polling Approach

    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.

    hashtag
    WebSocket Approach

    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.

    hashtag
    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.

    hashtag
    Example Use Cases for WebSocket Event Listening

    hashtag
    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):

    Indexed String (hashed for filtering):

    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.

    hashtag
    Code Breakdown

    hashtag
    Connect to Somnia WebSocket

    The WebSocket URL for Somnia mainnet is . This creates a persistent connection to the network.

    hashtag
    Create Contract Instance

    You need:

    • Contract address: The deployed address on Somnia.

    • ABI: At minimum, include the events you want to listen for.

    • Provider: The WebSocket connection.

    hashtag
    Define Event Filter

    The filter specifies:

    • Which contract to monitor

    • Which event signature to listen for

    hashtag
    Set Up Event Listener

    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

    hashtag
    Maintain Connection

    This is because WebSocket connections can timeout. Send periodic requests to keep the connection alive.

    hashtag
    Update the ABI

    Include only the events and functions you need:

    hashtag
    Update the Event Filter

    Change the event signature to match your event:

    hashtag
    Handle Event Data

    Process the event based on your needs:

    hashtag
    Common Patterns

    hashtag
    Multiple Events

    Listen for multiple events from the same contract:

    hashtag
    Error Recovery

    Add reconnection logic for production applications:

    hashtag
    Event History

    Get recent events on startup:

    hashtag
    Test Your WebSocket Connection

    1. Deploy your Smart Contract to Somnia network.

    2. Run the listener in one terminal:

    1. Trigger events from another script or dApp

    2. Observe real-time updates in your listener

    hashtag
    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

    With this foundation, you can build responsive dApps that react instantly to blockchain events without polling.

    Debug Playbook

    hashtag
    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.

    hashtag
    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:

    Type
    Selector
    Description
    Example

    On Somnia, these behave identically to Ethereum but may differ in gas costs and stack trace length depending on the validator node configuration.

    hashtag
    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.

    In Chai matchers:

    If your contract uses custom errors, Hardhat will not automatically print the name. Decode it manually:

    hashtag
    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

    To detect Panic errors dynamically:

    hashtag
    1.4 Advanced Revert Inspection with Hardhat Traces

    Hardhat’s tracing layer can reveal the full execution path of a revert.

    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:

    hashtag
    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:

    Then use iface.parseError(error.data) to decode reverts directly from on-chain logs or transactions.


    hashtag
    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

    hashtag
    2.1 Example: Catching a Custom Error in Somnia Treasury Contract

    Decoding in JS:

    hashtag
    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:

    This reveals each opcode execution, event emission, and revert reason.

    hashtag
    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:


    hashtag
    3. Transaction Simulation

    Simulating transactions allows developers to predict revert causes, estimate gas usage, and test behaviors without risking real SOMI or STT.

    hashtag
    3.1 Fork Somnia Networks Locally

    Create a local fork of Somnia Mainnet or Shannon Testnet:

    Or in configuration:

    This mirrors on-chain state locally, so you can safely replay any transaction.

    hashtag
    3.2 Using callStatic for Dry-Run Simulation

    callStatic runs a transaction without broadcasting or altering state.

    hashtag
    3.3 Using eth_call Manually

    For raw RPC simulation:

    If the call reverts, inspect error.data to decode it with the ABI.

    hashtag
    3.4 Impersonating Accounts for Privileged Actions

    Simulate admin or contract-controlled operations:

    Stop impersonation when finished:

    hashtag
    3.5 Snapshot and Rollback Control

    Snapshots let you test different outcomes quickly:

    This resets the blockchain to its previous state instantly.

    hashtag
    3.6 Simulating On-Chain Transactions

    If you have a failed transaction hash from Somnia Mainnet:

    This reproduces the failure locally and lets you inspect the revert reason directly in Hardhat.

    hashtag
    3.7 Advanced Fork Testing with Foundry

    You can use cheatcodes like:

    hashtag
    3.8 Gas Profiling and Cost Analysis

    Somnia gas costs can differ from Ethereum due to consensus differences. Always estimate gas usage per function:

    Compare against Shannon and Mainnet to identify anomalies.

    hashtag
    3.9 Full Transaction Lifecycle Test

    1

    hashtag
    Fork Somnia Testnet

    Create a fork of the testnet to reproduce on-chain state locally.

    2


    circle-info

    Summary

    • Always decode revert data rather than relying on generic error strings.

    • Decode custom errors to get structured failure context.

    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.


    hashtag
    Prerequisites

    Before we begin, make sure you have:

    Node/Infra Security

    hashtag
    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.

    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.

    hashtag
    Mainnet VRF Smart Contracts

    Contract

    DAO UI Tutorial p2

    This guide will focus exclusively on implementing Read Operations, which fetches data from your deployed . By the end of this article, you’ll be able to:

    1. Understand how to read data from your smart contract using .

    2. Implement functions to fetch the total number of proposals and specific proposal details.

    import { DIAOracleLib } from "./libraries/DIAOracleLib.sol";
    function getPrice(
            address oracle,
            string memory key
            )
            public
            view
            returns (uint128 latestPrice, uint128 timestampOflatestPrice);
    function getPriceIfNotOlderThan(
            address oracle,
            string memory key,
            uint128 maxTimePassed
            )
            public
            view
            returns (uint128 price, bool inTime)
        {
    // 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); 
        }
    }
    // 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);
        }
    }
    
    mapping(uint256 => Proposal) public proposals;
    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(address => uint256) public votingPower;
    mapping(uint256 => mapping(address => bool)) public hasVoted;
    constructor() {
        owner = msg.sender;
    }
    function deposit() external payable {
        require(msg.value >= 0.001 ether, "Minimum deposit is 0.001 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;
        payable(proposal.proposer).transfer(0.001 ether);
    }
    uint256 public totalProposals;
    uint256 public votingDuration = 10 minutes;
    address public owner;
    import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";
    
    const dao = buildModule("DAO", (m) => {
      const contract = m.contract("DAO");
      return { contract };
    });
    
    module.exports = dao;
    const config = {
      solidity: "0.8.28",
      networks: {
        somnia: {
          url: "https://dream-rpc.somnia.network",
          accounts: ["YOUR_PRIVATE_KEY"],
        },
      },
    };
    npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia
    await dao.deposit({ value: ethers.utils.parseEther("0.001") });
    await dao.createProposal("Fund development of new feature");
    await dao.vote(0, true); // Vote ‘yes’ on proposal 0
    await dao.executeProposal(0);
    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");
      });
    });
    npx hardhat test
    // Schema: 'uint64 timestamp, address player, uint256 score'
    export const leaderboardSchema = 
      'uint64 timestamp, address player, uint256 score'
    
    // 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);
        }
    }
    
    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)
    
    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)
    
    // 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);
    }
    // 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);
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.22;
    
    
    import "@openzeppelin/[email protected]/token/ERC20/ERC20.sol";
    import "@openzeppelin/[email protected]/token/ERC20/extensions/ERC20Burnable.sol";
    import "@openzeppelin/[email protected]/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);
        }
    }
    curl -L "https://mainnet.somnia.w3us.site/api/v2/smart-contracts/verification/config"
    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
      }'
    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"
    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"
    npm install --save-dev @nomicfoundation/hardhat-verify
    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;
    npx hardhat verify \
      --network somnia-mainnet \
      <DEPLOYED_CONTRACT_ADDRESS> \
      "<CONSTRUCTOR_ARG_1>" "<CONSTRUCTOR_ARG_2>"
    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/
    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/

    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.

    Compiler version
  • EVM version

  • Optimizer and runs

  • Constructor args

  • License type

  • https://docs.blockscout.com/devs/verification/foundry-verificationarrow-up-right

    Field

    Description

    timestamp

    Time of the tap

    player

    Wallet address of the player

    Faucetarrow-up-right
    Somnia Streamsarrow-up-right
    http://localhost:3000arrow-up-right

    assert(x > 0);

    Custom Errors

    Function selector of custom error

    error Unauthorized(address caller);

    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

    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

    hashtag
    Impersonate the deployer account

    Use impersonation to perform privileged actions and reproduce behavior.

    3

    hashtag
    Run callStatic to simulate critical functions

    Dry-run core functions to inspect return values and revert reasons.

    4

    hashtag
    Capture reverts and decode with ABI

    Decode revert data, custom errors, and panic codes to get actionable context.

    5

    hashtag
    Use snapshot/revert to iterate quickly

    Take snapshots to test multiple scenarios and revert between them.

    6

    hashtag
    Once clean, deploy and verify on testnet

    After local verification, deploy to the testnet and verify behavior.

    7

    hashtag
    Run same steps on mainnet fork before release

    Final validation on a mainnet fork ensures production parity.

    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.

    Error(string)

    0x08c379a0

    Standard revert reason with message

    require(balance > 0, "Zero balance");

    Panic(uint256)

    0x4e487b71

    0x01

    Assertion failed

    Logic invariant broken

    0x11

    Arithmetic overflow/underflow

    Unchecked math operation

    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

    Internal error (e.g., overflow, div/0, invalid enum)

    0x12

    invalid opcode

    hashtag
    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

    hashtag
    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.

    hashtag
    Using Ankr Provider

    ❌ Bad Practice:

    ✅ Good Practice:

    hashtag
    Environment Variable Management

    hashtag
    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.

    hashtag
    Environment Variable Best Practices

    A .env file is a standard way to manage environment-specific configuration:

    hashtag
    Never Commit .env Files

    The .env file should be added to your .gitignore file.

    hashtag
    Create Separate Environment Files

    Use separate configuration files for different environments.

    hashtag
    Reference Keys in Code

    Always reference environment variables rather than hardcoding sensitive keys.

    hashtag
    Environment Variable Testing for Applications

    Note: This testing approach is designed for application projects only, not system-wide configurations.

    hashtag
    Implementation Examples

    hashtag
    Complete Project Setup

    hashtag
    Secure Contract Interaction

    hashtag
    RPC Key Management

    hashtag
    IP Whitelisting

    If your RPC provider supports it, restrict access to your API key by creating an allowlist of trusted IP addresses.

    hashtag
    Key Rotation and Expiration

    Regularly rotate your RPC keys and immediately revoke any that are no longer in use.

    chevron-rightManual key rotation implementationhashtag

    hashtag
    Secrets Management for Production

    For production environments, use a dedicated secrets management platform.

    hashtag
    Private Key Security

    Private keys authorize all transactions on a blockchain and should be protected with the utmost vigilance.

    hashtag
    Secure Key Generation

    Use reputable tools that follow industry standards for cryptographically random key generation.

    hashtag
    Access Control

    Private keys should never be shared. For team access, use multisig wallets or role-based access control.

    hashtag
    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.

    hashtag
    Secure Logging Practices

    chevron-rightSecure logging implementationhashtag

    hashtag
    Error Recovery Strategies

    hashtag
    Security Checklist

    hashtag
    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.

    Integrate these READ operations into your Next.js pages to display dynamic data.

    Prerequisite: Ensure you’ve completed Part 1 of this series, where you initialized a Next.js project, set up a WalletContext for global state management, and added a global NavBar.


    hashtag
    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.


    hashtag
    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.

    hashtag
    Fetch Total Proposals

    Functionality: This function calls the totalProposals method in your smart contract to determine how many proposals have been created so far.

    chevron-rightcontexts/walletcontext.jshashtag

    fetchTotalProposals() uses publicClient.readContract to call the totalProposals function in the Smart Contract. This function returns a BigInt representing the total number of proposals. 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.


    hashtag
    Integrate Read Operations into Pages

    With the read functions in place, let’s integrate them into Next.js pages to display dynamic data.

    hashtag
    Home Page

    Update the index.js page to show the total number of proposals created in your DAO on the home page.

    chevron-rightpages/index.jshashtag

    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.


    hashtag
    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:

    chevron-rightpages/fetch-proposal.jshashtag

    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 buttons for users to vote YES/NO or execute the proposal if eligible.


    hashtag
    Edge Cases and Errors

    For better UX, consider adding loading indicators while fetching data or awaiting transaction confirmations.

    Example:

    chevron-rightpages/fetch-proposal.jshashtag


    hashtag
    Test Read Operations

    hashtag
    Populate Some Data

    Before testing read operations, make sure there are some proposals created:

    1. Load your Smart Contract on the Remix IDE.

    2. Deposit 0.001 ETH to gain voting power.

    3. Create one or more proposals via the Create Proposal page.

    hashtag
    Verify Read Operations

    Run your application using the command:

    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.


    hashtag
    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.

    hashtag
    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!

    DAO Smart Contract
    viem
    npx create-next-app@latest somnia-chat --ts --app --no-tailwind
    cd somnia-chat
    npm i @somnia-chain/streams viem
    NEXT_PUBLIC_PUBLISHER_ADDRESS=0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03
    NEXT_PUBLIC_RPC_URL=https://dream-rpc.somnia.network
    // lib/schema.ts
    export const tapSchema = 'uint64 timestamp, address player'
    // 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'),
      })
    }
    // 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'),
      })
    }
    const [address, setAddress] = useState('')
    const [walletClient, setWalletClient] = useState<any>(null)
    const [cooldownMs, setCooldownMs] = useState(0)
    const [pending, setPending] = useState(false)
    const [error, setError] = useState('')
    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)
    }
    const sdk = new SDK({
      public: getPublicHttpClient(),
      wallet: walletClient,
    })
    tapSchema = 'uint64 timestamp, address player'
    const schemaId = await sdk.streams.computeSchemaId(tapSchema)
    // 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 })
      }
    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(nonce)}`))
    await sdk.streams.set([{ id, schemaId, data }])
    setCooldownMs(1000)
    setPending(false)
    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)
    }
    '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>
      )
    }
    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)
    }
    import { NextResponse } from 'next/server'
    import { getLeaderboard } from '@/lib/store'
    
    export async function GET() {
      const leaderboard = await getLeaderboard()
      return NextResponse.json({ leaderboard })
    }
    npm run dev
    example.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);
    }
    chai-example.ts
    await expect(treasury.connect(user).withdraw(1000))
      .to.be.revertedWith('Insufficient funds');
    decode-custom-error.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);
      }
    }
    detect-panic.ts
    if (error.data?.startsWith('0x4e487b71')) {
      const code = parseInt(error.data.slice(10), 16);
      console.log('Panic Code:', `0x${code.toString(16)}`);
    }
    trace
    npx hardhat test --trace
    CALL treasury.withdraw
     └─ CALL token.transfer -> reverted with reason: 'Insufficient balance'
    fetch-abi.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));
    Treasury.sol
    error Unauthorized(address caller);
    
    function mint(address to, uint amount) external {
      if (msg.sender != owner) revert Unauthorized(msg.sender);
      _mint(to, amount);
    }
    catch-unauthorized.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]);
    }
    foundry-verbosity
    forge test -vvvv
    get-impl.ts
    const implAddr = await provider.getStorageAt(proxyAddress, '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc');
    const iface = new ethers.utils.Interface(implementationABI);
    hardhat-fork
    npx hardhat node --fork https://api.infra.mainnet.somnia.network
    hardhat-config.ts
    networks: {
      hardhat: {
        forking: {
          url: process.env.SOMNIA_RPC_TESTNET,
          blockNumber: 123456, //example
        }
      }
    }
    callstatic.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);
    }
    eth-call.ts
    const tx = {
      to: contract.address,
      data: contract.interface.encodeFunctionData('stake', [amount])
    };
    const result = await provider.call(tx);
    console.log('Returned data:', result);
    impersonate.ts
    await network.provider.request({
      method: 'hardhat_impersonateAccount',
      params: ['0xAdminAddress']
    });
    const admin = await ethers.getSigner('0xAdminAddress');
    await treasury.connect(admin).setFee(5);
    stop-impersonate.ts
    await network.provider.request({
      method: 'hardhat_stopImpersonatingAccount',
      params: ['0xAdminAddress']
    });
    snapshot.ts
    const snapshot = await network.provider.send('evm_snapshot', []);
    await treasury.mint(100);
    await network.provider.send('evm_revert', [snapshot]);
    replay-tx.ts
    const tx = await provider.getTransaction('0x123...');
    await provider.call({ to: tx.to!, data: tx.data });
    anvil-forge
    anvil --fork-url https://dream-rpc.somnia.network --fork-block-number 3456789
    forge test -vvvv
    foundry-cheatcodes.sol
    vm.startPrank(admin);
    contract.withdraw(1000);
    vm.stopPrank();
    estimate-gas.ts
    const gas = await contract.estimateGas.executeTrade(orderId);
    console.log('Estimated gas:', gas.toString());
    // 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();
    // 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}`);
      }
    };
    // 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');
    // 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);
    // Example configuration for private endpoint
    const config = {
      testnet: {
        url: process.env.SOMNIA_TESTNET_RPC_URL,
        accounts: [process.env.TESTNET_PRIVATE_KEY]
      }
    };
    # .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
    # .gitignore
    .env
    .env.local
    .env.*.local
    node_modules/
    dist/
    # 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)
    // 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
    };
    // 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;
    // 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();
    # 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
    // 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;
    # 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
    // 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;
    };
    // 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
      };
    };
    // 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;
      }
    }
    // 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));
        }
      }
    };
    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);
    }
    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>
      );
    }
    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>
      );
    }
    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>
    npm run dev
  • Adapting the pattern for any smart contract

  • Somnia Testnet WebSocket

    wss://api.infra.testnet.somnia.network/wsarrow-up-right

    DeFi Applications

    • Monitor price updates on DEX swaps

    NFT Marketplaces

    • Live bidding updates in auctions

    Gaming DApps

    • Real-time game state updates

    DAOs and Governance

    • Live voting updates

    Supply Chain

    wss://api.infra.mainnet.somnia.network/wsarrow-up-right
    wss://api.infra.mainnet.somnia.network/wsarrow-up-right
    // 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"
          { indexed: true, internalType: "string", name: "newGreeting"
        ],
        name: "GreetingSet",
        type: "event",
      },
      {
        inputs: [],
        name: "getGreeting",
        outputs: [{ internalType: "string", name: "", type: 
        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);
    
    • Product status updates

    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)


    hashtag
    Set up the SDK and Client

    We’ll initialize the SDK using Viem’s createPublicClient to communicate with Somnia’s blockchain.

    This sets up the data-reading connection between your frontend and the Somnia testnet.

    Think of it as the Streams version of readContract()arrow-up-right; it lets you pull structured data (not just variables) directly from the blockchain.


    hashtag
    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.

    If you don’t have the schema ID handy, you can generate it from its definition:

    This ensures that you’re referencing the same schema ID under which the data was published.


    hashtag
    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.

    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…”)


    hashtag
    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.

    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.


    hashtag
    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:

    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.


    hashtag
    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.

    Example Use Cases:

    • Displaying the last 10 chat messages: getBetweenRange(schemaId, publisher, 0n, 10n)

    • Loading older telemetry data

    • Implementing infinite scroll

    circle-info

    Tip: Treat start and end like array indices (inclusive start, exclusive end). start is inclusive and end is exclusive.


    hashtag
    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.

    When to use:

    • Generating analytics or trend charts

    • Migrating or syncing full datasets

    • Debugging data integrity or history

    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.

    circle-info

    This should be used for small data sets. For larger, paginated reading, getBetweenRange is recommended not to overwhelm the node returning the data.


    hashtag
    Count Total Entries

    Sometimes, you just want to know how many entries exist.

    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.


    hashtag
    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:

    This is critical to ensure your app doesn’t attempt to query a non-existent or unregistered schema — useful for user-facing dashboards.


    hashtag
    Retrieve Full Schema Information

    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:

    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.).


    hashtag
    Example Next.js App

    Now let’s render our fetched data in the UI.

    hashtag
    Project Setup


    hashtag
    Folder Structure


    hashtag
    lib/store.ts

    Sets up the Somnia SDK and connects to the testnet.


    hashtag
    lib/schema.ts

    Defines your schema and publisher.

    If you don’t know your schema ID yet, you can compute it later using:


    hashtag
    lib/read.ts

    Implements read helpers for your API and UI.


    hashtag
    app/api/latest/route.ts

    A serverless route to fetch the latest message (you can add more routes for range or schema info).


    hashtag
    components/StreamViewer.tsx

    A live component with interactive buttons for fetching data.

    chevron-rightStreamViewer.tsxhashtag

    hashtag
    components/SchemaInfo.tsx

    Displays the schema metadata.


    hashtag
    app/page.tsx

    Main dashboard combining both components.


    hashtag
    app/layout.tsx

    Wraps the layout globally.


    hashtag
    Run the App

    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)


    hashtag
    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)

    Address

    VRFV2PlusWrapper

    LINK Token

    LINK/NATIVE oracle

    hashtag
    Testnet VRF Smart Contracts

    Contract
    Address

    VRFV2PlusWrapper

    LINK Token

    LINK/NATIVE oracle

    hashtag
    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.

    hashtag
    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.

    hashtag
    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 Managerarrow-up-right 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 documentationarrow-up-right. 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 morearrow-up-right.

    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

    hashtag
    Prerequisites

    Before getting started:

    • You are familiar with Solidity (v0.8+)

    • You have the VRF Wrapper address for Protofire ChainLink VRF Wrapperarrow-up-right

    hashtag
    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. User Interfaces and Scripts can read getLatestRandomWord() or poll getRequestStatus().

    chevron-rightEXAMPLE - RandomNumberConsumer.solhashtag

    hashtag
    Code Breakdown

    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.

    hashtag
    State Variables

    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).

    hashtag
    VRF Request Parameters (constants)

    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.

    hashtag
    Events

    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.

    hashtag
    Custom errors

    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.

    hashtag
    Constructor

    Takes the VRF V2+ Wrapper address (the on-chain contract that mediates VRF requests) and initializes ownership to the deployer.

    hashtag
    Function Requesting Randomness

    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.

    hashtag
    READ OPERATIONS

    hashtag
    VRF callback (fulfillment)

    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.

    hashtag
    getRequestStatus()

    For frontends/monitoring: see the last request ID, whether it’s still pending, and whether it was fulfilled.

    hashtag
    getLatestRandomWord()

    Returns the three words from the most recent fulfilled request, which is actually a string on numbers for example: 93869141573160465677701763703933181905260360385351294458479680637737009096153

    hashtag
    Pricing Helper

    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.

    hashtag
    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.

    hashtag
    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.

    0xb12e1d47b0022fA577c455E7df2Ca9943D0152bEarrow-up-right
    BTC marketsarrow-up-right
    0x6a96a0232402c2BC027a12C73f763b604c9F77a6arrow-up-right
    ARB marketsarrow-up-right
    0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234arrow-up-right
    SOL marketsarrow-up-right
    0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52arrow-up-right
    WETH marketsarrow-up-right
    0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27arrow-up-right
    SOMI marketsarrow-up-right
    0x74952812B6a9e4f826b2969C6D189c4425CBc19Barrow-up-right
    ARB marketsarrow-up-right
    0xD5Ea6C434582F827303423dA21729bEa4F87D519arrow-up-right
    SOL marketsarrow-up-right
    0x786c7893F8c26b80d42088749562eDb50Ba9601Earrow-up-right
    WETH marketsarrow-up-right
    0xaEAa92c38939775d3be39fFA832A92611f7D6aDearrow-up-right
    SOMI marketsarrow-up-right

    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.

    hashtag

    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:

    This simple definition:

    • Establishes how data should be encoded and decoded on-chain.

    Client                    Server
      |                         |
      |----Connection Request-->|
      |<---Connection Accept----|
      |                         |
      |<===Open Connection====> |
      |                         |
      |----Send Message-------->|
      |<---Receive Message------|
      |<---Push Notification----|
      |----Send Message-------->|
      |                         |
      |<===Open Connection====> |
      |                         |
      |----Close Connection---->|
    // 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
    // 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
    });
    event MessageSent(string message); // Can read 'message' directly from logs
    event MessageSent(string indexed message); // 'message' is hashed, cannot read directly
    const wsUrl = 'wss://api.infra.testnet.somnia.network/ws'; //change url for Mainnet
    const provider = new ethers.WebSocketProvider(wsUrl);
    await provider._waitUntilReady();
    const contract = new ethers.Contract(contractAddress, abi, provider);
    const filter = {
        address: contractAddress,
        topics: [ethers.id("GreetingSet(string,string)")]
    };
    provider.on(filter, async (log) => {
        // Handle the event
        const greeting = await contract.getGreeting();
        console.log(`New greeting: "${greeting}"`);
    });
    setInterval(async () => {
        await provider.getBlockNumber();
    }, 30000);
    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"
        }
    ];
    const filter = {
        address: contractAddress,
        topics: [ethers.id("YourEventName(type1,type2)")]
    };
    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);
    });
    // Listen for Event1
    provider.on({
        address: contractAddress,
        topics: [ethers.id("Event1(...)")]
    }, handleEvent1);
    
    
    // Listen for Event2
    provider.on({
        address: contractAddress,
        topics: [ethers.id("Event2(...)")]
    }, handleEvent2);
    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));
            }
        }
    }
    // 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);
    });
    node websocket-listener.js
    "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>
      )
    }
    npm i @somnia-chain/streams viem
    // 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)
    // 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
    const computedId = await sdk.streams.computeSchemaId(helloWorldSchema)
    console.log('Computed Schema ID:', computedId)
    // 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
    }
    export async function getMessageById(messageKey: `0x${string}`) {
      const msg = await sdk.streams.getByKey(schemaId, publisher, messageKey)
      console.log('Message by key:', msg)
      return msg
    }
    export async function getMessageAtIndex(index: bigint) {
      const record = await sdk.streams.getAtIndex(schemaId, publisher, index)
      console.log(`Record at index ${index}:`, record)
      return record
    }
    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
    }
    export async function getAllPublisherData() {
      const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
      console.log('All publisher data:', allData)
      return allData
    }
    export async function getTotalEntries() {
      const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
      console.log(`Total entries: ${total}`)
      return Number(total)
    }
      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
        }
      }
    const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
    console.log('Schema Info:', schemaInfo)
    {
      baseSchema: 'uint64 timestamp, string message',
      finalSchema: 'uint64 timestamp, string message',
      schemaId: '0xabc123...'
    }
    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
    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
    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)
    export const helloWorldSchema = "uint64 timestamp, string message"
    export const schemaId = "0xabc123..." // replace with actual schemaId
    export const publisher = "0xF9D3...E5aC" // replace with actual publisher
    const computed = await sdk.streams.computeSchemaId(helloWorldSchema)
    console.log("Schema ID:", computed)
    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)
    }
    import { NextResponse } from "next/server"
    import { getLatestMessage } from "@/lib/read"
    
    export async function GET() {
      const data = await getLatestMessage()
      return NextResponse.json({ data })
    }
    "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>
      )
    }
    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>
      )
    }
    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>
      )
    }
    npm run dev
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.19;
    
    import {VRFConsumerBaseV2Plus} from "@chainlink/[email protected]/src/v0.8/vrf/dev/VRFConsumerBaseV2Plus.sol";
    import {VRFV2PlusClient} from "@chainlink/[email protected]/src/v0.8/vrf/dev/libraries/VRFV2PlusClient.sol";
    import {VRFV2PlusWrapperConsumerBase} from "@chainlink/[email protected]/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
    import {ConfirmedOwner} from "@chainlink/[email protected]/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 {}
    }
    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 {
      // 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");
      }
    }
    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);
    }
    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;
    }
    function getRequestPrice() public view returns (uint256) {
      return i_vrfV2PlusWrapper.calculateRequestPriceNative(CALLBACK_GAS_LIMIT, NUM_WORDS);
    }
    ,
    type
    :
    "
    string
    "
    },
    ,
    type
    :
    "
    string
    "
    },
    "
    string
    "
    }
    ]
    ,

    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

    Cover

    Learn more about Somnia

    Concepts You Should Know
    • IPFSarrow-up-right (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.

    • Pinataarrow-up-right: 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.

    hashtag
    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

    hashtag
    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:

    Run the script:

    hashtag
    Recommended Folder Structure

    • 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).

    hashtag
    Upload Images to IPFS via Pinata

    Install Pinata SDK:

    Initialise Pinata using your JWT credentials:

    Create a .env file in your project root:

    Upload the images directory and get the root CID:

    Run the following command to upload the images:

    Copy the CID into .env as IMAGES_CID=....

    hashtag
    Generate Metadata JSON

    Each *.json should follow the de facto standards:

    Install fast-glob library:

    Run the script below to generate a file per image:

    To create the metadata files, run the command:

    hashtag
    Upload Metadata to IPFS

    Upload the metadata folder to generate the METADATA_CID

    Run the command:

    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

    hashtag
    NFT Smart Contract

    We’ll use ERC721URIStorage Smart Contract and mint with full URIs. Paste this into Remix as NFTTest.sol

    chevron-rightNFTTest.solhashtag

    hashtag
    Contract Walkthrough

    hashtag
    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

    hashtag
    Constructor

    Initializes collection name/symbol and sets the owner to initialOwner (the only account allowed to mint).

    hashtag
    _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.

    hashtag
    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.

    hashtag
    Compile and Deploy using Remix. Follow this guide

    Copy the deployed contract address.

    hashtag
    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.

    If you prefer scripts for multiple mints, you can write a script to Batch Mint using Hardhat, for example:

    hashtag
    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.

    hashtag
    MetadataOptional Fully Onchain Metadata

    chevron-rightSmart Contracthashtag

    If your art is small (SVG) or you only need text metadata, you can encode metadata JSON on-chain:

    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.

    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.

    This tutorial will walk you through building, registering, and validating your first schema step by step.

    hashtag
    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:

    4. A Funded Testnet Account. You’ll need an address with test tokens on the Somnia Testnet to register schemas or publish data.

    triangle-exclamation

    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.


    hashtag
    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

    We’ll use a chat message schema as a running example:

    This schema represents a single chat message, which can be used later to build a full on-chain chat application.


    hashtag
    Project Setup

    hashtag
    Install dependencies

    hashtag
    Define Chain configuration

    hashtag
    Set up your clients


    hashtag
    Define the Schema String

    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.


    hashtag
    Compute the schemaId

    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].


    hashtag
    Register the Schema

    Registration makes your schema discoverable and reusable by others. [confirm with Vincent for correctness].

    isSchemaRegistered() checks chain state. registerSchema() publishes the schema definition to Streams. Thus, the transaction is idempotent, meaning that it is safe to re-run.


    hashtag
    Encode and Decode a Sample Payload

    Test your schema locally before publishing any data.

    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.


    hashtag
    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.

    // 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);
        }
    }
    // 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)
            ));
        }
    }
    npm i sharp fast-glob
    // 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);
    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)
    npm i pinata dotenv
    // 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
    })
    PINATA_JWT=eyJhbGciOi...        # your Pinata JWT (keep secret)
    PINATA_GATEWAY=myxyz.mypinata.cloud
    IMAGES_CID=                     # leave blank until you upload images
    // 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)
    node dist/scripts/upload-images.js
    # => Images CID: bafybe...
    {
      "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 }
      ]
    }
    
    npm i fs-extra fast-glob
    / 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)
    
    node dist/scripts/make-metadata.js
    // 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)
    node dist/scripts/upload-metadata.js
    # => Metadata CID: bafybe...
    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);
    
    RPC_URL=https://dream-rpc.somnia.network
    PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
    uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
    uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
    npm i @somnia-chain/streams viem
    npm i -D @types/node
    // 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'] },
      },
    })
    // 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')),
    })
    // src/lib/chatSchema.ts
    export const chatSchema =
      'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
    // 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)
    })
    // 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)
    })
    // 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))
    emits RandomNumberFulfilled.
    0x606b2B36516AB7479D1445Ec14B6B39B44901bf8arrow-up-right
    0x0a4Db7035284566F6f676991ED418140dC01A2aaarrow-up-right
    0xEBD41881413dD76F42DF2902ee865099af9099B4arrow-up-right
    0x763cC914d5CA79B04dC4787aC14CcAd780a16BD2arrow-up-right
    0x30C75a2badF9b12733e831fcb5315C8f54e96f6darrow-up-right
    0xEc00df0e834AB878135b6554bb7438A2Ff66563barrow-up-right

    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.

    hashtag
    Prerequisites

    • Basic knowledge of React and Next.js

    • Node.js 20+

    • A deployed subgraph on Somnia (we'll use as an example)

    hashtag
    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

    hashtag
    Create a Next.js Project

    Start by creating a new Next.js application with TypeScript and TailwindCSS:

    Install the required GraphQL dependencies:

    hashtag
    Understand the Architecture

    Before we code, let's understand how the pieces fit together:

    hashtag
    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

    The URI is the endpoint where your subgraph is hosted, and InMemoryCache will store the query results in memory for fast access

    hashtag
    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.

    ApolloProvider: Makes the Apollo Client available to all child components

    hashtag
    Update app/layout.tsx

    hashtag
    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.

    chevron-rightqueries.tshashtag

    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)

    hashtag
    Build the All Flips Component

    Let's build the All Flips component step by step, understanding each part in detail.

    hashtag
    Set Up the Component File

    In the components directory create AllFlips.tsx file and add the following imports.

    hashtag
    Create Utility Functions

    Add these helper functions at the top of your component:

    hashtag
    Component Function and State Management

    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.

    hashtag
    Execute the GraphQL Query

    The useQuery function is set to loading: true while fetching data and the data contains the query results when successful.

    hashtag
    Handle Query States

    This prevents rendering errors and andles edge cases gracefully

    hashtag
    Render the Table View

    chevron-rightAllFlips.tsx - Table Viewhashtag

    hashtag
    Build the Live Feed Component

    Now let's build the Live Feed component that automatically refreshes to show new flips.

    hashtag
    Set Up the Component

    In the components directory create a LiveFeed.tsx file and update the imports.

    hashtag
    Create Utility Functions

    hashtag
    Component Function with Auto-Refresh

    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

    hashtag
    Handle Query States

    hashtag
    Complete Live Feed Component

    chevron-rightLiveFeed.tsxhashtag

    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.

    hashtag
    Update the Main Page.tsx

    chevron-rightpage.tsxhashtag

    hashtag
    Run Your Application

    Visit http://localhost:3000 to see your UI in action.

    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.

    hashtag
    Prerequisites

    To complete this guide, you will need:

    • Basic understanding of React and TypeScript

    • An Ormi API key. Get one at.

    hashtag
    What is Ormi Data API?

    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.

    hashtag
    API Base URL

    The Ormi Data API for Somnia Network uses the following base URL:

    hashtag
    API Endpoints

    The API follows a RESTful structure. For fetching ERC20 token balances, the endpoint structure is:

    Where:

    • somnia - The network identifier

    • v1 - API version

    • {walletAddress} - The wallet address you want to query

    hashtag
    Authentication

    The Ormi API requires authentication using a Bearer token. Every request must include an Authorization header:

    Important: Never expose your API key in client-side code. Always make API calls from a server-side route to keep your key secure.

    hashtag
    Example API Request

    Here's an example of how to make a direct API call using curl:

    hashtag
    Set up the Project

    Create a new Next.js application with TypeScript and Tailwind CSS:

    hashtag
    Create the Type Definitions

    First, we need to define TypeScript interfaces for the API response. Update app/page.tsx:

    hashtag
    Build the User Interface

    Now, let's create the main component with an input field and button. Update your app/page.tsx:

    hashtag
    Create a .env file

    The .env is for keeping secrets such as the Ormi API KEY. Add the API KEY:

    hashtag
    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:

    Important: Replace YOUR_API_KEY_HERE with your actual Ormi API key.

    hashtag
    Implement the Fetch Function

    Add the fetch function to handle form submission. Update your app/page.tsx:

    Don't forget to add the onSubmit handler to your form:

    hashtag
    Display the Results

    Add error handling and a table to display the token balances. Add this code after your form in app/page.tsx:

    hashtag
    Test Your dApp

    Start the development server:

    Open 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.

    hashtag
    Complete Code

    chevron-rightpage.tsxhashtag

    chevron-rightroute.tshashtag

    hashtag
    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 .

    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 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 (useContextarrow-up-right 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.


    hashtag
    Pre-requisites:

    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 .

    hashtag
    Create Your Next.js Project

    To create a NextJS project, run the command:

    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.

    hashtag
    (Optional) Add Tailwind CSS

    If you plan to style your app with Tailwind, install and configure it now:

    Then edit your tailwind.config.js:

    Finally, include Tailwind in styles/globals.css:


    hashtag
    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 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:

    chevron-rightwalletcontext.jshashtag

    We parse three class methods from React: createContext, useContext, anduseState

    createContext is used to create a 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:

    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.


    hashtag
    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

    hashtag
    Create the _app.js and add the code:

    <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.

    hashtag
    NavBar

    Create a sub directory components and add a file called navbar.js Add the code:

    It uses useWallet() to read the global connected and addressstates, 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.


    hashtag
    Test Your Setup

    Start the dev server:

    Open http://localhost:3000 in a Web Browser. You should see your NavBar at the top.

    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.


    hashtag
    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!

    Smart Contracts

    List of critical Smart Contract addresses for Somnia network.

    Contract
    Address

    MultiCallV3

    WSOMI

    USDC

    hashtag
    Omnichain SOMI Deployments

    Mainnet
    Address
    Type

    hashtag
    LayerZero Contracts

    • chainKey : somnia

    • stage : mainnet

    Contract
    Address

    hashtag
    Oracles

    hashtag
    DIA - Mainnet

    Contract
    Address

    hashtag
    Protofire - Mainnet

    Contract
    Address

    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 Ormiarrow-up-right, a powerful gateway that simplifies subgraph deployment through a hosted Graph Node and IPFS infrastructure.

    hashtag
    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.

    hashtag
    Install Graph CLI globally

    hashtag
    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:

    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.yamlDefines the data sources and events to index

    • schema.graphql Structure of your data

    • src/mytoken.tsTypeScript logic to handle events

    hashtag
    Define the Subgraph Schema

    For the exampleMyToken Contract, which is an ERC20 Token, Edit schema.graphql to index all the Transfer events emitted from the Smart Contract.

    hashtag
    Build the Subgraph

    After customizing your schema and mapping logic, build the subgraph by running the command:

    This will generate the necessary artifacts for deployment.

    hashtag
    Deploy Using Ormi

    Open the Somnia Ormi website and create an account.

    On the left navigation menu, click the "key" icon to access your privateKey.

    Deploy your subgraph to Ormi’s hosted infrastructure with the following command:

    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.

    Open the deployed subgraph in the explorer to interact with it:

    hashtag
    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.

    For more information, visit the Ormi .

    SomFliparrow-up-right
    Connect Your Walletarrow-up-right
    Contextarrow-up-right
    contextarrow-up-right
    Not Logged In
    Logged In
    npx create-next-app@latest somnia-subgraph-ui --typescript --tailwind --app
    cd somnia-subgraph-ui
    npm install @apollo/client graphql
    User Interface (React Components)
            ↓
    Apollo Client (GraphQL Client)
            ↓
    GraphQL Queries
            ↓
    Somnia Subgraph API
            ↓
    Blockchain Data
    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;
    '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>
      );
    }
    import ApolloWrapper from '@/components/ApolloWrapper';
    export default function RootLayout({
      children,
    }: {
      children: React.ReactNode;
    }) {
      return (
        <html lang="en">
          <body>
            <ApolloWrapper>
              {children}
            </ApolloWrapper>
          </body>
        </html>
      );
    }
    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
        }
      }
    `;
    'use client';  
    import { useState } from 'react';
    import { useQuery } from '@apollo/client';
    import { GET_FLIP_RESULTS } from '@/lib/queries';
    / 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();
    };
    export default function AllFlips() {
      // Track which page of results we're viewing
      const [page, setPage] = useState(0);
      const itemsPerPage = 30;
    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
        },
      });
    // 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>;
      }
    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>
      );
    }
    'use client';
    import { useQuery } from '@apollo/client';
    import { GET_RECENT_FLIPS } from '@/lib/queries';
    const truncateHash = (hash: string) => {
      return `${hash.slice(0, 6)}...${hash.slice(-4)}`;
    };
    
    const formatEther = (wei: string) => {
      return (parseFloat(wei) / 1e18).toFixed(4);
    };
    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)
      });
    // 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>;
      }
    
    '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>
      );
    }
    '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>
      );
    }
    npm run dev
    npx create-next-app my-dapp-ui
    npm install -D tailwindcss postcss autoprefixer
    npx tailwindcss init -p
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    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);
    }
    
    const { connected, ... } = useWallet()
    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;
    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>
      );
    }
    npm run dev

    balance/erc20 - Specifies that you want ERC-20 token balances

    arrow-up-right
    https://subgraph.somnia.network/dashboard/apiarrow-up-right
    Ormiarrow-up-right
    http://localhost:3000arrow-up-right
    endpointsarrow-up-right

    OFT

    Somnia

    NativeOFTAdapter

    EID : 30380

    deadDVN

    BTC

    ARB

    SOL

    WETH

    SOMI

    0x28bec7e30e6faee657a03e19bf1128aad7632a00arrow-up-right

    WETH

    0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8arrow-up-right

    USDT

    0x67B302E35Aef5EEE8c32D934F5856869EF428330arrow-up-right

    BNB Chain

    0xa9616e5e23ec1582c2828b025becf3ef610e266farrow-up-right

    OFT

    Base

    0x47636b3188774a3E7273D85A537b9bA4Ee7b253arrow-up-right

    OFT

    endpointV2 (main entrypoint)

    0x6F475642a6e85809B1c36Fa62763669b1b48DD5Barrow-up-right

    sendUln302

    0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7arrow-up-right

    receiveUln302

    0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043arrow-up-right

    executor

    Oracle

    0xbA0E0750A56e995506CA458b2BdD752754CF39C4arrow-up-right

    Gas Wallet

    0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04aarrow-up-right

    USDT

    0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267arrow-up-right

    USDC

    VRFV2PlusWrapper

    0x606b2B36516AB7479D1445Ec14B6B39B44901bf8arrow-up-right

    LINK Token

    0x0a4Db7035284566F6f676991ED418140dC01A2aaarrow-up-right

    LINK/NATIVE oracle

    0xEBD41881413dD76F42DF2902ee865099af9099B4arrow-up-right

    Price Feeds
    VRF Smart Contracts
    0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11arrow-up-right
    0x046EDe9564A72571df6F5e44d0405360c0f4dCabarrow-up-right

    Ethereum

    https://subgraph.somnia.network/arrow-up-right
    docsarrow-up-right

    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.

    hashtag
    Prerequisites

    Integrate Chainlink Oracles

    Somnia Data Streams provides a powerful, on-chain, and composable storage layer. 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.

    https://api.subgraph.somnia.network/public_api/data_api
    /somnia/v1/address/{walletAddress}/balance/erc20
    Authorization: Bearer YOUR_API_KEY
    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"
    npx create-next-app@latest somnia-balance-demo --typescript --tailwind --app
    cd somnia-balance-demo
    '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>('')
    
      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>
      )
    }
    PRIVATE_KEY=YOUR_API_KEY_HERE
    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 }
        )
      }
    }
    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)
      }
    }
    <form onSubmit={fetchBalance} className="mb-8">
    {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>
    )}
    npm run dev
    '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>
      );
    }
    
    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 }
        );
      }
    }
    
    npm install -g @graphprotocol/graph-cli
    graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken
    type Transfer @entity(immutable: true) {
      id: Bytes!
      from: Bytes!
      to: Bytes!
      value: BigInt!
      blockNumber: BigInt!
      blockTimestamp: BigInt!
      transactionHash: Bytes!
    }
    graph codegen && graph build
    graph deploy mytoken --node https://api.subgraph.somnia.network/deploy --ipfs https://api.subgraph.somnia.network/ipfs --deploy-key yourORMIPrivateKey
    0x1B0F6590d21dc02B92ad3A7D00F8884dC4f1aed9arrow-up-right
    0xC3D4E9Ac47D7f37bB07C2f8355Bb4940DEA3bbC3arrow-up-right
    0x4208D6E27538189bB48E603D6123A94b8Abe0A0barrow-up-right
    0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842arrow-up-right
    0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93arrow-up-right
    0xb12e1d47b0022fA577c455E7df2Ca9943D0152bEarrow-up-right
    0x6a96a0232402c2BC027a12C73f763b604c9F77a6arrow-up-right
    0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234arrow-up-right
    0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52arrow-up-right
    0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27arrow-up-right

    Node.js 20+

  • A funded Somnia Testnet wallet. Kindly get some from the Faucetarrow-up-right

  • Basic familiarity with TypeScript and Next.js

  • hashtag
    Project Setup

    Create the app by creating a directory where the app will live

    Install the Somnia Streamsarrow-up-right and ViemJS dependencies

    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:

    Next.js provides a simple, full-stack environment.

    hashtag
    Configure environment variables

    Create .env.local file.

    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.

    triangle-exclamation

    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

    hashtag
    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

    hashtag
    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

    hashtag
    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

    hashtag
    Chat Service

    We’ll build chatService.ts in small pieces so it’s easy to follow.

    hashtag
    Imports and helpers

    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.

    hashtag
    Ensure the schema is registered

    If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.

    hashtag
    Publish a message

    • 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:

    chevron-rightchatService.tshashtag

    hashtag
    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:

    chevron-rightchatMessages.tshashtag

    hashtag
    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

    hashtag
    Write Messages Endpoint

    src/app/api/send/route.ts

    Publishes messages with the server wallet. We validate the input and return the tx hash.

    hashtag
    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.

    hashtag
    Component Setup

    • 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.

    hashtag
    UI Rendering

    • 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.

    hashtag
    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.

    Complete code below:

    chevron-rightpage.tsxhashtag

    hashtag
    Run the App

    Run the program using the command:

    Your app will be LIVE at http://localhost:3000arrow-up-right in your browser

    circle-check

    Tip

    Open two browser windows to simulate two users watching the same room. Both will see new messages as the poller fetches fresh data.

    hashtag
    Codebase

    https://github.com/emmaodia/somnia-streams-chat-demoarrow-up-right

    hashtag
    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.

    hashtag
    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.

    hashtag
    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).

    hashtag
    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).

    hashtag
    Project Setup

    Set up your project with viem and the Streams SDK.

    hashtag
    Chain Configuration

    We need to define both chains we are interacting with.

    src/lib/chain.ts

    hashtag
    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

    hashtag
    Define the Price Feed Schema

    Our schema will store the core data from Chainlink's feed.

    src/lib/schema.ts

    • 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").

    hashtag
    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

    hashtag
    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

    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.

    hashtag
    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

    To read the history:

    Add to package.json: "history": "ts-node src/scripts/readHistory.ts"

    Run it: npm run history

    Expected Output:

    hashtag
    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.

    Chainlink Oraclesarrow-up-right
    // 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 }
    }
    
    '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 }
    }
    
    '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>
      )
    }
    
    npx create-next-app@latest somnia-chat --ts --app --no-tailwind
    cd somnia-chat
    npm i @somnia-chain/streams viem
    npm i -D @types/node
    RPC_URL=https://dream-rpc.somnia.network
    PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
    CHAT_PUBLISHER=0xYOUR_WALLET_ADDRESS
    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)
    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
    export const chatSchema = 'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
    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(),
    })
    // 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 })
      }
    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 }
    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 })
      }
    }
    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')
        }
      }
      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>
      )
    }
    +-----------+       +--------------------+      +---------------------+
    |  User UI  | --->  |  Next.js API /send | ---> | Somnia Data Streams |
    +-----------+       +--------------------+      +---------------------+
          ^                       |                           |
          |                       v                           |
          |             +------------------+                  |
          |             |   Blockchain     |                  |
          |             |  (Transaction)   |                  |
          |             +------------------+                  |
          |                       |                           |
          |<----------------------|                           |
          |     Poll via SDK or Subscribe (useChatMessages)   |
          +---------------------------------------------------+
    
    npm run dev
    # .env
    RPC_URL_SOMNIA=[https://dream-rpc.somnia.network]
    RPC_URL_SEPOLIA=[https://sepolia.drpc.org]
    PRIVATE_KEY_SOMNIA=0xYOUR_SOMNIA_PRIVATE_KEY
    npm i @somnia-chain/streams viem dotenv
    npm i -D @types/node typescript ts-node
    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
    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')),
    })
    // This schema will store historical price snapshots
    export const priceFeedSchema = 
      'uint64 timestamp, int256 price, uint80 roundId, string pair'
    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
      }
    }
    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
      const regTx = await sdk.streams.registerDataSchemas([
            { 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)
    })
    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)
    })
    --- 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...)

    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.

    circle-check

    Somnia Mainnet is LIVE. To deploy on Somnia Mainnet, you will need SOMI Tokens. Please refer to the on Moving from Testnet to Mainnet.

    hashtag

    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 . 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 .

    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 .

    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.

    hashtag

    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 or Foundry guides.

    hashtag
    Core Concepts

    hashtag
    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%).

    hashtag
    AMMs are fully decentralized with no need for counterparties and are Open and permissionless to use and contribute liquidity.

    hashtag
    Liquidity Pools

    Pairs of tokens locked in Smart Contracts that facilitate trading without traditional order books.

    hashtag
    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

    hashtag
    Implementation

    hashtag
    ERC-20 Interface

    First, let's define the ERC-20 interface we'll use.

    chevron-rightSomniaPair.solhashtag

    hashtag
    SomniaPair Contract

    The pair contract manages individual Liquidity Pools.

    chevron-rightSomniaPair.solhashtag

    hashtag
    SomniaFactory Contract

    The factory creates and tracks all pair contracts.

    chevron-rightSomniaFactory.solhashtag

    hashtag
    SomniaRouter Contract

    The router provides user-friendly functions for swapping and liquidity management.

    chevron-rightSomniaRouter.solhashtag

    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.

    hashtag
    Usage Example

    chevron-rightAdd Liquidityhashtag

    chevron-rightSwap Tokenshashtag

    chevron-rightRemove Liquidityhashtag

    hashtag
    Test Your DEX

    Create a test script to verify functionality:

    chevron-rightTestDEX.solhashtag

    hashtag
    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.

    guide
    // 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);
    }
    
    // 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;
        }
    }
    
    // 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);
        }
    }
    
    // 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');
        }
    }
    
    // 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
    );
    // 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
    );
    // Approve router to spend LP tokens
    pair.approve(router.address, lpTokenAmount);
    
    // Remove liquidity
    router.removeLiquidity(
        tokenA.address,
        tokenB.address,
        lpTokenAmount,
        minAmountA,
        minAmountB,
        userAddress,
        deadline
    );
    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
            );
        }
    }
    Integrate these operations into your Next.js pages with intuitive UI components.
  • Handle transaction states and provide user feedback.

  • Prerequisite: Ensure you’ve completed Part 2 of this series, where you set up the WalletContext for global state management and added a global NavBar.


    hashtag
    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.


    hashtag
    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.

    hashtag
    Implement deposit

    Allows users to deposit a fixed amount of ETH (e.g., 0.001 ETH) into the DAO contract to gain voting power.

    chevron-rightcontexts/walletContext.jshashtag

    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.

    hashtag
    Implement createProposal

    Allows users to create a new proposal by submitting a description.

    chevron-rightcontexts/walletContext.jshashtag

    createProposal(description): Takes a proposal description as an argument and sends a transaction to the DAO contract to create the proposal.

    hashtag
    Implement voteOnProposal

    Allows users to vote on a specific proposal by its ID, supporting either a Yes or No vote.

    chevron-rightcontexts/walletContext.jshashtag

    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.

    hashtag
    Implement executeProposal

    Allows users to execute a proposal if it meets the necessary conditions (e.g., quorum reached).

    chevron-rightcontexts/walletContext.jshashtag

    executeProposal(proposalId): Takes a proposal ID and sends a transaction to execute the proposal.


    hashtag
    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.

    hashtag
    Create-Proposal Page

    Allow users to submit new proposals by entering a description.

    chevron-rightpages/create-proposal.jshashtag

    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.

    hashtag
    Fetch-Proposal Page: Vote and Execution

    Allow users to fetch proposal details, vote on them, and execute if eligible.

    chevron-rightpages/fetch-proposal.jshashtag

    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 supportparameter. 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.


    hashtag
    Transaction States and User Feedback

    Clear feedback during and after transactions enhances user experience and trust in your dApp. Consider using libraries like react-toastifyarrow-up-right for non-intrusive notifications. Example with Toast Notifications:

    Install react-toastify

    Inside the _app.js

    In your WalletContext or Pages

    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.


    hashtag
    Test Write Operations

    Thorough testing ensures the reliability and trustworthiness of your dApp. Here's how to effectively test your write operations:

    hashtag
    Connect to a Test Network

    Run your application using the command:

    Your application will be running on localhost:3000 in your web browser.

    hashtag
    Obtain STT from the Faucet.arrow-up-right

    hashtag
    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.


    hashtag
    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.

    DAO Smart Contract
    WalletContext
    Installation

    The SDK depends on viemarrow-up-right for blockchain interactions.

    hashtag
    Project Setup

    Create a .env.local or .env file in your project root:

    ⚠️ Never expose private keys in client-side code. Keep writes (publishing data) in server routes or backend environments.

    hashtag
    Basic Initialization

    You’ll typically use two clients:

    • A public client for reading and subscribing

    • A wallet client for writing to the Somnia chain

    hashtag
    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:

    You’ll interact primarily through the sdk.streams interface.

    hashtag
    Core Methods Overview

    hashtag
    Write

    hashtag
    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

    Always register your schema before calling set() ; otherwise, the transaction will revert.

    hashtag
    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

    Common Use includes notifying subscribers when something happens, e.g., “new message sent” or “order filled”.

    hashtag
    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

    It is ideal for chat apps, game updates, or IoT streams — where data must be recorded and instantly broadcast.

    hashtag
    Manage

    hashtag
    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

    Register before writing any new data type. If you modify the schema structure later, register it again as a new schema version.

    hashtag
    registerEventSchemas(ids: string[], schemas: EventSchema[]): Promise<Hex | Error | null>

    Description

    Registers event definitions that can later be emitted or subscribed to.

    Example

    Use before calling emitEvents() or subscribe() for a specific event.

    hashtag
    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

    Used for access control in multi-publisher systems.

    hashtag
    Read

    hashtag
    getByKey(schemaId: SchemaID, publisher: Address, key: Hex): Promise<Hex[] | SchemaDecodedItem[][] | null>

    Description

    Retrieves data stored under a schema by its unique ID.

    Example

    An example includes fetching a specific record, e.g., “fetch message by message ID”.

    hashtag
    getAtIndex(schemaId: SchemaID, publisher: Address, idx: bigint): Promise<Hex[] | SchemaDecodedItem[][] | null>

    Description

    Fetches the record at a given index (0-based).

    Example

    It is useful for sequential datasets like logs or telemetry streams.

    hashtag
    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

    hashtag
    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

    hashtag
    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

    hashtag
    totalPublisherDataForSchema(schemaId: SchemaID, publisher: Address): Promise<bigint | null>

    Description

    Returns how many records a publisher has stored under a schema.

    Example

    hashtag
    isDataSchemaRegistered(schemaId: SchemaID): Promise<boolean | null>

    Description

    Checks if a schema exists on-chain.

    Example

    hashtag
    parentSchemaId(schemaId: SchemaID): Promise<Hex | null>

    Description

    Finds the parent schema of a given schema, if one exists.

    Example

    hashtag
    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

    hashtag
    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

    hashtag
    getAllSchemas(): Promise<string[] | null>

    Description

    Retrieves a list of all registered schema identifiers.

    Use Case

    Discovering available schemas in the protocol.

    Example

    hashtag
    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

    hashtag
    computeSchemaId(schema: string): Promise<Hex | null>

    Description

    Computes the deterministic schemaId without registering it.

    Example

    hashtag
    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

    hashtag
    Helpers

    hashtag
    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

    hashtag
    Subscribe

    hashtag
    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

    With ethCalls

    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.

    hashtag
    Protocol

    hashtag
    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

    hashtag
    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

    hashtag
    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.

    @somnia-chain/streamsarrow-up-right
    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>
      );
    }
    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>
      );
    }
    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>
      );
    }
    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>
      );
    }
    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>
      );
    }
    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>
      );
    }
    npm install react-toastify
    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;
    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.");
    npm run dev
    npm i @somnia-chain/streams viem dotenv
    RPC_URL=https://dream-rpc.somnia.network
    PRIVATE_KEY=your_private_key_here
    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) })
    })
    const sdk = new SDK({
      public: getPublicClient(),  // for reading and subscriptions
      wallet: getWalletClient()   // for writing
    })
    const tx = await sdk.streams.set([
      { id: dataId, schemaId, data }
    ])
    console.log('Data published with tx hash:', tx)
    await sdk.streams.emitEvents([
      {
        id: 'ChatMessage',
        argumentTopics: [topic],
        data: '0x' // optional encoded payload
      }
    ])
    await sdk.streams.setAndEmitEvents(
      [{ id: dataId, schemaId, data }],
      [{ id: 'ChatMessage', argumentTopics: [topic], data: '0x' }]
    )
    await sdk.streams.registerDataSchemas([
      {
        schemaName: "chat",
        schema: 'uint64 timestamp, string message, address sender',
        parentSchemaId: zeroBytes32 // root schema
      }
    ], true) // Optionally ignore if already registered
    await sdk.streams.registerEventSchemas(
      ['ChatMessage'],
      [{
        params: [{ name: 'roomId', paramType: 'bytes32', isIndexed: true }],
        eventTopic: 'ChatMessage(bytes32 indexed roomId)'
      }]
    )
    await sdk.streams.manageEventEmittersForRegisteredStreamsEvent(
      'ChatMessage',
      '0x1234abcd...',
      true // allow this address to emit
    )
    const msg = await sdk.streams.getByKey(schemaId, publisher, dataId)
    console.log('Data:', msg)
    const record = await sdk.streams.getAtIndex(schemaId, publisher, 0n)
    const records = await sdk.streams.getBetweenRange(schemaId, publisher, 0n, 10n)
    console.log('Records in range:', records)
    const allData = await sdk.streams.getAllPublisherDataForSchema(schemaReference, publisher)
    console.log('All publisher data:', allData)
    const latest = await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher)
    console.log('Latest data:', latest)
    const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher)
    console.log(`Total entries: ${total}`)
    const exists = await sdk.streams.isDataSchemaRegistered(schemaId)
    if (!exists) console.log('Schema not found')
    const parent = await sdk.streams.parentSchemaId(schemaId)
    console.log('Parent Schema ID:', parent)
    const id = await sdk.streams.schemaIdToId(schemaId)
    console.log('Schema ID string:', id)
    const schemaId = await sdk.streams.idToSchemaId('chat')
    console.log('Schema ID:', schemaId)
    const schemas = await sdk.streams.getAllSchemas()
    console.log('All schemas:', schemas)
    const eventSchemas = await sdk.streams.getEventSchemasById(['ChatMessage'])
    console.log('Event schemas:', eventSchemas)
    const schemaId = await sdk.streams.computeSchemaId('uint64 timestamp, string content')
    const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId)
    console.log('Schema info:', schemaInfo)
    const decoded = await sdk.streams.deserialiseRawData(rawData, parentSchemaId, schemaLookup)
    console.log('Decoded data:', decoded)
    // Wildcard subscription to all events emitted by all contracts
    await sdk.streams.subscribe({
        ethCalls: [], // No view calls
        onData: (data) => {}
    })
    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)
    })
    const info = await sdk.streams.getSomniaDataStreamsProtocolInfo()
    console.log('Protocol info:', info)

    { params: EventParameter[], eventTopic: string } – For registerEventSchemas() .

    EthCall

    { to: Address, data: Hex } – Defines on-chain calls for event enrichment.

    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.

    hashtag
    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

    hashtag
    Vulnerability Overview

    The following table categorizes vulnerabilities by severity and implementation difficulty:

    Vulnerability Type
    Severity
    Frequency
    Detection Difficulty
    Financial Impact

    hashtag
    1. Reentrancy Vulnerabilities

    hashtag
    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.

    hashtag
    Vulnerable Implementation Analysis

    chevron-rightReentrancyVulnerable Contracthashtag

    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.

    hashtag
    Attack Mechanism

    1

    hashtag
    Attack contract & setup

    Attacker contract:

    Initial state example:

    hashtag
    Secure Implementation

    chevron-rightSecure Reentrancy Contracthashtag

    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

    How It Prevents Attacks:

    • First call sets _status = _ENTERED

    • Any reentrant call fails the require(_status == _NOT_ENTERED) check

    • Transaction reverts with "reentrant" error

    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.

    hashtag
    Prevention Strategies (summary)

    Strategy
    Implementation
    Effectiveness
    Gas Cost

    hashtag
    2. Access Control Vulnerabilities

    hashtag
    Understanding the Flaw

    Poor access control allows unauthorized users to execute privileged functions, leading to complete contract compromise.

    hashtag
    Vulnerable Implementation Analysis

    chevron-rightAccess Control Vulnerable Contracthashtag

    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).

    hashtag
    Attack Mechanism

    1

    hashtag
    Setup & initial compromise

    Initial state:

    • Deployer has ADMIN and WRITER.

    hashtag
    Secure Implementation

    chevron-rightSecure Access Control Contracthashtag

    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

    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

    hashtag
    Best Practices (summary)

    Practice
    Purpose
    Implementation

    hashtag
    3. Integer Overflow/Underflow

    hashtag
    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.

    hashtag
    Vulnerable Implementation Analysis

    chevron-rightInteger Overflow Vulnerable Contracthashtag

    Critical vulnerabilities:

    • Unchecked addition (overflow) on balances and totalSupply.

    • Unchecked subtraction (underflow) in transfer.

    • Batch operations amplify overflow risks.

    hashtag
    Attack Mechanisms

    1

    hashtag
    Overflow via mint()

    Initial state:

    • totalSupply = expected supply

    hashtag
    Secure Implementation

    chevron-rightSecure Integer Overflow Contracthashtag

    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

    How it prevents attacks:

    • All arithmetic operations are validated before execution

    • Supply limits prevent economic manipulation

    • Access controls prevent unauthorized minting

    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

    Result:

    • All arithmetic operations are safe

    • Economic model is protected

    • Attacks are blocked before execution

    hashtag
    Protection Methods (summary)

    Method
    Solidity Version
    Effectiveness
    Gas Cost

    hashtag
    Prevention Strategies (Consolidated)

    1. Reentrancy Prevention

      • Primary: Checks-Effects-Interactions pattern

      • Secondary: Reentrancy guards (e.g., OpenZeppelin ReentrancyGuard)


    hashtag
    Testing Vulnerabilities

    Use these commands and steps to test examples in Remix (manual steps):

    Verification steps:

    1. Deploy vulnerable contract on testnet (Somnia Testnet recommended).

    2. Attempt exploit using attacker contract.

    3. Observe vulnerability in action.

    You can successfully identify and exploit vulnerabilities in a controlled test environment and verify mitigations on secure contracts.


    hashtag
    Common Vulnerability Patterns

    Red flags in code review:

    • External calls before state updates

    • Missing access control modifiers

    • Unchecked arithmetic operations

    Security scanning tools:

    Tool
    Type
    Effectiveness
    Cost

    hashtag
    Additional Resources

    • - Smart Contract Weakness Classification


    ✅ 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.

    Critical

    Medium

    Low

    High

    Integer Overflow

    High

    Low

    Low

    Medium

    Vulnerable contract has 10 ETH from other users
  • Attacker deposits 1 ETH

  • 2

    hashtag
    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.

    3

    hashtag
    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.

    : Tracks if function is currently executing
  • nonReentrant Modifier:

    • Checks if function is already running (_status == _NOT_ENTERED)

    • Sets lock before execution (_status = _ENTERED)

    • Releases lock after completion (_status = _NOT_ENTERED)

  • 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

  • Only one withdrawal per transaction is possible

    Medium

    Pull Payment

    Users withdraw instead of push

    High

    Low

    OpenZeppelin ReentrancyGuard

    Battle-tested implementation

    Very High

    Medium

  • Legitimate user Alice has WRITER.

  • Attacker Bob has no roles.

  • Attacker compromises Alice or Alice becomes malicious.

    2

    hashtag
    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.

    3

    hashtag
    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.

    Role Hierarchy:
    • Owner > Admin > Writer

    • Only ADMIN can grant WRITER roles

    • WRITER cannot grant any roles

  • 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

  • Enable audit trails

    Emit on all role changes

    Zero Address Check

    Prevent accidental locks

    Validate addresses

    No access control on minting.

    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.

  • 2

    hashtag
    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.

    3

    hashtag
    Batch amplification

    • Attacker uses many entries with large but individually safe amounts.

    • Cumulative additions overflow totalSupply or balances.

    • Economic model breaks.

    Checks happen BEFORE arithmetic operations

  • Supply Cap Enforcement:

    • Actually enforces MAX_SUPPLY limit

    • Prevents unlimited token creation

  • Access Control:

    • Only owner can mint tokens

    • Prevents unauthorized token creation

  • Input Validation:

    • Checks for zero addresses and amounts

    • Batch size limits prevent gas attacks

  • Batch limits prevent gas-based attacks
    Input validation prevents edge cases

    Medium

    Manual Checks

    Any

    High

    Low

    Unchecked Blocks

    0.8+ (when safe)

    N/A

    Very Low

    Additional: Pull payments, limit gas forwarded, prefer transfer() for simple ETH sends
  • 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

  • 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

  • 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

  • Deploy secure version with protections.
  • Verify exploit fails against secure implementation.

  • Missing input validation
  • No event emissions for critical actions

  • Hardcoded addresses or values

  • Complex inheritance hierarchies

  • Missing reentrancy protection

  • Paid

    Securify

    Academic

    Medium

    Free

    Manticore

    Symbolic Execution

    High

    Free

    Reentrancy

    Critical

    High

    Medium

    Very High

    CEI Pattern

    Update state before external calls

    High

    Low

    Reentrancy Guard

    Mutex-style protection

    Role Hierarchy

    Prevent privilege escalation

    Owner > Admin > User

    Immutable Owner

    Prevent ownership takeover

    Set in constructor

    Built-in Protection

    0.8+

    Very High

    Low

    SafeMath Library

    <0.8

    Slither

    Static Analysis

    High

    Free

    MythX

    Comprehensive

    SWC Registryarrow-up-right
    Consensys Security Best Practicesarrow-up-right

    Access Control

    Very High

    Event Logging

    High

    Very High

    // 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;
        }
    }
    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);
            }
        }
    }
    // 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;
        }
    }
    // 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
    // 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];
        }
    }
    // 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];
        }
    }
    // 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];
        }
    }
    # Deploy vulnerable contract in Remix
    # Deploy attacker contract and execute attack
    # Observe behavior and balances
    # Deploy secure version and verify attack fails

    Protofire Price Feeds

    hashtag
    Somnia Mainnet Price Feeds

    Asset Pair

    OCR Aggregator

    Proxy (read‑only)

    hashtag
    Somnia Testnet Price Feeds

    Use any of these when deploying the PriceConsumer Smart Contract for your dApp.

    hashtag
    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.

    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.

    hashtag
    Prerequisites

    1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.

    2. Basic knowledge of React & Next.js.

    hashtag
    Solidity Contract (Chainlink Oracle Consumer)

    hashtag
    Code Breakdown

    Pulls in the AggregatorV3Interface from the Chainlink library. This interface allows interaction with Chainlink oracle contracts for Price Feeds.

    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.

    Instantiates the interface with the provided address, enabling function calls to the external oracle.

    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:

    This function extracts only the price and ignores the rest using commas. It returns the latest price as an int256.

    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.

    hashtag
    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:

    • 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

    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.

    hashtag
    Deploy with Hardhat

    Update ignition/modules/deploy.js:

    Deploy using:

    hashtag
    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.

    Then, install required dependencies.

    Add imports to the index.js file

    • 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).

    hashtag
    Create a Viem Client for Somnia

    Creates a blockchain client configured for Somnia Testnet using its RPC URL and allows reading smart contract data without needing a wallet or signer.

    hashtag
    Declare a variable for the deployed Smart Contracts for your Price Feed Addresses

    This will map token pairs to their corresponding Chainlink oracle contract addresses deployed on Somnia.

    Parse the ABI

    hashtag
    Set up the State

    The price state will store the formatted token price and selectedToken will track which token is selected from the dropdown (default: ETH).

    hashtag
    Fetch the Latest Price

    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.

    hashtag
    Fetch Price on Load & Every 10 Seconds

    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.

    hashtag
    Complete Code

    chevron-rightindex.jshashtag

    hashtag
    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.

    timeStamp: When the answer was last updated

  • answeredInRound: The round in which the answer was submitted

  • parseAbi: Parses the contract's ABI.
  • formatUnits: Converts big numbers (like token prices) to a human readable format.s.

  • USDC / USD

    ETH / USD

    BTC / USD

    Token Pair

    Contract Address

    USDC/USD

    ETH/USD

    BTC/USD

    protofire.ioarrow-up-right
    // 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();
        }
    }
    import `@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol
    constructor(address _priceFeed) { }
    priceFeed = AggregatorV3Interface(_priceFeed);
    function getLatestPrice() public view returns (int256) {
            (
                /* uint80 roundID */,
                int256 price,
                /* uint startedAt */,
                /* uint timeStamp */,
                /* uint80 answeredInRound */
            ) = priceFeed.latestRoundData();
            return price;
    }
    (uint80 roundId, int256 price, uint startedAt, uint timeStamp, uint80 answeredInRound)
    function getDecimals() public view returns (uint8) {
            return priceFeed.decimals();
        }
    (uint80 roundId, int256 answer, uint startedAt, uint timeStamp, uint80 answeredInRound)
    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;
    npx hardhat ignition deploy ./ignition/modules/Lock.js --network somnia
    npx create-next-app@latest somnia-protofire-example
    cd somnia-protofire-example
    npm install viem
    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 = { //Testnet Price 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 (
            ...
            <p>${price}</p>
            ...
            )
    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>
      );
    }
    0x4b74EcA574Ce996448b485100e4FFf84866911dFarrow-up-right
    0x843B6812E9Aa67b3773675d2836646BCbd216642arrow-up-right
    0xa3060dd6Bb56EdfB2E0d78c88ef63A974a392D36arrow-up-right
    0xeC25a820A6F194118ef8274216a7F225Da019526arrow-up-right
    0x3cBdF7F02956c8e946192Bff64bb2Dd470dd589Carrow-up-right
    0xa57d637618252669fD859B1F4C7bE6F52Bef67edarrow-up-right
    0xa2515C9480e62B510065917136B08F3f7ad743B4arrow-up-right
    0xd9132c1d762D432672493F640a63B758891B449earrow-up-right
    0x8CeE6c58b8CbD8afdEaF14e6fCA0876765e161fEarrow-up-right

    Solidity on-chain Reactivity Tutorial

    Create a smart contract that reacts to events from other contracts automatically

    This tutorial guides you through building on-chain reactivity on Somnia. You'll create a smart contract that reacts to events from other contracts automatically—invoked by chain validators. Subscriptions trigger handler logic directly in the EVM.

    hashtag
    Overview

    On-chain reactivity lets Solidity contracts "subscribe" to events emitted by other contracts. When an event fires, your handler contract gets called with the event data, enabling automated business logic like auto-swaps or updates. This is powered by Somnia's native push mechanism, funded by a minimum SOMI balance (currently 32 SOMI) held by the subscription owner.

    Key benefits:

    • Atomic: Event + state from the same block (state reads have to be handled by your own contract unlike off-chain subscriptions).

    • Decentralized: Runs on-chain without off-chain servers removing liveness assumptions.

    • Efficient: Pay only for invocations via gas params.

    Prerequisites:

    • Solidity basics (e.g., Remix, Hardhat, or Foundry).

    • Somnia testnet wallet with 32+ SOMI (faucet: ).

    • TypeScript SDK for subscription management (install: npm i @somnia-chain/reactivity) or manage the subscription directly with the precompile contract on-chain

    hashtag
    Key Objectives

    1. Create a SomniaEventHandler Contract: Inherit from the abstract contract and override _onEvent virtual function with your logic.

    2. Deploy the Handler: Use your tool of choice (e.g., Remix or Hardhat).

    3. Create a Subscription: Use the SDK to set up and fund the sub (owner must hold min SOMI) or setup the subscription in Solidity.

    hashtag
    Step 1: Install Dependencies

    Install the reactivity contracts package for the SomniaEventHandler abstract contract:

    This provides the interface to import. (Public Foundry repo coming soon for easier Forge integration.)

    hashtag
    Step 2: Create the Handler Contract

    Inherit from SomniaEventHandler and implement _onEvent. This is where your business logic goes—e.g., update state or call other contracts.

    Example: A simple handler that logs or reacts to any event (wildcard).

    • Customization: Filter in the subscription (Step 4) or add checks in _onEvent (e.g., if emitter == specificAddress).

    • Warnings: Keep gas usage low; handlers run in validator context. Test for reentrancy.

    hashtag
    Step 3: Deploy the Handler

    • Using Remix: Paste code, compile, deploy to Somnia testnet RPC ().

    • Using Hardhat: Set up a project, add the contract, and deploy script.

    Example Hardhat deploy script (scripts/deploy.ts):

    Run: npx hardhat run scripts/deploy.ts --network somniaTestnet (configure networks in hardhat.config.ts).

    Note the deployed address (e.g., 0x123...).

    hashtag
    Step 4: Create and Manage the Subscription

    Use the TypeScript SDK to create the subscription. The caller becomes the owner and must hold 32+ SOMI.

    • Funding: Chain enforces min SOMI; top up if needed.

    • Filters: See API reference for more filters that can be passed to createSoliditySubscription

    hashtag
    Step 5: Test the Callback

    1. Deploy an emitter contract that emits events (e.g., simple ERC20 with Transfer).

    2. Trigger an event (e.g., transfer tokens).

    3. Check your handler: Use explorers or logs to see _onEvent executed (e.g., your ReactedToEvent emitted).

    hashtag
    Troubleshooting

    • No Invocation? Verify sub ID (via sdk.getSubscriptionInfo), filters match, and balance funded.

    • Gas Errors? Increase gasLimit or optimize handler.

    • Cancel: await sdk.cancelSoliditySubscription(subId);

    hashtag
    Next Steps

    • Add filters for targeted reactivity.

    • Integrate with DeFi/NFT logic.

    • Explore hybrid: Off-chain monitoring + on-chain actions.

    Handle Callbacks: The chain invokes your handler on matching filters based on subscription configuration.

    (get ID from listing).
    https://docs.somnia.network/developer/network-infoarrow-up-right
    https://docs.somnia.network/developer/network-infoarrow-up-right
    npm i @somnia-chain/reactivity-contracts
    pragma solidity ^0.8.20;
    
    import { SomniaEventHandler } from "@somnia-chain/reactivity-contracts/contracts/SomniaEventHandler.sol";
    
    contract MyEventHandler is SomniaEventHandler {
    
        event ReactedToEvent(address emitter, bytes32 topic);
    
        function _onEvent(
            address emitter,
            bytes32[] calldata eventTopics,
            bytes calldata data
        ) internal override {
            // Your business logic here
            // Example: Emit a new event or update storage
            emit ReactedToEvent(emitter, eventTopics[0]);
    
            // Be cautious: Avoid reentrancy or infinite loops (e.g., don't emit events that trigger this handler)
        }
    }
    import { ethers } from "hardhat";
    
    async function main() {
      const Handler = await ethers.getContractFactory("MyEventHandler");
      const handler = await Handler.deploy();
      await handler.deployed();
      console.log("Handler deployed to:", handler.address);
    }
    
    main().catch((error) => {
      console.error(error);
      process.exitCode = 1;
    });
    import { SDK } from '@somnia-chain/reactivity';
    import { somniaTestnet } from 'viem/chains';
    import { privateKeyToAccount } from 'viem/accounts'; 
    import { createPublicClient, createWalletClient, http } from 'viem';
    
    // Initialize SDK with the required clients
    const sdk = new SDK({
      public: createPublicClient({
        chain: somniaTestnet,
        transport: http()
      }),
      wallet: createWalletClient({
          account: privateKeyToAccount(process.env.PRIVATE_KEY),
          chain: somniaTestnet,
          transport: http(),
      })
    });
    
    const subData = {
      handlerContractAddress: '0xYourDeployedHandlerAddress',
      priorityFeePerGas: parseGwei('2'),
      maxFeePerGas: parseGwei('10'),
      gasLimit: 2_000_000n, // Minimum recommended for state changes
      isGuaranteed: true, // Retry on failure
      isCoalesced: false, // One call per event
      // Optional filters: eventTopics: ['0x...'], emitter: '0xTargetContract'
    };
    
    const txHash = await sdk.createSoliditySubscription(subData);
    if (txHash instanceof Error) {
      console.error('Creation failed:', txHash.message);
    } else {
      console.log('Subscription created! Tx:', txHash);
    }