bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 13쪽
on.
**Example**
```typescript
const info = await sdk.streams.getSomniaDataStreamsProtocolInfo()
console.log('Protocol info:', info)
```
### Key Types Reference
| Type | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
| `DataStream` | `{ id: Hex, schemaId: Hex, data: Hex }` – Used with `set()` or `setAndEmitEvents()` . |
| `EventStream` | `{ id: string, argumentTopics: Hex[], data: Hex }` – Used with `emitEvents()` and `setAndEmitEvents()` . |
| `DataSchemaRegistration` | `{ schemaName: string, schema: string, parentSchemaId: Hex }` – For `registerDataSchemas()` . |
| `EventSchema` | `{ params: EventParameter[], eventTopic: string }` – For `registerEventSchemas()` . |
| `EthCall` | `{ to: Address, data: Hex }` – Defines on-chain calls for event enrichment. |
### Developer Tips
* Always compute your schema ID locally before deploying: `await sdk.streams.computeSchemaId(schema)`.
* For chat-like or telemetry apps, pair `setAndEmitEvents()` (write) with `subscribe()` (read).
* Use `zeroBytes32` for base schemas that don’t extend others.
* All write methods return transaction hashes, use `waitForTransactionReceipt()` to confirm.
* Data Streams focus on persistent, schema-based storage, while Event Streams enable reactive notifications; use them together for comprehensive applications.
# Tutorials
# “Hello World” App
Build a Hello World program to understand Somnia Data Streams.
If you’ve ever wanted to see your data travel onchain in real time, this is the simplest way to begin. In this guide, we’ll build and run a Hello World Publisher and Subscriber using the Somnia Data Streams SDK. It demonstrates how to define a schema, publish onchain data, and read it in real time.
Somnia Data Streams enables developers to store, retrieve, and react to real-time blockchain data without needing to build indexers or manually poll the chain.
Each app works around three key ideas:
1. Schemas – define the data format.
2. Data IDs – uniquely identify each record.
3. Publishers – wallet addresses that own and post data.
Your app can write (“publish”) data using one account, and another app (or user) can “subscribe” to read or monitor that data stream. This “Hello World” project demonstrates exactly how that works.
## Prerequisites
Before you begin:
* Node.js 20+
* A Somnia Testnet wallet with STT test tokens
* `.env` file containing your wallet credentials
## Project Setup
Create a Project Directory and install dependencies [@somnia-chain/streams](https://www.npmjs.com/package/@somnia-chain/streams) and [viem](https://viem.sh/):
```bash
npm i @somnia-chain/streams viem dotenv
```
Now create a .env file to hold your test wallet’s private key:
```bash
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
PUBLIC_KEY=0xYOUR_PUBLIC_ADDRESS
```
## Project Overview
The project contains four files:
| File | Description |
| -------------- | --------------------------------------------------- |
| publisher.js | Sends “Hello World” messages to Somnia Data Streams |
| subscriber.js | Reads and displays those messages |
| dream-chain.js | Configures the Somnia Dream testnet connection |
| package.json | Handles dependencies and npm scripts |
## Network Configuration
The file `dream-chain.js` defines the blockchain network connection.
```javascript
const { defineChain } = require("viem");
const dreamChain = defineChain({
id: 50312,
name: "Somnia Dream",
network: "somnia-dream",
nativeCurrency: { name: "STT", symbol: "STT", decimals: 18 },
rpcUrls: {
default: { http: ["https://dream-rpc.somnia.network"] },
},
});
module.exports = { dreamChain };
```
This allows both publisher and subscriber scripts to easily reference the same testnet environment.
## Hello World Publisher
The publisher connects to the blockchain, registers a schema if necessary, and sends a “Hello World” message every few seconds.
```javascript
const { SDK, SchemaEncoder, zeroBytes32 } = require("@somnia-chain/streams")
const { createPublicClient, http, createWalletClient, toHex } = require("viem")
const { privateKeyToAccount } = require("viem/accounts")
const { waitForTransactionReceipt } = require("viem/actions")
const { dreamChain } = require("./dream-chain")
require("dotenv").config()
async function main() {
const publicClient = createPublicClient({ chain: dreamChain, transport: http() })
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY),
chain: dreamChain,
transport: http(),
})
const sdk = new SDK({ public: publicClient, wallet: walletClient })
// 1️⃣ Define schema
const helloSchema = `string message, uint256 timestamp, address sender`
const schemaId = await sdk.streams.computeSchemaId(helloSchema)
console.log("Schema ID:", schemaId)
// 2️⃣ Safer schema registration
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
}
}
// 3️⃣ Publish messages
const encoder = new SchemaEncoder(helloSchema)
let count = 0
setInterval(async () => {
count++
const data = encoder.encodeData([
{ name: 'message', value: `Hello World #${count}`, type: 'string' },
{ name: 'timestamp', value: BigInt(Math.floor(Date.now() / 1000)), type: 'uint256' },
{ name: 'sender', value: walletClient.account.address, type: 'address' },
])
const dataStreams = [{ id: toHex(`hello-${count}`, { size: 32 }), schemaId, data }]
const tx = await sdk.streams.set(dataStreams)
console.log(`✅ Published: Hello World #${count} (Tx: ${tx})`)
}, 3000)
}
main()
```
This function connects to Somnia Dream Testnet using your wallet and computes the schema ID for the message structure. It then registers the schema if not already registered.\
The \`encodeData\` method encodes each message as a structured data packet, and it then publishes data to the chain using sdk.streams.set().
Each transaction is a verifiable, timestamped on-chain record.
***
## Hello World Subscriber
The subscriber listens for any messages published under the same schema and publisher address.\
It uses a simple polling mechanism, executed every 3 seconds, to fetch and decode updates.
```javascript
const { SDK, SchemaEncoder } = require("@somnia-chain/streams");
const { createPublicClient, http } = require("viem");
const { dreamChain } = require("./dream-chain");
require('dotenv').config();
async function main() {
const publisherWallet = process.env.PUBLISHER_WALLET;
const publicClient = createPublicClient({ chain: dreamChain, transport: http() });
const sdk = new SDK({ public: publicClient });
const helloSchema = `string message, uint256 timestamp, address sender`;
const schemaId = await sdk.streams.computeSchemaId(helloSchema);
const schemaEncoder = new SchemaEncoder(helloSchema);
const result = new Set();
setInterval(async () => {
const allData = await sdk.streams.getAllPublisherDataForSchema(schemaId, publisherWallet);
for (const dataItem of allData) {
const fields = dataItem.data ?? dataItem;
let message = "", timestamp = "", sender = "";
for (const field of fields) {
const val = field.value?.value ?? field.value;
if (field.name === "message") message = val;
if (field.name === "timestamp") timestamp = val.toString();
if (field.name === "sender") sender = val;
}
const id = `${timestamp}-${message}`;
if (!result.has(id)) {
result.add(id);
console.log(`🆕 ${message} from ${sender} at ${new Date(Number(timestamp) * 1000).toLocaleTimeString()}`);
}
}
}, 3000);
}
main();
```
This function computes the same Schema ID used by the publisher. It polls the blockchain for all messages from that publisher and decodes data according to the schema fields. Then, it displays any new messages with timestamps and sender addresses.
***
## Run the App
Run both scripts in separate terminals:
```bash
npm run publisher
```
and then
```bash
npm run subscriber
```
You’ll see Publisher Output:
```bash
Schema ID: 0x27c30fa6547c34518f2de6a268b29ac3b54e51c98f8d0ef6018bbec9153e9742
⚠️ Schema already registered. Continuing...
✅ Published: Hello World #1 (Tx: 0xf21ad71a6c7aa54c171ad38b79ef417e8488fd750ce00c1357918b7c7fa5c951)
✅ Published: Hello World #2 (Tx: 0xe999b0381ba9d937d85eb558fefe214fa4e572767c4e698c6e31588ff0e68f0a)
```
Subscriber Output
```bash
🆕 Hello World #2 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:04 PM
🆕 Hello World #3 from 0xb6e4fa6ff2873480590c68D9Aa991e5BB14Dbf03 at 2:24:07 PM
```
Congratulations 🎉 You’ve just published and read blockchain data using Somnia Data Streams!
This is the foundation for real-time decentralized apps, chat apps, dashboards, IoT feeds, leaderboards, and more.
***
## Conclusion
You’ve just learned how to:
* Define and compute a schema and schema ID
* Register it on the Somnia Testnet
* Publish and subscribe to on-chain structured data
* Decode and render blockchain messages in real time
This simple 'Hello World' app is your first step toward building real-time, decentralized applications on Somnia.
# Build Your First Schema
Before you can publish or read structured data on the Somnia Network using Somnia Data Streams, you must first define a Schema.\
A schema acts as the blueprint or data contract between your publisher and all subscribers who wish to interpret your data correctly.
In the Somnia Data Streams system, every schema is expressed as a canonical string. A strict, ordered list of fields with Solidity compatible types.
\
For example, a chat application schema:
```solidity
uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
```
This simple definition:
* Establishes how data should be encoded and decoded on-chain.
* Produces a unique schemaId derived from its exact string representation.
* Enables multiple publishers and readers to exchange data consistently, without needing to redeploy contracts or agree on custom ABIs.
Each schema you define becomes a typed, reusable data model, similar to a table definition in a database or an ABI for events, but far simpler. Once created, schemas can be:
* Reused across many applications.
* Extended to create hierarchical data definitions (e.g., “GPS coordinates” → “Vehicle telemetry”).
* Versioned by creating new schemas when structure changes occur.<br>
This tutorial will walk you through building, registering, and validating your first schema step by step.
## Prerequisites
Before continuing, ensure you have the following:
1. Node.js 20+
2. TypeScript configured in your project
3. `.env.local` file for environment variables
Add your credentials to .env.local:
```markup
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
```
4. A Funded Testnet Account. You’ll need an address with test tokens on the Somnia Testnet to register schemas or publish data.<br>
{% hint style="danger" %}
NOTE: The Private Key is only required if connecting a Private Key via a Viem wallet account.\
\
Important: Never expose your private key to a client-side environment. Keep it in server scripts or backend environments only.
{% endhint %}
***
## What You’ll Build
In this tutorial, you will:
* Create a canonical schema string (your “data ABI”)
* Compute the schema ID
* Register your schema on-chain (idempotently)
* Validate your schema with a simple encode/decode test<br>
We’ll use a chat message schema as a running example:
```solidity
uint64 timestamp, bytes32 roomId, string content, string senderName, address sender
```
This schema represents a single chat message, which can be used later to build a full on-chain chat application.
***
## Project Setup
### Install dependencies
```bash
npm i @somnia-chain/streams viem
npm i -D @types/node
```
### Define Chain configuration
```typescript
// src/lib/chain.ts
import { defineChain } from 'viem'
export const somniaTestnet = defineChain({
id: 50312,
name: 'Somnia Testnet',
network: 'somnia-testnet',
nativeCurrency: { name: 'STT', symbol: 'STT', decimals: 18 },
rpcUrls: {
default: { http: ['https://dream-rpc.somnia.network'] },
public: { http: ['https://dream-rpc.somnia.network'] },
},
})
```
### Set up your clients
```typescript
// src/lib/clients.ts
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'
function need(key: 'RPC_URL' | 'PRIVATE_KEY') {
const v = process.env[key]
if (!v) throw new Error(`Missing ${key} in .env.local`)
return v
}
export const publicClient = createPublicClient({
chain: somniaTestnet,
transport: http(need('RPC_URL')),
})
export const walletClient = createWalletClient({
account: privateKeyToAccount(need('PRIVATE_KEY') as `0x${string}`),
chain: somniaTestnet,
transport: http(need('RPC_URL')),
})
```
***
## Define the Schema String
```typescript
// src/lib/chatSchema.ts
export const chatSchema =
'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
```
Field order matters, and ensure to always use Solidity-compatible types. It is important to keep the `string` fields short to minimize gas. Note that changing type or order creates a new schema ID.<br>
***
## Compute the schemaId
```typescript
// scripts/compute-schema-id.ts
import 'dotenv/config'
import { SDK } from '@somnia-chain/streams'
import { publicClient } from '../src/lib/clients'
import { chatSchema } from '../src/lib/chatSchema'
async function main() {
const sdk = new SDK({ public: publicClient })
const id = await sdk.streams.computeSchemaId(chatSchema)
console.log('Schema ID:', id)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
```
The SDK computes a unique hash of the schema string. This `schemaId` is your permanent identifier. Anyone using the same schema string will derive the same ID *\[confirm with Vincent for correctness]*.<br>
***
## Register the Schema
Registration makes your schema discoverable and reusable by others. *\[confirm with Vincent for correctness]*.
```typescript
// scripts/register-schema.ts
import 'dotenv/config'
import { SDK, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../src/lib/clients'
import { chatSchema } from '../src/lib/chatSchema'
import { waitForTransactionReceipt } from 'viem/actions'
async function main() {
const sdk = new SDK({ public: publicClient, wallet: walletClient })
const id = await sdk.streams.computeSchemaId(chatSchema)
const isRegistered = await sdk.streams.isSchemaRegistered(id)
if (isRegistered) {
console.log('Schema already registered.')
return
}
const txHash = await sdk.streams.registerDataSchemas({ schemaName: "chat", schema: chatSchema })
console.log('Register tx:', txHash)
const receipt = await waitForTransactionReceipt(publicClient, { hash: txHash })
console.log('Registered in block:', receipt.blockNumber)
}
main().catch((e) => {
console.error(e)
process.exit(1)
})
```
`isSchemaRegistered()` checks chain state. `registerSchema()` publishes the schema definition to Streams. Thus, the transaction is idempotent, meaning that it is safe to re-run.
***
## Encode and Decode a Sample Payload
Test your schema locally before publishing any data.
```typescript
// scripts/encode-decode.ts
import 'dotenv/config'
import { SchemaEncoder } from '@somnia-chain/streams'
import { toHex, type Hex } from 'viem'
import { chatSchema } from '../src/lib/chatSchema'
const encoder = new SchemaEncoder(chatSchema)
const encodedData: Hex = encoder.encodeData([
{ name: 'timestamp', value: Date.now().toString(), type: 'uint64' },
{ name: 'roomId', value: toHex('general', { size: 32 }), type: 'bytes32' },
{ name: 'content', value: 'Hello Somnia!', type: 'string' },
{ name: 'senderName', value: 'Victory', type: 'string' },
{ name: 'sender', value: '0x0000000000000000000000000000000000000001', type: 'address' },
])
console.log('Encoded:', encodedData)
console.log('Decoded:', encoder.decodeData(encodedData))
```
`encodeData()` serializes the payload according to the schema definition. `decodeData()` restores readable field values from the encoded hex. This step ensures your schema fields align correctly.
***
## Conclusion
You’ve just built and registered your first schema on Somnia Data Streams.
Your schema now acts as a public data contract between any publisher and subscriber that wants to communicate using this structure.<br>
# Streams Case Study: Formula 1
Streaming data from OpenF1 on-chain and building reactive applications
### Schemas
Driver schema
```
uint32 number, string name, string abbreviation, string teamName, string teamColor
```
Cartesian 3D coordinates schema
```
int256 x, int256 y, int256 z
```
The driver schema can extend the cartesian coordinates since the 3D coordinates will be used widely for other applications. Again this promotes re-usability of schemas.
### Schema registration and re-use
```javascript
const { SDK, zeroBytes32, SchemaEncoder } = require("@somnia-chain/streams");
const {
createPublicClient,
http,
createWalletClient,
toHex,
defineChain,
} = require("viem");
const { privateKeyToAccount } = require("viem/accounts");
const dreamChain = defineChain({
id: 50312,
name: "Somnia Testnet",
network: "testnet",
nativeCurrency: {
decimals: 18,
name: "STT",
symbol: "STT",
},
rpcUrls: {
default: {
http: [
"https://dream-rpc.somnia.network",
],
},
public: {
http: [
"https://dream-rpc.somnia.network",
],
},
},
})
async function main() {
// Connect to the blockchain to read data with the public client
const publicClient = createPublicClient({
chain: dreamChain,
transport: http(),
})
const walletClient = createWalletClient({
account: privateKeyToAccount(process.env.PRIVATE_KEY),
chain: dreamChain,
transport: http(),
})
// Connect to the SDK
const sdk = new SDK({
public: publicClient,
wallet: walletClient,
})
// Setup the schemas
const coordinatesSchema = `int256 x, int256 y, int256 z`
const driverSchema = `uint32 number, string name, string abbreviation, string teamName, string teamColor`
// Derive Etherbase schema metadata
const coordinatesSchemaId = await sdk.streams.computeSchemaId(
coordinatesSchema
)
if (!coordinatesSchemaId) {
throw new Error("Unable to compute the schema ID for the coordinates schema")
}
const driverSchemaId = await sdk.streams.computeSchemaId(
driverSchema
)
if (!driverSchemaId) {
throw new Error("Unable to compute the schema ID for the driver schema")
}
const extendedSchema = `${driverSchema}, ${coordinatesSchema}`
console.log("Schemas in use", {
coordinatesSchemaId,
driverSchemaId,
coordinatesSchema,
driverSchema,
extendedSchema
})
const isCoordinatesSchemaRegistered = await sdk.streams.isDataSchemaRegistered(coordinatesSchemaId)
if (!isCoordinatesSchemaRegistered) {
// We want to publish the driver schema but we need to publish the coordinates schema first before it can be extended
const registerCoordinatesSchemaTxHash =
await sdk.streams.registerDataSchemas([
{ schemaName: "coords", schema: coordinatesSchema }
])
if (!registerCoordinatesSchemaTxHash) {
throw new Error("Failed to register coordinates schema")
}
console.log("Registered coordinates schema on-chain", {
registerCoordinatesSchemaTxHash
})
await publicClient.waitForTransactionReceipt({
hash: registerCoordinatesSchemaTxHash
})
}
const isDriverSchemaRegistered = 프로젝트로 돌아가기