Only this pageAll pages
Powered by GitBook
1 of 59

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

Litepaper

Loading...

Loading...

Somnia Blockchain

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Ecosystem

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Removing the Somnia Devnet Network

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

To do this open the networks on metamask:

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

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

Update the block explorer in metamask

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

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

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

Here click on the down arrow on the block explorer URL and click add block explorer URL

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

It should look like this:

Have fun exploring the Testnet!

Network Info

The Somnia Testnet is LIVE. Share your feedback at help@virtualsociety.foundation

Network Details

Network
Somnia Testnet

Chain ID

50312

Block Explorer

Symbol

STT

RPC

Alternative Block Explorer

Faucet

Thirdweb Faucet

RPC Nodes: A Bridge to the Blockchain

What is an RPC Node?

An RPC (Remote Procedure Call) node is a gateway between decentralized applications (dApps) and blockchain networks. It acts as a bridge, enabling communication and data exchange between the two.

Key Functions of RPC Nodes:

  • Information Retrieval: Users can query RPC nodes to fetch data from the blockchain, such as transaction history, block information, and smart contract data.

  • Transaction Broadcasting: RPC nodes allow users to broadcast transactions to the network, which are then validated and added to the blockchain.

  • Smart Contract Execution: RPC nodes can be used to execute smart contract functions, enabling complex interactions and automation.

Why are RPC Nodes Important?

RPC nodes play a crucial role in the functioning of decentralized applications and the overall blockchain ecosystem. By providing a reliable and efficient way to interact with the blockchain, RPC nodes empower developers to build innovative dApps and enable users to participate in decentralized finance and other blockchain-based activities.

Connect your Wallet

The Somnia Testnet is LIVE. Share your feedback at help@virtualsociety.foundation

  1. First, Connect your Wallet. You may use any desired wallet or Metamask (preferred)

  1. Just the last step is to approve the Somnia testnet network in Metamask.

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.

In early MVPs, the Somnia blockchain achieved 1,000,000 TPS running over 100 nodes distributed globally. This TPS was ERC-20 transfers between several hundred thousand accounts. Our next step will be deploying Uniswap, measuring how many swaps per second our chain can do, and simulating a real-world NFT mint similar to the Otherside other deed mint. This actual user workload-style benchmarking is a true way to assess blockchain performance.

Somnia has four key innovations in blockchain architecture to achieve this performance level:

In the URL box type:

Or raise them in our

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

Add Somnia Testnet to your wallet through -

For lower latency RPCs and websocket connections, we partner with Ankr. .

Head over to the section to get started with building on Somnia Testnet!

Or raise them in our

You can create your from here. If you already have a wallet you may skip this step

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

Visit and Click on connect at the top right corner:

Click on Connect Metamask, to add into your wallet

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

- through compiled EVM bytecode.

- a faster, more predictable database for storing blockchain state.

- a proof-of-stake, partially synchronous BFT protocol inspired by .

- to deal with increased node-to-node data traffic due to the throughput.

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 fulfil its vision.

These docs have two parts. which will give you more information on the project and / for using the Somnia Devnet.

https://shannon-explorer.somnia.network/
Discord
Discord
https://testnet.somnia.network/
Go here to use Ankr RPCs
Tutorials
Discord
MetaMask Wallet
Discord
https://testnet.somnia.network/
https://testnet.somnia.network/
Discord
Accelerated Sequential Execution
IceDB
MultiStream consensus
Autobahn BFT
Advanced compression techniques
Improbable
MSquared
The Somnia Litepaper
guides
developer resources
https://shannon-explorer.somnia.network/
https://dream-rpc.somnia.network/
https://somnia-testnet.socialscan.io/
https://testnet.somnia.network/
https://thirdweb.com/somnia-shannon-testnet

Request STT Tokens & Try sending tokens to a Random address

The Somnia Testnet is very early, and there may be bugs. Any issues, please: Share your feedback at help@virtualsociety.foundation

  1. To Request for Somnia Testnet Token, Click on Get STT Tokens button.

  1. Click on "Get STT"

  1. You should have it in your wallet

  1. You can Select Somnia Testnet and See your tokens

Try Sending Tokens

  1. Click on Send Tokens

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

  1. Check our Activity Section in Metamask

Resources & Important Links

The The Somnia Testnet is LIVE. Share your feedback at help@virtualsociety.foundation

1
2
3
4

Faucet

Community

IDE

Solidity Resources

Toolkit

How to Deploy Your First Smart Contract to Somnia Network

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.

Pre-requisites:

  1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

Connect to Somnia Testnet

Create the Smart Contract

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

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.

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

Create and Deploy your ERC20 Smart Contract to Somnia Network

Pre-requisite

  1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

ERC-20 tokens provide functionality to

  • Transfer tokens

  • Allow others to transfer tokens on behalf of the token holder

It is important to note that ERC20 Smart Contracts are different from the Native Somnia Contracts, which are used to pay gas fees when transacting on Somnia. In the following steps, we will create an ERC20 Token by following the EIP Standard and also demonstrate the option to use a Library to create the ERC20 Token.

IERC-20

According to the EIP Standard, certain Smart Contract methods must be implemented adhering to the standard so that other Smart Contracts can interact with the deployed Smart Contracts and call the method. To achieve this, we will use the Solidity Interface Type to create the Smart Contract standard.

In Solidity, an interface is a special contract that defines a set of function signatures without implementation. It acts as a "blueprint" for other contracts, ensuring they adhere to a specific structure. Interfaces are crucial for creating standards, such as the ERC-20 token standards, allowing different contracts to interact seamlessly within the EVM ecosystem.

Create an Interface for the ERC20 Token. Copy and paste the code below into a file named IERC20.sol

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

Functions

transfer: Moves a specified amount of tokens from the sender to a recipient.

It checks if the sender has enough balance and deducts the amount from the sender's balance, and adds it to the recipient's. It also emits a Transfer event to log the transaction and returns `true` if the transfer is successful.

approve: It allows a spender to spend up to a certain amount of tokens on behalf of the owner. It sets the allowance for the spender to the specified amount. The spender is usually another Smart Contract. It then emits an Approval event to record the spender's allowance. It returns true if the approval is successful. A common use case is to enable spending through third-party contracts like decentralized exchanges (DEXs).

transferFrom: It allows a spender to transfer tokens on behalf of another account.

It checks if the sender has an approved allowance for the spender and ensures it is sufficient for the amount and deducts the amount from the allowance and the sender's balance and adds the amount to the recipient's balance. It also emits a Transfer event to log the transaction. It returns `true` if the transfer is successful. A common use case is DEXs or smart contracts to handle token transactions on behalf of users.

_mint: Creates new tokens and adds them to a specified account's balance.

Calling the method increases the recipient's balanceOf by the specified amount. It also increases the totalSupply of the ERC20 Tokens by the same amount. A transfer event with the from address as address(0) (indicating tokens are created).

_burn: Destroys a specified number of tokens from an account's balance.

It reduces the account's balanceOf and decreases the totalSupply by the amount. It emits a Transfer event with the to address as address(0) (indicating tokens are burned).

It is typically implemented in token-burning mechanisms to reduce supply, increasing scarcity.

mint: Public wrapper for _mint. It calls _mint to create new tokens for a specified to address. Allows the contract owner or authorized accounts to mint new tokens.

burn: Public wrapper for _burn. Calls _burn to destroy tokens from a specified from address.

Events

Transfer:

Logs token transfers, including minting and burning events. Parameters: from (sender), to (recipient), value (amount).

Approval:

Logs approvals of allowances for spenders. Parameters: owner, spender, value (allowance amount).

Compile Smart Contract

The ERC20 Token is now ready to be deployed to the Somnia Blockchain.

On the left tab, click the “Solidity Compiler” menu item and then the “ Compile ERC20.sol” button. This will compile the Solidity file and convert the Solidity code into machine-readable bytecode.

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


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.

Deploy a Smart Contract on Somnia Testnet using 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.

This guide will teach you how to deploy a “Voting” Smart Contract to the Somia Network using Foundry.

Pre-requisites:

  1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

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.

Create the Smart Contract

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

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:

Deploy Contract.

You will see a status response:

NOTE: Contract Verification on SomniaScan is COMING SOON!

Or raise them in our

To Proceed with this step, you should with Somnia Testnet

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

If something is not working while building on Somnia Testnet, its Probably us For any Feedback & Support - and share with us.

Or raise them in our

(Faucet, Blockexplorer, Connect to network)

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

- Click get STT, the native token of the Somnia Devnet

to chat with devs and raise issues

for updates

- Solidity IDE with interactive features

+

by Learn Web3

- Solidity development framework paired with a JavaScript testing framework

- Solidity framework for both development and testing.

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.

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

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 .

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 .

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 is an ERC-20 token; these Smart Contracts are often referred to as Token Contracts.

is a blazing fast, portable and modular toolkit for EVM application development written in Rust.

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 .

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

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

Deploying Smart Contracts to the Somnia Network is very straightforward. All you need the RPC URL and the Private Key froman 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:

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

// 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, "!"));
    }
}
// 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/contracts@4.0.0/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts@4.0.0/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts@4.0.0/access/Ownable.sol";


contract MyToken is ERC20, ERC20Burnable, Ownable {
    constructor(address initialOwner)
        ERC20("MyToken", "MTK")
        Ownable()
    {}


    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }
}
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
Discord
connect your wallet
Discord
Join our Discord Community
Discord
Somnia Testnet URL
Somnia Testnet Block Explorer
Join the Exclusive Developer Community
Apply for the Somnia Network Grants Program
Discord
Somnia Faucet
Join us on Discord
Follow us on Twitter
Remix
VS Code
Solidity Extension
Ethereum Developer
Solidity by Example
CryptoZombies
Cookbook.dev
Alchemy Unversity
Hardhat Toolkit
Foundry Toolkit
Connect Your Wallet
Remix
Get STT Tokens
Remix IDE
Connect Your Wallet
ERC-20 standard
Foundry
Connect Your Wallet
Guide
Foundry book
Faucet
guide
Explorer

Deploy and Verify A Smart Contract on Somnia using Hardhat

Pre-requisites

  1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

Initialise Hardhat Project

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

npx hardhat init

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.

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:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

contract BuyMeCoffee {
    event CoffeeBought(
        address indexed supporter,
        uint256 amount,
        string message,
        uint256 timestamp
    );

    address public owner;

    struct Contribution {
        address supporter;
        uint256 amount;
        string message;
        uint256 timestamp;
    }
    
    Contribution[] public contributions;

    constructor() {
        owner = msg.sender;
    }

    function buyCoffee(string memory message) external payable {
        require(msg.value > 0, "Amount must be greater than zero.");
        contributions.push(
            Contribution(msg.sender, msg.value, message, block.timestamp)
        );

        emit CoffeeBought(msg.sender, msg.value, message, block.timestamp);
    }

    function withdraw() external {
        require(msg.sender == owner, "Only the owner can withdraw funds.");
        payable(owner).transfer(address(this).balance);
    }

    function getContributions() external view returns (Contribution[] memory) {
        return contributions;
    }

    function setOwner(address newOwner) external {
        require(msg.sender == owner, "Only the owner can set a new owner.");
        owner = newOwner;
    }
}

Compile the Smart Contract

To compile your contracts, you need to customize the Solidity compiler options, open the hardhat.config.js file and ensure the Solidity version is 0.8.28 and then run the command:

npx hardhat compile

It will return the response:

Compiling...
Compiled 1 contract successfully

This will compile the Solidity file and convert the Solidity code into machine-readable bytecode. By default, the compiled artifacts will be saved in the newly created artifacts directory. The next step is to deploy the contracts to the Somnia Network. In Hardhat, deployments are defined through Ignition Modules. These modules are abstractions that describe a deployment, specifically, JavaScript functions that process the file you want to deploy. Open the ignition directory inside the project root's directory, then enter the directory named modules. Delete the Lock.ts file. Create a deploy.ts file and paste the following code:

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const BuyMeCoffee = buildModule("BuyMeCoffee", (m) => {
  const contract = m.contract("BuyMeCoffee");
  return { contract };
});

module.exports = BuyMeCoffee;

Deploy Contract

module.exports = {
  // ...
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};

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

npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia

You will see a confirmation message asking if you want to deploy to the Somnia Network. Answer by hitting “y” on your keyboard. This will confirm the deployment of the Smart Contract to the Somnia Network.

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

Verify Your Smart Contract

Update hardhat.config.jsAdd the following to your config file:

jsCopyEditmodule.exports = {
  solidity: "0.8.28",
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["YOUR_PRIVATE_KEY"],
    },
  },
  sourcify: {
    enabled: false,
  },
  etherscan: {
    apiKey: {
      somnia: "ETHERSCAN_API_KEY",
    },
    customChains: [
      {
        network: "somnia",
        chainId: 50312,
        urls: {
          apiURL: "https://shannon-explorer.somnia.network/api",
          browserURL: "https://shannon-explorer.somnia.network",
        },
      },
    ],
  },
};

Store your private key in a .env file and import it securely to avoid hardcoding.

After deploying your contract, run the Verify command. Copy the deployed address and run:

npx hardhat verify --network somnia DEPLOYED_CONTRACT_ADDRESS "ConstructorArgument1" ...

Example for a contract with one string constructor arg:

npx hardhat verify --network somnia 0xYourContractAddress "YourDeployerWalletAddress"

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.

Tutorials

Welcome to the Somnia Tutorials

The Tutorials section is designed to help developers quickly get started with building on Somnia. Whether deploying your first smart contract, exploring blockchain analytics, or using Ecosystem Partner resources, these hands-on guides will walk you through every step.

Each tutorial includes code samples, explanations, and troubleshooting tips to ensure a smooth learning experience. Start exploring and Build On Somnia

How to Connect to Somnia Network via Viem Library

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.

Example Smart Contract

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

Example ABI

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

ABI

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:

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.

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:

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.

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.

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

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

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 .

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

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.

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

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

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

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.

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.

Hardhat
Connect Your Wallet
Guide
Faucet
guide
Hardhat Verify plugin
Somnia Explorer
Somnia Explorer
Discord
// 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
Viem
https://gist.github.com/emmaodia/bdb9b84998b1e4f3f19d0ae27c541e63
https://dream-rpc.somnia.network

How to Setup MetaMask Authentication to Connect Somnia Network

Start a NextJS Project

Run the command below to start a NextJS project:

npx create-next-app metamask-example

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:

 <p>Hello, World!</p>

Install Viem

npm i viem

Import Methods

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

import { useState } from "react";
import {
  createPublicClient,
  http,
  createWalletClient,
} from "viem";

Import Somnia

Import Somnia Testnet

import { somniaTestnet } from "viem/chains";

Declare React States

State allows us to manage changing data in the User Interface. For this example application, we are going to manage two states:

  • When we can read the User's Address

  • When a User is connected (Authorization)

Add the states inside the export statement:

const [address, setAddress] = useState<string>("");
const [connected, setConnected] = useState(false);

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

Connect MetaMask Function

Add the function below inside the export statement:

const connectToMetaMask = async () => {
    if (typeof window !== "undefined" && window.ethereum !== undefined) {
      try {
        await window.ethereum.request({ method: "eth_requestAccounts" });
        const walletClient = createWalletClient({
          chain: SOMNIA,
          transport: custom(window.ethereum),
        });
        const [userAddress] = await walletClient.getAddresses();
        setClient(walletClient);
        setAddress(userAddress);
        setConnected(true);
        console.log("Connected account:", userAddress);
      } catch (error) {
        console.error("User denied account access:", error);
      }
    } else {
      console.log(
        "MetaMask is not installed or not running in a browser environment!"
      );
    }
  };

Update the UI

MetaMask connection is set up, and the final step is to test the connection via the User Interface. Update the <p>Hello, World!</p> in the return statement to the following:

{!connected ? (
        <button
          onClick={connectToMetaMask}
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
        >
          Connect Wallet
        </button>
      ) : (
        <div>
          <p>Connected as: {address}</p>
         </div>
      )}

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

npm run dev

How To Build A User Interface For DAO Smart Contract p1

  1. Initialize a Next.js project.

  2. Add a global NavBar in _app.js so it appears on every page.

You will have a basic skeleton of a DApp, ready for READ/WRITE operations and UI components —topics we’ll cover in the subsequent articles.


Pre-requisites:

  1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.

Create Your Next.js Project

To create a NextJS project, run the command:

npx create-next-app my-dapp-ui

Accept the prompts and change directory into the folder after the build is completed.

This gives you a minimal Next.js setup with a pages folder (holding your routes), a public folder (for static assets), and config files.

(Optional) Add Tailwind CSS

If you plan to style your app with Tailwind, install and configure it now:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Then edit your tailwind.config.js:

module.exports = {
  content: [
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Finally, include Tailwind in styles/globals.css:

@tailwind base;
@tailwind components;
@tailwind utilities;

Setting Up a React Context for Global State

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

walletcontext.js
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);
}

We parse three class methods from React: createContext, useContext, anduseState

useWallet() is a custom hook, so any page or component can do:

const { connected, ... } = useWallet()

to access the global wallet state i.e. any of the Wallet.Provider methods

connectToMetaMask() triggers the MetaMask connection flow.

WalletProvider manages State and Methods in the application.


Creating a Global NavBar in _app.js

Next.js automatically uses pages/_app.js to initialize every page. We will wrap the entire app in the WalletProvider inside _app.js and inject a NavBar menu that appears site-wide in the application

Create the _app.js and add the code:

import "../styles/globals.css";
import { WalletProvider } from "../contexts/walletcontext";
import NavBar from "../components/navbar";

function MyApp({ Component, pageProps }) {
  return (
    <WalletProvider>
      <NavBar />
      <main className="pt-16">
        <Component {...pageProps} />
      </main>
    </WalletProvider>
  );
}

export default MyApp;

<WalletProvider> wraps the entire <Component /> tree so that every page can share the same wallet state.

<NavBar /> is placed above <main>, so it’s visible on all pages. We give <main> a pt-16 to avoid content hiding behind a fixed navbar.

NavBar

Create a sub directory components and add a file called navbar.js Add the code:

import { useWallet } from "../contexts/walletcontext";
import Link from "next/link";

export default function NavBar() {
  const { connected, address, disconnectWallet } = useWallet();


  return (
    <nav className="fixed w-full bg-white shadow z-50">
      <div className="mx-auto max-w-7xl px-4 flex h-16 items-center justify-between">
        <Link href="/">
          <h1 className="text-xl font-bold text-blue-600">MyDAO</h1>
        </Link>
<div>
          {connected ? (
            <div className="flex items-center space-x-4 text-blue-500">
              <span>{address.slice(0, 6)}...{address.slice(-4)}</span>
              <button onClick={disconnectWallet} className="px-4 py-2 bg-red-500 text-white rounded">
                Logout
              </button>
            </div>
          ) : (
            <span className="text-gray-500">Not connected</span>
          )}
        </div>
      </div>
    </nav>
  );
}

It uses useWallet() to read the global connected and 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.


Test Your Setup

Start the dev server:

npm run dev

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.


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!

Build a Simple 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.

Example Use Case: DAO Implementation in Gaming

DAOs can be particularly impactful in gaming environments. Imagine a massive multiplayer online game (MMO) with a shared in-game economy. A DAO can be used to manage a treasury funded by player contributions, allowing players to propose and vote on game updates, community events, or rewards.

For example:

  1. In-Game Treasury Management: Players deposit some of their in-game earnings into a DAO treasury. Proposals for using these funds—such as hosting tournaments or funding new content—are created and voted on.

  2. Player-Driven Governance: Gamers vote on new features like maps, characters, or weapons, giving them a direct say in the game's evolution.

  3. Community Rewards: DAOs could allocate funds to reward top-performing players or teams, enhancing engagement and competition.

This decentralized approach ensures that game updates align with player interests, creating a more engaging and community-driven gaming experience.

Prerequisites

Before starting, ensure you have:

  1. This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

  2. To complete this guide, you will need MetaMask installed and the Somnia Network added to the list of Networks. If you have yet to install MetaMask, please follow this guide to Connect Your Wallet.

  3. You can deploy the Smart Contracts using our Hardhat or Foundry guides.

Overview of the DAO Contract

The provided DAO contract allows users to:

  1. Deposit funds to gain voting power.

  2. Create proposals.

  3. Vote on proposals.

  4. Execute proposals if they pass.

The key features of the contract include:

  • Proposal Struct: Stores details of proposals.

  • Voting Mechanism: Allows weighted voting based on deposited funds.

  • Execution Logic: Ensures proposals are executed only if approved.

Setting Up the Development Environment

Create the Smart Contract

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

DAO.sol
// 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);
    }
}

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

Mappings

Mappings are used to store structured data efficiently:

  1. proposals

mapping(uint256 => Proposal) public proposals;
  • Stores all proposals created in the DAO.

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

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

mapping(address => uint256) public votingPower;
  • Tracks the voting power of each address.

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

  1. hasVoted

mapping(uint256 => mapping(address => bool)) public hasVoted;
  • Tracks whether a specific address has voted on a specific proposal.

  • Prevents double voting.

Functions

The contract includes several key functions:

  1. Constructor

constructor() {
    owner = msg.sender;
}
  • Sets the deployer as the owner of the contract.

  1. deposit

function deposit() external payable {
    require(msg.value >= 0.001 ether, "Minimum deposit is 0.001 STT");
    votingPower[msg.sender] += msg.value;
}
  • Allows users to deposit STT Tokens to gain voting power.

  • Increases their votingPower by the amount deposited.

  1. createProposal

function createProposal(string calldata description) external {
    require(votingPower[msg.sender] > 0, "No voting power");
    proposals[totalProposals] = Proposal({
        description: description,
        deadline: block.timestamp + votingDuration,
        yesVotes: 0,
        noVotes: 0,
        executed: false,
        proposer: msg.sender
    });
    totalProposals++;
}    
  • Allows users with voting power to create new proposals.

  • Adds the proposal to the proposals mapping.

  1. vote

function vote(uint256 proposalId, bool support) external {
    Proposal storage proposal = proposals[proposalId];


    require(block.timestamp < proposal.deadline, "Voting has ended");
    require(!hasVoted[proposalId][msg.sender], "Already voted");
    require(votingPower[msg.sender] > 0, "No voting power");


    hasVoted[proposalId][msg.sender] = true;


    if (support) {
        proposal.yesVotes += votingPower[msg.sender];
    } else {
        proposal.noVotes += votingPower[msg.sender];
    }
}
  • Allows users to cast a vote on a proposal.

  • Updates the yesVotes or noVotes in the Proposal struct based on the user's choice.

  • Prevents double voting by using the hasVoted mapping.

  1. executeProposal

function executeProposal(uint256 proposalId) external {
    Proposal storage proposal = proposals[proposalId];

    require(block.timestamp >= proposal.deadline, "Voting still active");
    require(!proposal.executed, "Proposal already executed");
    require(proposal.yesVotes > proposal.noVotes, "Proposal did not pass");
    
    proposal.executed = true;
    payable(proposal.proposer).transfer(0.001 ether);
}
  • Executes a proposal if it passes (more yes votes than no votes).

  • Transfers a fixed amount of ETH to the proposer as an example of execution logic.

  • Ensures proposals cannot be executed multiple times.

Key Variables

  1. totalProposals

uint256 public totalProposals;
  • Tracks the total number of proposals created.

  1. votingDuration

uint256 public votingDuration = 10 minutes;
  • Sets the default duration for voting on proposals.

  1. owner

address public owner;
  • Stores the address of the contract owner.

  • Used for functions that require administrative control.

Understanding these components shows how the DAO enables decentralized governance while maintaining transparency and fairness.

Deploy the Smart Contract to Somnia

This is an example deployment script using Hardhat. Create a file in the /ignition/module folder and name it deploy.js

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

const dao = buildModule("DAO", (m) => {
  const contract = m.contract("DAO");
  return { contract };
});

module.exports = dao;

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

const config = {
  solidity: "0.8.28",
  networks: {
    somnia: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["YOUR_PRIVATE_KEY"],
    },
  },
};

Run the deployment script:

npx hardhat ignition deploy ./ignition/modules/deploy.ts --network somnia

Interacting with the Contract

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

1. Deposit Funds

Call the deposit function to gain voting power:

await dao.deposit({ value: ethers.utils.parseEther("0.001") });

2. Create a Proposal

Create a new proposal by calling createProposal:

await dao.createProposal("Fund development of new feature");

3. Vote on a Proposal

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

await dao.vote(0, true); // Vote ‘yes’ on proposal 0

4. Execute a Proposal

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

await dao.executeProposal(0);

Testing the Contract

Writing Tests

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

const { expect } = require("chai");
const { ethers } = require("hardhat");


describe("DAO", function () {
  let dao;
  let owner, addr1;


  beforeEach(async function () {
    const DAO = await ethers.getContractFactory("DAO");
    dao = await DAO.deploy();
    [owner, addr1] = await ethers.getSigners();
  });


  it("Should allow deposits and update voting power", async function () {
    await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
    expect(await dao.votingPower(addr1.address)).to.equal(ethers.utils.parseEther("0.001"));
  });


  it("Should allow proposal creation", async function () {
    await dao.connect(addr1).deposit({ value: ethers.utils.parseEther("0.001") });
    await dao.connect(addr1).createProposal("Test Proposal");
    const proposal = await dao.proposals(0);
    expect(proposal.description).to.equal("Test Proposal");
  });
});

Run the tests:

npx hardhat test

Enhance the DAO

You can expand this DAO contract by:

  1. Adding Governance Tokens: Reward participants with tokens for voting or executing proposals. Follow the ERC20 Token Guide here.

  2. Implementing Quorums: Require a minimum number of votes for proposals to pass.

  3. Flexible Voting Power: Allow dynamic voting power allocation.

Conclusion

This tutorial provided a foundational understanding of building and deploying a simple DAO on Somnia. Experiment with enhancements to create more complex governance structures. DAOs are a powerful tool for decentralized decision-making, and the possibilities for innovation are limitless!

How To Build A User Interface For DAO Smart Contract p2

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

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


Understand READ Operations

In decentralized applications (dApps), Read Operations involve fetching data from the blockchain without altering its state. In the example DAO Smart Contract, this is crucial for displaying dynamic information such as:

  • Total Number of Proposals: How many proposals have been created.

  • Proposal Details: Information about a specific proposal, including its description, votes, and execution status.

These operations are read-only and do not require the user to sign any transactions, making them free of gas costs.

We’ll use the viem library to interact with our smart contract and perform these READ operations.


Expand walletcontext.js for Read Operations

walletcontext.js is the central hub for managing wallet connections and interacting with your Smart Contract. We’ll add two primary READ functions:

  1. fetchTotalProposals(): Retrieves the total number of proposals created.

  2. fetchProposal(proposalId): Fetches details of a specific proposal by its ID.

Fetch Total Proposals

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

contexts/walletcontext.js

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.


Integrate Read Operations into Pages

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

Home Page

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

pages/index.js

Here we set the totalProposals state variable to store the fetched total number of proposals. The ConnectButton implements the MetaMask authentication. The useWallet hook parse the function from WalletContext.

The useEffect Hook is applied on component mount, fetchTotalProposals() is then called to retrieve the total number of proposals from the Smart Contract.

The page displays a loading message until totalProposals is fetched. Once fetched, it displays the total number of proposals. Users will have to click the ConnectButton to connect their wallets for WRITE operations. See part 3.


Fetch-Proposal Page

This page allow users to input a proposal ID, fetch its details, and display them. Additionally, on the page users are provided options for voting on or executing the proposal.

Implementation:

pages/fetch-proposal.js

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.


Edge Cases and Errors

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

Example:

pages/fetch-proposal.js


Test Read Operations

Populate Some Data

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

  1. Deposit 0.001 ETH to gain voting power.

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

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.


Conclusion and Next Steps

In Part 2, you successfully implemented Read Operations in your DAO front end:

  • fetchTotalProposals(): Displayed the total number of proposals on the Home page.

  • fetchProposal(proposalId): Retrieved and displayed specific proposal details on the Fetch-Proposal page.

What's Next?

Stay tuned for Part 3 of this series, where we’ll dive into building UI Components—crafting forms, buttons, and enhancing event handling to create a more polished and user-friendly interface for your DAO dApp.


Congratulations! You’ve now built a robust foundation for reading data from your DAO smart contract within your Next.js front end. Keep experimenting and enhancing your dApp’s capabilities in the upcoming sections!

How To Build A User Interface For DAO Smart Contract p3

  1. Understand the Write Operations necessary for the DAO.

  2. Integrate these operations into your Next.js pages with intuitive UI components.

  3. Handle transaction states and provide user feedback.


Overview of Write Operations

Write Operations in a DAO involve actions that modify the blockchain state. These include:

  • Depositing Funds: Adding 0.001 STT to the DAO to gain voting power.

  • Creating Proposals: Submitting new proposals for the DAO to consider.

  • Voting on Proposals: Casting votes (Yes/No) on existing proposals.

  • Executing Proposals: Finalizing and implementing approved proposals.

These operations require users to sign transactions, incurring gas fees. Proper handling of these interactions is crucial for a smooth user experience.


Expand WalletContext with Write Functions

We’ll enhance the existing WalletContext by adding functions to handle the aforementioned write operations. This centralized approach ensures that all blockchain interactions are managed consistently.

Implement deposit

Allows users to deposit a fixed amount of ETH (e.g., 0.001 ETH) into the DAO contract to gain voting power.

contexts/walletContext.js

parseEther("0.001"): Converts 0.001 STT to Wei, the smallest denomination of Ether.

writeContract: Sends a transaction to call the deposit function on the DAO contract, transferring 0.001 STT.

Implement createProposal

Allows users to create a new proposal by submitting a description.

contexts/walletContext.js

createProposal(description): Takes a proposal description as an argument and sends a transaction to the DAO contract to create the proposal.

Implement voteOnProposal

Allows users to vote on a specific proposal by its ID, supporting either a Yes or No vote.

contexts/walletContext.js

voteOnProposal(proposalId, support): Takes a proposal ID and a boolean indicating support (true for Yes, false for No). Sends a transaction to cast the vote.

Implement executeProposal

Allows users to execute a proposal if it meets the necessary conditions (e.g., quorum reached).

contexts/walletContext.js

executeProposal(proposalId): Takes a proposal ID and sends a transaction to execute the proposal.


Integrate Write Operations

With the write functions added to WalletContext, the next step is to integrate these operations into your Next.js pages, providing users with interactive UI components to perform actions.

Create-Proposal Page

Allow users to submit new proposals by entering a description.

pages/create-proposal.js

State Variables:

  • description: Stores the user's input for the proposal description.

  • loading: Indicates whether the submission is in progress.

  • success & error: Handle user feedback messages.

The handleSubmit function undergoes validation, ensuring that the user is connected and has entered a description. It then calls the createProposal from WalletContext. It displays success or error messages based on the outcome.

The return statement contains the UI Components:

  • Label & TextInput: For user input.

  • Button: Triggers the submission. Disabled and shows a loading state when processing.

  • Alert: Provides visual feedback for success and error messages.

Fetch-Proposal Page: Vote and Execution

Allow users to fetch proposal details, vote on them, and execute if eligible.

pages/fetch-proposal.js

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.


Transaction States and User Feedback

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.


Test Write Operations

Thorough testing ensures the reliability and trustworthiness of your dApp. Here's how to effectively test your write operations:

Connect to a Test Network

Run your application using the command:

Your application will be running on localhost:3000 in your web browser.

Perform Write Operations

Deposit Funds:

  • Navigate to the Home page.

  • Click the Deposit button.

  • Confirm the transaction in MetaMask.

  • Verify that the deposit is reflected in the contract's state.

Create a Proposal:

  • Go to the Create Proposal page.

  • Enter a proposal description and submit.

  • Confirm the transaction in MetaMask.

  • Check that the proposal count increments and the new proposal is retrievable.

Vote on a Proposal:

  • Access the Fetch-Proposal page.

  • Enter a valid proposal ID and fetch details.

  • Click Vote YES or Vote NO.

  • Confirm the transaction in MetaMask.

  • Verify that vote counts update accordingly.

Execute a Proposal:

  • After a proposal meets the execution deadline, execute it.

  • Confirm the transaction in MetaMask.

  • Ensure that the proposal's execution status is updated.

  • Monitor the browser console for any errors or logs that aid in debugging.


Conclusion and Next Steps

In Part 3, you successfully implemented Write Operations in your DAO front end:

  • deposit: Allowed users to deposit ETH into the DAO.

  • createProposal: Enabled users to submit new proposals.

  • voteOnProposal: Provided functionality to cast votes on proposals.

  • executeProposal: Facilitated the execution of approved proposals.


Congratulations! Using Next.js and React Context, you’ve built a fully functional set of Write Operations for your DAO’s front end. This foundation empowers users to interact with your DAO seamlessly, fostering a decentralized, community-driven governance model.

Continue refining and expanding your dApp to cater to your community’s evolving needs.

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

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.

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.

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

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 using Next.js and React Context. It is divided into three parts. At the end of this guide, you’ll learn how to:

Set up a global state using the Context API ( hook).

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 .

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.

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.

Follow the or guides.

Follow the or guides. First, compile the Smart Contract to Bytecode by running the Hardhat or Foundry compile instructions.

Ensure that the deploying address has enough STT Tokens. You can get STT Tokens from the .

Congratulations. You have successfully deployed the DAO Smart Contract.

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:

Understand how to read data from your smart contract using .

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

Load your Smart Contract on the .

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:

Implement these operations within the existing .

Prerequisite: Ensure you’ve completed of this series, where you set up the WalletContext for global state management and added a global NavBar.

Clear feedback during and after transactions enhances user experience and trust in your dApp. Consider using libraries like for non-intrusive notifications. Example with Toast Notifications:

Obtain STT from the

🎉
🎉
MetaMask
Viem
https://dream-rpc.somnia.network
here
DAO Smart Contract
useContext
Connect Your Wallet
Context
context
Hardhat
Foundry
Hardhat
Foundry
Faucet
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
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

Using Native Somnia Token (STT)

STT is the native token of the Somnia Network, similar to ETH on Ethereum. Unlike ERC20 tokens, STT is built into the protocol itself and does not have a contract address.

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

  • Payments

  • Escrow

  • Donations & Tipping

  • Sponsored gas via Account Abstraction

Use STT for Payments in Smart Contracts

A simple contract that accepts exact STT payments:

function payToAccess() external payable {
  require(msg.value == 0.01 ether, "Must send exactly 0.01 STT");
}

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

To withdraw collected STT:

function withdraw() external onlyOwner {
  payable(owner).transfer(address(this).balance);
}

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

Example.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract STTPayment {
    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 STT to access this feature
    function payToAccess() external payable {
        require(msg.value == 0.01 ether, "Must send exactly 0.01 STT");

        // Logic for access: mint token, grant download, emit event, etc.
    }

    // Withdraw collected STT to owner
    function withdraw() external onlyOwner {
        payable(owner).transfer(address(this).balance);
    }
}

Build an STT Escrow Contract

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

constructor(address payable _seller) payable {
  buyer = msg.sender;
  seller = _seller;
  amount = msg.value;
}

Release funds to the seller:

function release() external onlyBuyer {
  seller.transfer(amount);
}

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

Example.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract STTEscrow {
    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 STT");
        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);
    }
}

STT Tip Jar

Allow any wallet to send tips directly:

receive() external payable {
  emit Tipped(msg.sender, msg.value);
}

Withdraw all tips:

function withdraw() external onlyOwner {
  payable(owner).transfer(address(this).balance);
}
Example.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract STTTipJar {
    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);
    }
}

Frontend tip

await walletClient.sendTransaction({
  to: '0xTipJarAddress',
  value: parseEther('0.05'),
});

Sponsor STT Transactions with Account Abstraction

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

await sendTransaction({
  to: contractAddress,
  data: mintFunctionEncoded,
  value: 0n, // user sends no STT
});

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

Conclusion

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

  • There is no contract address for STT.

  • You can integrate STT into any Solidity or Viem app with no ERC20 logic.

  • Account Abstraction enables gasless dApps using STT as sponsor currency.

DAO Smart Contract
viem
Part 1
Remix IDE
DAO Smart Contract
WalletContext
Part 2
react-toastify
Faucet.

How to deploy Smart Contracts to Somnia using Thirdweb

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.

Prerequisites

  • This guide is not an introduction to Solidity Programming; you are expected to understand Basic Solidity Programming.

  • To complete this guide, you will need MetaMask installed and the Somnia DevNet added to the list of Networks. If you have yet to install MetaMask, please follow this guide to Connect Your Wallet.

  • Thirdweb CLI: Install globally with:

npm thirdweb install

Set Up the Project

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

mkdir somnia-thirdweb-example
cd somnia-thirdweb-example

Write the Smart Contract

OpenGreeter.sol
// 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, "!"));
    }
}

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.

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:

npx thirdweb deploy -k your_secret_key

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

Partners

Welcome to Partner Tutorials

The Partner Tutorials section provides step-by-step guides on integrating Somnia with leading Web3 tools and services. Across various Infrastructure providers, these tutorials help you leverage partner ecosystems to build scalable and efficient blockchain applications.

Whether deploying dApps, integrating wallets, or optimizing smart contract interactions, these guides will ensure a smooth development experience. Explore the best tools to enhance your build and Build On Somnia!

Integrate ConnectKit with Somnia in a Next.js Application

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. Familiarity with React and Next.js is assumed.

Create the Next.js Project

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

npx create-next-app@latest somnia-connectkit
cd somnia-connectkit

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

npm install wagmi viem @tanstack/react-query connectkit

Set Up Providers in Next.js

We'll set up several providers to manage the application's state and facilitate interactions with the blockchain.

Create a components directory in the app folder. Inside the components directory, create a file named ClientProvider.tsx with the following content:

'use client';

import { WagmiConfig, createConfig } from 'wagmi';
import { ConnectKitProvider, getDefaultConfig } from 'connectkit';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { somniaTestnet } from 'viem/chains';


const queryClient = new QueryClient();


const config = createConfig(
  getDefaultConfig({
    autoConnect: true,
    appName: 'Somnia DApp',
    chains: [somniaTestnet],
  })
);

export default function ClientProvider({ children }) {
  return (
    <WagmiConfig config={config}>
      <QueryClientProvider client={queryClient}>
        <ConnectKitProvider>{children}</ConnectKitProvider>
      </QueryClientProvider>
    </WagmiConfig>
  );
}

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

import ClientProvider from './components/ClientProvider';
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>Somnia DApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>
        <ClientProvider>{children}</ClientProvider>
      </body>
    </html>
  );
}

Build the Home Page

We'll create a simple home page that allows users to connect their wallets and displays their address upon connection.

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

'use client';

import { useAccount } from 'wagmi';
import { ConnectKitButton } from 'connectkit';

export default function Home() {
  const { address, isConnected } = useAccount();
return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        Hello, world!
        {/* Connect Button */}
        <div className="mt-4">
          <ConnectKitButton />
        </div>
        {/* Show Wallet Address */}
        {isConnected && (
          <p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
        )}
      </main>
    </div>
  );
}

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

npm run dev

Open your browser and navigate to http://localhost:3000. You should see the ConnectKit button, allowing users to connect their wallets to the Somnia network.

Conclusion

You've successfully integrated ConnectKit with the Somnia Network in a Next.js application. This setup provides a foundation for building decentralized applications on Somnia, enabling seamless wallet connections and interactions with the Somnia Network.

For further exploration, consider adding features such as interacting with smart contracts, displaying user balances, or implementing transaction functionalities.

Integrating RainbowKit with Somnia in a Next.js Application

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. Familiarity with React and Next.js is assumed.

Create the Next.js Project

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

npx create-next-app@latest somnia-rainbowkit
cd somnia-rainbowkit

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

npm install wagmi viem @tanstack/react-query rainbowkit

Set Up Providers in Next.js

We'll set up several providers to manage the application's state and facilitate interactions with the blockchain.

Create a components directory in the app folder. Inside the components directory, create a file named ClientProvider.tsx with the following content:

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

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

import ClientProvider from './components/ClientProvider';


export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <title>Somnia DApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
      </head>
      <body>
        <ClientProvider>{children}</ClientProvider>
      </body>
    </html>
  );
}

Build the Home Page

We'll create a simple home page that allows users to connect their wallets and displays their address upon connection.

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

'use client';

import { useAccount } from 'wagmi';
import { ConnectButton } from "@rainbow-me/rainbowkit";


export default function Home() {
  const { address, isConnected } = useAccount();


  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <ConnectButton />
        {isConnected && (
          <p className="mt-4 text-lg text-blue-600">Connected as: {address}</p>
        )}
      </main>
    </div>
  );
}

Run the Application

Start the Development Server by running the following command:

npm run dev

Open your browser and navigate to http://localhost:3000. You should see the RainbowKit button, allowing users to connect their wallets to the Somnia network.

Conclusion

You've successfully integrated RainbowKit with the Somnia Network in a Next.js application. This setup provides a foundation for building decentralized applications on Somnia, enabling seamless wallet connections and interactions with the Somnia Network.

For further exploration, consider adding features such as interacting with smart contracts, displaying user balances, or implementing transaction functionalities.

Indexing Data on Somnia using Graph Services

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.

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.

npm install -g @graphprotocol/graph-cli

Deploy a Simple ERC20 Token on Somnia

We will deploy a basic ERC20 token on the Somnia network using Hardhat. Ensure you have Hardhat, OpenZeppelin, and dotenv installed:

npm install --save-dev hardhat @nomicfoundation/hardhat-ignition-ethers @openzeppelin/contracts dotenv ethers

Create an ERC20 Token Contract

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

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.25;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply * 10**decimals());
    }
function mint(address to, uint256 amount) external {
        _mint(to, amount);
    }
function burn(uint256 amount) external {
        _burn(msg.sender, amount);
    }
}

Create a Deployment Script

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

import { buildModule } from "@nomicfoundation/hardhat-ignition/modules";

export default buildModule("MyTokenModule", (m) => {
    const initialSupply = m.getParameter("initialSupply", 1000000n * 10n ** 18n);
    
    const myToken = m.contract("MyToken", [initialSupply]);
    return { myToken };
});

Deploy the Smart Contract

Open the hardhat.config.js file and update the network information by adding Somnia Network to the list of networks. Copy your Wallet Address Private Key from MetaMask, and add it to the accounts section. Ensure there are enough STT Token in the Wallet Address to pay for Gas. You can get some from the Somnia Faucet.

module.exports = {
  // ...
  networks: {
    somniaTestnet: {
      url: "https://dream-rpc.somnia.network",
      accounts: ["0xPRIVATE_KEY"], // put dev menomonic or PK here,
    },
   },
  // ...
};

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

npx hardhat ignition deploy ./ignition/modules/MyTokenModule.ts --network somniaTestnet

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

Simulate On-Chain Activity

Once deployed, we will create a script to generate multiple transactions on the blockchain.

Create a new file scripts/interact.js

interact.js

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

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

SOMNIA_RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY_1=0x...
PRIVATE_KEY_2=0x...
PRIVATE_KEY_3=0x...
PRIVATE_KEY_4=0x...
PRIVATE_KEY_5=0x...

Run the Script

node scripts/interact.js

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

Deploy a Subgraph on Somnia

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

Initialize the Subgraph

graph init --contract-name MyToken --from-contract 0xYourTokenAddress --network somnia-testnet mytoken

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

{
  "somnia-testnet": {
    "network": "Somnia Testnet",
    "rpc": "https://dream-rpc.somnia.network",
    "startBlock": 12345678
  }
}

Define the Subgraph Schema

📁 Edit schema.graphql

type Transfer @entity(immutable: true) {
  id: Bytes!
  from: Bytes!
  to: Bytes!
  value: BigInt!
  blockNumber: BigInt!
  blockTimestamp: BigInt!
  transactionHash: Bytes!
}

Build the Subgraph

graph codegen
graph build

Deploy the Subgraph

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

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.

Fetch Latest Transfers

{
  transfers(first: 10, orderBy: blockTimestamp, orderDirection: desc) {
    id
    from
    to
    value
    blockTimestamp
    transactionHash
  }
}

Get Transfers by Address

{
  transfers(where: { from: "0xUserWalletAddress" }) {
    id
    from
    to
    value
  }
}

Get Transfers in a Time Range

{
  transfers(where: { blockTimestamp_gte: "1700000000", blockTimestamp_lte: "1710000000" }) {
    id
    from
    to
    value
  }
}

Conclusion

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

Integrating DIA Oracles on Somnia

Overview

Oracle Details

Contracts on Somnia Testnet

DIA Oracle contract address:

Oracle Configuration

  • Pricing Methodology: MAIR

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

  • Refresh Frequency: Every 120 seconds

  • Heartbeat: Forced price update every 24 hours

Supported Asset Feeds

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.

Using the Solidity Library

DIA has a dedicated Solidity library to facilitate the integration of DIA oracles in your own contracts. The library consists of two functions, getPrice and getPriceIfNotOlderThan.

Access the library

getPrice

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

getPriceIfNotOlderThan

Checks if the oracle price is older than maxTimePassed

Using DIAOracleV2 Interface

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

Glossary

Support

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

Deploy a Subgraph on Somnia using Ormi

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.

Prerequisites

  • GraphQL is installed and set up on your local machine.

  • A verified smart contract address deployed on Somnia.

  • An Ormi account and Private Key.

Install Graph CLI globally

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

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.

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.

Deploy Using Ormi

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:

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.

Build a Live Crypto Price dApp Using Protofire Oracle

Available Price Feed Contracts on Somnia Testnet

Use any of these when deploying the PriceConsumer Smart Contract for your dApp.

What Are Oracles and Why Do They Matter

Oracles are critical infrastructure in the blockchain ecosystem that enable Smart Contracts to interact with real-world data. Blockchains are deterministic by design and cannot fetch off-chain information directly. This creates a need for oracles, which are trusted data feeds that can push external data, like market prices, sports scores, or weather conditions, into Smart Contracts.

Chainlink is the most widely adopted decentralized oracle network. It allows developers to access reliable data feeds that are resistant to manipulation and downtime. In this tutorial, we focus on Protofire Chainlink Price Feeds, which provide real-time market prices for assets like ETH, BTC, and USDC on Somnia

Smart Contracts that rely on accurate pricing (e.g., lending, trading, insurance) benefit immensely from using decentralized oracles like Protofire. Oracles unlock use cases that were previously impossible due to blockchain isolation.

In this guide, we will build a live crypto price tracker that displays BTC/USD, ETH/USD, and USDC/USD using Protofire Chainlink Price Feeds deployed on the Somnia Testnet.

Prerequisites

  1. This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.

  2. Basic knowledge of React & Next.js.

Solidity Contract (Chainlink Oracle Consumer)

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.

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

  • timeStamp: When the answer was last updated

  • answeredInRound: The round in which the answer was submitted

Most price consumer contracts use only price, but for more robust designs, timeStamp can be checked to ensure the price is recent.

Additionally, Protofire Chainlink Price Feeds often return prices with 8 decimals. This means the raw price value needs to be normalized by dividing it by 10 ** decimals. This is essential because Solidity doesn't support floating-point math. Prices are represented using fixed-point math.

For example, if ETH/USD is $1,940.82 and the feed uses 8 decimals, the returned price would be 194082000000. You must divide by 10 ** 8 to display $1940.82 in your UI.

Always use the getDecimals() method provided by the Aggregator interface to dynamically adjust for different feeds that may use 6, 8, or 18 decimals depending on the asset.

Deploy with Hardhat

Update ignition/modules/deploy.js:

Deploy using:

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

  • parseAbi: Parses the contract's ABI.

  • formatUnits: Converts big numbers (like token prices) to a human readable format.s.

Create a Viem Client for Somnia

Creates a blockchain client configured for Somnia Testnet using its RPC URL and allows reading smart contract data without needing a wallet or signer.

Declare a variable for the deployed Smart Contracts for your Price Feed Addresses

This will map token pairs to their corresponding Chainlink oracle contract addresses deployed on Somnia.

Parse the ABI

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

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.

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.

Complete Code

index.js

Conclusion

The Protofire Oracle integration on Somnia provides developers with reliable, on-chain price feeds for key assets like ETH, BTC, and USDC. By using verified oracles and standardized data formats, it enables accurate, real-time pricing essential for building GaemFi, DeFi, trading, and financial applications.

Using Privy Wallet on Somnia

Prerequisites

  • This guide is not an introduction to JavaScript Programming; you are expected to understand JavaScript.

  • Familiarity with React and Next.js is assumed.

Installation

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

Set Up PrivyProvider

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:

Privy Hooks

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

Authenticate

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

page.tsx

Send Transactions

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

Complete Code

Complete page.tsx code

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.

Build a NextJS UI for Subgraphs on Somnia

  • Fetch blockchain data from a Subgraph API

  • Fix CORS errors using a NextJS API route

  • Display token transfers in a real-time UI

By the end of this guide, you'll have a fully functional UI that fetches and displays token transfers from Somnia’s Subgraph API.

Prerequisites

  • Basic knowledge of React & Next.js.

Create a NextJS Project

Start by creating a new Next.js app.

Then, install required dependencies.

Define the Subgraph API in Environment Variables

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

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

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.

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

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.

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

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.

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

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

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.

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 .

If you encounter any issues or need assistance, join the.

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.

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 .

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

If you encounter any issues or need assistance, join the.

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.

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

This section demonstrates how to write and execute GraphQL queries to fetch blockchain data indexed by the subgraph. Go to

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.

Asset
Adapter Address

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 .

Term
Definition

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

This tutorial demonstrates how to deploy a subgraph on the Somnia Testnet using , a powerful gateway that simplifies subgraph deployment through a hosted Graph Node and IPFS infrastructure.

Open the Somnia Ormi website and create an account.

For more information, visit the Ormi .

This service is powered by Protofire, an infrastructure provider that integrates decentralized oracle networks for Somnia. Learn more at.

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.

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

Go to to set up an account.

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:

A deployed Subgraph API on Somnia ().

Account on see .

Thirdweb
Remix IDE
Discord
ConnectKit
Connect Your Wallet guide
Somnia Developer Discord
RainbowKit
Connect Your Wallet guide
here
Somnia Developer Discord
https://somnia.chain.love/
Somnia Protofire Service
https://somnia.chain.love/
https://somnia.chain.love/graph/17
0x9206296Ea3aEE3E6bdC07F7AaeF14DfCf33d865D

USDT

0x67d2C2a87A17b7267a6DBb1A59575C0E9A1D1c3e

USDC

0x235266D5ca6f19F134421C49834C108b32C2124e

BTC

0x4803db1ca3A1DA49c3DB991e1c390321c20e1f21

ARB

0x74952812B6a9e4f826b2969C6D189c4425CBc19B

SOL

0xD5Ea6C434582F827303423dA21729bEa4F87D519

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

Deviation

Percentage threshold that triggers a price update when exceeded.

Refresh Frequency

Time interval for checking and updating prices if conditions are met.

Trade Window

Time interval used to aggregate trades for price calculation.

Heartbeat

Forced price update at a fixed interval.

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

Token Pair

Contract Address

USDC/USD

0xa2515C9480e62B510065917136B08F3f7ad743B4

ETH/USD

0xd9132c1d762D432672493F640a63B758891B449e

BTC/USD

0x8CeE6c58b8CbD8afdEaF14e6fCA0876765e161fE

// 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 = {
  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>
  );
}
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';
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>
        )}
        
}
 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>
'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-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
DIA
here
Full Example on DIA Docs
official documentation
Discord
Ormi
https://subgraph.somnia.network/
docs
protofire.io
Privy
here
Privy
https://dashboard.privy.io/
Subgraphs
or use an existing one
https://somnia.chain.love
guide

How to Build a Token Balance dApp on Somnia Network

The Somnia mission is to enable the building of mass-consumer real-time applications. As a Developer, you need to understand how to interact with onchain data to build UIs. This guide will teach you how to build a Token Balance dApp that fetches and displays ERC20 token balances from the Somnia Network using Next.js and the Ormi Data APIs.

Prerequisites

To complete this guide, you will need:

  • Basic understanding of React and TypeScript

What is Ormi Data API?

API Base URL

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

https://api.subgraph.somnia.network/public_api/data_api

API Endpoints

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

/somnia/v1/address/{walletAddress}/balance/erc20

Where:

  • somnia - The network identifier

  • v1 - API version

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

  • balance/erc20 - Specifies that you want ERC-20 token balances

Authentication

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

Authorization: Bearer YOUR_API_KEY

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

Example API Request

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

curl -X GET "https://api.subgraph.somnia.network/public_api/data_api/somnia/v1/address/0xYOUR_WALLET_ADDRESS/balance/erc20" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json"

Set up the Project

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

npx create-next-app@latest somnia-balance-demo --typescript --tailwind --app
cd somnia-balance-demo

Create the Type Definitions

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

'use client'
import { useState, FormEvent } from 'react'
// Type definitions for the API response
interface TokenBalance {
  balance: string
  contract: {
    address: string
    decimals: number
    erc_type: string
    logoUri: string | null
    name: string
    symbol: string
  }
  raw_balance: string
}

interface BalanceResponse {
  erc20TokenBalances: TokenBalance[]
  resultCount: number
}

Build the User Interface

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

export default function Home() {
  const [walletAddress, setWalletAddress] = useState<string>('')
  const [loading, setLoading] = useState<boolean>(false)
  const [data, setData] = useState<BalanceResponse | null>(null)
  const [error, setError] = useState<string>('')

  return (
    <main className="min-h-screen bg-white p-8">
      <div className="max-w-6xl mx-auto">
        <h1 className="text-3xl font-bold mb-8">Somnia Network Balance Demo</h1>
        
        <form className="mb-8">
          <div className="flex gap-4">
            <input
              type="text"
              value={walletAddress}
              onChange={(e) => setWalletAddress(e.target.value)}
              placeholder="Enter wallet address (0x...)"
              className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
            />
            <button
              type="submit"
              disabled={loading}
              className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
            >
              {loading ? 'Loading...' : 'Fetch Balance'}
            </button>
          </div>
        </form>
      </div>
    </main>
  )
}

Create a .env file

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

PRIVATE_KEY=YOUR_API_KEY_HERE

Create the API Route

To avoid CORS issues and keep your API key secure, we'll create an API route. Create a directory and a new file app/api/balance/route.ts:

import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  try {
    const searchParams = request.nextUrl.searchParams;
    const walletAddress = searchParams.get('address');

    if (!walletAddress) {
      return NextResponse.json(
        { error: 'Wallet address is required' },
        { status: 400 }
      )
    }

    const apiKey = process.env.PRIVATE_KEY 
    const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api'

    const response = await fetch(
      `${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
      {
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json',
          'Accept': 'application/json',
        },
      }
    )

    const data = await response.json()

    if (!response.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch data from Ormi API', details: data },
        { status: response.status }
      )
    }
    
    return NextResponse.json(data)
  } catch (error) {
    console.error('API Error:', error)
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    )
  }
}

Important: Replace YOUR_API_KEY_HERE with your actual Ormi API key.

Implement the Fetch Function

Add the fetch function to handle form submission. Update your app/page.tsx:

const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
  e.preventDefault()
  
  if (!walletAddress) {
    setError('Please enter a wallet address')
    return
  }

  setLoading(true)
  setError('')
  setData(null)

  try {
    const response = await fetch(`/api/balance?address=${walletAddress}`, {
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ walletAddress }),
    })

    const result = await response.json()

    if (!response.ok) {
      throw new Error(result.error || 'Failed to fetch balance')
    }

    setData(result)
  } catch (err) {
    setError(err instanceof Error ? err.message : 'An error occurred')
  } finally {
    setLoading(false)
  }
}

Don't forget to add the onSubmit handler to your form:

<form onSubmit={fetchBalance} className="mb-8">

Display the Results

Add error handling and a table to display the token balances. Add this code after your form in app/page.tsx:

{error && (
  <div className="p-4 mb-4 bg-red-50 border border-red-200 rounded-md">
    <p className="text-red-600">{error}</p>
  </div>
)}

{data && data.erc20TokenBalances.length > 0 && (
  <div className="bg-white rounded-lg shadow overflow-hidden">
    <div className="px-6 py-4 bg-gray-50 border-b">
      <h2 className="text-xl font-semibold">Token Balances ({data.resultCount} tokens)</h2>
    </div>
    <div className="overflow-x-auto">
      <table className="min-w-full divide-y divide-gray-200">
        <thead className="bg-gray-50">
          <tr>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Name
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Symbol
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Balance
            </th>
            <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
              Contract Address
            </th>
          </tr>
        </thead>
        <tbody className="bg-white divide-y divide-gray-200">
          {data.erc20TokenBalances.map((token, index) => (
            <tr key={index} className="hover:bg-gray-50">
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {token.contract.name || 'Unknown'}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {token.contract.symbol || '-'}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
                {parseFloat(token.balance).toLocaleString()}
              </td>
              <td className="px-6 py-4 whitespace-nowrap text-sm">
                <a
                  href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
                  target="_blank"
                  rel="noopener noreferrer"
                  className="text-blue-600 hover:text-blue-800 font-mono"
                >
                  {token.contract.address.slice(0, 6)}...{token.contract.address.slice(-4)}
                </a>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  </div>
)}

{data && data.erc20TokenBalances.length === 0 && (
  <div className="bg-gray-50 p-6 rounded-md text-center">
    <p className="text-gray-600">No ERC-20 tokens found for this address</p>
  </div>
)}

Test Your dApp

Start the development server:

npm run dev

Example test address: 0xC4890Bc98273424a18626772F266C35bf57FA56A

Look at the browser for the response and the displayed token balances. You can click on any contract address to view it in the Shannon Explorer.

Complete Code

page.tsx
'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>
  );
}

route.ts
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 }
    );
  }
}

Congratulations

You have built your first API enabled dApp on the Somnia Network!

Safes

Palmera is a multi-safe and multi-chain treasury management platform designed to simplify on-chain financial operations. It offers a unified dashboard for managing multiple Safes across various supported chains, enhancing financial transparency and control for DAOs and organizations.

Resources

RPC

Ankr provides scalable RPC and API services to interact with the Somnia blockchain efficiently. Developers can use Ankr's enterprise-grade infrastructure to enhance dApp performance.

Resources

Explorers

Blockscout

Blockscout provides blockchain analytics & explorer services for Somnia. Developers and users can access on-chain transaction data, addresses, and smart contracts.

Resources

Oracles

DIA (Decentralized Information Asset) provides real-time on-chain oracles and trusted price feeds for decentralized applications on Somnia. This is critical for DeFi, lending, and trading protocols.

Resources

Infrastructure Providers

Somnia is built with a robust ecosystem of infrastructure providers that enable developers, projects, and businesses to integrate seamlessly with the network.

With Somnia's ecosystem of infrastructure partners, developers have access to scalable RPCs, smart contract tools, oracles, account abstraction, identity solutions, and analytics.

Each partner provides critical tooling, APIs, and services to power decentralized applications (dApps) on Somnia. Below is an overview of our key Infrastructure Partners and resources to get started.

An Ormi API key. Get one at.

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.

Open in your browser. Enter a wallet address that has tokens on Somnia Network.

Now that you have a working Token Balance dApp, you can extend it by using other Ormi API .

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, @emma_odiaand request Test Tokens. You can also email developers@somnia.network with a brief description of what you are building and your GitHub profile.

Service
Provider
https://subgraph.somnia.network/dashboard/api
Ormi
http://localhost:3000
endpoints
Palmera DAO
Documentation
Ankr
Ankr RPC for Somnia
Blockscout Explorer
DIA
Somnia Price Oracles
How to Integrate DIA Price Feeds

Somnia-Mission

To enable mass consumer real-time applications to be built at web2 scale with web3 properties, creating a more open, equitable internet.

Key Objectives

  • Applications that can scale to web2 usage levels (millions of CCU, 100,000’s of transactions per second)

  • Open censorship-resistant systems accessible to anyone with access to the internet

  • No one counterparty controlling and hoarding value

  • Composable systems where value is shared among the participants

  • Free movement of people and assets between platforms so creators and users have freedom of choice

A more open and equitable world.

Core Principles

On-Chain Experiences

We are committed to building technology to make fully on-chain, real-time mass-scale applications possible and practical. We believe this is essential for creating a composable, open internet.

Multiverse over Monolith

We believe in a collection of applications, each with distinct, interconnected experiences, much like countries within a global community, sharing utilities that enhance mutual growth.

Empowering Composability

Central to our vision is the principle of composability—the ability for builders to build upon each other’s work, creating a culture of collaboration in which the collective output surpasses the sum of its parts.

Builder Empowerment

We will empower builders with the freedom to build sustainable models of engagement and ownership. This includes safeguarding the rights to digital assets and creating an environment where innovation is not stifled by platform constraints.

Accessibility for All

The promise of the virtual society is in its potential for universal access, removing barriers for content creation and participation. Leveling the playing field such that anyone with an internet connection can participate and succeed.

Discord

MultiStream Consensus

Somnia then includes a consensus chain, with each consensus block including the current head of every data chain. This chain uses a modified PBFT consensus algorithm and is a typical proof of stake consensus setup. The consensus chain, including the tip of each data chain, provides full security against validators forking their own data chain. Each consensus block then semantically includes all of the transactions in all of the data chains whose tip it advanced. A deterministic pseudorandom ordering of these data chains then provides a single globally ordered stream of bytes to be executed across all data chains.

This setup completely decouples the production and distribution of new data (advancements of the data chains) with the consensus algorithm. It has a number of crucial benefits described below in the Advanced Compression Techniques section (e.g. streaming compression), which let this data frontend reach almost a gigabit per second of published transaction data.

Problem

Web3 has created a new era of decentralised finance, democratising financial access. However, it has fallen short in generating mass consumer applications and remains largely centred around finance. We believe blockchain technology is a cornerstone for the future of new, open Internet applications.

Today, there are limits to what you can build on-chain. Many factors constrain this, from the cost of running applications to the fundamental performance limitations of existing blockchains. We believe that new technologies can unlock a new class of real-time applications that normally must be built on Web2 foundations. Enabling these systems to be built on-chain will allow the free movement of businesses and users between online platforms, creating what we call a true virtual society.

We are building Somnia, a fast and cost-effective EVM-based blockchain, to achieve this. Somnia is a layer-one blockchain with full EVM compatibility, capable of processing over 400,000 transactions per second with sub-second finality and low fees. This will unlock a new wave of on-chain applications. Initially, we are focusing on gaming, metaverse, and social experiences. The use cases will likely extend far beyond these sectors. We are not even 100% certain of what can be built with the technology we are creating.

The Current State

Average DPS

Max DPS

Finality

ETH

9.19

18.38

66s

Polygon

47.67

95.33

Variable

AVAX

31.65

175.68

3.7s

BNB

194.6

194.6

75s

Name

Max Recorded TPS

Block Time

Time to finality

Base

293

2s

16m

BNB Chain

1731

3s

7.5s

Polygon

429

2.22s

4m 16s

Arbitrum

944

0.25s

16m

Ethereum

62.34

12.08

16m

Optimism

67.41

2s

16m

Avalanche

92.74

2.03s

0s

More recent benchmarks look as gas per second.

If you go outside of the EVM ecosystem, you can do a lot better. Note that this TPS is not the same as above, as the tests differ.

TPS

Finality

Aptos

SUI

Solana

Aptos and SUI's real-world live tests have not hit these levels yet. These numbers are based on their benchmarks. The Solana numbers differ from those on the Solana webpage as we are using the real-world observed numbers. Using the same methodology as the article above, Solana’s numbers are significantly worse at 273.34 swaps per second.

Despite this performance increase, we believe the EVM is important. There is already a rich ecosystem of developers and content around the EVM. Not being able to access that ecosystem seems like a missed opportunity. These chains had to make that tradeoff to create the needed performance. But what if we could have similar performance on the EVM?

The bottlenecks we see today with approaches on blockchain implemented with the EVM today are:

  • Execution speed – The rate at which smart contract code can be executed, and block creation can occur.

  • Bandwidth — The amount of bandwidth needed to send data between nodes on the network when running at high transaction levels.

Overview

The Somnia blockchain has many innovations that enable it to increase performance by several orders of magnitude compared to other EVM chains:

    • Independent Data Chains - Each validator operates its own blockchain, or “data chain,” which allows for independent block production. This unique approach eliminates the need for a consensus mechanism within individual data chains, streamlining the data processing workflow.

    • Consensus Chain - A separate blockchain aggregates the heads of all data chains, employing a modified PBFT algorithm for proof of stake consensus. This structure decouples data production from the consensus process, significantly enhancing overall efficiency.

SDKs

Sequence is a smart contract wallet & account abstraction provider that enables gasless transactions and seamless user onboarding to dApps.

Resources

Thirdweb simplifies smart contract deployment, account abstraction, and Web3 development on Somnia. It offers gasless transactions, token management, and SDKs for seamless dApp building.

Resources

Ankr

DIA

Palmera DAO

Block Scout

Sequence, Thirdweb

Image showing conesus algorithm

In Somnia, every validator publishes their own blockchain, their data chain. This innovation was inspired by the 2024 whitepaper “”. Every data chain is completely independent, and each block in a data chain contains a blob of bytes. Only the owning validator ever adds blocks to their data chain, and there are no safety mechanisms in place to avoid them forking their data chain or proposing invalid blocks. In other words, the data chains have no consensus mechanism at all.

Although many blockchains support the EVM bytecode standard, they all leave much to be desired from a throughput perspective. Below are transactions per second (TPS) figures for a typical UniswapV3 transaction (Dex swaps per second (DPS)), which is 130k gas. As you can see, most chains offer up to ~200 DPS, or ~20M DPS per day. These numbers were taken from :

These are somewhat similar to real world observed numbers (). Note that Uniswaps are a more complex user interaction and are composed of many transactions. Thus these numbers will be higher than above.

These numbers are taken from . To translate this into DPS, it’s about 6.5 DPS for every 1mg/s. So even the fastest chain on this list has a theoretical limit of 650 DPS (about 3700 TPS).

30,000 ()

0.9s ()

11,000 – 297,000 ()

480m/s ()

1608 ()

12.8s ()

Storage — Retrieval of storing historical data of a chain. Ethereum was a recent upgrade that significantly improved the cost of storage for ETH and other L2s. However, we still need improvements in reading and writing data to blockchains.

- a proof-of-stake, partially synchronous BFT protocol inspired by .

- By translating EVM bytecode to highly optimised native code, Somnia achieves execution speeds close to hand-written C++ contracts, facilitating the execution of millions of transactions per second on a single core.

- Somnia has a custom database called IceDB. It employs performance reports for predictable read and write performance as well as a custom database architecture that enables average read/write operations 15-100 nanoseconds with built in snapshotting.

- The Somnia data chain architecture is designed to enable streaming compression in order to maximise data throughput. Somnia combines this with aggregation in order to achieve extremely high compression ratios, allowing for massive transaction data throughput. This allows theoretical performance above other preported .

RPC
Oracles
Safes
Explorers
SDKs
Autobahn: Seamless high speed BFT
this article
taken from Chaininspect
this article
EIP-4844
MultiStream consensus
Autobahn BFT
Compiled Bytecode
Faster and predictable database performance
Advanced Compression Techniques
BLS signature
“limits due to bandwidth”

Use Cases

Gaming - Fully On-Chain, Forever Evolving Games

Somnia enables fully on-chain games where creators can easily build, modify, and extend games. Developers can create games that live forever on-chain, allowing players to own their in-game assets, modding systems, and worlds that grow continuously without centralized control.

SocialFi - True Ownership for Social Media Accounts

Somnia powers full on-chain social media platforms where users own their accounts and data. Creators aren’t locked into one platform and can freely port their content and followers between ecosystems, ensuring freedom and control.

Metaverse - Building Interoperable Virtual Socieities

Somnia is the backbone for metaverse applications where entire economies and ecosystems are built with on-chain ownership and logic. Developers can create worlds with seamless interoperability, allowing assets, avatars, and experiences to cross over between multiple virtual environments.

DeFi - Fully On-Chain Limit Order Books (LOBs)

Somnia’s performance enables fully on-chain limit order books (LOBs), offering price discovery and order matching similar to centralized exchanges but with full transparency and self-custody. This innovation brings more efficiency and fairness to decentralized finance (DeFi).

Real-Time Applications - Powering Real-Time, Large-Scale Applications

Somnia’s ultra-fast processing and sub-second finality are designed for real-time, mass-consumer applications. This goes beyond the categories above to places we have not imagined, Ultimately any web2 style application can now be built on web3 rails, offering the best of both worlds. We don’t even know all the possibilities yet and would love for builders to help us discover what is now possible when limits are removed.

Protocols

The Somnia protocols aim to address three key challenges: enabling the free movement of users and assets, facilitating unrestricted commerce, and accelerating the composability of experiences and objects. With this we hope to increase the value of digital assets, increase their commercial viability and enable accelerated development and experimentation for metaverse applications. Collectively, these protocols establish the foundational layer for a collaborative and open content ecosystem.

We currently have 2 major protocols that will be released:

  • SOM0 - Enabling interoperability of assets and their commercialization

  • SOM1 - Enabling composable virtual assets and experiences

cite
cite
cite
cite
cite
cite
Sequence
Documentation
Thirdweb
Thirdweb Somnia Docs
Deploy Smart Contracts on Somnia
Account Abstraction with Somnia
Thirdweb Faucet

Accelerated Sequential Execution

The Problem With Parallel Execution

A large number of modern blockchains have attempted to scale their execution using parallelism. This means they execute transactions which are unrelated, on different cores. This can work well when the transactions in a block are unrelated, but breaks down when those transactions modify the same state.

The reality, however, is that load spikes mostly happen when there is some event which has caused the spike. This implies a massive correlation between the transactions, which is not normally there in day to day execution.

For example:

A DEX will contain trading on many unrelated asset pairs throughout each day, but their real load spikes will come from volatility on a specific asset pair. These trades will all modify the same state, meaning parallel execution would not have helped.

In other words, parallel execution breaks down exactly when you need it.

This observation is why Somnia has opted to make a single core go extremely fast instead of relying on parallel execution.

So, how does Somnia make a single core go this fast?

EVM Compilation

Somnia executes EVM transactions. It scales by reaching a very high single-core speed instead of attempting to parallelise over multiple cores.

The EVM is a relatively simple stack-based architecture. Due to the implicit stack, stack-based bytecodes are often naturally smaller in size than register-based bytecodes, but they contain many redundant operations.

There are broadly two ways to execute a VM's bytecode, interpreted or native (which itself can be split into JIT or AOT compilation). The former effectively simulates the VM itself in software, while the latter translates the bytecode to native instructions, which your CPU then executes directly.

Ethereum and most EVM blockchains run an interpreted VM to execute EVM bytecode. This is often a relatively naive implementation which keeps its own stack, loops through each operation, and looks up the functionality of each operation in a lookup table. This is very slow compared to native execution.

Further, Solidity and other Ethereum compilers often optimise for bytecode size over gas. For example, they will include code to generate large constants instead of including them inline. This ends up including a large amount of runtime computation that can actually be statically resolved at compile time.

To exploit these redundancies, Somnia includes its own EVM compiler, which translates EVM bytecode to x86. This approaches near-native speed (native speed being the equivalent functionality handwritten in C++). In benchmarks, this can execute ERC-20 transfers in hundreds of nanoseconds, achieving millions of TPS on a single core.

However, there is no free lunch as the compilation process is relatively expensive. For this reason, you would only do this on contracts that are called frequently, falling back to standard interpreted EVM on the rest.

Hardware Level Parallelism

As mentioned above, "parallel execution", attempting to run unrelated transactions on different cores, breaks down when you need it the most because it cannot handle correlated transactions, which are much more common during load spikes. This type of parallelism is also known as "software parallelism".

For this reason, Somnia does not attempt to parallelise unrelated transactions. It does, however, enable the CPU to hardware parallelise each individual transaction.

This harnesses the single core parallelism available in modern CPUs to speed up every individual transaction, meaning this technique also works for transactions which modify the same state, helping during the load spikes as well.

So, what is hardware-level parallelism?

Modern CPU cores give the appearance that they execute assembly instructions in order, implementing control flow primitives by jumping around the assembly. The reality is that they are actually executing these instructions completely out of order, and often in parallel with each other.

For example, when reading a value from memory, the CPU will run ahead and work on future computation while waiting for the result. This is all completely invisible to the developer. This can cause massive speedups. Consider the case of an ERC-20 token swap. The program, at a high level, completes the following steps in the first half of it's execution:

1. Hash the sender account.

2. Lookup the sender balance using this hash.

3. Hash the receiver account.

4. Lookup the receiver balance using this hash.

5. ...

Let's say the speed to hash an account is 150ns, and the speed to lookup the sender balance is 100ns (a single RAM read).

If the CPU ran these steps in serial, this would take 150 + 100 + 150 + 100 = 500ns. However, in practice, if these steps are compiled to native code, your CPU hardware would execute 1 and 2 completely in parallel to 3 and 4, causing the execution speed to be 250ns, double the speed.

The problem is, interpreting the EVM stops your CPU core from executing assembly in parallel. To really harness the available silicon in your CPU, you need to execute native assembly. Somnia's compiler and database are built to enable hardware-level parallelism of smart contracts.

Somnia's IceDB

Somnia also includes its own database called IceDB. It has three key properties:

1. Deterministic performance

2. In-memory cache with read promotions

3. Built-in snapshotting

Deterministic Performance

This means that reads from these databases can hit RAM or many different places on disk. Depending on where the value is stored, the latency difference is enormous, differing by up to 1000x.

So, how much gas should a user be charged for a read? Should we assume the worst-case scenario that every read hits disk multiple times? Or should we assume some amount will hit memory? The former drastically limits the speed of your blockchain, and the latter allows an attacker to critically slow down your chain.

Could we time the read on each node and charge the user based on how long the request took? Unfortunately not, as this would not be deterministic across nodes (LevelsDB, RocksDB, etc., make no attempt to store data in deterministic places). Therefore, we can not change the gas based on this information.

Somnia's IceDb has been built from the ground up to have fully deterministic performance. Every read and write to IceDb returns the result and a "performance report". This report details exactly how many cold cache lines were read from RAM, and exactly how many disk pages were read from the SSD.

Due to this information being deterministic, we can charge the user based on the actual load they put on the system. Reads that accessed frequent data (and therefore hit RAM) use less gas, and we can then fit more into the block.

Improved read/write cache

As mentioned above, databases that persist to the disk often have an in-memory cache which attempts to store frequently accessed data in RAM, to serve frequent data with a much lower latency.

Most databases tend to optimise for either read or write. With Somnia, we have created a cache that can do both. This enables the average read/writes of IceDB to be between 15-100 nanoseconds.

Built-in Snapshotting

Blockchains require all nodes to periodically agree on exactly which state is in the blockchain after a given block.

Most blockchains use a data structure such as a Merkle tree to retrieve a single-state hash that encapsulates all of the data in the blockchain. Merkle trees allow this hash to be updated with every write with reasonable performance while allowing proofs of a particular piece of state to be generated for parties without access to the database.

Most blockchains then store each node in this Merkle tree as standard keys into their database. This makes reads or writes from this tree very expensive, as every single node can result in massive latencies from these embedded databases.

The reality, however, is the data structure that these databases use under the hood (a log structured merge tree) already lends itself to a tree formation, with vast sections stored immutably on disk. IceDB utilises this underlying immutable structure to support first class state snapshots, without needing the user space execution engine to store a Merkle tree using the databases' key value abstraction.

This massively accelerates the performance of IceDB, and reduces the amount of overhead for each value.

Security

Decentralisation

For Somnia, the main validators of the network will be targeting hardware specs between a Solana and Aptos node. This will allow a large group of participants to join the network but not have sub-par hardware and connectivity. This will ensure the high level of performance needed for real-time mass-consumer applications. There will initially be 100 globally distributed validator nodes. We expect this to grow as the network matures. We also incentivize decentralisation and global footprint for the chain through our tokenomics.

Securing The Network

As stated in the introduction the network is secured by validators staking tokens to participate in the network. This is a PoS network similar to other major blockchain networks (e.g. ETH). Node providers are subject to slashing if they act maliciously against the network. This will be further explored in our tokenomics.

Advanced Compression Techniques

When you have a lot of transactions, a lot of data needs to go between nodes

Once you are getting to the world of 100,000’s or even millions of transactions per second you start creating a lot of data.

A standard ERC-20 transfer is about 200 bytes when you consider all its parts. Now, imagine we are doing 1 million ERC-20 swaps per second. That’s 190 MBytes/s or 1.5 GBits/s. It's not going to pass over the public Internet, so you are either going to have a very centralised chain or limit your transaction rate.

Power law distribution

However, there is a lot of redundant information in those bytes. The number of bits theoretically required to send a piece of information is the logarithm of its probability of occuring. In practice, the probability distribution of which account is making a transaction, or which contract is being executed, or the arguments to the method being called, is very sharp (often a power law). This means that a minority of those accounts or contracts are highly likely to occur in the data to be sent. For example, if a particular contract was being called by 10% of transactions, its address can be encoded in 3.3 bits. That is a 48x compression ratio on the uncompressed 20 byte address.

Streaming Compression

This leads to the question of how we can effectively compress the transaction data moving between nodes. There are broadly two forms of compression:

  • Block compression

  • Streaming compression

Block compression is given a single block of data, and compresses in a way where the receiver only needs that block of data to decompress it. This is the typical form of compression, such as zip or tar files. It is very convenient because the compressor doesn't need to make assumptions about what other information the client has, or the order that information was sent.

Streaming compression is able to assume that the sender and receiver both share an identical history of the data that was compressed and decompressed, and it uses this assumption to build a large amount of internal implicit information which never needs to be sent over the wire. It can say, "Use the address from 3.456 megabytes ago". For this reason, it achieves much better compression ratios than block compression. The downside, though, is that the sender and receiver must share an identical stream of data, and the 'implicit' data cannot be moved across machines or processes without using a lot of bandwidth, meaning the same process must compress the full stream of data (there are ways around this depending on the compression algorithm but it often requires a large amount of CPU time).

This poses a problem for a blockchain, which typically has each block proposed by a different machine. It forces blockchains to use block-based compression, if anything, significantly impacting the compression ratios they can achieve.

Somnia has a consensus and data availability algorithm designed to support streaming-based compression. Each validator is responsible for publishing their own stream of data to their own blockchain. These are the data chains introduced earlier in this section. The fact that data chains use the same process for publishing this stream unlocks the ability for streaming compression.

Hashes and Signatures

A lot of data in Ethereum transactions have a tight power law distribution, making them very compressible. There are, however two important exceptions: hashes and signatures. By definition, these completely change with a uniform distribution if any bit in the transaction is different, making them completely uncompressable (no two executed Ethereum transactions are identical due to nonces, which avoid replay attacks).

Transaction hashes are easy: they are, again by definition, reproducible based on the transaction data itself, so they can simply not be sent (a receiving client is required to recalculate them anyway).

Signatures are more challenging. They are required to be sent alongside each transaction. Due to them being uncompressable, this would heavily limit the compression ratios we are able to achieve on transaction data.

However, we can aggregate signatures if we use the BLS signature scheme. BLS signatures can aggregate any number of BLS signatures into a single signature, effectively achieving a constant size for any number of transaction signatures.

Somnia uses BLS signature aggregation for signature verification speed and, crucially, because it enables a far better compression ratio.

This aggregate cryptography is an optional mechanism to submit batches of transactions, where the cost to verify the signatures of all transactions in the batch is similar to the cost of verifying just one transaction. Using large batches this reduces the cost of signature verification by many orders of magnitude.

Bandwidth Symmetry

In a typical blockchain, one validator is responsible for proposing each block and therefore, publishing the transaction data for that 'slot' of time to all of its peers. If the blockchain throughput is X bytes per second, and there are B blocks per second and N peers, the leader must send N * X / B bytes in 1 / B seconds, requiring an upload speed of N * X bytes per second (some blockchains will fan out in a tree architecture, which still requires the branching factor multiplied by X ).

In comparison, due to Somnia having all peers publish their own shard of data, each peer is responsible for publishing X / (N * B) bytes in 1 / B seconds, requiring an upload speed of X bytes per second. They also need to download everybody else's data for each block, requiring a download of N * (X / N) bytes per second, which is just X.

Note that no less data is being sent overall, but the bandwidth profile has been amortised to be symmetrical across all peers at all times. No peer ever needs to upload at a faster rate than the bandwidth of the blockchain itself, but they need to do this constantly instead of only when they propose a block. This enables the overall blockchain throughput to reach much closer to the bandwidth of the peers themselves.

SOM0

SOM0 is the initial set of protocols Somnia will launch with. These are designed to enable interoperability of people, places and objects and allow for commerce to flow across the Somnia network.

  • Object protocol - This protocol enables: (i) Users to create virtual objects that can exist and move between applications that are part of the Network (and to store metadata about these objects); and (ii) where permitted under relevant licensing terms, also allows Users to adapt existing objects, like NFTs, from outside of the Network to enable them to exist and move between applications on the Network, while storing metadata about such objects.

  • Attestation protocol - This protocol enables attestations about places, objects, and people made by network participants.

  • Marketplace protocol - This protocol provides a global liquidity layer which allows dApp owners and creators to sell objects such as NFTs in any virtual experience that is part of the Network.

SOM0 has adopted a blockchain-agnostic approach. This means that all of the Protocols are compatible with multiple blockchain networks, which we refer to as "omni-chain".

Object Protocol

The Object Protocol enables content creators to both: (i) create new, virtual objects that are compatible with all applications within the Network; and, (ii) subject to underlying licensing arrangements, import existing virtual objects from outside of the Network and adapt them so that they can be used within applications on the Network. Content creators will also be able to store and access metadata about such objects through the Attestation Protocol.

Once originated or adapted for the Object Protocol, the virtual object (for example, an NFT) is capable of being a tradable asset within Virtual Societies on the Network. This is accomplished by creating a record within an on-chain registry contract that is connected to the applicable virtual object. The registry includes a metadata structure that provides crucial information, enabling the experiences within the Network to understand the data assigned to the associated object. It allows for either linking to an existing object or minting a new one, both of which can represent the ownership of the object.

Making the Object Protocol “Omni-chain”

The Object Protocol handles the registration and management of virtual objects in the Network, allowing application owners to create virtual experiences on various blockchain networks. It ensures that virtual objects from any application in the network can move smoothly between different worlds and applications, no matter where the blockchain originated. This is achieved by indexing the registry across multiple chains through services from ecosystem members, enabling universal access to object details and interoperability across all applications in the Network.

Attestation protocol

Attestations allow network participants to attest to the authenticity of a virtual object by assigning particular properties to identities, places, and objects on the Network. These attestations can then be used by other network participants for any purpose. Examples of attestations and their value include:

  • A major brand attesting that a virtual object is an official virtual object (e.g., Nike attesting to say this object is an official Nike virtual shoe). This could then be used by application owners to ensure no counterfeit objects are in their applications.

  • A KYC provider attesting that the owner of an identity is over 18 years of age. This could then be used by application owner to ensure that no minors are in their applications.

  • Application owners can use the Attestation Protocol to engage a third-party moderation service to moderate all virtual objects that Network Participants wish to bring into that application. The object moderator could attest that an object complies with the application owner’s moderation guidelines to determine whether or not such an object is permitted within the applications. For example, an application owner of a family-focused application could engage a third-party moderator to review virtual objects and confirm that they contain no violent material. The moderator’s attestation could then be used as a filter by the application owner to ensure no violent content in its application.

Making the Attestation Protocol Omni-chain

The Attestation Protocol is vital for proving the authenticity and ownership of virtual objects. This registry is also global in nature, using a cross-chain approach to verify and record attestations across all open blockchain networks integrated with the Network. This will ensure that all digital assets, identities, and places on the Network are verifiable, regardless of the blockchain on which they were originated.

Marketplace Protocol

  • Each application owner that participates in the Network will be able to sell items within their application, and each content creator within that application will be able to sell their items in the application Owner’s store. Each application owner will be responsible for deploying, operating and maintaining these marketplaces. This includes making a front end (either web-based on in engine), processing payments and other user-facing flows.

  • The Marketplace Protocol enables all application owners/content creators to upload their items to an intra-application marketplace, one where digital items from any application can be uploaded and sold in the same place. This will be a “one-stop marketplace” for all items available for sale across all applications that are part of the Network.

  • The Marketplace Protocol will be “omni-chain” to allow for global liquidity and NFTs to be sold between two parties on any two supported open blockchain networks.

  • Technically, any NFT can be listed for sale in the Marketplace Protocol. However in reality, we expect mostly manufactured virtual objects (i.e., objects created using the Object Protocol) to use it.

It is important to understand that the Marketplace Protocol just provides a platform for this commerce. Applications built on top of this Protocol will determine a ruleset for what can and can’t be sold and the fee structures associated with permitted sales. The Marketplace Protocol only mandates protocol fees and original creator fee’s be upheld.

  • In very permissive application, all listed objects on the Marketplace Protocol may be sold in all experiences. With no additional fees.

  • In other applications, it may be necessary for the application owner to accept a listing, creating a curated set of items for sale. With a 2% sales fee for the application owner.

  • In another application, it may be necessary for the content creator to create a deal directly with the application owner in order to list their object. With a 2% sales fee for the application owner and a further fee for where the virtual object was sold (e.g. a content creator owning an experience selling virtual objects would get 5% of the item’s sale proceeds).

Marketplace Front End Example

To help application owners create their own marketplace front ends, we have developed a template marketplace front end. This is web-based and allows for all core functionality of a marketplace (listing objects, bidding, buying, etc.). It integrates directly with Web3 wallets for payment and is backed by the Marketplace Protocol.

Making the Marketplace Protocol Omni-chain

The suggested approach is to use a LayerZero protocol, which provides a foundational layer for interoperability between different blockchains. This will ensure that all transactions on the marketplace are secure and that all digital assets retain their value and functionality, regardless of the chain they are moved to.

One of Ethereum's biggest load spikes was the Otherside Otherdeed mint. When this happened, the vast majority of all transactions in each block were all modifying the same state (). Parallel execution would not have worked here.

Parallel works well for many individual apps or accounts, swapping tokens
When you have a hot path with many threads touching the same state parallel breaks down

Most blockchains use an embedded database like or . These databases are often optimised for write throughput and eventually flush all written data to disk to maintain a steady RAM load.

Somnia philosophically believes in having sufficiently decentralised services, not maximally decentralised. What this means is that you have enough decentralisation of infrastructure to enable the good properties of decentralisation (increased security, censorship resistance, no single owner/counterparty) whilst not trading off to degrade performance significantly (all decentralisation will inherently decrease performance ).

Image showing distribution of the power law
Image explaining BLS signature scheme
Diagram of linked and manufactured NFT
Example marketplace frontend
as they were minting Otherdeeds
LevelsDB
RocksDB
see blockchain trilema

Metaverse Browser

Engaging with blockchain applications can be a complex task, particularly for users unfamiliar with decentralised applications, web3 wallets, and the mechanics of a blockchain-based ecosystem. The Metaverse Browser aims to address the challenges of accessibility and usability. It's designed to simplify the process of navigating and participating in the Somnia ecosystem, making the application exploration more engaging and rewarding while also abstracting the complexities of blockchain technology for the end-user. This is similar to Warpcast in the Farcaster ecosystem. The browser is an easy-to-use front end (Warpcast) for the back-end protocol/chain (Farcaster/Optimism).

The Metaverse Browser serves as a gamified gateway to the Somnia ecosystem and chain, offering a user-friendly interface for engaging with a variety of content and key decentralised applications (dApps). It provides an immersive, game-like experience with pre-bundled dApps, ensuring a cohesive and interactive journey within the Virtual Society.

To facilitate seamless participation, the Metaverse Browser is equipped with a fully integrated web3 wallet, abstracted from the user, making digital transactions effortless. It also introduces a mobile-like "quest" system, rewarding users for their active participation in events, systems, and social activities, thereby enhancing user engagement within the Somnia ecosystem.

Conclusion

We believe in the vision of a world computer. A world where computing is accessible and usable by anyone with an internet connection, where data is shared across a global network that enables composable and interoperable applications. Ethereum started this movement and has provided the backbone for finance. We want to carry that flag for all application classes that can scale and compete with the large-scale web2 market while giving users and builders freedom, ownership and agency. A free and open internet.

Somnia Playground

The web-based Playground, is a feature that allows Content Creators to quickly and easily spin up web-hosted Virtual Society experiences in a “sandbox” environment.

The Playground offers a suite of intuitive, web-based authoring tools that facilitate the rapid iteration and publishing of virtual experiences that interoperate with the Somnia Network. This allows Content Creators to test their ideas and creations without having to deploy them onto server infrastructure, thereby saving time and resources. From immersive virtual environments to interactive games and educational experiences, Playground provides a platform for Content Creators to experiment, innovate, and bring their unique visions to life.

In addition to its utility for Content Creators, Playground also serves as a demonstration tool for the Somnia Network. Here, Network Participants can explore how the Protocols and infrastructure can be combined end-to-end to create a digital economy. An example of this is validating that a User owns an object and is only able to spawn it based on ownership (utilising the Object Protocol) or having a shop to sell items from the Marketplace Protocol.

Experiences

Metaverses

Metaverses are end user experiences where people can engage with each other in some form. This could be in an interactive game, a concert style experience or a formal meeting/workplace.

These projects are all on the MSquared Network and an ambition of the Somnia project is to use MSquared as a channel for rapid adoption of the protocol and blockchain, these projects are examples of the potential reach we could achieve.

Project Edison - Web3 gaming metaverse. Working with the famous web3 influencer Board Elon Musk Improbable is co-creating a web3 gaming metaverse. Events have varied from minecraft style crafting experiences to large scale races for crypto prizes. Attracting 100’s of consumers to each event and allowing people to be their web3 selves.

Kosmopop - Twice album launch event. Improbable partnered with Universal music to create an album launch event with Twice. Twice fans were able to listen to the new album and interact in a large virtual space.

Victory league - A football focused metaverse. The virtual home of football fandom. Victory league hosts large scale events connecting football fans with influencers and their favourite stars. They have hosted soccer match watch events and meet and greats with football superstars.

The virtual baseball metaverse with a virtual ballpark at the centre of it. Improbable has partnered with MLB to do several events. One of which created a digital replica of a baseball game in real-time allowing fans to be at the game virtually.

Collections

Collections are NFT collections that import themselves onto the Somnia protocol. This will then allow them to be associated with interoperable objects/avatars which can be used across experiences. The range of types of collections is quite large but here are some examples:

  • PFP projects - BAYC, Pudgy penguins, Forgotten Runes

  • Existing metaverse collections - Sandbox, Decentraland collections

  • Placeable objects - Runestones, EtherRocks

Browser

The browser would be a universal front end which users can use to browse metaverses and mapps on the network. Somnia is building a first party browser, but we imagine a future with a competitive market where anyone can build a browser (similar to game distribution or web3 wallets).

Profile Viewer

Profile viewers are a class of applications that allow users to view themselves, the items they own and their friends. These are similar to Steam accounts but based on on-chain data. We envision these would often be built into the metaverse browser but there could also be Mapps that browsers hook into.

SOM1

The SOM1 Protocol allows for the creation of composable and interoperable virtual worlds by integrating concepts from blockchain technology and game design architecture.

Overview

Blockchains are a substrate that allow untrusted parties to interact with one another based upon a series of composable rules called “Smart Contracts”. Smart Contracts almost always refer to addresses (either Smart Contracts or external users) and define various rules around how they relate to one another.

If you look abstractly, this architecture is in fact quite similar to an Entity-Component-System architecture in-game engine architecture - used for making very composable game worlds.

  • Entities → Addresses

  • Systems → Smart Contracts

  • Components → Data implicit inside Smart Contracts

In order to ensure that what you are doing can truly grow and be extended, we believe the following must be true:

  • Anybody can introduce any new “subject” into the world through adding a new address

  • Anybody can introduce any new “ruleset” into the world through adding a new smart contract

  • Any system is free to introduce any data about any subject

We call this vertical and horizontal composability:

  • Vertical composability - anybody is able to add new “things” to the world at will, in the form of new addresses

  • Horizontal composability - anybody is able to add new “systems” to the world, in the form of smart contracts

How to stop spam if anybody can do anything?

It’s all about who you listen to. In blockchains you are free to ignore whatever Smart Contract you like - most wallets have dozens of random coins and NFTs airdropped into them that wallet apps automatically ignore for you.

SOM1 Foundations

This section outlines the core concepts of the SOM1 protocol.

  1. Everything is an Entity: Every participant, whether it's a user or an object, is an Entity represented by an address. This approach gives objects the same degree of importance as users, enabling a more dynamic and interactive virtual world.

  2. Every entity is an address: Normally users and smart contracts are addresses. In SOM1 to put entities on an equal footing they are all considered an address. To extend an existing NFT to be an object in SOM1 a mapping would need to be created between the NFT and object:

  3. Entities are a set of components: An Entity is defined by its Components, which are features or functionalities added by different parties. These Components are defined in smart contracts, providing transparency and security.

You may wonder why it is implemented this way, rather than a smart contract for each component instance. Implementing it as one contract per type, rather than one contract per instance drastically reduces the trust surface area - you only need to trust a single contract to trust a whole ruleset, rather than potentially thousands of sub-contracts. This also will considerably save on gas costs, as contracts are O(component types) vs. O(entity count)

  1. Component state and actions are defined by _entity prefix methods

This pattern will be familiar to anybody who has coded golang, rust, python, C - the _entity argument to methods functions as a self/this pointer.

By looking at the generated Solidity ABI (a JSON file of the public schema of a smart contract) it’s possible to enumerate:

  • All single-argument view methods, which constitute the State of an Entity

  • All _entity prefixed write methods, which constitute the Actions you can take on an Entity via this Component

It’s worth noting that Actions are typical Solidity methods, so could guard their action on being payable, debiting various tokens, etc - this creates the foundation for economic systems, like having to pay a Jukebox to play a track.

SOM1 Key Components

The foundations above are very abstract. In this section we outline some of the potential implementations on top of the protocol. We will be releasing an initial set of these for others to build on.

  • Ownership and Permissions: SOM1 introduces an Ownership Component that abstracts the different modes of ownership, providing utilities for querying the owner. It can also be extended to a Permissions Component handling different roles fulfilled by an Entity.

  • Name: Referring to Entities as addresses is cumbersome from a UX perspective, and it’s useful to be able to define unique names for addresses across the network. This could be achieved via respecting existing systems like ENS - and this is certainly doable. However it would be useful for SOM1 to have a very cheap naming system that can be used to name any Entity - be it a user, an object, a component definition, etc

  • NFT metadata: One of the biggest challenges of composing existing NFTs is that the vast majority of them have their metadata off-chain. The Nft Metadata Component would allow Entities associated with an NFT to have their off-chain metadata written back onto the SOM1 protocol via oracles. This would then let other components compose functionality back on top of this.

  • Component Registries: The SOM1 Protocol allows the creation of Component Registries. These registries help define a set of Component types relevant to a specific context. This would allow creators to define permissions of who can add to registries and experiences to specify which component sets they understand.

  • Category component: While Entities can be totally arbitrary, in practice game engines are quite opinionated around what core types of things can be spawned in them. a player is very different to a wearable / equipable gadget which is quite different to something spawned on the ground. We’d likely define some base semantic categories that Entities can be, which will aid in game environments understanding in what context to use them, and how to spawn them in the world.

Practical Applications

The SOM1 Protocol can support a wide range of applications, from entertainment and gaming to education and virtual workspaces. Here are a few examples:

  1. Moderation and Classification: The protocol can apply moderation and classification to each entity within the virtual world. Different providers using various methods like AI or peer-to-peer human classification can enforce moderation. Virtual experiences can then mandate the existence of a moderation component for any entity loaded into their world.

  2. Autonomous Objects: Objects within SOM1 can have their addresses, making them "autonomous objects". These objects can interact in the virtual world just like any other user, opening the way for new interactive possibilities.

  3. Dynamic NFTs: By extending existing NFTs the SOM1 protocol could enable them to have dynamically updatable metadata. This would allow the NFT to change over time giving in new characteristics and rarity. Imagine a tradable creature that evolves as a player uses it in an experience.

The SOM1 Protocol brings the principles of blockchain and game design architecture together, enabling the creation of highly composable, interactive, and moddable virtual worlds.

The will be one of the major applications to be built on top of the Somnia chain and protocol. MSquared has already supported many high profile public events using its origin platform for partners such as Kosmopop (Twice), MLB and Edison.

There is already a first browser that is part of the network called the

MSquared project
Other experiences
metaverse browser.
Welcome to Edison! - A Massive Scale Virtual Gaming Events Metaverse World!
Kosmopop - Twice Virtual Listening Party
Victory League - The virtual home of football fandom
Major League Baseball (MLB) - Virtual Ball Park

Content Creation

Experience engine

Engines that enable metaverse experiences to be created. These could be rich interactive 3d experiences with 1000’s of people or smaller 2d web experiences. Example engines:

Object creation

Object creation is a very broad area. This could be an application that enables a user to create an avatar with a simple interface, it could be a rich 3d editor to create an object or some AI generative tool. The key element is the tool helps either a creator or a user create an object/avatar and creates an MML object at the end. Examples:

Avatar creation

Avatars are technically just another form of object, but we have broken out avatar creation into a separate bucket as we imagine there will be many tools/libraries specifically for the creation of avatars. Examples would include:

To refer to things that aren’t addresses, like an NFT, you could create an address by deterministically hashing the chain/token address/token id
Diagram showing Entity made up of components
Example code of IComponent interface which all components would implement
An example component definition that adds a counter to any entity that anybody can increment. Note the ‘_entity’ arguments
Service
Description
Use-case
Service
Description
Use case
Service
Description
Use case
Drop down button on the top left of Metamask
Cover

Learn more about Somnia

Cover

Start developing on Somnia

Cover

Try the Somnia Testnet

Not Logged In
Logged In
Diagram showing the difference between sequential execution (left) and hardware parralised execution (right)
Diagram of how attestation protocol works

Unreal based platform for creating high-desntiy experiences.

Music and sporting events. Large scale gameplay.

Open web engine (, Playground)

Open source web engines like 3js or the Playground.

Simple easy access experiences

Self hosted server running or

Hosted experience using Unreal or Unity

Any smaller scale multiplayer experience

Web based enabling collaboration

Can create anything

Web based enabling only certain levels of control

Generative AI tool means anyone can create object

Tool is early may not create correct item

Realistic 3d avatar creator

Creating avatars that look like you based on photos.

Create stylised personalized avatars

Creating more stylised avatars.

MSquared Avatar Builder

Create stylised avatars compatible with M2 experiences

Creating random avatars guaranteed to work across M2 experiences

MSquared Origin Platform
Playcanvas
Unreal
Unity
MML Editor
Meshy.ai
Avatrun
Ready Player Me

Somnia Account Abstraction Apps using Thirdweb React SDK

  • Connect Smart Wallets (Account Abstraction)

  • Read Wallet Balance

  • Send STT Tokens

Pre-requisites

Before we start, ensure you have:

  • Basic knowledge of React

  • Node.js & npm installed

Install Dependencies

Run the following command to set up your project:

npx create-next-app@latest somnia-thirdweb
cd somnia-thirdweb
npm install thirdweb ethers viem dotenv

This installs:

thirdweb → The Thirdweb React SDK.

ethers → To interact with blockchain transactions.

dotenv → To securely store API keys.

Create the Thirdweb Client

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

import { createThirdwebClient } from "thirdweb";
export const client = createThirdwebClient({
  clientId: process.env.NEXT_PUBLIC_THIRDWEB_CLIENT_ID as string, // Replace with your actual Client ID
});

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

Add Environment Variables

Store API keys in a .env.local file:

NEXT_PUBLIC_THIRDWEB_CLIENT_ID=your-client-id-here
NEXT_PUBLIC_SOMNIA_RPC_URL=https://dream-rpc.somnia.network/

Restart Next.js after modifying .env.local

Build the Account Abstraction App

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

import { ThirdwebProvider } from 'thirdweb/react';

<body>
    <ThirdwebProvider>
        {children}
    </ThirdwebProvider>
 </body>

Create & Connect Smart Contract Wallet

The useActiveAccount hook allows us to detect the connected Smart Wallet Account. We use ConnectButton to handle authentication and connection to the blockchain.

import { useActiveAccount } from "thirdweb/react";

const smartAccount = useActiveAccount();

<ConnectButton
  client={client}
  appMetadata={{
    name: "Example App",
    url: "https://example.com",
  }}
/>

This button will connect the user's Smart Contract Wallet and authenticate the user with Thirdweb. After connection, it will also display the wallet address.

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

{smartAccount ? (
  <div className="mt-6 p-4 bg-white rounded-lg shadow">
    <p className="text-lg font-semibold text-gray-700">
      Connected as: {smartAccount.address}
    </p>
    {message && <p className="mt-2 text-green-600">{message}</p>}
  </div>
) : (
  <p className="text-lg text-red-600 text-center">
    Please connect your wallet.
  </p>
)}

Token Transfer

The useSendTransaction hook is used to send STT tokens to another address. The function sendTokens will check that the Smart Account is connected and then send 0.01 STT tokens to a recipient address. First, copy your Smart Wallet Address and request for Tokens on Discord in the dev-chat, you can also Transfer some from your EOA.

Log transaction success or failure messages to the console.

import { useSendTransaction } from "thirdweb/react";
import { ethers } from "ethers";

const { mutate: sendTransaction, isPending } = useSendTransaction();

const sendTokens = async () => {
  if (!smartAccount) {
    setMessage("No smart account connected.");
    return;
  }
  console.log("Sending 0.01 STT from:", smartAccount.address);
  sendTransaction(
    {
      to: "0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03",
      value: ethers.parseUnits("0.01", 18),
      chain: somniaTestnet,
      client,
    },
    {
      onSuccess: (receipt) => {
        console.log("Transaction Success:", receipt);
        setMessage(`Sent 0.01 STT! TX: ${receipt.transactionHash}`);
      },
      onError: (error) => {
        console.error("Transaction Failed:", error);
        setMessage("Transaction failed! Check console.");
      },
    }
  );
};

Button Integration

The button UI provides a clear interaction for sending STT tokens. The button shows a loading state (Sending...) when the transaction is pending and displays a success or failure message once the transaction is complete.

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>}
Complete Code
'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>
  );
}

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

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.

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

A account & Client ID

click Connect
Connected Wallet
Smart Contract Wallet
Thirdweb React SDK
Thirdweb