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프로젝트로 돌아가기