bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 18쪽
read/write access to the Somnia Streams contract on Somnia Testnet.
***
### **Compute the Schema ID**
Schemas define how the onchain data is structured. In this case, the tap schema looks like this:
```ts
tapSchema = 'uint64 timestamp, address player'
```
Before writing data, we must compute its **unique Schema ID**:
```tsx
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
```
This produces a deterministic ID derived from the schema text, ensuring that any app using the same schema can read or decode your data.
***
### **Register the Schema**
```typescript
// Register schema
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const isRegistered = await sdk.streams.isDataSchemaRegistered(schemaId)
if (!isRegistered) {
const ignoreAlreadyRegistered = true
const txHash = await sdk.streams.registerDataSchemas(
[{ schemaName: 'tap', schema: tapSchema, parentSchemaId: zeroBytes32 }],
ignoreAlreadyRegistered
)
if (!txHash) throw new Error('Failed to register schema')
await waitForTransactionReceipt(getPublicHttpClient(), { hash: txHash })
}
```
If this schema wasn’t registered yet, we register it once. It’s safe to call this before sending the first message.
### **Encode the Data**
Somnia Streams stores structured data using its `SchemaEncoder` class. We create an encoder and provide each field according to the schema definition.
```tsx
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
])
```
This converts your JavaScript values into the precise binary format that can be stored onchain and later decoded.
***
### **Generate a Unique Data ID**
Each record needs a **unique identifier** within the schema. We use the `keccak256` hash of the player’s address and timestamp to ensure that it is packed into 32 bits of data.
```tsx
const id = keccak256(toHex(`${address}-${Number(nonce)}`))
```
This ensures no two taps collide, even if the same player taps rapidly.
***
### **Store the Tap Onchain**
Finally, we push the structured data to the blockchain using:
```tsx
await sdk.streams.set([{ id, schemaId, data }])
```
The `set()` method writes one or more records (called *Data Streams*) to the chain.\
Each record is cryptographically signed by the player’s wallet, and gets stored on Somnia’s decentralized data infrastructure. It can also be retrieved instantly using the same schema
***
### **Manage Cooldowns and Feedback**
After the tap is sent, we apply a 1-second cooldown to avoid flooding transactions and reset the pending state.
```tsx
setCooldownMs(1000)
setPending(false)
```
This gives players a smooth UX while maintaining blockchain transaction integrity.
***
#### Putting It All Together
Here’s the complete `sendTap()` method with all steps combined:
```tsx
async function sendTap() {
if (!walletClient || !address) return
setPending(true)
const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
])
const id = keccak256(toHex(`${address}-${Number(now)}`))
await sdk.streams.set([{ id, schemaId, data }])
setCooldownMs(1000)
setPending(false)
}
```
### Complete \`page.tsx\` Code
<details>
<summary>page.tsx</summary>
```typescript
'use client'
import { useState, useEffect, useRef } from 'react'
import { SDK, SchemaEncoder } from '@somnia-chain/streams'
import { getPublicHttpClient } from '@/lib/clients'
import { tapSchema } from '@/lib/schema'
import { keccak256, toHex, createWalletClient, custom } from 'viem'
import { somniaTestnet } from 'viem/chains'
export default function Page() {
const [address, setAddress] = useState('')
const [walletClient, setWalletClient] = useState<any>(null)
const [leaderboard, setLeaderboard] = useState<{ address: string; count: number }[]>([])
const [cooldownMs, setCooldownMs] = useState(0)
const [pending, setPending] = useState(false)
const [error, setError] = useState('')
const lastNonce = useRef<number>(0)
async function connectWallet() {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
const wallet = createWalletClient({
chain: somniaTestnet,
transport: custom(window.ethereum),
})
setAddress(accounts[0])
setWalletClient(wallet)
}
async function sendTap() {
if (!walletClient || !address) return
setPending(true)
const sdk = new SDK({ public: getPublicHttpClient(), wallet: walletClient })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const encoder = new SchemaEncoder(tapSchema)
const now = BigInt(Date.now())
const data = encoder.encodeData([
{ name: 'timestamp', value: now, type: 'uint64' },
{ name: 'player', value: address, type: 'address' },
{ name: 'nonce', value: BigInt(lastNonce.current++), type: 'uint256' },
])
const id = keccak256(toHex(`${address}-${Number(now)}`))
await sdk.streams.set([{ id, schemaId, data }])
setCooldownMs(1000)
setPending(false)
}
return (
<main style={{ padding: 24 }}>
<h1>🚀 Somnia Tap Game</h1>
{!address ? (
<button onClick={connectWallet}>🦊 Connect MetaMask</button>
) : (
<p>Connected: {address.slice(0, 6)}...{address.slice(-4)}</p>
)}
<button onClick={sendTap} disabled={pending || cooldownMs > 0 || !address}>
{pending ? 'Sending...' : '🖱️ Tap'}
</button>
{error && <p style={{ color: 'red' }}>{error}</p>}
<Leaderboard leaderboard={leaderboard} />
</main>
)
}
function Leaderboard({ leaderboard }: { leaderboard: { address: string; count: number }[] }) {
if (!leaderboard.length) return <p>No taps yet</p>
return (
<ol>
{leaderboard.map((p, i) => (
<li key={p.address}>
#{i + 1} {p.address} — {p.count} taps
</li>
))}
</ol>
)
}
```
</details>
***
## Reading Leaderboard Data Onchain
The leaderboard is calculated server-side by reading all tap data stored onchain. Create a `lib/store.ts` file and add the following code:
```typescript
lib/store.ts
import { SDK } from '@somnia-chain/streams'
import { getServerPublicClient } from './serverClient'
import { tapSchema } from './schema'
const publisher =
process.env.NEXT_PUBLIC_PUBLISHER_ADDRESS ||
'0x0000000000000000000000000000000000000000'
const val = (f: any) => f?.value?.value ?? f?.value
export async function getLeaderboard() {
const sdk = new SDK({ public: getServerPublicClient() })
const schemaId = await sdk.streams.computeSchemaId(tapSchema)
const rows = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)
if (!Array.isArray(rows)) return []
const counts = new Map<string, number>()
for (const row of rows) {
const player = String(val(row[1]) ?? '').toLowerCase()
if (!player.startsWith('0x')) continue
counts.set(player, (counts.get(player) || 0) + 1)
}
return Array.from(counts.entries())
.map(([address, count]) => ({ address, count }))
.sort((a, b) => b.count - a.count)
}
```
The leaderboard logic begins inside the `getLeaderboard()` function, where we use the **SDK** to read structured tap data directly from the blockchain. First, the function initializes the SDK with a **server-compatible public client**, which allows read-only access to the chain without a connected wallet. The next step computes the `schemaId` by passing our `tapSchema` to `sdk.streams.computeSchemaId()`. This produces a deterministic identifier that ensures we’re always referencing the correct data structure.
Once the `schemaId` is known, the core operation happens through `sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)`. This method queries the blockchain for all records written by the specified publisher under that schema. Each returned record is an array of fields that align with the schema’s definition, in this case `[timestamp, player]`. The helper function `val()` is then used to unwrap nested field values (`f?.value?.value`) from the SDK’s response format, giving us clean, readable values.
`getAllPublisherDataForSchema` acts like a decentralized “SELECT \* FROM” query, fetching all onchain data tied to a schema and publisher, while the rest of the function transforms that raw blockchain data into a structured leaderboard the app can display.<br>
***
Creat a api route to retrieve Leaderboard score. Create the file `app/api/leaderboard/route.ts`
```typescript
import { NextResponse } from 'next/server'
import { getLeaderboard } from '@/lib/store'
export async function GET() {
const leaderboard = await getLeaderboard()
return NextResponse.json({ leaderboard })
}
```
This endpoint imports the `getLeaderboard()` function from `lib/store.ts`, which handles the heavy lifting of querying Somnia Data Streams, and then exposes that onchain data as a clean, JSON-formatted response for your application. The client simply fetches the leaderboard via `/api/leaderboard`.<br>
The page.tsx fetches /api/leaderboard every few seconds to stay updated.
***
Every tap executes a real blockchain transaction:
| Field | Description |
| --------- | ---------------------------- |
| timestamp | Time of the tap |
| player | Wallet address of the player |
When the `set()` call succeeds, Somnia Data Streams stores the record and indexes it under your publisher’s address. Any application (including yours) can then read this data and build dashboards, analytics, or game leaderboards.
***
## Run the App
```bash
npm run dev
```
Open[ http://localhost:3000](http://localhost:3000) and connect your MetaMask wallet. Click 🖱️ Tap to send onchain transactions, and watch your leaderboard update live.
***
## Conclusion
You’ve built a fully onchain game where player interactions are stored via Somnia Data Streams and leaderboard rankings are derived from immutable blockchain data. MetaMask provides secure, user-friendly authentication. This same pattern powers realtime Web3 experiences, from social apps to competitive games, using Somnia’s high-performance onchain data infrastructure.
# Smart Contracts
List of critical Smart Contract addresses for Somnia network.
<table><thead><tr><th width="290.111328125">Contract</th><th>Address</th></tr></thead><tbody><tr><td>MultiCallV3</td><td><a href="https://explorer.somnia.network/address/0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11">0x5e44F178E8cF9B2F5409B6f18ce936aB817C5a11</a></td></tr><tr><td>WSOMI</td><td><a href="https://explorer.somnia.network/token/0x046EDe9564A72571df6F5e44d0405360c0f4dCab">0x046EDe9564A72571df6F5e44d0405360c0f4dCab</a></td></tr><tr><td>USDC</td><td><a href="https://explorer.somnia.network/address/0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00">0x28bec7e30e6faee657a03e19bf1128aad7632a00</a></td></tr><tr><td>WETH</td><td><a href="https://explorer.somnia.network/token/0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8">0x936Ab8C674bcb567CD5dEB85D8A216494704E9D8</a></td></tr><tr><td>WBTC</td><td><a href="https://explorer.somnia.network/token/0xC5098b3cA516784323872F17235fa074E167D3D2">0xC5098b3cA516784323872F17235fa074E167D3D2</a></td></tr><tr><td>USDT</td><td><a href="https://explorer.somnia.network/token/0x67B302E35Aef5EEE8c32D934F5856869EF428330">0x67B302E35Aef5EEE8c32D934F5856869EF428330</a></td></tr><tr><td>USDso</td><td><a href="https://explorer.somnia.network/token/0x00000022dA000002656c64D9eA6011ea952D008A">0x00000022dA000002656c64D9eA6011ea952D008A</a></td></tr></tbody></table>
## Omnichain SOMI Deployments
<table><thead><tr><th width="123.634765625">Mainnet</th><th width="459.333984375">Address</th><th>Type</th></tr></thead><tbody><tr><td>BNB Chain</td><td><a href="https://bscscan.com/token/0xa9616e5e23ec1582c2828b025becf3ef610e266f">0xa9616e5e23ec1582c2828b025becf3ef610e266f</a></td><td>OFT</td></tr><tr><td>Base</td><td><a href="https://basescan.org/token/0x47636b3188774a3E7273D85A537b9bA4Ee7b2535">0x47636b3188774a3E7273D85A537b9bA4Ee7b253</a></td><td>OFT</td></tr><tr><td>Ethereum</td><td><a href="https://etherscan.io/token/0x1B0F6590d21dc02B92ad3A7D00F8884dC4f1aed9">0x1B0F6590d21dc02B92ad3A7D00F8884dC4f1aed9</a></td><td>OFT</td></tr><tr><td>Somnia</td><td><a href="https://explorer.somnia.network/address/0xC3D4E9Ac47D7f37bB07C2f8355Bb4940DEA3bbC3">0xC3D4E9Ac47D7f37bB07C2f8355Bb4940DEA3bbC3</a></td><td>NativeOFTAdapter</td></tr></tbody></table>
## LayerZero Contracts
* **`chainKey` :** `somnia`
* **`stage` :** `mainnet`
* **`EID` :** `30380`
<table><thead><tr><th width="286.4140625">Contract</th><th>Address</th></tr></thead><tbody><tr><td>endpointV2 (main entrypoint)</td><td><a href="https://explorer.somnia.network/address/0x6F475642a6e85809B1c36Fa62763669b1b48DD5B">0x6F475642a6e85809B1c36Fa62763669b1b48DD5B</a></td></tr><tr><td>sendUln302</td><td><a href="https://explorer.somnia.network/address/0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7">0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7</a></td></tr><tr><td>receiveUln302</td><td><a href="https://explorer.somnia.network/address/0xC39161c743D0307EB9BCc9FEF03eeb9Dc4802de7">0xe1844c5D63a9543023008D332Bd3d2e6f1FE1043</a></td></tr><tr><td>executor</td><td><a href="https://explorer.somnia.network/address/0x4208D6E27538189bB48E603D6123A94b8Abe0A0b">0x4208D6E27538189bB48E603D6123A94b8Abe0A0b</a></td></tr><tr><td>deadDVN</td><td><a href="https://explorer.somnia.network/address/0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842">0x6788f52439ACA6BFF597d3eeC2DC9a44B8FEE842</a></td></tr></tbody></table>
## Oracles
### DIA - Mainnet [Price Feeds](/developer/building-dapps/oracles/dia-price-feeds)
<table><thead><tr><th width="276.751953125">Contract</th><th>Address</th></tr></thead><tbody><tr><td>Oracle</td><td><a href="https://explorer.somnia.network/address/0xbA0E0750A56e995506CA458b2BdD752754CF39C4">0xbA0E0750A56e995506CA458b2BdD752754CF39C4</a></td></tr><tr><td>Gas Wallet</td><td><a href="https://mainnet.somnia.w3us.site/address/0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a">0x3073d2E61ecb6E4BF4273Af83d53eDAE099ea04a</a></td></tr><tr><td>USDT</td><td><a href="https://mainnet.somnia.w3us.site/address/0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267">0x936C4F07fD4d01485849ee0EE2Cdcea2373ba267</a></td></tr><tr><td>USDC</td><td><a href="https://explorer.somnia.network/address/0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93">0x5D4266f4DD721c1cD8367FEb23E4940d17C83C93</a></td></tr><tr><td>BTC</td><td><a href="https://explorer.somnia.network/address/0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE">0xb12e1d47b0022fA577c455E7df2Ca9943D0152bE</a></td></tr><tr><td>ARB</td><td><a href="https://explorer.somnia.network/address/0x6a96a0232402c2BC027a12C73f763b604c9F77a6">0x6a96a0232402c2BC027a12C73f763b604c9F77a6</a></td></tr><tr><td>SOL</td><td><a href="https://mainnet.somnia.w3us.site/address/0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234">0xa4a3a8B729939E2a79dCd9079cee7d84b0d96234</a></td></tr><tr><td>WETH</td><td><a href="https://explorer.somnia.network/address/0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52">0x4E5A9Ebc4D48d7dB65bCde4Ab9CBBE89Da2Add52</a></td></tr><tr><td>SOMI</td><td><a href="https://explorer.somnia.network/address/0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27">0x1f5f46B0DABEf8806a1f33772522ED683Ba64E27</a></td></tr></tbody></table>
### Protofire - Mainnet [VRF Smart Contracts](/developer/building-dapps/oracles/using-verifiable-randomness-vrf)
<table><thead><tr><th width="261.458984375">Contract</th><th>Address</th></tr></thead><tbody><tr><td>VRFV2PlusWrapper</td><td><a href="https://explorer.somnia.network/address/0x606b2B36516AB7479D1445Ec14B6B39B44901bf8">0x606b2B36516AB7479D1445Ec14B6B39B44901bf8</a></td></tr><tr><td>LINK Token</td><td><a href="https://explorer.somnia.network/address/0x0a4Db7035284566F6f676991ED418140dC01A2aa">0x0a4Db7035284566F6f676991ED418140dC01A2aa</a></td></tr><tr><td>LINK/NATIVE oracle</td><td><a href="https://explorer.somnia.network/address/0xEBD41881413dD76F42DF2902ee865099af9099B4">0xEBD41881413dD76F42DF2902ee865099af9099B4</a></td></tr></tbody></table>
# Development Frameworks
Developing a robust decentralized application (dApp) requires a complex stack of technologies. Software frameworks streamline this process by bundling essential features or offering flexible plugin architectures to customize your toolkit. These frameworks provide immediate, out-of-the-box utility, including:
* Local Environments: Tools to instantly launch a local blockchain instance.
* Development Suite: Utilities for compiling and testing smart contracts.
* Integrated Frontend: Add-ons that allow for client-side development within the same repository.
* Deployment Management: Configurations for connecting to networks (local or public) and deploying contracts.
# Local Testing and Forking
{% hint style="info" %}
This page shows how to spin up a local EVM node with **Hardhat** (and optionally **Anvil**) and fork **Somnia Testnet (Shannon)** or **Somnia Mainnet** for realistic testing.
{% endhint %}
***
## Prerequisites
* Node.js 20+
* Hardhat (or Foundry/Anvil if you prefer)
* A `.env` file with Somnia RPCs:
{% code title=".env (example)" %}
```bash
# .env (example)
SOMNIA_RPC_MAINNET=https://api.infra.mainnet.somnia.network/
SOMNIA_RPC_TESTNET=https://dream-rpc.somnia.network/
# Optional: pin block numbers for reproducible forks
FORK_BLOCK_MAINNET=
FORK_BLOCK_TESTNET=
```
{% endcode %}
> Do not commit real keys/tokens. Use environment variables.
***
## Quick Start (Hardhat)
{% stepper %}
{% step %}
{% code title="Install packages" %}
```bash
npm i -D hardhat @nomicfoundation/hardhat-ethers ethers dotenv
npx hardhat # if project not initialized yet
cp .env.example .env || true
```
{% endcode %}
{% endstep %}
{% step %}
{% code title="hardhat.config.ts" %}
```ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-ethers";
import * as dotenv from "dotenv";
dotenv.config();
const cfgFromEnv = {
mainnetUrl: process.env.SOMNIA_RPC_MAINNET || "https://api.infra.mainnet.somnia.network/",
testnetUrl: process.env.SOMNIA_RPC_TESTNET || "https://dream-rpc.somnia.network/",
forkMainnetBlock: process.env.FORK_BLOCK_MAINNET ? Number(process.env.FORK_BLOCK_MAINNET) : undefined,
forkTestnetBlock: process.env.FORK_BLOCK_TESTNET ? Number(process.env.FORK_BLOCK_TESTNET) : undefined,
};
const config: HardhatUserConfig = {
solidity: "0.8.19",
networks: {
hardhat: {
chainId: 31337,
},
localhost: {
url: "http://127.0.0.1:8545",
chainId: 31337,
},
somnia_testnet: {
url: cfgFromEnv.testnetUrl,
chainId: 50312,
},
somnia_mainnet: {
url: cfgFromEnv.mainnetUrl,
chainId: 5031,
},
},
};
export default config;
```
{% endcode %}
{% endstep %}
{% endstepper %}
***
## Testing Your DApp Locally
Once your Hardhat environment is set up, you can write and run tests for your smart contracts. This ensures your code works as expected before deploying it.
**Create a Simple Smart Contract**
First, create a basic smart contract to test. The `Counter.sol` contract below includes fundamental functions for incrementing a counter and retrieving its current value. Save this file in your project's `contracts` directory.
{% code title="contracts/Counter.sol" %}
```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Counter {
uint256 private count;
event CountedTo(uint256 number);
function getCount() public view returns (uint256) {
return count;
}
function increment() public {
count += 1;
emit CountedTo(count);
}
}
```
{% endcode %}
### **Write a Test File**
Next, write a test file to verify the functionality of your smart contract. The `Counter.test.ts` file below deploys the `Counter` contract on a local test network, calls the `increment` function, and checks whether the outcome is as expected. Save this file in your project's `test` directory.
{% code title="test/Counter.test.ts" %}
```ts
import { expect } from "chai"프로젝트로 돌아가기