Somnia 백서

Somnia

원문

bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 14쪽

await sdk.streams.isDataSchemaRegistered(driverSchemaId) if (!isDriverSchemaRegistered) { // Now, publish the driver schema but extend the coordinates schema! const registerDriverSchemaTxHash = sdk.streams.registerDataSchemas([ { schemaName: "driver", schema: driverSchema, parentSchemaId: coordinatesSchemaId } ]) if (!registerDriverSchemaTxHash) { throw new Error("Failed to register schema on-chain") } console.log("Registered driver schema on-chain", { registerDriverSchemaTxHash, }) await publicClient.waitForTransactionReceipt({ hash: registerDriverSchemaTxHash }) } // Publish some data!! const schemaEncoder = new SchemaEncoder(extendedSchema) const encodedData = schemaEncoder.encodeData([ { name: "number", value: "44", type: "uint32" }, { name: "name", value: "Lewis Hamilton", type: "string" }, { name: "abbreviation", value: "HAM", type: "string" }, { name: "teamName", value: "Ferrari", type: "string" }, { name: "teamColor", value: "#F91536", type: "string" }, { name: "x", value: "-1513", type: "int256" }, { name: "y", value: "0", type: "int256" }, { name: "z", value: "955", type: "int256" }, ]) console.log("encodedData", encodedData) const dataStreams = [{ // Data id: DRIVER number - index will be a helpful lookup later and references ./data/f1-coordinates.js Cube 4 coordinates (driver 44) - F1 telemetry data id: toHex(`44-0`, { size: 32 }), schemaId: driverSchemaId, data: encodedData }] const publishTxHash = await sdk.streams.set(dataStreams) console.log("\nPublish Tx Hash", publishTxHash) } ``` # READ Stream Data from a UI (Next.js Example) In this guide, you’ll learn how to read data published to Somnia Data Streams directly from a Next.js frontend, the same way you’d use readContract with Viem. We’ll build a simple HelloWorld schema and use it to demonstrate all the READ methods in the Somnia Data Streams SDK, from fetching the latest message to retrieving complete datasets or schema metadata. *** ## Prerequisites Before we begin, make sure you have: ```bash npm i @somnia-chain/streams viem ``` Also ensure: * Node.js 20+ * A Somnia Testnet wallet with STT test tokens * `.env` file containing your wallet credentials: * A working Next.js app (npx create-next-app somnia-streams-read) * Access to a publisher address and schema ID (or one you’ve created earlier) *** ## Set up the SDK and Client We’ll initialize the SDK using Viem’s createPublicClient to communicate with Somnia’s blockchain. ```typescript // lib/store.ts import { SDK } from '@somnia-chain/streams' import { createPublicClient, http } from 'viem' import { somniaTestnet } from 'viem/chains' const publicClient = createPublicClient({ chain: somniaTestnet, transport: http(), }) export const sdk = new SDK(publicClient) ``` This sets up the data-reading connection between your frontend and the Somnia testnet. Think of it as the Streams version of [readContract()](https://viem.sh/docs/contract/readContract#readcontract); it lets you pull structured data (not just variables) directly from the blockchain. *** ## Define Schema and Publisher A schema describes the structure of data stored in Streams, just like how a smart contract defines the structure of state variables. ```typescript // lib/schema.ts export const helloWorldSchema = 'uint64 timestamp, string message' export const schemaId = '0xabc123...' // Example Schema ID export const publisher = '0xF9D3...E5aC' // Example Publisher Address ``` If you don’t have the schema ID handy, you can generate it from its definition: ```typescript const computedId = await sdk.streams.computeSchemaId(helloWorldSchema) console.log('Computed Schema ID:', computedId) ``` This ensures that you’re referencing the same schema ID under which the data was published. *** ## Fetch Latest “Hello World” Message This is the most common use case: getting the most recent data point. For example, displaying the latest sensor reading or chat message. ```typescript // lib/read.ts import { sdk } from './store' import { schemaId, publisher } from './schema' export async function getLatestMessage() { const latest = await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher) console.log('Latest data:', latest) return latest } ``` This method retrieves the newest record from that schema-publisher combination. It’s useful when: * You’re showing a live dashboard * You need real-time data polling * You want to auto-refresh a view (e.g., “Last Updated at…”) *** ## Fetch by Key (e.g., message ID) Each record can have a unique key, such as a message ID, sensor UUID, or user reference. When you know that key, you can fetch the exact record. ```typescript export async function getMessageById(messageKey: `0x${string}`) { const msg = await sdk.streams.getByKey(schemaId, publisher, messageKey) console.log('Message by key:', msg) return msg } ``` When to use: * Fetching a message by its ID (e.g., “message #45a1”) * Retrieving a transaction or sensor entry when you know its hash * Building a detail view (e.g., /message/\[id] route in Next.js) Think of it like calling readContract for one item by ID. *** ## Fetch by Index (Sequential Logs) In sequential datasets such as logs, chat history, and telemetry, each record is indexed numerically.\ You can fetch a specific record by its position: ```typescript export async function getMessageAtIndex(index: bigint) { const record = await sdk.streams.getAtIndex(schemaId, publisher, index) console.log(`Record at index ${index}:`, record) return record } ``` When to use: * When looping through entries in order (0, 1, 2, ...) * To replay logs sequentially * To test pagination logic Example: getAtIndex(schemaId, publisher, 0n) retrieves the very first message. *** ## Fetch a Range of Records (Paginated View) You can fetch multiple entries at once using index ranges.\ This is perfect for pagination or time-series queries. ```typescript export async function getMessagesInRange(start: bigint, end: bigint) { const records = await sdk.streams.getBetweenRange(schemaId, publisher, start, end) console.log('Records in range:', records) return records } ``` Example Use Cases: * Displaying the last 10 chat messages: getBetweenRange(schemaId, publisher, 0n, 10n) * Loading older telemetry data * Implementing infinite scroll {% hint style="info" %} Tip: Treat start and end like array indices (inclusive start, exclusive end).\ `start` is inclusive and `end` is exclusive. {% endhint %} *** ## Fetch All Publisher Data for a Schema If you want to retrieve all content a publisher has ever posted to a given schema, use this. ```typescript export async function getAllPublisherData() { const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher) console.log('All publisher data:', allData) return allData } ``` When to use: * Generating analytics or trend charts * Migrating or syncing full datasets * Debugging data integrity or history<br> You can think of this as: “Give me the entire dataset under this schema from this publisher.” It’s the Streams equivalent of querying all events from a contract. {% hint style="info" %} This should be used for small data sets. For larger, paginated reading, `getBetweenRange` is recommended not to overwhelm the node returning the data. {% endhint %} *** ## Count Total Entries Sometimes, you just want to know how many entries exist. ```typescript export async function getTotalEntries() { const total = await sdk.streams.totalPublisherDataForSchema(schemaId, publisher) console.log(`Total entries: ${total}`) return Number(total) } ``` When to use: * To know the total record count for pagination * To display dataset stats (“42 entries recorded”) * To monitor the growth of a stream This helps determine boundaries for getBetweenRange() or detect when new data arrives. *** ## Inspect Schema Metadata Schemas define structure, and sometimes you’ll want to validate or inspect them before reading data. First, check that a schema exists when publishing a new schema: ```typescript const ignoreAlreadyRegistered = true try { const txHash = await sdk.streams.registerDataSchemas( [ { schemaName: 'hello_world', schema: helloSchema, parentSchemaId: zeroBytes32 }, ], ignoreAlreadyRegistered ) if (txHash) { await waitForTransactionReceipt(publicClient, { hash: txHash }) console.log(`Schema registered or confirmed, Tx: ${txHash}`) } else { console.log('Schema already registered — no action required.') } } catch (err) { // fallback: if the SDK doesn’t support the flag yet if (String(err).includes('SchemaAlreadyRegistered')) { console.log('Schema already registered. Continuing...') } else { throw err } } ``` This is critical to ensure your app doesn’t attempt to query a non-existent or unregistered schema — useful for user-facing dashboards. *** ## Retrieve Full Schema Information ```typescript const schemaInfo = await sdk.streams.getSchemaFromSchemaId(schemaId) console.log('Schema Info:', schemaInfo) ``` This method retrieves both the base schema and its extended structure, if any.\ It automatically resolves inherited schemas, so you get the full picture of what fields exist. Example output: ```json { baseSchema: 'uint64 timestamp, string message', finalSchema: 'uint64 timestamp, string message', schemaId: '0xabc123...' } ``` This is important when you’re visualizing or decoding raw stream data, you can use the schema structure to parse fields correctly (timestamp, string, address, etc.). *** ## Example Next.js App Now let’s render our fetched data in the UI. ### Project Setup ```bash npx create-next-app somnia-streams-reader --typescript cd somnia-streams-reader npm install @somnia-chain/streams viem npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p ``` *** ### Folder Structure ``` somnia-streams-reader/ ├── app/ │ ├── api/ │ │ └── latest/route.ts │ ├── page.tsx │ ├── layout.tsx │ └── globals.css ├── components/ │ ├── StreamViewer.tsx │ └── SchemaInfo.tsx ├── lib/ │ ├── store.ts │ ├── schema.ts │ └── read.ts ├── tailwind.config.js └── package.json ``` *** ### lib/store.ts Sets up the Somnia SDK and connects to the testnet. ```ts import { SDK } from "@somnia-chain/streams" import { createPublicClient, http } from "viem" import { somniaTestnet } from "viem/chains" const publicClient = createPublicClient({ chain: somniaTestnet, transport: http(), }) export const sdk = new SDK(publicClient) ``` *** ### lib/schema.ts Defines your schema and publisher. ```ts export const helloWorldSchema = "uint64 timestamp, string message" export const schemaId = "0xabc123..." // replace with actual schemaId export const publisher = "0xF9D3...E5aC" // replace with actual publisher ``` If you don’t know your schema ID yet, you can compute it later using: ```ts const computed = await sdk.streams.computeSchemaId(helloWorldSchema) console.log("Schema ID:", computed) ``` *** ### lib/read.ts Implements read helpers for your API and UI. ```ts import { sdk } from "./store" import { schemaId, publisher } from "./schema" export async function getLatestMessage() { return await sdk.streams.getLastPublishedDataForSchema(schemaId, publisher) } export async function getMessagesInRange(start: bigint, end: bigint) { return await sdk.streams.getBetweenRange(schemaId, publisher, start, end) } export async function getSchemaInfo() { return await sdk.streams.getSchemaFromSchemaId(schemaId) } ``` *** ### app/api/latest/route.ts A serverless route to fetch the latest message (you can add more routes for range or schema info). ```ts import { NextResponse } from "next/server" import { getLatestMessage } from "@/lib/read" export async function GET() { const data = await getLatestMessage() return NextResponse.json({ data }) } ``` *** ### components/StreamViewer.tsx A live component with interactive buttons for fetching data. <details> <summary>StreamViewer.tsx</summary> ```tsx "use client" import { useState } from "react" export default function StreamViewer() { const [data, setData] = useState<any>(null) const [loading, setLoading] = useState(false) const fetchLatest = async () => { setLoading(true) try { const res = await fetch("/api/latest") const { data } = await res.json() setData(data) } catch (err) { console.error(err) } finally { setLoading(false) } } return ( <div className="bg-white shadow-md p-6 rounded-2xl border"> <h2 className="text-xl font-semibold mb-4">HelloWorld Stream Reader</h2> <button onClick={fetchLatest} className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md" disabled={loading} > {loading ? "Loading..." : "Fetch Latest Message"} </button> {data && ( <pre className="bg-gray-900 text-green-300 p-4 mt-4 rounded overflow-x-auto text-sm"> {JSON.stringify(data, null, 2)} </pre> )} </div> ) } ``` </details> *** ### components/SchemaInfo.tsx Displays the schema metadata. ```tsx "use client" import { useState } from "react" export default function SchemaInfo() { const [info, setInfo] = useState<any>(null) const fetchInfo = async () => { const res = await fetch("/api/latest") // just for demo; replace with /api/schema if separate route const { data } = await res.json() setInfo(data) } return ( <div className="bg-gray-50 p-6 rounded-xl shadow"> <h2 className="font-semibold text-lg mb-3">Schema Information</h2> <button onClick={fetchInfo} className="bg-gray-800 text-white px-3 py-2 rounded-md" > Load Schema Info </button> {info && ( <pre className="bg-black text-green-300 mt-3 p-3 rounded"> {JSON.stringify(info, null, 2)} </pre> )} </div> ) } ``` *** ### app/page.tsx Main dashboard combining both components. ```tsx import StreamViewer from "@/components/StreamViewer" import SchemaInfo from "@/components/SchemaInfo" export default function Home() { return ( <main className="p-10 min-h-screen bg-gray-100"> <h1 className="text-3xl font-bold mb-8">🛰️ Somnia Data Streams Reader</h1> <div className="grid gap-6 md:grid-cols-2"> <StreamViewer /> <SchemaInfo /> </div> </main> ) } ``` *** ### app/layout.tsx Wraps the layout globally. ```tsx import "./globals.css" export const metadata = { title: "Somnia Streams Reader", description: "Read on-chain data from Somnia Data Streams", } export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( <html lang="en"> <body className="antialiased">{children}</body> </html> ) } ``` *** ### Run the App ```bash npm run dev ``` Visit <http://localhost:3000> to open your dashboard.\ You’ll see a **“Fetch Latest Message”** button that retrieves data via `/api/latest` and a **Schema Info** section (ready to expand) *** ## Summary Table | Method | Purpose | Example | | ----------------------------- | -------------------------------- | -------------------------------------------------- | | getByKey | Fetch a record by unique ID | getByKey(schemaId, publisher, dataId) | | getAtIndex | Fetch record at position | getAtIndex(schemaId, publisher, 0n) | | getBetweenRange | Retrieve records in range | getBetweenRange(schemaId, publisher, 0n, 10n) | | getAllPublisherDataForSchema | Fetch all data by publisher | getAllPublisherDataForSchema(schemaRef, publisher) | | getLastPublishedDataForSchema | Latest record only | getLastPublishedDataForSchema(schemaId, publisher) | | totalPublisherDataForSchema | Count of entries | totalPublisherDataForSchema(schemaId, publisher) | | isDataSchemaRegistered | Check if schema exists | isDataSchemaRegistered(schemaId) | | schemaIdToId / idToSchemaId | Convert between Hex and readable | Useful for UI & schema mapping | | getSchemaFromSchemaId | Inspect full schema definition | Retrieves base + extended schema | <br> # Integrate Chainlink Oracles Somnia Data Streams provides a powerful, on-chain, and composable storage layer. [Chainlink Oracles](https://docs.chain.link/data-feeds/price-feeds/addresses?page=1\&testnetPage=1\&networkType=testnet\&search=\&testnetSearch=) provide secure, reliable, and decentralized external data feeds. When you combine them, you unlock a powerful new capability: **creating historical, queryable, on-chain data streams from real-world data.** Chainlink Price Feeds are designed to provide the *latest* price of an asset. They are not designed to provide a queryable history. You cannot easily ask a Price Feed, "What was the price of ETH 48 hours ago?" By integrating Chainlink with Somnia Streams, you can build a "snapshot bot" that reads from Chainlink at regular intervals and appends the price to a Somnia Data Stream. This creates a permanent, verifiable, and historical on-chain feed that any other DApp or user can read and trust. ## **Objectives & Deliverable** * **Objective:** Fetch off-chain data (a price feed) with Chainlink and store it historically via Somnia Streams. * **Key Takeaway:** Combining external "truth" sources with Somnia's composable storage to create new, valuable on-chain data products. * **Deliverable:** A hybrid "Snapshot Bot" that reads from Chainlink on the Sepolia testnet and publishes to a historical price feed on the Somnia Testnet. ## What You'll Build 1. **A New Schema:** A `priceFeedSchema` to store price data. 2. **A Chainlink Reader:** A script using `viem` to read the `latestRoundData` from Chainlink's ETH/USD feed on the Sepolia testnet. 3. **A Snapshot Bot:** A script that reads from Chainlink (Sepolia) and writes to Somnia Data Streams (Somnia Testnet). 4. **A History Reader:** A script to read our new historical price feed from Somnia Data Streams. This tutorial demonstrates a true hybrid-chain application. ## Prerequisites * Node.js 20+. * `@somnia-chain/streams`, `viem`, and `dotenv` installed. * A wallet with Somnia Testnet tokens (for publishing) and Sepolia testnet ETH (for gas, though we are only reading, so a public RPC is fine). ## **Environment Setup** Create a `.env` file. You will need RPC URLs for **both** chains and a private key for the Somnia Testnet (to pay for publishing). ```bash # .env RPC_URL_SOMNIA=[https://dream-rpc.somnia.network] RPC_URL_SEPOLIA=[https://sepolia.drpc.org] PRIVATE_KEY_SOMNIA=0xYOUR_SOMNIA_PRIVATE_KEY ``` ## Project Setup Set up your project with `viem` and the Streams SDK. ```bash npm i @somnia-chain/streams viem dotenv npm i -D @types/node typescript ts-node ``` ## **Chain Configuration** We need to define both chains we are interacting with. **`src/lib/chain.ts`** ```typescript import { defineChain } from 'viem' import { sepolia as sepoliaBase } from 'viem/chains' // 1. Somnia Testnet export const somniaTestnet = defineChain({ id: 50312, name: 'Somnia Testnet', network: 'somnia-testnet', nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 }, rpcUrls: { default: { http: [process.env.RPC_URL_SOMNIA || ''] }, public: { http: [process.env.RPC_URL_SOMNIA || ''] }, }, } as const) // 2. Sepolia Testnet (for Chainlink) export const sepolia = sepoliaBase ``` ## **Client Configuration** We will create two separate clients: * A **Somnia SDK client** (with a wallet) to *write* data. * A **Sepolia Public Client** (read-only) to *read* from Chainlink. **`src/lib/clients.ts`** ```typescript import 'dotenv/config' import { createPublicClient, createWalletClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { SDK } from '@somnia-chain/streams' import { somniaTestnet, sepolia } from './chain' function getEnv(key: string): string { const value = process.env[key] if (!value) throw new Error(`Missing environment variable: ${key}`) return value } // === Client 1: Somnia SDK (Read/Write) === const somniaWalletClient = createWalletClient({ account: privateKeyToAccount(getEnv('PRIVATE_KEY_SOMNIA') as `0x${string}`), chain: somniaTestnet, transport: http(getEnv('RPC_URL_SOMNIA')), }) const somniaPublicClient = createPublicClient({ chain
프로젝트로 돌아가기