bitpro가 원문에서 글자만 모은 내용이에요. 표·그림·수식은 빠지거나 깨질 수 있어요. 전체 54쪽 중 10쪽
lse,
});
if (scheduleTx instanceof Error) {
throw scheduleTx;
}
console.log('Scheduled callback tx:', scheduleTx);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
```
Run it:
```bash
PRIVATE_KEY=0x... npx tsx cron.ts <handlerContractAddress>
```
## Cancel from TypeScript
The SDK can also cancel subscriptions owned by the same wallet client. Pass the subscription ID, not the creation transaction hash:
```typescript
const cancelTx = await sdk.cancelSoliditySubscription(subscriptionId);
if (cancelTx instanceof Error) {
throw cancelTx;
}
console.log('Cancelled subscription tx:', cancelTx);
```
## Equivalent Solidity Helpers
If a Solidity contract owns the subscription, prefer `SomniaExtensions` from inside that contract instead of the TypeScript SDK. The snippets below assume `options` is a `SomniaExtensions.SubscriptionOptions` value.
One-shot block subscription:
```solidity
// Any future block works. This example uses a small offset for easy testing.
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtBlock(
address(this),
uint64(block.number + 20),
options
);
```
One-shot timestamp subscription:
```solidity
uint256 subscriptionId = SomniaExtensions.scheduleSubscriptionAtTimestamp(
address(this),
(block.timestamp + 5 minutes) * 1000,
options
);
```
Recurring every-block subscription:
```solidity
SomniaExtensions.SubscriptionFilter memory filter =
SomniaExtensions.SubscriptionFilter({
eventTopics: [
ISomniaReactivityPrecompile.BlockTick.selector,
bytes32(0),
bytes32(0),
bytes32(0)
],
origin: address(0),
emitter: SomniaExtensions.SOMNIA_REACTIVITY_PRECOMPILE_ADDRESS
});
uint256 subscriptionId = SomniaExtensions.subscribe(
address(this),
filter,
options
);
```
The `scheduleSubscriptionAt*` Solidity helpers are one-shot. For recurring every-block or every-epoch subscriptions, build the filter and call `SomniaExtensions.subscribe` directly.
# Somnia Data Streams
Read, write, and react to structured data or events broadcast on-chain. With a sharp focus on reusability and composability, applications can interoperate and coalesce around commonly agreed-upon data structures that are built around the native reactivity offered by the protocol suite.
# What is Somnia Data Streams?
Somnia Data Streams is a structured data layer for EVM chains. Somnia Data streams enable developers to build applications that both emit EVM event logs and write data to the Somnia chain without Solidity. This means developers do not need to know Solidity to build applications using Somnia Data Streams.
Somnia Data streams allow parsing schema data into contract storage, where developers define a schema (a typed, ordered layout of fields), then publish and subscribe to data that conforms to that schema.
Think of reading data from Streams as an emitted event, but with an SDK: publishers write strongly-typed records; subscribers read them by schema and publisher, and decode to rich objects.
***
## Why Streams?
Traditional approaches each have trade-offs:
* Contract events are great for signaling, but untyped at the app level (you still write your own ABI and decoders across projects). Events are also hard to stitch into reusable data models.<br>
* Custom contract storage is powerful but heavyweight, and you maintain the whole schema logic, CRUD, indexing patterns, and migrations.<br>
* Off-chain DB and proofs are flexible but brittle; either centralized or require extra machinery.<br>
* Oracles are useful for external data, but not a generic modeling layer for app-originated data.<br>
Streams solves this by standardizing:
1. Schemas (the “data ABI”)<br>
2. Publish/Subscribe primitives (SDK, not boilerplate contracts)<br>
3. Deterministic IDs (schemaId, dataId) and provenance (publisher address)<br>
This results in interoperable, verifiable, composable data with minimal app code.
***
## When to Use Streams
Use Streams when you need:
* Typed, shareable data across apps (chat messages, GPS, player stats, telemetry)
* Multiple publishers writing the same kind of record
* A standard decode flow with minimal custom indexing
* You need instant push to clients (Streams also works well with polling; you can add WS if desired)
Avoid Streams if:
* You need complex transactional logic/state machines tightly bound to contract invariants (build a contract)
* You must store large blobs (store off-chain, publish references/URIs in Streams)
***
## Definition of Terms
* **Schema**: a canonical string describing fields in order, e.g.\
uint64 timestamp, bytes32 roomId, string content, string senderName, address sender\
The exact string determines a schemaId (hash).<br>
* **Publisher**: The signer that writes data. EOA or Smart Contract that writes data under a schema. Readers trust provenance by address.<br>
* **Subscriber**: reader that fetches all records for a (schemaId, publisher) pair.<br>
* **Data ID (dataId)**: developer-chosen 32-byte key per record (helps with lookups, dedup, pagination). Pick dataIds with predictable structure to enable point lookups or pagination seeds. E.g:
* Game: toHex('matchId-index', { size: 32 })
* Chat: toHex('room-timestamp', { size: 32 })
* GPS: toHex('device-epoch', { size: 32 })<br>
* **Encoder**: converts typed values ⇄ bytes according to the schema.<br>
* **schemaId**: computed from the schema string. Treat it like a contract address for data shape.
#### Data flow
```markup
+-------------+ publishData(payload) +--------------------+
| Publisher | --------------------------------> | Somnia Streams L1 |
| (wallet) | | (on-chain data) |
+-------------+ +--------------------+
^ |
| getAllPublisherDataForSchema
| v
+-------------+ +-----------+
| Subscriber | <------------------------------------ | Reader |
| (frontend) | | (SDK) |
+-------------+ +-----------+
```
You can have multiple publishers writing under the same schema; subscribers can aggregate them if desired.
***
## The Schema: Your “Data ABI”
A schema is a compact, ordered list of typed fields. The exact string determines the computed `schemaId` Even whitespace and order matter.
Design guidance
* Put stable fields first (e.g., timestamp, entityId, type).
* Prefer fixed-width ints (e.g., uint64 for timestamps).
* Use bytes32 for keys/IDs (room, device, etc.).
* Use string for human-readable info (names, messages), but keep it short for gas efficiency.<br>
***
### Data Writing Patterns
* Single Publisher\
One server wallet publishes; User Interaces can read the schema using `getByKey` , `getAtIndex` , `getAllPublisherDataForSchema`.<br>
* Multi-Publisher\
Many devices publish under a shared schema. Your API aggregates across a list of publisher addresses.<br>
* Derived Views\
Build REST endpoints that query Streams and derive higher-level views (e.g., “latest per room”).
***
## Quickstart in 5 Minutes
You’ll register a schema, publish one message, and read it back — just to feel the flow.
### Install
```bash
npm i @somnia-chain/streams viem
```
### Set up Env
```markup
RPC_URL=https://dream-rpc.somnia.network
PRIVATE_KEY=0xYOUR_FUNDED_PRIVATE_KEY
```
### Define Chain
```typescript
// 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'] } },
} as const)
```
### Define Client
```typescript
// lib/clients.ts
import { createPublicClient, createWalletClient, http } from 'viem'
import { privateKeyToAccount } from 'viem/accounts'
import { somniaTestnet } from './chain'
const RPC = process.env.RPC_URL as string
const PK = process.env.PRIVATE_KEY as `0x${string}`
export const publicClient = createPublicClient({ chain: somniaTestnet, transport: http(RPC) })
export const walletClient = createWalletClient({ account: privateKeyToAccount(PK), chain: somniaTestnet, transport: http(RPC) })
```
### Schema
```typescript
// lib/schema.ts
export const chatSchema =
'uint64 timestamp, bytes32 roomId, string content, string senderName, address sender'
```
#### Register Schema (optional but recommended)
<details>
<summary>scripts/register.ts</summary>
```typescript
import 'dotenv/config'
import { SDK, zeroBytes32 } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { waitForTransactionReceipt } from 'viem/actions'
async function main() {
const sdk = new SDK({ public: publicClient, wallet: walletClient })
const id = await sdk.streams.computeSchemaId(chatSchema)
const exists = await sdk.streams.isDataSchemaRegistered(id)
if (!exists) {
const tx = await sdk.streams.registerDataSchemas([
{ schemaName: 'chat', schema: chatSchema, parentSchemaId: zeroBytes32 }
])
if (tx instanceof Error) throw tx
await waitForTransactionReceipt(publicClient, { hash: tx })
}
console.log('schemaId:', id)
}
main()
```
</details>
### Publish (Write)
```typescript
// scripts/publish-one.ts
import 'dotenv/config'
import { SDK, SchemaEncoder } from '@somnia-chain/streams'
import { publicClient, walletClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { toHex, type Hex } from 'viem'
import { waitForTransactionReceipt } from 'viem/actions'
async function main() {
const sdk = new SDK({ public: publicClient, wallet: walletClient })
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const enc = new SchemaEncoder(chatSchema)
const payload: Hex = enc.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: 'Alice', type: 'string' },
{ name: 'sender', value: walletClient.account!.address, type: 'address' },
])
const dataId = toHex(`general-${Date.now()}`, { size: 32 })
const tx = await sdk.streams.set([
{ id: dataId, schemaId, data: payload }
])
if (tx instanceof Error) throw tx
await waitForTransactionReceipt(publicClient, { hash: tx })
return { txHash: tx }
}
main()
```
### Read Data
```typescript
// scripts/read-all.ts
import 'dotenv/config'
import { SDK } from '@somnia-chain/streams'
import { publicClient } from '../lib/clients'
import { chatSchema } from '../lib/schema'
import { toHex } from 'viem'
type Field = { name: string; type: string; value: any }
const val = (f: Field) => f?.value?.value ?? f?.value
async function main() {
const sdk = new SDK({ public: publicClient })
const schemaId = await sdk.streams.computeSchemaId(chatSchema)
const publisher = process.env.PUBLISHER as `0x${string}` || '0xYOUR_PUBLISHER_ADDR'
const rows = (await sdk.streams.getAllPublisherDataForSchema(schemaId, publisher)) as Field[][]
const want = toHex('general', { size: 32 }).toLowerCase()
for (const r of rows || []) {
const ts = Number(val(r[0]))
const ms = String(ts).length <= 10 ? ts * 1000 : ts
if (String(val(r[1])).toLowerCase() !== want) continue
console.log({
time: new Date(ms).toLocaleString(),
content: String(val(r[2])),
senderName: String(val(r[3])),
sender: String(val(r[4])),
})
}
}
main()
```
That’s your first end-to-end loop.
***
## FAQs
Q: Do I need to register a schema?\
A: Registration is optional but recommended. You can publish to an unregistered schema (readers just need the exact string to decode). Registration helps discoverability and tooling.
Q: Can I change a schema later?\
A: Changing order or types yields a new schemaId. Plan for versioning (run v1 + v2 together).
Q: How do I page data?\
A: Use structured dataIds, or build a thin index off-chain that records block numbers / tx hashes per record.
Q: How does Streams differ from subgraphs?\
A: Streams defines how you write/read structured records with an SDK. Subgraphs (or other indexers) sit on top to query across many publishers, paginate, and filter efficiently.
Q: How do I handle large payloads?\
A: Store the payload elsewhere (IPFS, Arweave, S3) and put the URI + hash in Streams. Optionally encrypt off-chain.
# Quickstart
Example pseudo code for publishing data associated with a schema (public or private)
## Pre-requisites
A typescript environment with [`viem`](https://viem.sh/) and [`@somnia-chain/streams`](https://www.npmjs.com/package/@somnia-chain/streams) installed
## Steps
### 1. Define your schema as a string and plug it into the schema encoder
```typescript
import { SDK, zeroBytes32, SchemaEncoder } from "@somnia-chain/streams"
const gpsSchema = `uint64 timestamp, int32 latitude, int32 longitude, int32 altitude, uint32 accuracy, bytes32 entityId, uint256 nonce`
const schemaEncoder = new SchemaEncoder(gpsSchema)
```
`schemaEncoder` can now be used to encode data for broadcast and also decode data when reading it from Somnia Data Stream SDK.
### 2. Compute your unique schema identifier from the schema
<pre class="language-typescript"><code class="lang-typescript"><strong>const sdk = new SDK({
</strong> public: getPublicClient(),
wallet: getWalletClient(),
})
const schemaId = await sdk.streams.computeSchemaId(gpsSchema)
console.log(`Schema ID ${schemaId}`)
</code></pre>
All data broadcast with the Somnia Data Stream SDK write mechanism must be linked to a schema ID so that we know how to decode the data on read.
### 3. Encode the data you want to store that is compatible with the schema
```typescript
const encodedData: Hex = schemaEncoder.encodeData([
{ name: "timestamp", value: Date.now().toString(), type: "uint64" },
{ name: "latitude", value: "51509865", type: "int32" },
{ name: "longitude", value: "-0118092", type: "int32" },
{ name: "altitude", value: "0", type: "int32" },
{ name: "accuracy", value: "0", type: "uint32" },
{ name: "entityId", value: zeroBytes32, type: "bytes32" }, // object providing GPS data
{ name: "nonce", value: "0", type: "uint256" },
])
```
The value returned is a raw hex encoded bytes value that can be broadcast on-chain via the Somnia Data Stream SDK.
### 4. Publish data (with our without a public schema)
```typescript
const publishTxHash = await sdk.streams.set([{
id: toHex("london", { size: 32 }),
schemaId: computedGpsSchemaId,
data: encodedData,
}])
```
`set` has the following parameter `dataStreams` which is a list of data points being written to chain\
\
`dataStreams` has the `DataStream[]` type:
```typescript
type Hex = `0x{string}`
type DataStream = {
id: Hex // Unique data key for the publisher
schemaId: Hex // Computed from the raw schema string
data: Hex // From step 3, raw bytes data formated as a hex string
}
```
### 5. Direct data read without reactivity
```typescript
const data = await sdk.streams.getByKey(
computedGpsSchemaId,
publisherWalletAddress,
dataKey
)
```
This last step shows how you request data from Somnia data streams filtering on:
1. Schema ID
2. Address of the account that wrote the data to chain
1. This could be an EOA or another smart contract
The response from `getByKey` will be the data published but decoded for the specified schema.
Note: where the schema ID is associated with a public data schema that has been registered on-chain, the SDK will automatically decode the raw data published on-chain and return that decoded data removing the need for the decoder. If the schema is not public, the schema decoder will be required outside of the SDK and you will instead get raw bytes from the chain. Example:
```typescript
if (data) {
schemaEncoder.decode(data)
}
```
Further filters can be applied client side to the data in order to filter for specifics within the data. GitBook also allows you to set up a bi-directional sync with an existing repository on GitHub or GitLab. Setting up Git Sync allows you and your team to write content in GitBook or in code, and never have to worry about your content becoming out of sync.
# Concepts
# Understanding Schemas, Schema IDs, Data IDs, and Publisher
Somnia Data Streams uses a schema-driven architecture to store and manage blockchain data. Every piece of information stored on the network, whether it’s a chat message, leaderboard score, or todo item, follows a structured schema, is identified by a Schema ID, written with a Data ID, and associated with a Publisher.
In this guide, you’ll learn the difference between Schemas and Schema IDs, how Data IDs uniquely identify records, and how Publishers own and manage their data streams.
* Schemas define the structure of your data.
* Data IDs uniquely identify individual records.
* Publisher determines who owns or controls the data stream.
By the end, you’ll understand how to organize, reference, and manage your application’s data on Somnia.
## What Are Schemas?
A Schema defines the structure and types of the data you want to store onchain. It’s like a blueprint for how your application’s data is encoded, stored, and decoded. A Schema ID, on the other hand, is a unique deterministic hash computed from that schema definition.
When you register or compute a schema, the SDK automatically generates a unique hash (Schema ID) that permanently represents that schema definition.
A schema describes the structure of your data, much like a table in a relational database defines its columns.
#### Example: Defining a Schema
```typescript
const userSchema = `
uint64 timestamp,
string username,
string bio,
address owner
`
```
This schema tells the Somnia Data Streams system how your data is structured and typed.
## Schema ID: The Unique Identifier
A Schema ID is derived from your schema using a hashing algorithm. It uniquely represents this structure onchain, ensuring consistency and integrity. You can compute its Schema ID before even deploying it onchain.
```typescript
import { SDK } from '@somnia-chain/streams'
import { getSdk } from './clients'
const sdk = getSdk()
const schemaId = await sdk.streams.computeSchemaId(userSchema)
console.log("Computed Schema ID:", schemaId)
```
Example Output:
```bash
Computed Schema ID: 0x5e4bce54a39b42b5b8a235b5d9e27e7031e39b65d7a42a6e0ac5e8b2c79e17b0
```
{% hint style="danger" %}
This hash (schemaId) uniquely identifies the schema onchain. If you change even one character in the schema definition, the Schema ID will change.
{% endhint %}
The Schema ID is the hash that ensures the same structure is used everywhere, preventing mismatched or corrupted data.
## Registering a Schema
To make the schema usable onchain, it has to be registered by calling the `registerDataSchemas()` method. This ensures other nodes and apps can decode your data correctly:
```typescript
import { zeroBytes32 } from '@somnia-chain/streams'
const ignoreExistingSchemas = true
await sdk.streams.registerDataSchemas([
{ schemaName: "MySchema", schema: userSchema, parentSchemaId: zeroBytes32 }
], ignoreExistingSchemas)
```
`id` is a string. human human-readable identifier`ignoreExistingSchemas` is for telling the SDK not to worry about already registered schemas.\
Once registered, any publisher can use this Schema ID to store or retrieve data encoded according to this structure. The schema defines structure. The Schema ID becomes its permanent onchain reference.
| Concept | Database Equivalent | Description |
| --------- | ------------------- | ------------------------------------------ |
| Schema | Table Definition | Defines data fields and types |
| Schema ID | Table Hash | Uniquely identifies that schema definition |
For instance:
`Schema → CREATE TABLE Users (id INT, name TEXT)`
`Schema ID → 0x9f3a...a7c (hash of the above definition)`
## What Are Data IDs?
Every record written to Somnia (e.g., a single message, transaction, or post) must have a Data ID, a unique key representing that entry. It uniquely identifies a specific record (or row). The Data ID 프로젝트로 돌아가기