ery States
```
// Same loading/error handling as AllFlips
if (loading) {
return <div className="text-center py-8 text-gray-500">Loading...</div>;
}
if (error) {
return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
}
if (!data?.flipResults?.length) {
return <div className="text-center py-8 text-gray-500">No recent flips</div>;
}
```
### Complete Live Feed Component
<details>
<summary>LiveFeed.tsx</summary>
```
'use client';
import { useQuery } from '@apollo/client';
import { GET_RECENT_FLIPS } from '@/lib/queries';
const truncateHash = (hash: string) => `${hash.slice(0, 6)}...${hash.slice(-4)}`;
const formatEther = (wei: string) => (parseFloat(wei) / 1e18).toFixed(4);
export default function LiveFeed() {
const { loading, error, data } = useQuery(GET_RECENT_FLIPS, {
variables: { first: 10 },
pollInterval: 5000,
});
if (loading) return <div className="text-center py-8 text-gray-500">Loading...</div>;
if (error) return <div className="text-center py-8 text-red-500">Error: {error.message}</div>;
if (!data?.flipResults?.length) return <div className="text-center py-8 text-gray-500">No recent flips</div>;
return (
<div className="space-y-2">
{data.flipResults.map((flip: any) => {
const won = flip.payout !== '0';
return (
<div key={flip.id} className={`p-3 rounded border ${won ? 'border-green-200 bg-green-50' : 'border-gray-200'}`}>
<div className="flex justify-between items-center">
<div>
<span className="font-mono text-sm">{truncateHash(flip.player)}</span>
<span className="text-sm text-gray-500 ml-2">bet {formatEther(flip.betAmount)} STT</span>
</div>
<div className="text-right">
<div className="text-sm">
<span className={flip.choice === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
{flip.choice}
</span>
<span className="mx-1">→</span>
<span className={flip.result === 'HEADS' ? 'text-blue-600' : 'text-purple-600'}>
{flip.result}
</span>
</div>
<div className={`text-sm font-semibold ${won ? 'text-green-600' : 'text-gray-400'}`}>
{won ? `Won ${formatEther(flip.payout)} STT` : 'Lost'}
</div>
</div>
</div>
</div>
);
})}
<p className="text-center text-xs text-gray-500 pt-2">Auto-refreshing every 5 seconds</p>
</div>
);
}
```
</details>
The key differences from the AllFlips page are that there is no pagination (shows most recent only), and it auto-refreshes with pollInterval, with a visual emphasis on win/loss status.
## Update the Main Page.tsx
<details>
<summary>page.tsx</summary>
```
'use client';
import { useState } from 'react';
import AllFlips from '@/components/AllFlips';
import LiveFeed from '@/components/LiveFeed';
export default function Home() {
const [activeTab, setActiveTab] = useState('allFlips');
return (
<div className="max-w-4xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">SomFlip</h1>
{/* Tab Navigation */}
<div className="flex gap-4 mb-6 border-b">
<button
onClick={() => setActiveTab('allFlips')}
className={`pb-2 px-1 ${
activeTab === 'allFlips'
? 'border-b-2 border-black font-semibold'
: 'text-gray-500'
}`}
>
All Flips
</button>
<button
onClick={() => setActiveTab('liveFeed')}
className={`pb-2 px-1 ${
activeTab === 'liveFeed'
? 'border-b-2 border-black font-semibold'
: 'text-gray-500'
}`}
>
Live Feed
</button>
</div>
{/* Conditional Rendering Based on Active Tab */}
{activeTab === 'allFlips' ? <AllFlips /> : <LiveFeed />}
</div>
);
}
```
</details>
## Run Your Application
```
npm run dev
```
Visit `http://localhost:3000` to see your UI in action.
<br>
# Using Data APIs (Ormi)
The Somnia mission is to enable the building of mass-consumer real-time applications. As a Developer, you need to understand how to interact with onchain data to build UIs. This guide will teach you how to build a Token Balance dApp that fetches and displays ERC20 token balances from the Somnia Network using Next.js and the Ormi Data APIs.
## Prerequisites
To complete this guide, you will need:
* Basic understanding of React and TypeScript
* An Ormi API key. Get one at[ ](https://ormi.xyz/)<https://subgraph.somnia.network/dashboard/api>.
{% embed url="<https://www.youtube.com/watch?v=6fuKQilbh2c>" %}
## What is Ormi Data API?
[Ormi](https://docs.ormilabs.com/dedicated-env/somnia/data-apis/overview) provides a unified crypto data infrastructure for live and historical blockchain data. The Data APIs allow developers to query blockchain data without running their own nodes, making it easy to build data-rich applications on the Somnia Network.
#### API Base URL
The Ormi Data API for Somnia Network uses the following base URL:
```
https://api.subgraph.somnia.network/public_api/data_api
```
#### API Endpoints
The API follows a RESTful structure. For fetching ERC20 token balances, the endpoint structure is:
```
/somnia/v1/address/{walletAddress}/balance/erc20
```
Where:
* `somnia` - The network identifier
* `v1` - API version
* `{walletAddress}` - The wallet address you want to query
* `balance/erc20` - Specifies that you want ERC-20 token balances
#### Authentication
The Ormi API requires authentication using a Bearer token. Every request must include an Authorization header:
```
Authorization: Bearer YOUR_API_KEY
```
**Important**: Never expose your API key in client-side code. Always make API calls from a server-side route to keep your key secure.
#### Example API Request
Here's an example of how to make a direct API call using curl:
```bash
curl -X GET "https://api.subgraph.somnia.network/public_api/data_api/somnia/v1/address/0xYOUR_WALLET_ADDRESS/balance/erc20" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json"
```
## Set up the Project
Create a new Next.js application with TypeScript and Tailwind CSS:
```bash
npx create-next-app@latest somnia-balance-demo --typescript --tailwind --app
cd somnia-balance-demo
```
## Create the Type Definitions
First, we need to define TypeScript interfaces for the API response. Update `app/page.tsx`:
```typescript
'use client'
import { useState, FormEvent } from 'react'
// Type definitions for the API response
interface TokenBalance {
balance: string
contract: {
address: string
decimals: number
erc_type: string
logoUri: string | null
name: string
symbol: string
}
raw_balance: string
}
interface BalanceResponse {
erc20TokenBalances: TokenBalance[]
resultCount: number
}
```
## Build the User Interface
Now, let's create the main component with an input field and button. Update your `app/page.tsx`:
```typescript
export default function Home() {
const [walletAddress, setWalletAddress] = useState<string>('')
const [loading, setLoading] = useState<boolean>(false)
const [data, setData] = useState<BalanceResponse | null>(null)
const [error, setError] = useState<string>('')
return (
<main className="min-h-screen bg-white p-8">
<div className="max-w-6xl mx-auto">
<h1 className="text-3xl font-bold mb-8">Somnia Network Balance Demo</h1>
<form className="mb-8">
<div className="flex gap-4">
<input
type="text"
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder="Enter wallet address (0x...)"
className="flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
type="submit"
disabled={loading}
className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed"
>
{loading ? 'Loading...' : 'Fetch Balance'}
</button>
</div>
</form>
</div>
</main>
)
}
```
## Create a `.env` file
The `.env` is for keeping secrets such as the Ormi API KEY. Add the API KEY:
```bash
PRIVATE_KEY=YOUR_API_KEY_HERE
```
## Create the API Route
To avoid CORS issues and keep your API key secure, we'll create an API route. Create a directory and a new file `app/api/balance/route.ts`:
```typescript
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const walletAddress = searchParams.get('address');
if (!walletAddress) {
return NextResponse.json(
{ error: 'Wallet address is required' },
{ status: 400 }
)
}
const apiKey = process.env.PRIVATE_KEY
const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api'
const response = await fetch(
`${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
{
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
}
)
const data = await response.json()
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch data from Ormi API', details: data },
{ status: response.status }
)
}
return NextResponse.json(data)
} catch (error) {
console.error('API Error:', error)
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
)
}
}
```
Important: Replace `YOUR_API_KEY_HERE` with your actual Ormi API key.
## Implement the Fetch Function
Add the fetch function to handle form submission. Update your `app/page.tsx`:
```typescript
const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!walletAddress) {
setError('Please enter a wallet address')
return
}
setLoading(true)
setError('')
setData(null)
try {
const response = await fetch(`/api/balance?address=${walletAddress}`, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ walletAddress }),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Failed to fetch balance')
}
setData(result)
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred')
} finally {
setLoading(false)
}
}
```
Don't forget to add the onSubmit handler to your form:
```typescript
<form onSubmit={fetchBalance} className="mb-8">
```
## Display the Results
Add error handling and a table to display the token balances. Add this code after your form in `app/page.tsx`:
```typescript
{error && (
<div className="p-4 mb-4 bg-red-50 border border-red-200 rounded-md">
<p className="text-red-600">{error}</p>
</div>
)}
{data && data.erc20TokenBalances.length > 0 && (
<div className="bg-white rounded-lg shadow overflow-hidden">
<div className="px-6 py-4 bg-gray-50 border-b">
<h2 className="text-xl font-semibold">Token Balances ({data.resultCount} tokens)</h2>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Name
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Symbol
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Balance
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Contract Address
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{data.erc20TokenBalances.map((token, index) => (
<tr key={index} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{token.contract.name || 'Unknown'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{token.contract.symbol || '-'}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
{parseFloat(token.balance).toLocaleString()}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<a
href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:text-blue-800 font-mono"
>
{token.contract.address.slice(0, 6)}...{token.contract.address.slice(-4)}
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{data && data.erc20TokenBalances.length === 0 && (
<div className="bg-gray-50 p-6 rounded-md text-center">
<p className="text-gray-600">No ERC-20 tokens found for this address</p>
</div>
)}
```
### Test Your dApp
Start the development server:
```bash
npm run dev
```
Open[ http://localhost:3000](http://localhost:3000/) in your browser. Enter a wallet address that has tokens on Somnia Network.
Example test address: `0xC4890Bc98273424a18626772F266C35bf57FA56A`
Look at the browser for the response and the displayed token balances. You can click on any contract address to view it in the Shannon Explorer.
## Complete Code
<details>
<summary>page.tsx</summary>
```typescript
'use client';
import { useState, FormEvent } from 'react';
// Type definitions for the API response
interface TokenBalance {
balance: string;
contract: {
address: string;
decimals: number;
erc_type: string;
logoUri: string | null;
name: string;
symbol: string;
};
raw_balance: string;
}
interface BalanceResponse {
erc20TokenBalances: TokenBalance[];
resultCount: number;
}
export default function Home() {
const [walletAddress, setWalletAddress] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<BalanceResponse | null>(null);
const [error, setError] = useState<string>('');
const fetchBalance = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!walletAddress) {
setError('Please enter a wallet address');
return;
}
setLoading(true);
setError('');
setData(null);
try {
const response = await fetch(`/api/balance?address=${walletAddress}`, {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ walletAddress }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Failed to fetch balance');
}
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<main className='min-h-screen bg-white p-8'>
<div className='max-w-6xl mx-auto'>
<h1 className='text-3xl font-bold mb-8 text-gray-900'>
Somnia Network Balance Demo
</h1>
<form onSubmit={fetchBalance} className='mb-8'>
<div className='flex gap-4'>
<input
type='text'
value={walletAddress}
onChange={(e) => setWalletAddress(e.target.value)}
placeholder='Enter wallet address (0x...)'
className='flex-1 px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-500'
/>
<button
type='submit'
disabled={loading}
className='px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed'
>
{loading ? 'Loading...' : 'Fetch Balance'}
</button>
</div>
</form>
{error && (
<div className='p-4 mb-4 bg-red-50 border border-red-200 rounded-md'>
<p className='text-red-600'>{error}</p>
</div>
)}
{data && data.erc20TokenBalances.length > 0 && (
<div className='bg-white rounded-lg shadow overflow-hidden'>
<div className='px-6 py-4 bg-gray-50 border-b'>
<h2 className='text-xl font-semibold text-gray-900'>
Token Balances ({data.resultCount} tokens)
</h2>
</div>
<div className='overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-200'>
<thead className='bg-gray-50'>
<tr>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-900 uppercase tracking-wider'>
Name
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Symbol
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Balance
</th>
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'>
Contract Address
</th>
</tr>
</thead>
<tbody className='bg-white divide-y divide-gray-200'>
{data.erc20TokenBalances.map((token, index) => (
<tr key={index} className='hover:bg-gray-50'>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{token.contract.name || 'Unknown'}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{token.contract.symbol || '-'}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{parseFloat(token.balance).toLocaleString()}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm'>
<a
href={`http://shannon-explorer.somnia.network/address/${token.contract.address}`}
target='_blank'
rel='noopener noreferrer'
className='text-blue-600 hover:text-blue-800 font-mono'
>
{token.contract.address.slice(0, 6)}...
{token.contract.address.slice(-4)}
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{data && data.erc20TokenBalances.length === 0 && (
<div className='bg-gray-50 p-6 rounded-md text-center'>
<p className='text-gray-600'>
No ERC-20 tokens found for this address
</p>
</div>
)}
</div>
</main>
);
}
```
</details>
<details>
<summary>route.ts</summary>
```typescript
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const walletAddress = searchParams.get('address');
if (!walletAddress) {
return NextResponse.json(
{ error: 'Wallet address is required' },
{ status: 400 }
);
}
const apiKey = process.env.PRIVATE_KEY;
const baseUrl = 'https://api.subgraph.somnia.network/public_api/data_api';
const response = await fetch(
`${baseUrl}/somnia/v1/address/${walletAddress}/balance/erc20`,
{
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
}
);
const data = await response.json();
if (!response.ok) {
return NextResponse.json(
{ error: 'Failed to fetch data from Ormi API', details: data },
{ status: response.status }
);
}
return NextResponse.json(data);
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
```
</details>
## Congratulations
You have built your first API enabled dApp on the Somnia Network!
Now that you have a working Token Balance dApp, you can extend it by using other Ormi API [endpoints](https://subgraphs.somnia.network/).
# Listening to Blockchain Events (WebSocket)
This guide teaches developers how to create WebSocket connections to listen for smart contract events on the Somnia network in real-time. We'll use a simple Greeting contract as an example to demonstrate the core concepts.
## Resources
| Somnia Mainnet WebSocket | <wss://api.infra.mainnet.somnia.network/ws> |
| ------------------------ | ------------------------------------------- |
| Somnia Testnet WebSocket | <wss://api.infra.testnet.somnia.network/ws> |
For the underlying subscription and log-filtering RPC methods, see the [JSON-RPC API Reference](/developer/json-rpc-api).
## Example Script
<details>
<summary>websocket-listener.js</summary>
```javascript
// const { ethers } = require("ethers");
import { ethers } from "ethers";
// Configuration
const wsUrl = "wss://dream-rpc.somnia.network/ws";
const contractAddress = "0xADA7b2953E7d670092644d37b6a39BAE3237beD7"; // Replace with your contract address
// Contract ABI
const abi = [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: "string", name: "oldGreeting", type: "string" },
{ indexed: true, internalType: "string", name: "newGreeting", type: "string" },
],
name: "GreetingSet",
type: "event",
},
{
inputs: [],
name: "getGreeting",
outputs: [{ internalType: "string", name: "", type: "string" }],
stateMutability: "view",
type: "function",
},
];
async function listen() {
// Create WebSocket provider and contract
const provider = new ethers.WebSocketProvider(wsUrl);
await provider._waitUntilReady();
const contract = new ethers.Contract(contractAddress, abi, provider);
console.log("Listening for events...\n");
// Event filter
const filter = {
address: contractAddress,
프로젝트로 돌아가기